201 lines
6.9 KiB
JavaScript
201 lines
6.9 KiB
JavaScript
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;
|