-
- {messages.length === 0 ? (
-
暂无对话,在下方输入开始交流
- ) : (
- messages.map((msg) => (
-
-
{roleLabel(msg.role)}
-
- {renderMessageBody(msg.text, renderMode, msg.role === 'user')}
+
+ {historyMessages.length > 0 && (
+
+ {historyMessages.map((msg) => (
+
+
+ {msg.role === 'user' ? '你' : '助手'}
+
+ {msg.content}
-
- ))
+ ))}
+
)}
-
+
+
+ {!hasFocusContent ? (
+
+ 暂无本轮对话,在下方输入开始交流
+
+ ) : (
+ <>
+ {lastUserMessage && !pendingUserText && (
+
+ 你
+
+ {lastUserMessage.content}
+
+
+ )}
+
+ {pendingUserText && (
+
+ 你
+
+ {pendingUserText}
+
+
+ )}
+
+ {sending && !thinking && (
+
+ 思考
+ 正在等待模型回复…
+
+ )}
+
+ {thinking && (
+
+
思考
+
+ {renderBody(thinking, renderMode, false)}
+
+
+ )}
+
+ {summaryText && (
+
+
评价与修改建议
+
+ {renderBody(summaryText, renderMode, false)}
+
+
+ )}
+ >
+ )}
+
)}
diff --git a/frontend/src/components/Studio/StudioRunPage.css b/frontend/src/components/Studio/StudioRunPage.css
index d12b966..7fcd161 100644
--- a/frontend/src/components/Studio/StudioRunPage.css
+++ b/frontend/src/components/Studio/StudioRunPage.css
@@ -6,70 +6,119 @@
overflow: hidden;
}
-.studio-run-header {
+.studio-run-preview__meta {
display: flex;
- align-items: center;
- gap: var(--spacing-sm);
- padding: var(--spacing-md) var(--spacing-lg);
- border-bottom: 1px solid var(--color-border-light);
- background: var(--color-bg-secondary);
- flex-shrink: 0;
flex-wrap: wrap;
-}
-
-.studio-run-header-icon {
- font-size: 1.25rem;
-}
-
-.studio-run-header-title {
- margin: 0;
- font-size: 1.1rem;
- font-weight: 600;
- margin-right: auto;
-}
-
-.studio-run-toolbar {
- display: flex;
- align-items: center;
- gap: var(--spacing-md);
- flex-wrap: wrap;
-}
-
-.studio-run-label {
- display: flex;
- align-items: center;
- gap: var(--spacing-xs);
- font-size: 0.8rem;
+ gap: var(--spacing-sm) var(--spacing-md);
+ font-size: 0.75rem;
color: var(--color-text-muted);
}
-.studio-run-select {
- padding: var(--spacing-xs) var(--spacing-sm);
- border-radius: var(--radius-md);
+.studio-run-preview__character {
+ color: var(--color-text-secondary);
+}
+
+.studio-run-tool-option.is-selected {
+ border-color: var(--color-accent);
+ background: var(--color-accent-ultra-light);
+}
+
+.studio-run-tool-copy {
+ margin-top: var(--spacing-sm);
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-xs);
+}
+
+.studio-run-tool-copy__head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--spacing-sm);
+}
+
+.studio-run-tool-copy__label {
+ font-size: 0.72rem;
+ color: var(--color-text-muted);
+}
+
+.studio-run-tool-copy__btn {
+ padding: 2px var(--spacing-sm);
border: 1px solid var(--color-border);
+ border-radius: var(--radius-sm);
+ background: var(--color-bg-secondary);
+ color: var(--color-text-primary);
+ font-size: 0.72rem;
+ cursor: pointer;
+ font-family: inherit;
+}
+
+.studio-run-tool-copy__btn:hover {
+ border-color: var(--color-accent);
+ color: var(--color-accent);
+}
+
+.studio-run-tool-copy__textarea {
+ width: 100%;
+ padding: var(--spacing-sm);
+ border: 1px solid var(--color-border-light);
+ border-radius: var(--radius-sm);
+ background: var(--color-bg-primary);
+ color: var(--color-text-primary);
+ font-size: 0.78rem;
+ font-family: inherit;
+ line-height: 1.45;
+ resize: vertical;
+}
+
+.studio-run-tool-copy__hint {
+ font-size: 0.68rem;
+ color: var(--color-accent);
+}
+
+[data-color-theme='dark'] .studio-run-message.role-assistant,
+[data-color-theme='dark'] .studio-run-message.role-system {
+ background-color: rgba(45, 45, 48, 0.5);
+ color: var(--color-text-primary);
+}
+
+[data-color-theme='dark'] .studio-run-message.role-user {
+ background: rgba(212, 167, 106, 0.1);
+}
+
+[data-color-theme='dark'] .studio-run-icon-btn,
+[data-color-theme='dark'] .studio-run-tool-option {
background: var(--color-bg-tertiary);
color: var(--color-text-primary);
- font-size: 0.875rem;
- min-width: 160px;
+ border-color: var(--color-border);
}
-.studio-run-new-btn {
- padding: var(--spacing-xs) var(--spacing-md);
- border-radius: var(--radius-md);
- border: none;
- background: var(--color-accent);
- color: var(--color-text-inverse, #fff);
- font-size: 0.875rem;
- cursor: pointer;
+[data-color-theme='dark'] .studio-run-list-item {
+ background: var(--color-bg-tertiary);
}
-.studio-run-new-btn:hover:not(:disabled) {
- opacity: 0.9;
+[data-color-theme='dark'] .studio-run-list-action {
+ background: var(--color-bg-elevated);
+ color: var(--color-text-secondary);
}
-.studio-run-new-btn:disabled {
- opacity: 0.5;
- cursor: not-allowed;
+[data-color-theme='dark'] .studio-run-product-main__body,
+[data-color-theme='dark'] .studio-run-variables__row,
+[data-color-theme='dark'] .studio-run-guidance-form {
+ background: var(--color-bg-tertiary);
+ border-color: var(--color-border);
+}
+
+[data-color-theme='dark'] .studio-run-form-input,
+[data-color-theme='dark'] .studio-run-insertion-entry,
+[data-color-theme='dark'] .studio-run-tool-carousel {
+ background: var(--color-bg-elevated);
+ color: var(--color-text-primary);
+}
+
+[data-color-theme='dark'] .studio-run-banner.error {
+ background: rgba(239, 68, 68, 0.15);
+ color: #f87171;
}
.studio-run-banner {
@@ -290,11 +339,6 @@
white-space: nowrap;
}
-.studio-run-preview__meta {
- font-size: 0.75rem;
- color: var(--color-text-muted);
-}
-
.studio-run-preview__body {
flex: 1;
min-height: 0;
diff --git a/frontend/src/components/Studio/StudioRunPage.jsx b/frontend/src/components/Studio/StudioRunPage.jsx
index 804bc9e..f328255 100644
--- a/frontend/src/components/Studio/StudioRunPage.jsx
+++ b/frontend/src/components/Studio/StudioRunPage.jsx
@@ -6,6 +6,8 @@ import {
buildWorkflowVariableLabelMap,
getWorkflowVariableLabel,
} from './edit/workflowVariableLabels';
+import { resolveBoundCharacterName } from './studioRunUtils';
+import useCharacterStore from '../../Store/SideBarLeft/CharacterSlice';
import StudioContextBlockPopup from './StudioContextBlockPopup';
import StudioInsertionPopup from './StudioInsertionPopup';
@@ -118,10 +120,34 @@ function InitBindGuidanceForm({ node, runAdvancing, onSubmit, compact = false })
);
}
-function ToolOptionsCarousel({ options, index, onPrev, onNext, onSelect, runAdvancing }) {
+function ToolOptionsCarousel({ options, index, onPrev, onNext, runAdvancing }) {
const current = options[index];
+ const [selectedOption, setSelectedOption] = useState(null);
+ const [copyHint, setCopyHint] = useState('');
+
+ useEffect(() => {
+ setSelectedOption(null);
+ setCopyHint('');
+ }, [index, current?.question]);
+
if (!options.length || !current) return null;
+ const handleSelect = (opt) => {
+ setSelectedOption(opt);
+ setCopyHint('');
+ };
+
+ const handleCopy = async () => {
+ if (!selectedOption) return;
+ try {
+ await navigator.clipboard.writeText(selectedOption);
+ setCopyHint('已复制到剪贴板');
+ setTimeout(() => setCopyHint(''), 2000);
+ } catch {
+ setCopyHint('复制失败,请手动选择文本');
+ }
+ };
+
return (
@@ -146,8 +172,8 @@ function ToolOptionsCarousel({ options, index, onPrev, onNext, onSelect, runAdva
+ {selectedOption ? (
+
+
+ 复制以下内容到输入框
+
+
+
+ {copyHint ? (
+
{copyHint}
+ ) : null}
+
+ ) : null}
);
}
@@ -400,25 +450,25 @@ function InsertionSidebar({ insertionPreview, onOpenEntry }) {
function StudioRunPage() {
const {
- projects,
pipeline: projectPipeline,
workflowVariables: workflowVariablesCatalog,
+ meta,
runProjectId,
runs,
currentRunId,
currentRun,
runLoading,
- runCreating,
runAdvancing,
+ runMessaging,
+ runStreamingThinking,
runError,
+ runSessionEntered,
+ setRunSessionEntered,
initStudioRun,
fetchProject,
- fetchWorkflowVariables,
- setRunProjectId,
- fetchRuns,
- createRun,
selectRun,
advanceRun,
+ sendRunMessage,
deleteRun,
renameRun,
} = useStudioStore();
@@ -457,13 +507,13 @@ function StudioRunPage() {
() => buildWorkflowVariableLabelMap(workflowVariablesCatalog),
[workflowVariablesCatalog]
);
- const [sessionEntered, setSessionEntered] = useState(false);
const [insertionPopupOpen, setInsertionPopupOpen] = useState(false);
const [insertionPopupExpanded, setInsertionPopupExpanded] = useState(true);
const [contextBlockPopup, setContextBlockPopup] = useState(null);
const [chatInput, setChatInput] = useState('');
+ const [streamOutput, setStreamOutput] = useState(false);
const [toolOptionIndex, setToolOptionIndex] = useState(0);
- const [chatMessages, setChatMessages] = useState([]);
+ const [pendingUserText, setPendingUserText] = useState(null);
const [runListExpanded, setRunListExpanded] = useState(false);
const [renamingId, setRenamingId] = useState(null);
const [renameValue, setRenameValue] = useState('');
@@ -479,10 +529,11 @@ function StudioRunPage() {
}, [runProjectId, fetchProject]);
useEffect(() => {
- setSessionEntered(false);
+ setRunSessionEntered(false);
setInsertionPopupOpen(false);
setContextBlockPopup(null);
- }, [currentRunId, runProjectId]);
+ setPendingUserText(null);
+ }, [currentRunId, runProjectId, setRunSessionEntered]);
const activeNodeState = useMemo(() => {
if (!currentRun?.currentNodeId) return null;
@@ -531,71 +582,19 @@ function StudioRunPage() {
const promptBlocks = currentRun?.lastPromptBlocks || [];
const runWorkflowVariables = currentRun?.workflowVariables || {};
+ const stepMessages = activeNodeState?.stepMessages || [];
+ const lastToolResponse = activeNodeState?.lastToolResponse;
- useEffect(() => {
- if (!currentRun || !sessionEntered) {
- setChatMessages([]);
- return;
- }
-
- const msgs = [
- {
- id: 'sys-welcome',
- role: 'system',
- text: `已开始运行 · 项目 ${currentRun.projectId}`,
- },
- ];
-
- if (isInitBindActive) {
- msgs.push({
- id: 'sys-init',
- role: 'assistant',
- text: '请先创建角色卡与世界书,完成后将进入创作步骤。',
- });
- } else if (isWorldbookActive) {
- msgs.push({
- id: 'sys-step',
- role: 'assistant',
- text: `当前步骤:${activeNodeState.displayName}。请在下方输入修改意见或与模型对话(R3 接入流式输出)。`,
- });
- } else if (currentRun.status === 'completed') {
- msgs.push({
- id: 'sys-done',
- role: 'assistant',
- text: '本次运行已全部完成。',
- });
- }
-
- setChatMessages(msgs);
- }, [currentRun, currentRunId, isInitBindActive, isWorldbookActive, activeNodeState, sessionEntered]);
-
- const handleProjectChange = async (e) => {
- const projectId = e.target.value;
- setRunProjectId(projectId);
- setSessionEntered(false);
- await fetchWorkflowVariables(projectId);
- const list = await fetchRuns(projectId);
- if (list[0]?.id) {
- await selectRun(list[0].id);
- }
- };
-
- const handleNewRun = async () => {
- if (!runProjectId) return;
- setSessionEntered(false);
- await createRun(runProjectId);
- };
-
- const handleSelectRun = async (runId) => {
- setSessionEntered(false);
- await selectRun(runId);
- };
-
- const handleEnterSession = () => {
- if (currentRun) {
- setSessionEntered(true);
- }
- };
+ const characters = useCharacterStore((s) => s.characters);
+ const boundCharacterName = useMemo(
+ () =>
+ resolveBoundCharacterName({
+ boundCharacterVar: runWorkflowVariables['workflow.boundCharacter'],
+ characterId: meta?.characterId,
+ characters,
+ }),
+ [runWorkflowVariables, meta?.characterId, characters]
+ );
const handleOpenInsertion = () => {
setInsertionPopupExpanded(true);
@@ -606,28 +605,38 @@ function StudioRunPage() {
await advanceRun(displayParams);
};
- const handleChatSend = (text) => {
- if (!text || isInitBindActive) return;
- setChatMessages((prev) => [
- ...prev,
- { id: `user-${Date.now()}`, role: 'user', text },
- {
- id: `stub-${Date.now()}`,
- role: 'assistant',
- text: '(R3 占位:模型回复与流式输出尚未接入)',
- },
- ]);
+ const handleSelectRun = async (runId) => {
+ setRunSessionEntered(false);
+ await selectRun(runId);
+ };
+
+ const handleEnterSession = () => {
+ if (currentRun) {
+ setRunSessionEntered(true);
+ }
+ };
+
+ 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 */
+ }
};
const handleNextStep = () => {
- window.alert('(R3 占位:确认当前产物后将进入下一步)');
+ window.alert('(R5 占位:确认当前产物后将进入下一步)');
};
const handleDeleteRun = async (runId) => {
const name = runDisplayName(runs.find((r) => r.id === runId) || {});
if (!window.confirm(`确定删除运行「${name}」?此操作不可撤销。`)) return;
- if (currentRunId === runId) setSessionEntered(false);
+ if (currentRunId === runId) setRunSessionEntered(false);
await deleteRun(runId);
};
@@ -646,19 +655,8 @@ function StudioRunPage() {
}
};
- const handleToolSelect = (option) => {
- setChatMessages((prev) => [
- ...prev,
- {
- id: `tool-opt-${Date.now()}`,
- role: 'user',
- text: `[选项] ${option}`,
- },
- ]);
- };
-
const runListNeedsCollapse = runs.length > RUN_LIST_COLLAPSE_THRESHOLD;
- const inSession = sessionEntered && !!currentRun;
+ const inSession = runSessionEntered && !!currentRun;
const showToolCarousel = inSession && isWorldbookActive && toolQuestions.length > 0;
const showNextStep = inSession && isWorldbookActive;
@@ -778,6 +776,11 @@ function StudioRunPage() {