Studio: fix edit/run UI and write worldbook on advance (R6)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -19,12 +19,11 @@
|
||||
--studio-top-control-h: 2.75rem;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: minmax(200px, 240px) minmax(280px, 1fr) minmax(160px, 220px);
|
||||
grid-template-rows: var(--studio-top-label-h) var(--studio-top-control-h) auto;
|
||||
grid-template-columns: minmax(200px, 1fr) minmax(280px, 1.4fr) minmax(160px, 220px);
|
||||
grid-template-rows: var(--studio-top-label-h) var(--studio-top-control-h);
|
||||
grid-template-areas:
|
||||
'label-project label-goal label-desc'
|
||||
'ctrl-project ctrl-goal ctrl-desc'
|
||||
'actions . .';
|
||||
'ctrl-project ctrl-goal ctrl-desc';
|
||||
align-items: stretch;
|
||||
gap: 6px var(--spacing-md);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
@@ -40,7 +39,6 @@
|
||||
grid-template-areas:
|
||||
'label-project label-project'
|
||||
'ctrl-project ctrl-project'
|
||||
'actions actions'
|
||||
'label-goal label-goal'
|
||||
'ctrl-goal ctrl-goal'
|
||||
'label-desc label-desc'
|
||||
@@ -89,6 +87,10 @@
|
||||
|
||||
.studio-edit-top__control--project {
|
||||
grid-area: ctrl-project;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-xs);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.studio-edit-top__control--goal {
|
||||
@@ -100,20 +102,32 @@
|
||||
}
|
||||
|
||||
.studio-edit-top__actions {
|
||||
grid-area: actions;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
padding-top: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.studio-edit-select--project {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: var(--studio-top-control-h);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.studio-edit-top__control--project {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.studio-edit-top__actions {
|
||||
flex: 1 1 100%;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.studio-edit-textarea--goal {
|
||||
width: 100%;
|
||||
min-height: var(--studio-top-control-h);
|
||||
|
||||
@@ -286,6 +286,33 @@ function StudioEditPage() {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="studio-edit-top__actions">
|
||||
<button
|
||||
type="button"
|
||||
className="studio-edit-btn studio-edit-btn-danger studio-edit-btn-sm"
|
||||
onClick={handleDeleteProject}
|
||||
disabled={!currentProjectId || loading || saving}
|
||||
title="删除当前项目及其运行记录"
|
||||
>
|
||||
删除项目
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="studio-edit-btn studio-edit-btn-sm"
|
||||
onClick={openRenameProjectModal}
|
||||
disabled={!currentProjectId || loading || saving}
|
||||
>
|
||||
改名
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="studio-edit-btn studio-edit-btn-sm"
|
||||
onClick={openNewProjectModal}
|
||||
disabled={loading}
|
||||
>
|
||||
新建项目
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="studio-edit-top__control studio-edit-top__control--goal">
|
||||
<textarea
|
||||
@@ -313,33 +340,6 @@ function StudioEditPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="studio-edit-top__actions">
|
||||
<button
|
||||
type="button"
|
||||
className="studio-edit-btn studio-edit-btn-danger studio-edit-btn-sm"
|
||||
onClick={handleDeleteProject}
|
||||
disabled={!currentProjectId || loading || saving}
|
||||
title="删除当前项目及其运行记录"
|
||||
>
|
||||
删除项目
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="studio-edit-btn studio-edit-btn-sm"
|
||||
onClick={openRenameProjectModal}
|
||||
disabled={!currentProjectId || loading || saving}
|
||||
>
|
||||
改名
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="studio-edit-btn studio-edit-btn-sm"
|
||||
onClick={openNewProjectModal}
|
||||
disabled={loading}
|
||||
>
|
||||
新建项目
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
{(error || saveMessage) && (
|
||||
<div
|
||||
|
||||
@@ -50,39 +50,6 @@
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.studio-run-chat-history {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin-bottom: var(--spacing-md);
|
||||
opacity: 0.35;
|
||||
max-height: 120px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.studio-run-chat-history-item {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.35;
|
||||
color: var(--color-text-muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.studio-run-chat-history-role {
|
||||
flex-shrink: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.studio-run-chat-history-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.studio-run-chat-focus {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -90,38 +57,6 @@
|
||||
min-height: min(40vh, 320px);
|
||||
}
|
||||
|
||||
.studio-run-chat-last-user {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
align-items: flex-start;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(102, 126, 234, 0.08);
|
||||
border-left: 3px solid var(--color-accent);
|
||||
}
|
||||
|
||||
[data-color-theme='dark'] .studio-run-chat-last-user {
|
||||
background: rgba(212, 167, 106, 0.1);
|
||||
}
|
||||
|
||||
.studio-run-chat-last-user--pending {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.studio-run-chat-last-user-label {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.studio-run-chat-last-user-text {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-primary);
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.studio-run-chat-thinking,
|
||||
.studio-run-chat-summary {
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import MarkdownRenderer from '../shared/MarkdownRenderer';
|
||||
import { findLastUserMessageIndex } from './studioRunUtils';
|
||||
|
||||
import './StudioRunChat.css';
|
||||
|
||||
@@ -31,7 +30,6 @@ function StudioRunChat({
|
||||
stepMessages = [],
|
||||
thinking,
|
||||
evaluation,
|
||||
pendingUserText,
|
||||
inputValue,
|
||||
onInputChange,
|
||||
onSend,
|
||||
@@ -47,27 +45,16 @@ function StudioRunChat({
|
||||
const [showOptions, setShowOptions] = useState(false);
|
||||
const [renderMode, setRenderMode] = useState('markdown');
|
||||
|
||||
const lastUserIdx = findLastUserMessageIndex(stepMessages);
|
||||
const historyMessages =
|
||||
lastUserIdx > 0
|
||||
? stepMessages.slice(0, lastUserIdx).filter((msg) => msg.role === 'user')
|
||||
: [];
|
||||
const lastUserMessage =
|
||||
lastUserIdx >= 0 ? stepMessages[lastUserIdx] : pendingUserText
|
||||
? { role: 'user', content: pendingUserText }
|
||||
: null;
|
||||
|
||||
const summaryText = evaluation || null;
|
||||
|
||||
const hasFocusContent =
|
||||
thinking || summaryText || pendingUserText || sending || lastUserMessage;
|
||||
const hasFocusContent = thinking || summaryText || sending;
|
||||
|
||||
useEffect(() => {
|
||||
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]);
|
||||
}, [stepMessages, thinking, evaluation, sending]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
@@ -117,20 +104,6 @@ function StudioRunChat({
|
||||
return (
|
||||
<div className="studio-run-chat">
|
||||
<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-user"
|
||||
>
|
||||
<span className="studio-run-chat-history-role">你</span>
|
||||
<span className="studio-run-chat-history-text">{msg.content}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={focusAnchorRef} className="studio-run-chat-focus">
|
||||
{!hasFocusContent ? (
|
||||
<div className="studio-run-chat-empty">
|
||||
@@ -138,24 +111,6 @@ function StudioRunChat({
|
||||
</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>
|
||||
|
||||
@@ -515,7 +515,6 @@ function StudioRunPage() {
|
||||
const [chatInput, setChatInput] = useState('');
|
||||
const [streamOutput, setStreamOutput] = useState(false);
|
||||
const [toolOptionIndex, setToolOptionIndex] = useState(0);
|
||||
const [pendingUserText, setPendingUserText] = useState(null);
|
||||
const [runListExpanded, setRunListExpanded] = useState(false);
|
||||
const [renamingId, setRenamingId] = useState(null);
|
||||
const [renameValue, setRenameValue] = useState('');
|
||||
@@ -534,7 +533,6 @@ function StudioRunPage() {
|
||||
setRunSessionEntered(false);
|
||||
setInsertionPopupOpen(false);
|
||||
setContextBlockPopup(null);
|
||||
setPendingUserText(null);
|
||||
}, [currentRunId, runProjectId, setRunSessionEntered]);
|
||||
|
||||
const activeNodeState = useMemo(() => {
|
||||
@@ -621,11 +619,9 @@ function StudioRunPage() {
|
||||
const handleChatSend = async (text) => {
|
||||
if (!text || isInitBindActive || !isWorldbookActive || runMessaging) return;
|
||||
|
||||
setPendingUserText(text);
|
||||
setChatInput('');
|
||||
|
||||
const run = await sendRunMessage(text, { stream: streamOutput });
|
||||
setPendingUserText(null);
|
||||
if (!run) {
|
||||
/* error surfaced via runError */
|
||||
}
|
||||
@@ -649,7 +645,6 @@ function StudioRunPage() {
|
||||
if (run) {
|
||||
setChatInput('');
|
||||
setToolOptionIndex(0);
|
||||
setPendingUserText(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -864,7 +859,6 @@ function StudioRunPage() {
|
||||
stepMessages={stepMessages}
|
||||
thinking={runStreamingThinking ?? lastToolResponse?.thinking}
|
||||
evaluation={runStreamingThinking ? null : lastToolResponse?.evaluation}
|
||||
pendingUserText={pendingUserText}
|
||||
inputValue={chatInput}
|
||||
onInputChange={setChatInput}
|
||||
onSend={handleChatSend}
|
||||
|
||||
Reference in New Issue
Block a user