Studio: fix edit/run UI and write worldbook on advance (R6)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -30,6 +30,7 @@ from services.studio_step_respond import (
|
|||||||
studio_step_respond,
|
studio_step_respond,
|
||||||
studio_step_respond_stream,
|
studio_step_respond_stream,
|
||||||
)
|
)
|
||||||
|
from models.converters import WorldBookConverter
|
||||||
from services.worldbook_service import worldbook_service
|
from services.worldbook_service import worldbook_service
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -278,6 +279,103 @@ class StudioRunService:
|
|||||||
}
|
}
|
||||||
return workflow_variables, last_draft
|
return workflow_variables, last_draft
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_keyword_field(raw: Any) -> List[str]:
|
||||||
|
if raw is None:
|
||||||
|
return []
|
||||||
|
if isinstance(raw, list):
|
||||||
|
return [str(x).strip() for x in raw if str(x).strip()]
|
||||||
|
text = str(raw).strip()
|
||||||
|
if not text:
|
||||||
|
return []
|
||||||
|
return [part.strip() for part in text.replace(",", ",").split(",") if part.strip()]
|
||||||
|
|
||||||
|
def _resolve_bound_worldbook_name(
|
||||||
|
self, project_id: str, run: StudioRun
|
||||||
|
) -> str:
|
||||||
|
for state in run.nodeStates:
|
||||||
|
if state.skillId != "studio.init_bind" or state.status != "completed":
|
||||||
|
continue
|
||||||
|
draft = state.lastDraft or {}
|
||||||
|
name = (draft.get("worldbookName") or "").strip()
|
||||||
|
if name:
|
||||||
|
return name
|
||||||
|
|
||||||
|
try:
|
||||||
|
project = studio_project_service.get_project(project_id)
|
||||||
|
worldbook_id = project.meta.worldbookId
|
||||||
|
except FileNotFoundError:
|
||||||
|
worldbook_id = None
|
||||||
|
|
||||||
|
if worldbook_id:
|
||||||
|
for summary in worldbook_service.list_worldbooks():
|
||||||
|
wb_name = summary.get("name")
|
||||||
|
if not wb_name:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
data = worldbook_service.get_worldbook(wb_name)
|
||||||
|
except FileNotFoundError:
|
||||||
|
continue
|
||||||
|
if data.get("id") == worldbook_id:
|
||||||
|
return wb_name
|
||||||
|
|
||||||
|
raise ValueError("未找到绑定的世界书,请先完成「创建并绑定」步骤")
|
||||||
|
|
||||||
|
def _build_worldbook_entry_payload(
|
||||||
|
self, node: StudioNode, draft: Dict[str, Any]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
insertion = (node.config or {}).get("insertion") or {}
|
||||||
|
content = (draft.get("entryContent") or "").strip()
|
||||||
|
if not content:
|
||||||
|
raise ValueError("当前步骤产物为空,请先生成世界书条目内容后再进入下一步")
|
||||||
|
|
||||||
|
activation = (insertion.get("activationType") or "permanent").strip()
|
||||||
|
position = insertion.get("position", 0)
|
||||||
|
if isinstance(position, str):
|
||||||
|
position = WorldBookConverter.POSITION_MAP_ST_TO_INTERNAL.get(position, 0)
|
||||||
|
|
||||||
|
key = self._parse_keyword_field(
|
||||||
|
draft.get("insertionKey") or insertion.get("key")
|
||||||
|
)
|
||||||
|
keysecondary = self._parse_keyword_field(insertion.get("keysecondary"))
|
||||||
|
comment = (
|
||||||
|
(draft.get("insertionComment") or insertion.get("comment") or "").strip()
|
||||||
|
or f"Studio · {node.displayName}"
|
||||||
|
)
|
||||||
|
|
||||||
|
payload: Dict[str, Any] = {
|
||||||
|
"content": content,
|
||||||
|
"comment": comment,
|
||||||
|
"activationType": activation,
|
||||||
|
"position": position,
|
||||||
|
"key": key,
|
||||||
|
"keysecondary": keysecondary,
|
||||||
|
"order": insertion.get("order", 100),
|
||||||
|
"depth": insertion.get("depth", 4),
|
||||||
|
"probability": insertion.get("probability", 100),
|
||||||
|
"group": insertion.get("group") or [],
|
||||||
|
"disable": bool(insertion.get("disable", False)),
|
||||||
|
}
|
||||||
|
if activation == "rag" and insertion.get("ragConfig"):
|
||||||
|
payload["ragConfig"] = insertion["ragConfig"]
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def _write_worldbook_entry_on_advance(
|
||||||
|
self,
|
||||||
|
worldbook_name: str,
|
||||||
|
node: StudioNode,
|
||||||
|
draft: Dict[str, Any],
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
entry_payload = self._build_worldbook_entry_payload(node, draft)
|
||||||
|
try:
|
||||||
|
return worldbook_service.append_entry(worldbook_name, entry_payload)
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise ValueError(f"世界书「{worldbook_name}」不存在,无法写入条目")
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError(f"写入世界书失败:{exc}") from exc
|
||||||
|
except OSError as exc:
|
||||||
|
raise ValueError(f"写入世界书失败:{exc}") from exc
|
||||||
|
|
||||||
def advance_run(
|
def advance_run(
|
||||||
self,
|
self,
|
||||||
project_id: str,
|
project_id: str,
|
||||||
@@ -314,6 +412,15 @@ class StudioRunService:
|
|||||||
elif current_node.skillId == "studio.worldbook_entry":
|
elif current_node.skillId == "studio.worldbook_entry":
|
||||||
if not current_state.lastDraft:
|
if not current_state.lastDraft:
|
||||||
raise ValueError("请先生成并确认当前产物后再进入下一步")
|
raise ValueError("请先生成并确认当前产物后再进入下一步")
|
||||||
|
worldbook_name = self._resolve_bound_worldbook_name(project_id, run)
|
||||||
|
written_entry = self._write_worldbook_entry_on_advance(
|
||||||
|
worldbook_name,
|
||||||
|
current_node,
|
||||||
|
current_state.lastDraft,
|
||||||
|
)
|
||||||
|
last_draft = copy.deepcopy(current_state.lastDraft)
|
||||||
|
last_draft["writtenEntryUid"] = written_entry.get("uid")
|
||||||
|
last_draft["writtenWorldbookName"] = worldbook_name
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
f"技能「{current_node.skillId}」的执行尚未实现(R2+)"
|
f"技能「{current_node.skillId}」的执行尚未实现(R2+)"
|
||||||
|
|||||||
@@ -232,6 +232,32 @@ class WorldBookService:
|
|||||||
|
|
||||||
raise FileNotFoundError(f"Entry '{uid}' not found in worldbook '{name}'")
|
raise FileNotFoundError(f"Entry '{uid}' not found in worldbook '{name}'")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def append_entry(name: str, entry_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
在世界书中追加条目(规范化后写入,与 Chat 侧条目格式一致)。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 世界书名称(文件名,不含 .json)
|
||||||
|
entry_data: 条目字段(content、comment、activationType、position 等)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
写入后的规范化条目
|
||||||
|
"""
|
||||||
|
data = WorldBookService._load_worldbook(name)
|
||||||
|
if not data:
|
||||||
|
raise FileNotFoundError(f"Worldbook '{name}' not found")
|
||||||
|
|
||||||
|
if not isinstance(data.get("entries"), list):
|
||||||
|
data["entries"] = []
|
||||||
|
|
||||||
|
normalized = WorldBookConverter.normalize_entry(entry_data)
|
||||||
|
data["entries"].append(normalized)
|
||||||
|
now = int(datetime.now().timestamp())
|
||||||
|
data["updatedAt"] = now
|
||||||
|
WorldBookService._save_worldbook(name, data)
|
||||||
|
return normalized
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_entry(name: str, entry_data: Dict[str, Any]) -> Dict[str, Any]:
|
def create_entry(name: str, entry_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -19,12 +19,11 @@
|
|||||||
--studio-top-control-h: 2.75rem;
|
--studio-top-control-h: 2.75rem;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(200px, 240px) minmax(280px, 1fr) minmax(160px, 220px);
|
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) auto;
|
grid-template-rows: var(--studio-top-label-h) var(--studio-top-control-h);
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
'label-project label-goal label-desc'
|
'label-project label-goal label-desc'
|
||||||
'ctrl-project ctrl-goal ctrl-desc'
|
'ctrl-project ctrl-goal ctrl-desc';
|
||||||
'actions . .';
|
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 6px var(--spacing-md);
|
gap: 6px var(--spacing-md);
|
||||||
padding: var(--spacing-md) var(--spacing-lg);
|
padding: var(--spacing-md) var(--spacing-lg);
|
||||||
@@ -40,7 +39,6 @@
|
|||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
'label-project label-project'
|
'label-project label-project'
|
||||||
'ctrl-project ctrl-project'
|
'ctrl-project ctrl-project'
|
||||||
'actions actions'
|
|
||||||
'label-goal label-goal'
|
'label-goal label-goal'
|
||||||
'ctrl-goal ctrl-goal'
|
'ctrl-goal ctrl-goal'
|
||||||
'label-desc label-desc'
|
'label-desc label-desc'
|
||||||
@@ -89,6 +87,10 @@
|
|||||||
|
|
||||||
.studio-edit-top__control--project {
|
.studio-edit-top__control--project {
|
||||||
grid-area: ctrl-project;
|
grid-area: ctrl-project;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.studio-edit-top__control--goal {
|
.studio-edit-top__control--goal {
|
||||||
@@ -100,20 +102,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.studio-edit-top__actions {
|
.studio-edit-top__actions {
|
||||||
grid-area: actions;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-xs);
|
gap: var(--spacing-xs);
|
||||||
padding-top: 2px;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.studio-edit-select--project {
|
.studio-edit-select--project {
|
||||||
width: 100%;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
min-height: var(--studio-top-control-h);
|
min-height: var(--studio-top-control-h);
|
||||||
box-sizing: border-box;
|
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 {
|
.studio-edit-textarea--goal {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: var(--studio-top-control-h);
|
min-height: var(--studio-top-control-h);
|
||||||
|
|||||||
@@ -286,6 +286,33 @@ function StudioEditPage() {
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</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>
|
||||||
<div className="studio-edit-top__control studio-edit-top__control--goal">
|
<div className="studio-edit-top__control studio-edit-top__control--goal">
|
||||||
<textarea
|
<textarea
|
||||||
@@ -313,33 +340,6 @@ function StudioEditPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</header>
|
||||||
{(error || saveMessage) && (
|
{(error || saveMessage) && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -50,39 +50,6 @@
|
|||||||
color: var(--color-text-primary);
|
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 {
|
.studio-run-chat-focus {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -90,38 +57,6 @@
|
|||||||
min-height: min(40vh, 320px);
|
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-thinking,
|
||||||
.studio-run-chat-summary {
|
.studio-run-chat-summary {
|
||||||
padding: var(--spacing-md) var(--spacing-lg);
|
padding: var(--spacing-md) var(--spacing-lg);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import MarkdownRenderer from '../shared/MarkdownRenderer';
|
import MarkdownRenderer from '../shared/MarkdownRenderer';
|
||||||
import { findLastUserMessageIndex } from './studioRunUtils';
|
|
||||||
|
|
||||||
import './StudioRunChat.css';
|
import './StudioRunChat.css';
|
||||||
|
|
||||||
@@ -31,7 +30,6 @@ function StudioRunChat({
|
|||||||
stepMessages = [],
|
stepMessages = [],
|
||||||
thinking,
|
thinking,
|
||||||
evaluation,
|
evaluation,
|
||||||
pendingUserText,
|
|
||||||
inputValue,
|
inputValue,
|
||||||
onInputChange,
|
onInputChange,
|
||||||
onSend,
|
onSend,
|
||||||
@@ -47,27 +45,16 @@ function StudioRunChat({
|
|||||||
const [showOptions, setShowOptions] = useState(false);
|
const [showOptions, setShowOptions] = useState(false);
|
||||||
const [renderMode, setRenderMode] = useState('markdown');
|
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 summaryText = evaluation || null;
|
||||||
|
|
||||||
const hasFocusContent =
|
const hasFocusContent = thinking || summaryText || sending;
|
||||||
thinking || summaryText || pendingUserText || sending || lastUserMessage;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
const anchor = focusAnchorRef.current;
|
const anchor = focusAnchorRef.current;
|
||||||
if (!container || !anchor) return;
|
if (!container || !anchor) return;
|
||||||
container.scrollTop = Math.max(0, anchor.offsetTop - container.offsetTop);
|
container.scrollTop = Math.max(0, anchor.offsetTop - container.offsetTop);
|
||||||
}, [stepMessages, thinking, evaluation, pendingUserText, sending]);
|
}, [stepMessages, thinking, evaluation, sending]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event) => {
|
const handleClickOutside = (event) => {
|
||||||
@@ -117,20 +104,6 @@ function StudioRunChat({
|
|||||||
return (
|
return (
|
||||||
<div className="studio-run-chat">
|
<div className="studio-run-chat">
|
||||||
<div className="studio-run-chat-messages" ref={containerRef}>
|
<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">
|
<div ref={focusAnchorRef} className="studio-run-chat-focus">
|
||||||
{!hasFocusContent ? (
|
{!hasFocusContent ? (
|
||||||
<div className="studio-run-chat-empty">
|
<div className="studio-run-chat-empty">
|
||||||
@@ -138,24 +111,6 @@ function StudioRunChat({
|
|||||||
</div>
|
</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 && (
|
{sending && !thinking && (
|
||||||
<div className="studio-run-chat-thinking studio-run-chat-thinking--pending">
|
<div className="studio-run-chat-thinking studio-run-chat-thinking--pending">
|
||||||
<span className="studio-run-chat-block-label">思考</span>
|
<span className="studio-run-chat-block-label">思考</span>
|
||||||
|
|||||||
@@ -515,7 +515,6 @@ function StudioRunPage() {
|
|||||||
const [chatInput, setChatInput] = useState('');
|
const [chatInput, setChatInput] = useState('');
|
||||||
const [streamOutput, setStreamOutput] = useState(false);
|
const [streamOutput, setStreamOutput] = useState(false);
|
||||||
const [toolOptionIndex, setToolOptionIndex] = useState(0);
|
const [toolOptionIndex, setToolOptionIndex] = useState(0);
|
||||||
const [pendingUserText, setPendingUserText] = useState(null);
|
|
||||||
const [runListExpanded, setRunListExpanded] = useState(false);
|
const [runListExpanded, setRunListExpanded] = useState(false);
|
||||||
const [renamingId, setRenamingId] = useState(null);
|
const [renamingId, setRenamingId] = useState(null);
|
||||||
const [renameValue, setRenameValue] = useState('');
|
const [renameValue, setRenameValue] = useState('');
|
||||||
@@ -534,7 +533,6 @@ function StudioRunPage() {
|
|||||||
setRunSessionEntered(false);
|
setRunSessionEntered(false);
|
||||||
setInsertionPopupOpen(false);
|
setInsertionPopupOpen(false);
|
||||||
setContextBlockPopup(null);
|
setContextBlockPopup(null);
|
||||||
setPendingUserText(null);
|
|
||||||
}, [currentRunId, runProjectId, setRunSessionEntered]);
|
}, [currentRunId, runProjectId, setRunSessionEntered]);
|
||||||
|
|
||||||
const activeNodeState = useMemo(() => {
|
const activeNodeState = useMemo(() => {
|
||||||
@@ -621,11 +619,9 @@ function StudioRunPage() {
|
|||||||
const handleChatSend = async (text) => {
|
const handleChatSend = async (text) => {
|
||||||
if (!text || isInitBindActive || !isWorldbookActive || runMessaging) return;
|
if (!text || isInitBindActive || !isWorldbookActive || runMessaging) return;
|
||||||
|
|
||||||
setPendingUserText(text);
|
|
||||||
setChatInput('');
|
setChatInput('');
|
||||||
|
|
||||||
const run = await sendRunMessage(text, { stream: streamOutput });
|
const run = await sendRunMessage(text, { stream: streamOutput });
|
||||||
setPendingUserText(null);
|
|
||||||
if (!run) {
|
if (!run) {
|
||||||
/* error surfaced via runError */
|
/* error surfaced via runError */
|
||||||
}
|
}
|
||||||
@@ -649,7 +645,6 @@ function StudioRunPage() {
|
|||||||
if (run) {
|
if (run) {
|
||||||
setChatInput('');
|
setChatInput('');
|
||||||
setToolOptionIndex(0);
|
setToolOptionIndex(0);
|
||||||
setPendingUserText(null);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -864,7 +859,6 @@ function StudioRunPage() {
|
|||||||
stepMessages={stepMessages}
|
stepMessages={stepMessages}
|
||||||
thinking={runStreamingThinking ?? lastToolResponse?.thinking}
|
thinking={runStreamingThinking ?? lastToolResponse?.thinking}
|
||||||
evaluation={runStreamingThinking ? null : lastToolResponse?.evaluation}
|
evaluation={runStreamingThinking ? null : lastToolResponse?.evaluation}
|
||||||
pendingUserText={pendingUserText}
|
|
||||||
inputValue={chatInput}
|
inputValue={chatInput}
|
||||||
onInputChange={setChatInput}
|
onInputChange={setChatInput}
|
||||||
onSend={handleChatSend}
|
onSend={handleChatSend}
|
||||||
|
|||||||
Reference in New Issue
Block a user