feat(studio): R3 step_respond、运行页 UX 与 R4 思考流式

新增 worldbook 步骤 LLM 回复(thinking/draft/questions/evaluation),
运行页聊天区与产物/选项联动;评价区标签改为「评价与修改建议」;
流式开关开启时 NDJSON 推送思考内容,完成后拆分更新各字段。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-31 22:07:42 +08:00
parent fa6907fb8d
commit f3792915a3
20 changed files with 1639 additions and 304 deletions

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import MarkdownRenderer from '../shared/MarkdownRenderer';
import { findLastUserMessageIndex } from './studioRunUtils';
import './StudioRunChat.css';
@@ -11,7 +12,8 @@ const RENDER_MODE_LABELS = {
markdown: '📝 Markdown',
};
function renderMessageBody(text, renderMode, isUser) {
function renderBody(text, renderMode, isUser) {
if (!text) return null;
if (renderMode === 'html' && !isUser) {
const hasHtmlTags = /<[a-z][\s\S]*>/i.test(text);
if (hasHtmlTags) {
@@ -26,23 +28,48 @@ function renderMessageBody(text, renderMode, isUser) {
}
function StudioRunChat({
messages,
stepMessages = [],
thinking,
evaluation,
pendingUserText,
inputValue,
onInputChange,
onSend,
disabled = false,
placeholder = '输入消息…',
sending = false,
streamOutput = false,
onStreamOutputChange,
placeholder = '输入消息…',
}) {
const messagesEndRef = useRef(null);
const messagesContainerRef = useRef(null);
const containerRef = useRef(null);
const focusAnchorRef = useRef(null);
const optionsRef = useRef(null);
const [showOptions, setShowOptions] = useState(false);
const [renderMode, setRenderMode] = useState('markdown');
const lastUserIdx = findLastUserMessageIndex(stepMessages);
const historyMessages =
lastUserIdx > 0 ? stepMessages.slice(0, lastUserIdx) : [];
const lastUserMessage =
lastUserIdx >= 0 ? stepMessages[lastUserIdx] : pendingUserText
? { role: 'user', content: pendingUserText }
: null;
const summaryText =
evaluation ||
(lastUserIdx >= 0 && stepMessages[lastUserIdx + 1]?.role === 'assistant'
? stepMessages[lastUserIdx + 1].content
: null);
const hasFocusContent =
thinking || summaryText || pendingUserText || sending || lastUserMessage;
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const container = containerRef.current;
const anchor = focusAnchorRef.current;
if (!container || !anchor) return;
container.scrollTop = Math.max(0, anchor.offsetTop - container.offsetTop);
}, [stepMessages, thinking, evaluation, pendingUserText, sending]);
useEffect(() => {
const handleClickOutside = (event) => {
@@ -89,28 +116,77 @@ function StudioRunChat({
setRenderMode(RENDER_MODES[(idx + 1) % RENDER_MODES.length]);
};
const roleLabel = (role) => {
if (role === 'user') return '你';
if (role === 'system') return '系统';
return '助手';
};
return (
<div className="studio-run-chat">
<div className="studio-run-chat-messages" ref={messagesContainerRef}>
{messages.length === 0 ? (
<div className="studio-run-chat-empty">暂无对话在下方输入开始交流</div>
) : (
messages.map((msg) => (
<div key={msg.id} className={`studio-run-chat-message role-${msg.role}`}>
<span className="studio-run-chat-message-role">{roleLabel(msg.role)}</span>
<div className="studio-run-chat-message-body">
{renderMessageBody(msg.text, renderMode, msg.role === 'user')}
<div className="studio-run-chat-messages" ref={containerRef}>
{historyMessages.length > 0 && (
<div className="studio-run-chat-history" aria-hidden="true">
{historyMessages.map((msg) => (
<div
key={msg.id}
className={`studio-run-chat-history-item role-${msg.role}`}
>
<span className="studio-run-chat-history-role">
{msg.role === 'user' ? '你' : '助手'}
</span>
<span className="studio-run-chat-history-text">{msg.content}</span>
</div>
</div>
))
))}
</div>
)}
<div ref={messagesEndRef} />
<div ref={focusAnchorRef} className="studio-run-chat-focus">
{!hasFocusContent ? (
<div className="studio-run-chat-empty">
暂无本轮对话在下方输入开始交流
</div>
) : (
<>
{lastUserMessage && !pendingUserText && (
<div className="studio-run-chat-last-user">
<span className="studio-run-chat-last-user-label"></span>
<span className="studio-run-chat-last-user-text">
{lastUserMessage.content}
</span>
</div>
)}
{pendingUserText && (
<div className="studio-run-chat-last-user studio-run-chat-last-user--pending">
<span className="studio-run-chat-last-user-label"></span>
<span className="studio-run-chat-last-user-text">
{pendingUserText}
</span>
</div>
)}
{sending && !thinking && (
<div className="studio-run-chat-thinking studio-run-chat-thinking--pending">
<span className="studio-run-chat-block-label">思考</span>
<span className="studio-run-chat-pending">正在等待模型回复</span>
</div>
)}
{thinking && (
<div className="studio-run-chat-thinking">
<span className="studio-run-chat-block-label">思考</span>
<div className="studio-run-chat-block-body">
{renderBody(thinking, renderMode, false)}
</div>
</div>
)}
{summaryText && (
<div className="studio-run-chat-summary">
<span className="studio-run-chat-block-label">评价与修改建议</span>
<div className="studio-run-chat-block-body">
{renderBody(summaryText, renderMode, false)}
</div>
</div>
)}
</>
)}
</div>
</div>
<form className="studio-run-chat-input-wrapper" onSubmit={handleSubmit}>
@@ -135,6 +211,16 @@ function StudioRunChat({
>
{RENDER_MODE_LABELS[renderMode]}
</button>
<div className="studio-run-chat-options-title">功能</div>
<label className="studio-run-chat-option-checkbox">
<input
type="checkbox"
checked={streamOutput}
onChange={(e) => onStreamOutputChange?.(e.target.checked)}
/>
<span className="studio-run-chat-checkmark" />
<span className="studio-run-chat-option-label">流式输出</span>
</label>
</div>
)}
</div>