diff --git a/frontend/src/components/Studio/StudioBindingPreviewPopup.css b/frontend/src/components/Studio/StudioBindingPreviewPopup.css new file mode 100644 index 0000000..ef74577 --- /dev/null +++ b/frontend/src/components/Studio/StudioBindingPreviewPopup.css @@ -0,0 +1,165 @@ +.studio-binding-preview { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); + min-height: 200px; +} + +.studio-binding-preview__tabs { + display: flex; + gap: 4px; + padding-bottom: var(--spacing-xs); + border-bottom: 1px solid var(--color-border-light); +} + +.studio-binding-preview__tab { + flex: 1; + padding: 4px 8px; + border: 1px solid transparent; + border-radius: var(--radius-sm); + background: transparent; + color: var(--color-text-muted); + font-size: 0.72rem; + cursor: pointer; +} + +.studio-binding-preview__tab.is-active { + border-color: var(--color-border); + background: var(--color-bg-tertiary); + color: var(--color-text-primary); + font-weight: 600; +} + +.studio-binding-preview__panel { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); + max-height: min(50vh, 420px); + overflow: auto; +} + +.studio-binding-preview__status { + margin: 0; + font-size: 0.75rem; + color: var(--color-text-muted); +} + +.studio-binding-preview__status--error { + color: #f5576c; +} + +.studio-binding-preview__empty { + margin: 0; + font-size: 0.75rem; + color: var(--color-text-muted); +} + +.studio-binding-preview__hero { + display: flex; + gap: var(--spacing-sm); + align-items: center; +} + +.studio-binding-preview__avatar { + width: 48px; + height: 48px; + border-radius: var(--radius-sm); + object-fit: cover; + flex-shrink: 0; + border: 1px solid var(--color-border-light); +} + +.studio-binding-preview__avatar--placeholder { + display: flex; + align-items: center; + justify-content: center; + background: var(--color-bg-tertiary); + font-size: 1.2rem; +} + +.studio-binding-preview__name { + margin: 0; + font-size: 0.9rem; + color: var(--color-text-primary); +} + +.studio-binding-preview__meta { + margin: 2px 0 0; + font-size: 0.68rem; + color: var(--color-text-muted); +} + +.studio-binding-preview__field-label { + margin: 0 0 4px; + font-size: 0.68rem; + font-weight: 600; + color: var(--color-text-muted); +} + +.studio-binding-preview__field-value { + margin: 0; + padding: var(--spacing-xs); + border-radius: var(--radius-sm); + border: 1px solid var(--color-border-light); + background: var(--color-bg-tertiary); + white-space: pre-wrap; + font-size: 0.72rem; + line-height: 1.45; + max-height: 120px; + overflow: auto; +} + +.studio-binding-preview__entries { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 6px; +} + +.studio-binding-preview__entry { + border: 1px solid var(--color-border-light); + border-radius: var(--radius-sm); + overflow: hidden; +} + +.studio-binding-preview__entry-head { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; + padding: var(--spacing-xs) var(--spacing-sm); + border: none; + background: var(--color-bg-tertiary); + cursor: pointer; + text-align: left; +} + +.studio-binding-preview__entry-head:hover { + background: color-mix(in srgb, var(--color-accent) 8%, var(--color-bg-tertiary)); +} + +.studio-binding-preview__entry-title { + font-size: 0.78rem; + color: var(--color-text-primary); + font-weight: 500; +} + +.studio-binding-preview__entry-meta { + font-size: 0.65rem; + color: var(--color-text-muted); +} + +.studio-binding-preview__entry-content { + margin: 0; + padding: var(--spacing-sm); + border-top: 1px solid var(--color-border-light); + background: var(--color-bg-secondary); + white-space: pre-wrap; + font-size: 0.72rem; + line-height: 1.45; + max-height: 200px; + overflow: auto; +} diff --git a/frontend/src/components/Studio/StudioBindingPreviewPopup.jsx b/frontend/src/components/Studio/StudioBindingPreviewPopup.jsx new file mode 100644 index 0000000..31c707b --- /dev/null +++ b/frontend/src/components/Studio/StudioBindingPreviewPopup.jsx @@ -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 ( + + {() => ( +
+
+ + +
+ + {loading ? ( +

加载中…

+ ) : error ? ( +

+ {error} +

+ ) : tab === 'character' ? ( +
+ {!character ? ( +

暂无绑定角色卡

+ ) : ( + <> +
+ {character.avatar ? ( + + ) : ( +
+ 🎭 +
+ )} +
+

{character.name}

+ {worldbookName ? ( +

世界书:{worldbookName}

+ ) : null} +
+
+ {CHARACTER_FIELDS.map(({ key, label }) => { + const value = (character[key] || '').trim(); + if (!value) return null; + return ( +
+

{label}

+
{value}
+
+ ); + })} + + )} +
+ ) : ( +
+ {!worldbookName ? ( +

暂无绑定世界书

+ ) : entries.length === 0 ? ( +

世界书「{worldbookName}」暂无条目

+ ) : ( +
    + {entries.map((entry) => { + const expanded = expandedEntryUid === entry.uid; + const comment = entry.comment || entry.key?.join?.(', ') || '未命名条目'; + return ( +
  • + + {expanded ? ( +
    +                            {entry.content || '(空内容)'}
    +                          
    + ) : null} +
  • + ); + })} +
+ )} +
+ )} +
+ )} +
+ ); +} + +export default StudioBindingPreviewPopup; diff --git a/frontend/src/components/Studio/StudioInsertionPopup.css b/frontend/src/components/Studio/StudioInsertionPopup.css index 349cb07..ad49474 100644 --- a/frontend/src/components/Studio/StudioInsertionPopup.css +++ b/frontend/src/components/Studio/StudioInsertionPopup.css @@ -11,8 +11,8 @@ } .studio-insertion-popup.is-expanded { - width: min(420px, calc(100vw - 32px)); - max-height: min(70vh, 520px); + width: min(480px, calc(100vw - 32px)); + max-height: min(75vh, 560px); display: flex; flex-direction: column; } diff --git a/frontend/src/components/Studio/StudioRunChat.css b/frontend/src/components/Studio/StudioRunChat.css index 0e506d0..4bc8d0f 100644 --- a/frontend/src/components/Studio/StudioRunChat.css +++ b/frontend/src/components/Studio/StudioRunChat.css @@ -286,6 +286,14 @@ color: var(--color-text-primary); } +.studio-run-chat-option-unavailable { + display: block; + padding: var(--spacing-xs) 0; + font-size: 0.85rem; + color: var(--color-text-muted); + cursor: help; +} + .studio-run-chat-pending { color: var(--color-text-muted); font-style: italic; diff --git a/frontend/src/components/Studio/StudioRunChat.jsx b/frontend/src/components/Studio/StudioRunChat.jsx index 72e1d5a..96c4446 100644 --- a/frontend/src/components/Studio/StudioRunChat.jsx +++ b/frontend/src/components/Studio/StudioRunChat.jsx @@ -37,8 +37,8 @@ function StudioRunChat({ disabled = false, sending = false, canInterrupt = false, - streamOutput = false, - onStreamOutputChange, + streamOutput: _streamOutput = false, + onStreamOutputChange: _onStreamOutputChange, placeholder = '输入消息…', }) { const containerRef = useRef(null); @@ -186,15 +186,12 @@ function StudioRunChat({ {RENDER_MODE_LABELS[renderMode]}
功能
- + + 流式输出 · 暂不支持 + )} diff --git a/frontend/src/components/Studio/StudioRunNodeGraph.jsx b/frontend/src/components/Studio/StudioRunNodeGraph.jsx index 3a5b61f..d190d6c 100644 --- a/frontend/src/components/Studio/StudioRunNodeGraph.jsx +++ b/frontend/src/components/Studio/StudioRunNodeGraph.jsx @@ -76,7 +76,7 @@ function StudioRunNodeGraph({ if (nodeId === currentNodeId) return false; const state = stateMap[nodeId]; if (canSelectNode) return canSelectNode(state); - return state?.skillId === 'studio.worldbook_entry'; + return ['studio.worldbook_entry', 'studio.init_bind'].includes(state?.skillId); }, [onNodeSelect, currentNodeId, stateMap, canSelectNode] ); diff --git a/frontend/src/components/Studio/StudioRunPage.css b/frontend/src/components/Studio/StudioRunPage.css index 4a375c9..1182a6b 100644 --- a/frontend/src/components/Studio/StudioRunPage.css +++ b/frontend/src/components/Studio/StudioRunPage.css @@ -227,6 +227,11 @@ overflow: auto; } +.studio-run-left-session__graph .studio-run-graph { + min-height: 180px; + height: 200px; +} + .studio-run-center { min-height: 0; display: flex; @@ -789,6 +794,32 @@ background: color-mix(in srgb, var(--color-accent) 12%, var(--color-bg-secondary)); } +.studio-run-turn-btn--append { + border-color: rgba(16, 185, 129, 0.4); + color: #10b981; +} + +.studio-run-turn-btn--append.is-confirm-pending { + border-color: #10b981; + background: color-mix(in srgb, #10b981 12%, var(--color-bg-secondary)); + color: #10b981; +} + +.studio-run-turn-btn--overwrite { + border-color: rgba(245, 158, 11, 0.4); + color: #f59e0b; +} + +.studio-run-turn-btn--overwrite.is-confirm-pending { + border-color: #f59e0b; + background: color-mix(in srgb, #f59e0b 12%, var(--color-bg-secondary)); + color: #f59e0b; +} + +.studio-run-save-actions { + margin-top: var(--spacing-xs); +} + .studio-run-turn-btn.is-confirm-pending { border-color: #f5576c; background: color-mix(in srgb, #f5576c 12%, var(--color-bg-secondary));