fix(*): 平台稳定性 - 修复会话隔离、运行时缓存与部署镜像更新链路
This commit is contained in:
@@ -100,6 +100,7 @@ export function AgentChatMessage({
|
||||
}) {
|
||||
const isUser = item.role === "user";
|
||||
const isSystem = item.role === "system";
|
||||
const displayedText = useTypewriterText(item.text, item.role === "assistant" && isStreaming);
|
||||
const color = item.role === "error" ? "#ef4444" : theme.node.text;
|
||||
if (item.reasoning) {
|
||||
return (
|
||||
@@ -183,8 +184,8 @@ export function AgentChatMessage({
|
||||
</span>
|
||||
) : null}
|
||||
{item.role === "assistant" ? (
|
||||
<AIMessageMarkdown className="text-left" isStreaming={isStreaming}>
|
||||
{item.text}
|
||||
<AIMessageMarkdown className="text-left" isStreaming={isStreaming} streamingAnimation="none">
|
||||
{displayedText}
|
||||
</AIMessageMarkdown>
|
||||
) : (
|
||||
<AgentMessageText text={item.text} references={references} />
|
||||
@@ -196,6 +197,71 @@ export function AgentChatMessage({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent SSE events contain text chunks. Keep the full text in the message
|
||||
* state, but reveal one code point at a time so a chunk never appears as a
|
||||
* whole block. The loop continues briefly after the stream ends to drain any
|
||||
* text that was buffered by the network.
|
||||
*/
|
||||
function useTypewriterText(targetText: string, shouldAnimate: boolean) {
|
||||
const targetRef = useRef(targetText);
|
||||
const visibleRef = useRef(shouldAnimate ? "" : targetText);
|
||||
const hasAnimatedRef = useRef(shouldAnimate);
|
||||
const runningRef = useRef(false);
|
||||
const timerRef = useRef<number | null>(null);
|
||||
const [visibleText, setVisibleText] = useState(visibleRef.current);
|
||||
|
||||
targetRef.current = targetText;
|
||||
|
||||
const startLoop = useCallback(() => {
|
||||
if (runningRef.current) return;
|
||||
runningRef.current = true;
|
||||
|
||||
const step = () => {
|
||||
const target = targetRef.current;
|
||||
const targetCharacters = Array.from(target);
|
||||
const visibleCharacters = Array.from(visibleRef.current);
|
||||
const visibleIsPrefix = target.startsWith(visibleRef.current);
|
||||
|
||||
if (!visibleIsPrefix || visibleCharacters.length > targetCharacters.length) {
|
||||
visibleRef.current = "";
|
||||
setVisibleText("");
|
||||
}
|
||||
|
||||
const currentCharacters = Array.from(visibleRef.current);
|
||||
if (currentCharacters.length >= targetCharacters.length) {
|
||||
runningRef.current = false;
|
||||
timerRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const nextText = targetCharacters.slice(0, currentCharacters.length + 1).join("");
|
||||
visibleRef.current = nextText;
|
||||
setVisibleText(nextText);
|
||||
timerRef.current = window.setTimeout(step, 16);
|
||||
};
|
||||
|
||||
step();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldAnimate) hasAnimatedRef.current = true;
|
||||
if (!hasAnimatedRef.current && !shouldAnimate) {
|
||||
visibleRef.current = targetText;
|
||||
setVisibleText(targetText);
|
||||
return;
|
||||
}
|
||||
startLoop();
|
||||
}, [shouldAnimate, startLoop, targetText]);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (timerRef.current !== null) window.clearTimeout(timerRef.current);
|
||||
runningRef.current = false;
|
||||
}, []);
|
||||
|
||||
return visibleText;
|
||||
}
|
||||
|
||||
function AgentMessageText({ text, references }: { text: string; references: CanvasResourceReference[] }) {
|
||||
const parts = splitAgentMessageMentions(text, references);
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user