perf(frontend): split chat streaming, load Inter, isolate admin CSS
Settled messages no longer rebuild on every token, Inter is actually requested, and admin routes drop the 33 KB chat stylesheet. Root force-dynamic is gone so public shells can prerender without changing the no-store Cache-Control contract. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
"use client";
|
||||
|
||||
import { X } from "lucide-react";
|
||||
import { memo, useEffect, useState, type MutableRefObject, type ReactNode } from "react";
|
||||
|
||||
export type AccountOverlayLibraryControls = Readonly<{
|
||||
open: boolean;
|
||||
toggle: () => void;
|
||||
}>;
|
||||
|
||||
export type AccountOverlayModel = Readonly<{
|
||||
title: string;
|
||||
dialogClass: string;
|
||||
signingOut: boolean;
|
||||
close: () => void;
|
||||
takeOpenLibraryRequest: () => boolean;
|
||||
overlayRef: MutableRefObject<HTMLElement | null>;
|
||||
closeButtonRef: MutableRefObject<HTMLButtonElement | null>;
|
||||
renderProfile: (library: AccountOverlayLibraryControls) => ReactNode;
|
||||
renderLogout: () => ReactNode;
|
||||
}>;
|
||||
|
||||
export const AccountDialogOverlay = memo(function AccountDialogOverlay({
|
||||
open,
|
||||
dialog,
|
||||
openEpoch,
|
||||
modelRef,
|
||||
}: Readonly<{
|
||||
open: boolean;
|
||||
dialog: "profile" | "logout" | null;
|
||||
openEpoch: number;
|
||||
modelRef: MutableRefObject<AccountOverlayModel | null>;
|
||||
}>) {
|
||||
const [chartLibraryOpen, setChartLibraryOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (modelRef.current?.takeOpenLibraryRequest()) setChartLibraryOpen(true);
|
||||
}, [dialog, modelRef, open]);
|
||||
|
||||
if (!open || dialog === null) return null;
|
||||
const model = modelRef.current;
|
||||
if (!model) return null;
|
||||
void openEpoch;
|
||||
|
||||
return (
|
||||
<div className="account-modal-overlay" onMouseDown={model.close}>
|
||||
<section
|
||||
className={`account-modal ${model.dialogClass}`}
|
||||
ref={model.overlayRef}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="account-dialog-title"
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<header className="account-modal-header">
|
||||
<h2 id="account-dialog-title">{model.title}</h2>
|
||||
<button
|
||||
className="dialog-close"
|
||||
ref={model.closeButtonRef}
|
||||
aria-label="关闭"
|
||||
type="button"
|
||||
onClick={model.close}
|
||||
disabled={model.signingOut}
|
||||
>
|
||||
<X aria-hidden="true" />
|
||||
</button>
|
||||
</header>
|
||||
{dialog === "profile" && model.renderProfile({
|
||||
open: chartLibraryOpen,
|
||||
toggle: () => setChartLibraryOpen((current) => !current),
|
||||
})}
|
||||
{dialog === "logout" && model.renderLogout()}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -51,7 +51,7 @@ export function AdminApp({ children }: { children: ReactNode }) {
|
||||
borderRadiusLG: 8,
|
||||
boxShadowSecondary: "0 1px 2px rgba(29, 29, 31, .06)",
|
||||
boxShadowTertiary: "none",
|
||||
fontFamily: "StyreneB, Inter, system-ui, sans-serif",
|
||||
fontFamily: "StyreneB, var(--font-inter, Inter), system-ui, sans-serif",
|
||||
},
|
||||
components: {
|
||||
Layout: {
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
"use client";
|
||||
|
||||
import { ChatMessageRow } from "@/components/chat-message-row";
|
||||
import {
|
||||
ChatMessageActions,
|
||||
toggleChatMessageFeedback,
|
||||
type ChatMessageFeedback,
|
||||
} from "@/components/chat-message-actions";
|
||||
import { ConversationFollowUps } from "@/components/conversation-follow-ups";
|
||||
import { isGeneralDailyFortuneQuestion } from "@/lib/consultation-entrypoint";
|
||||
import { deriveConsultationFollowUps } from "@/lib/consultation-follow-ups";
|
||||
import type { ConsultationDomain } from "@/lib/consultation-domain-registry";
|
||||
import type { AgentActivityView, ChatMessage, ChatMessageView } from "@/lib/chat-message-view";
|
||||
import { chatMessageViews, settledChatMessageViews, streamingChatMessageView } from "@/lib/chat-message-view";
|
||||
import type { PublicThinkingSection } from "@/lib/consultation-thinking-plan";
|
||||
import type { ConsultationTimelineRow } from "@/lib/consultation-run-timeline";
|
||||
import {
|
||||
noteSettledListRender,
|
||||
noteSettledRowRender,
|
||||
noteStreamingRowRender,
|
||||
noteUnsplitListRender,
|
||||
} from "@/lib/home-streaming-render-probe";
|
||||
import { memo, type MutableRefObject } from "react";
|
||||
|
||||
export type ChatTranscriptActions = Readonly<{
|
||||
onFeedback: (feedbackKey: string, requested: ChatMessageFeedback) => void;
|
||||
onCopy: (feedbackKey: string, text: string) => void;
|
||||
onRegenerate: (renderKey: string) => void;
|
||||
onFollowUp: (question: string) => void;
|
||||
}>;
|
||||
|
||||
export type ChatTranscriptProps = Readonly<{
|
||||
messages: readonly ChatMessage[];
|
||||
loading: boolean;
|
||||
streamingText: string;
|
||||
streamingActivity?: AgentActivityView;
|
||||
streamingThinking?: string;
|
||||
streamingSections?: readonly PublicThinkingSection[];
|
||||
streamingTimeline?: readonly ConsultationTimelineRow[];
|
||||
sessionId: string;
|
||||
sessionType: "consultation" | "birth_time_rectification";
|
||||
theme?: ConsultationDomain;
|
||||
messageFeedback: Readonly<Record<string, ChatMessageFeedback>>;
|
||||
copiedMessageKey: string | null;
|
||||
cancellationPending: boolean;
|
||||
productEntrypointsDisabled: boolean;
|
||||
actionsRef: MutableRefObject<ChatTranscriptActions>;
|
||||
}>;
|
||||
|
||||
const SettledMessageEntry = memo(function SettledMessageEntry({
|
||||
message,
|
||||
views,
|
||||
index,
|
||||
sessionId,
|
||||
sessionType,
|
||||
theme,
|
||||
messageFeedback,
|
||||
copiedMessageKey,
|
||||
loading,
|
||||
cancellationPending,
|
||||
productEntrypointsDisabled,
|
||||
actionsRef,
|
||||
}: Readonly<{
|
||||
message: ChatMessageView;
|
||||
views: readonly ChatMessageView[];
|
||||
index: number;
|
||||
sessionId: string;
|
||||
sessionType: "consultation" | "birth_time_rectification";
|
||||
theme?: ConsultationDomain;
|
||||
messageFeedback: Readonly<Record<string, ChatMessageFeedback>>;
|
||||
copiedMessageKey: string | null;
|
||||
loading: boolean;
|
||||
cancellationPending: boolean;
|
||||
productEntrypointsDisabled: boolean;
|
||||
actionsRef: MutableRefObject<ChatTranscriptActions>;
|
||||
}>) {
|
||||
noteSettledRowRender();
|
||||
const showActions = message.role === "assistant"
|
||||
&& message.state === "settled"
|
||||
&& Boolean(message.text);
|
||||
const feedbackKey = `${sessionId}:${message.renderKey}`;
|
||||
const latestRegeneratableKey = !loading && !cancellationPending
|
||||
? [...views].reverse().find((item) => (
|
||||
item.role === "assistant" && item.state === "settled" && Boolean(item.text)
|
||||
))?.renderKey
|
||||
: undefined;
|
||||
const previousQuestion = views[index - 1]?.role === "user" ? views[index - 1]?.text : "";
|
||||
const followUps = showActions
|
||||
&& message.renderKey === latestRegeneratableKey
|
||||
&& sessionType === "consultation"
|
||||
&& previousQuestion
|
||||
? deriveConsultationFollowUps({
|
||||
question: previousQuestion,
|
||||
answer: message.text,
|
||||
theme,
|
||||
entrypoint: isGeneralDailyFortuneQuestion(previousQuestion) ? "daily_starlanguage" : null,
|
||||
})
|
||||
: [];
|
||||
return (
|
||||
<div className="message-entry">
|
||||
<ChatMessageRow message={message} />
|
||||
{showActions && (
|
||||
<ChatMessageActions
|
||||
feedback={messageFeedback[feedbackKey]}
|
||||
copied={copiedMessageKey === feedbackKey}
|
||||
canRegenerate={message.renderKey === latestRegeneratableKey}
|
||||
onFeedback={(requested) => actionsRef.current.onFeedback(feedbackKey, requested)}
|
||||
onCopy={() => actionsRef.current.onCopy(feedbackKey, message.text)}
|
||||
onRegenerate={() => actionsRef.current.onRegenerate(message.renderKey)}
|
||||
/>
|
||||
)}
|
||||
<ConversationFollowUps
|
||||
questions={followUps}
|
||||
disabled={productEntrypointsDisabled}
|
||||
onSelect={(question) => actionsRef.current.onFollowUp(question)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const SettledMessageList = memo(function SettledMessageList({
|
||||
messages,
|
||||
sessionId,
|
||||
sessionType,
|
||||
theme,
|
||||
messageFeedback,
|
||||
copiedMessageKey,
|
||||
loading,
|
||||
cancellationPending,
|
||||
productEntrypointsDisabled,
|
||||
actionsRef,
|
||||
}: Omit<ChatTranscriptProps, "streamingText" | "streamingActivity" | "streamingThinking" | "streamingSections" | "streamingTimeline">) {
|
||||
noteSettledListRender();
|
||||
const views = settledChatMessageViews(messages);
|
||||
return (
|
||||
<>
|
||||
{views.map((message, index) => (
|
||||
<SettledMessageEntry
|
||||
key={message.renderKey}
|
||||
message={message}
|
||||
views={views}
|
||||
index={index}
|
||||
sessionId={sessionId}
|
||||
sessionType={sessionType}
|
||||
theme={theme}
|
||||
messageFeedback={messageFeedback}
|
||||
copiedMessageKey={copiedMessageKey}
|
||||
loading={loading}
|
||||
cancellationPending={cancellationPending}
|
||||
productEntrypointsDisabled={productEntrypointsDisabled}
|
||||
actionsRef={actionsRef}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export const StreamingMessageEntry = memo(function StreamingMessageEntry({
|
||||
message,
|
||||
}: Readonly<{ message: ChatMessageView }>) {
|
||||
noteStreamingRowRender();
|
||||
return (
|
||||
<div className="message-entry">
|
||||
<ChatMessageRow message={message} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const ChatTranscript = memo(function ChatTranscript({
|
||||
messages,
|
||||
loading,
|
||||
streamingText,
|
||||
streamingActivity,
|
||||
streamingThinking,
|
||||
streamingSections,
|
||||
streamingTimeline,
|
||||
sessionId,
|
||||
sessionType,
|
||||
theme,
|
||||
messageFeedback,
|
||||
copiedMessageKey,
|
||||
cancellationPending,
|
||||
productEntrypointsDisabled,
|
||||
actionsRef,
|
||||
}: ChatTranscriptProps) {
|
||||
const streamingMessage = streamingChatMessageView(
|
||||
messages,
|
||||
loading,
|
||||
streamingText,
|
||||
streamingActivity,
|
||||
streamingThinking,
|
||||
streamingSections,
|
||||
streamingTimeline,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettledMessageList
|
||||
messages={messages}
|
||||
loading={loading}
|
||||
sessionId={sessionId}
|
||||
sessionType={sessionType}
|
||||
theme={theme}
|
||||
messageFeedback={messageFeedback}
|
||||
copiedMessageKey={copiedMessageKey}
|
||||
cancellationPending={cancellationPending}
|
||||
productEntrypointsDisabled={productEntrypointsDisabled}
|
||||
actionsRef={actionsRef}
|
||||
/>
|
||||
{streamingMessage ? <StreamingMessageEntry message={streamingMessage} /> : null}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export function UnsplitChatTranscript({
|
||||
messages,
|
||||
loading,
|
||||
streamingText,
|
||||
streamingActivity,
|
||||
streamingThinking,
|
||||
streamingSections,
|
||||
streamingTimeline,
|
||||
sessionId,
|
||||
sessionType,
|
||||
theme,
|
||||
messageFeedback,
|
||||
copiedMessageKey,
|
||||
cancellationPending,
|
||||
productEntrypointsDisabled,
|
||||
actionsRef,
|
||||
}: ChatTranscriptProps) {
|
||||
noteUnsplitListRender();
|
||||
const views = chatMessageViews(
|
||||
messages,
|
||||
loading,
|
||||
streamingText,
|
||||
streamingActivity,
|
||||
streamingThinking,
|
||||
streamingSections,
|
||||
streamingTimeline,
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{views.map((message, index) => {
|
||||
noteSettledRowRender();
|
||||
if (message.state !== "settled") noteStreamingRowRender();
|
||||
const showActions = message.role === "assistant"
|
||||
&& message.state === "settled"
|
||||
&& Boolean(message.text);
|
||||
const feedbackKey = `${sessionId}:${message.renderKey}`;
|
||||
const latestRegeneratableKey = !loading && !cancellationPending
|
||||
? [...views].reverse().find((item) => (
|
||||
item.role === "assistant" && item.state === "settled" && Boolean(item.text)
|
||||
))?.renderKey
|
||||
: undefined;
|
||||
const previousQuestion = views[index - 1]?.role === "user" ? views[index - 1]?.text : "";
|
||||
const followUps = showActions
|
||||
&& message.renderKey === latestRegeneratableKey
|
||||
&& sessionType === "consultation"
|
||||
&& previousQuestion
|
||||
? deriveConsultationFollowUps({
|
||||
question: previousQuestion,
|
||||
answer: message.text,
|
||||
theme,
|
||||
entrypoint: isGeneralDailyFortuneQuestion(previousQuestion) ? "daily_starlanguage" : null,
|
||||
})
|
||||
: [];
|
||||
return (
|
||||
<div className="message-entry" key={message.renderKey}>
|
||||
<ChatMessageRow message={message} />
|
||||
{showActions && (
|
||||
<ChatMessageActions
|
||||
feedback={messageFeedback[feedbackKey]}
|
||||
copied={copiedMessageKey === feedbackKey}
|
||||
canRegenerate={message.renderKey === latestRegeneratableKey}
|
||||
onFeedback={(requested) => actionsRef.current.onFeedback(feedbackKey, requested)}
|
||||
onCopy={() => actionsRef.current.onCopy(feedbackKey, message.text)}
|
||||
onRegenerate={() => actionsRef.current.onRegenerate(message.renderKey)}
|
||||
/>
|
||||
)}
|
||||
<ConversationFollowUps
|
||||
questions={followUps}
|
||||
disabled={productEntrypointsDisabled}
|
||||
onSelect={(question) => actionsRef.current.onFollowUp(question)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { toggleChatMessageFeedback };
|
||||
Reference in New Issue
Block a user