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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user