Files
SillyTavern_replica/frontend/src/components/Studio/StudioBindingPreviewPopup.jsx

201 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;