Studio workflow engine complete (R0-R6, run page, node switch, binding editor); R4 streaming deferred; R7 unchanged.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
200
frontend/src/components/Studio/StudioBindingPreviewPopup.jsx
Normal file
200
frontend/src/components/Studio/StudioBindingPreviewPopup.jsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import StudioInsertionPopup from './StudioInsertionPopup';
|
||||
|
||||
import './StudioBindingPreviewPopup.css';
|
||||
|
||||
const CHARACTER_FIELDS = [
|
||||
{ key: 'description', label: '描述' },
|
||||
{ key: 'personality', label: '性格' },
|
||||
{ key: 'scenario', label: '场景' },
|
||||
{ key: 'first_mes', label: '开场白' },
|
||||
{ key: 'mes_example', label: '对话示例' },
|
||||
];
|
||||
|
||||
function StudioBindingPreviewPopup({
|
||||
open,
|
||||
characterName,
|
||||
worldbookName,
|
||||
onClose,
|
||||
}) {
|
||||
const [tab, setTab] = useState('character');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [character, setCharacter] = useState(null);
|
||||
const [entries, setEntries] = useState([]);
|
||||
const [expandedEntryUid, setExpandedEntryUid] = useState(null);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
if (!characterName && !worldbookName) return;
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const tasks = [];
|
||||
|
||||
if (characterName) {
|
||||
tasks.push(
|
||||
fetch(`/api/characters/${encodeURIComponent(characterName)}`)
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw new Error('加载角色卡失败');
|
||||
return res.json();
|
||||
})
|
||||
.then(setCharacter)
|
||||
);
|
||||
} else {
|
||||
setCharacter(null);
|
||||
}
|
||||
|
||||
if (worldbookName) {
|
||||
tasks.push(
|
||||
fetch(
|
||||
`/api/worldbooks/${encodeURIComponent(worldbookName)}/entries?page=1&page_size=50`
|
||||
)
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw new Error('加载世界书条目失败');
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => setEntries(data.entries || []))
|
||||
);
|
||||
} else {
|
||||
setEntries([]);
|
||||
}
|
||||
|
||||
await Promise.all(tasks);
|
||||
} catch (e) {
|
||||
setError(e.message || '加载绑定资源失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [characterName, worldbookName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setTab('character');
|
||||
setExpandedEntryUid(null);
|
||||
loadData();
|
||||
}
|
||||
}, [open, loadData]);
|
||||
|
||||
const title = characterName ? `绑定预览 · ${characterName}` : '绑定预览';
|
||||
|
||||
return (
|
||||
<StudioInsertionPopup
|
||||
open={open}
|
||||
title={title}
|
||||
defaultExpanded
|
||||
onClose={onClose}
|
||||
>
|
||||
{() => (
|
||||
<div className="studio-binding-preview">
|
||||
<div className="studio-binding-preview__tabs" role="tablist">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={tab === 'character'}
|
||||
className={`studio-binding-preview__tab${tab === 'character' ? ' is-active' : ''}`}
|
||||
onClick={() => setTab('character')}
|
||||
>
|
||||
角色卡
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={tab === 'worldbook'}
|
||||
className={`studio-binding-preview__tab${tab === 'worldbook' ? ' is-active' : ''}`}
|
||||
onClick={() => setTab('worldbook')}
|
||||
>
|
||||
世界书
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p className="studio-binding-preview__status">加载中…</p>
|
||||
) : error ? (
|
||||
<p className="studio-binding-preview__status studio-binding-preview__status--error">
|
||||
{error}
|
||||
</p>
|
||||
) : tab === 'character' ? (
|
||||
<div className="studio-binding-preview__panel">
|
||||
{!character ? (
|
||||
<p className="studio-binding-preview__empty">暂无绑定角色卡</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="studio-binding-preview__hero">
|
||||
{character.avatar ? (
|
||||
<img
|
||||
src={character.avatar}
|
||||
alt=""
|
||||
className="studio-binding-preview__avatar"
|
||||
/>
|
||||
) : (
|
||||
<div className="studio-binding-preview__avatar studio-binding-preview__avatar--placeholder">
|
||||
🎭
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="studio-binding-preview__name">{character.name}</h3>
|
||||
{worldbookName ? (
|
||||
<p className="studio-binding-preview__meta">世界书:{worldbookName}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{CHARACTER_FIELDS.map(({ key, label }) => {
|
||||
const value = (character[key] || '').trim();
|
||||
if (!value) return null;
|
||||
return (
|
||||
<section key={key} className="studio-binding-preview__field">
|
||||
<h4 className="studio-binding-preview__field-label">{label}</h4>
|
||||
<pre className="studio-binding-preview__field-value">{value}</pre>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="studio-binding-preview__panel">
|
||||
{!worldbookName ? (
|
||||
<p className="studio-binding-preview__empty">暂无绑定世界书</p>
|
||||
) : entries.length === 0 ? (
|
||||
<p className="studio-binding-preview__empty">世界书「{worldbookName}」暂无条目</p>
|
||||
) : (
|
||||
<ul className="studio-binding-preview__entries">
|
||||
{entries.map((entry) => {
|
||||
const expanded = expandedEntryUid === entry.uid;
|
||||
const comment = entry.comment || entry.key?.join?.(', ') || '未命名条目';
|
||||
return (
|
||||
<li key={entry.uid} className="studio-binding-preview__entry">
|
||||
<button
|
||||
type="button"
|
||||
className="studio-binding-preview__entry-head"
|
||||
onClick={() =>
|
||||
setExpandedEntryUid(expanded ? null : entry.uid)
|
||||
}
|
||||
>
|
||||
<span className="studio-binding-preview__entry-title">
|
||||
{comment}
|
||||
</span>
|
||||
<span className="studio-binding-preview__entry-meta">
|
||||
{entry.activationType || '—'} · order {entry.order ?? 0}
|
||||
</span>
|
||||
</button>
|
||||
{expanded ? (
|
||||
<pre className="studio-binding-preview__entry-content">
|
||||
{entry.content || '(空内容)'}
|
||||
</pre>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</StudioInsertionPopup>
|
||||
);
|
||||
}
|
||||
|
||||
export default StudioBindingPreviewPopup;
|
||||
Reference in New Issue
Block a user