前端优化:chat-box、消息列表、rehype 及 Studio 相关交互改进

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
xuhdandClaude Sonnet 4.6 committed 2026-05-28 18:29:10 +08:00
1 parent b8a97f2138
commit 1ea521abab
5 files changed
+31 -8

No files matched your search

@@ -347,14 +347,14 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
/> />
<ResizablePanel <ResizablePanel
className={cn( className={cn(
"bg-[#fffdfa] transition-all duration-300 ease-in-out", "bg-background transition-all duration-300 ease-in-out",
!sidePanelOpen && "opacity-0", !sidePanelOpen && "opacity-0",
)} )}
id="artifacts" id="artifacts"
> >
<div <div
className={cn( className={cn(
"h-full bg-[#fffdfa] p-4 transition-transform duration-300 ease-in-out", "h-full bg-background p-4 transition-transform duration-300 ease-in-out",
sidePanelOpen ? "translate-x-0" : "translate-x-full", sidePanelOpen ? "translate-x-0" : "translate-x-full",
)} )}
> >
@@ -172,9 +172,21 @@ function MessageContent_({
extraRehypePlugins?: ReturnType<typeof useRehypeSplitWordsIntoSpans>; extraRehypePlugins?: ReturnType<typeof useRehypeSplitWordsIntoSpans>;
}) { }) {
const baseRehypePlugins = useRehypeSplitWordsIntoSpans(isLoading); const baseRehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
// Streamdown splits content into per-block processors; calling the factory
// once here ensures all blocks share the same transformer instance (and thus
// the same citation counter) for this message.
const stableExtraPlugins = useMemo(() => {
if (!extraRehypePlugins?.length) return [];
return extraRehypePlugins.map((factory) => {
const transformer = factory();
return () => transformer;
});
}, [extraRehypePlugins]);
const rehypePlugins = useMemo( const rehypePlugins = useMemo(
() => (extraRehypePlugins?.length ? [...baseRehypePlugins, ...extraRehypePlugins] : baseRehypePlugins), () => (stableExtraPlugins.length ? [...stableExtraPlugins, ...baseRehypePlugins] : baseRehypePlugins),
[baseRehypePlugins, extraRehypePlugins], [stableExtraPlugins, baseRehypePlugins],
); );
const isHuman = message.type === "human"; const isHuman = message.type === "human";
const resolveArtifact = useArtifactResolver(threadId); const resolveArtifact = useArtifactResolver(threadId);
+2 -2
View File
@@ -7,6 +7,8 @@ const CITE_RE = /\[\[cite:(\d+)\]\]/g;
export function rehypeCitations(sources: Array<{ id: number; title: string }>) { export function rehypeCitations(sources: Array<{ id: number; title: string }>) {
const sourceMap = new Map(sources.map((s) => [s.id, s.title])); const sourceMap = new Map(sources.map((s) => [s.id, s.title]));
const citeCounter = new Map<number, number>();
let citeIndex = 0;
return (tree: Root) => { return (tree: Root) => {
visit(tree, "text", (node, index, parent) => { visit(tree, "text", (node, index, parent) => {
@@ -14,8 +16,6 @@ export function rehypeCitations(sources: Array<{ id: number; title: string }>) {
CITE_RE.lastIndex = 0; CITE_RE.lastIndex = 0;
const newNodes: ElementContent[] = []; const newNodes: ElementContent[] = [];
const citeCounter = new Map<number, number>();
let citeIndex = 0;
let last = 0; let last = 0;
let match: RegExpExecArray | null; let match: RegExpExecArray | null;
+7 -2
View File
@@ -89,10 +89,15 @@ export function useNotebookChat(
const send = async (text: string, selectedSourceIds: number[]) => { const send = async (text: string, selectedSourceIds: number[]) => {
if (thread.isLoading || !token) return; if (thread.isLoading || !token) return;
// API 需要 document.id,而不是 source.id
const toDocIds = (ids: number[]) =>
ids
.map((id) => sources.find((s) => s.id === id)?.document?.id)
.filter((id): id is number => id !== undefined);
const sourceIds = const sourceIds =
selectedSourceIds.length > 0 selectedSourceIds.length > 0
? selectedSourceIds ? toDocIds(selectedSourceIds)
: sources.map((s) => s.id).filter(Boolean); : sources.map((s) => s.document?.id).filter((id): id is number => id !== undefined);
await thread.submit( await thread.submit(
{ messages: [{ type: "human", content: text }] }, { messages: [{ type: "human", content: text }] },
@@ -36,6 +36,8 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { MessageList } from "@/components/workspace/messages/message-list"; import { MessageList } from "@/components/workspace/messages/message-list";
import { ThreadContext } from "@/components/workspace/messages/context";
import { ChatBox } from "@/components/workspace/chats/chat-box";
import { rehypeCitations } from "@/core/rehype"; import { rehypeCitations } from "@/core/rehype";
// ── 来源卡片图标 ──────────────────────────────────────── // ── 来源卡片图标 ────────────────────────────────────────
@@ -268,6 +270,8 @@ export default function StudioNotebookDetailPage() {
const isChatView = activeThreadId !== null || hasMessages; const isChatView = activeThreadId !== null || hasMessages;
return ( return (
<ThreadContext.Provider value={{ thread }}>
<ChatBox threadId={chatThreadId ?? ""}>
<div className="relative flex h-full flex-col overflow-hidden bg-background"> <div className="relative flex h-full flex-col overflow-hidden bg-background">
{/* 背景渐变(列表视图时显示) */} {/* 背景渐变(列表视图时显示) */}
{!isChatView && ( {!isChatView && (
@@ -490,5 +494,7 @@ export default function StudioNotebookDetailPage() {
onToggle={handleToggleSource} onToggle={handleToggleSource}
/> />
</div> </div>
</ChatBox>
</ThreadContext.Provider>
); );
} }