dsh-muv-table 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -20
- package/LICENSE +85 -85
- package/LICENSE.md +9 -0
- package/README.md +59 -59
- package/cordis.patch.yml +5 -5
- package/lib/block-generator.js +56 -56
- package/lib/client.js +419 -419
- package/lib/initvar-parser.js +100 -100
- package/lib/muv-parser.js +121 -121
- package/package.json +21 -20
package/lib/client.js
CHANGED
|
@@ -1,420 +1,420 @@
|
|
|
1
|
-
// dsh-muv-table client v2: card-based tree view with modern DSH design
|
|
2
|
-
window.__ModuleLoader__.load({
|
|
3
|
-
id: 'dsh-muv-table',
|
|
4
|
-
factory: (require) => {
|
|
5
|
-
var module = { exports: {} }
|
|
6
|
-
var exports = module.exports
|
|
7
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
8
|
-
const React = require('react'), jsx = require('react/jsx-runtime'), ReactDOM = require('react-dom/client')
|
|
9
|
-
|
|
10
|
-
const ICON = `<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"><rect x="1.5" y="3" width="13" height="10" rx="1.5"/><path d="M1.5 6.5h13M6 6.5v6.5M11 6.5v6.5"/></svg>`
|
|
11
|
-
|
|
12
|
-
// ============ persistence ============
|
|
13
|
-
const SK = 'dsh-muv-table-state'
|
|
14
|
-
const loadSS = () => { try { const s=localStorage.getItem(SK); return s?JSON.parse(s):null } catch(_){return null} }
|
|
15
|
-
const saveSS = (s) => { try { localStorage.setItem(SK,JSON.stringify({...s,savedAt:Date.now()})) } catch(_){} }
|
|
16
|
-
let savedState = loadSS()
|
|
17
|
-
// ============ debounce utility ============
|
|
18
|
-
function debounce(fn, delay) {
|
|
19
|
-
let timer = null
|
|
20
|
-
return function (...args) {
|
|
21
|
-
if (timer) clearTimeout(timer)
|
|
22
|
-
timer = setTimeout(() => { timer = null; fn.apply(this, args) }, delay)
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// ============ color tokens ============
|
|
27
|
-
const C = {
|
|
28
|
-
bg: 'var(--dsw-alias-bg-base)', bg1: 'var(--dsw-alias-bg-layer-1)', bg2: 'var(--dsw-alias-bg-layer-2)', bg3: 'var(--dsw-alias-bg-layer-3)',
|
|
29
|
-
border: 'var(--dsw-alias-border-l2)', borderL: 'var(--dsw-alias-border-l1)',
|
|
30
|
-
text: 'var(--dsw-alias-label-primary)', sub: 'var(--dsw-alias-label-secondary)', dim: 'var(--dsw-alias-label-tertiary)',
|
|
31
|
-
accent: 'var(--dsw-alias-label-accent)', gold: '
|
|
32
|
-
hover: 'var(--dsw-specific-sidebar-nav-item-hover)',
|
|
33
|
-
font: 'var(--dsw-font-family)'
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ============ MacroPanel ============
|
|
37
|
-
function MacroPanel({ onClose }) {
|
|
38
|
-
const [input, setInput] = React.useState('')
|
|
39
|
-
const [result, setResult] = React.useState('')
|
|
40
|
-
const [picks, setPicks] = React.useState([])
|
|
41
|
-
const [diceResult, setDiceResult] = React.useState('')
|
|
42
|
-
|
|
43
|
-
const refreshPicks = () => {
|
|
44
|
-
if (typeof window._tavernListPicks === 'function') {
|
|
45
|
-
setPicks(window._tavernListPicks())
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
React.useEffect(() => { refreshPicks() }, [])
|
|
49
|
-
|
|
50
|
-
const handleExpand = () => {
|
|
51
|
-
if (typeof window._tavernExpandMacros === 'function') {
|
|
52
|
-
setResult(window._tavernExpandMacros(input))
|
|
53
|
-
} else {
|
|
54
|
-
setResult('⚠ muv-engine 插件未加载')
|
|
55
|
-
}
|
|
56
|
-
refreshPicks()
|
|
57
|
-
}
|
|
58
|
-
const handleReroll = (key) => {
|
|
59
|
-
if (typeof window._tavernRerollPick === 'function') {
|
|
60
|
-
window._tavernRerollPick(key)
|
|
61
|
-
refreshPicks()
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
const handleClearAll = () => {
|
|
65
|
-
picks.forEach(p => { if (typeof window._tavernRerollPick === 'function') window._tavernRerollPick(p.key) })
|
|
66
|
-
refreshPicks()
|
|
67
|
-
}
|
|
68
|
-
const quickDice = (expr) => {
|
|
69
|
-
if (typeof window._tavernExpandMacros === 'function') {
|
|
70
|
-
setDiceResult(window._tavernExpandMacros('{[roll::'+expr+']}'))
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const DICE = ['d4','d6','d8','d10','d12','d20','2d6','3d6','1d100']
|
|
75
|
-
|
|
76
|
-
return jsx.jsxs('div', { style:{position:'fixed',inset:0,zIndex:70,background:'rgba(0,0,0,0.5)',display:'flex',alignItems:'center',justifyContent:'center'}, onClick:onClose, children: [
|
|
77
|
-
jsx.jsxs('div', { onClick:e=>e.stopPropagation(), style:{width:'min(90vw,520px)',background:C.bg,display:'flex',flexDirection:'column',color:C.text,fontFamily:C.font,fontSize:13,borderRadius:12,border:'1px solid '+C.border,boxShadow:'0 20px 60px rgba(0,0,0,0.5)',overflow:'hidden'}, children: [
|
|
78
|
-
// Header
|
|
79
|
-
jsx.jsxs('div', { style:{display:'flex',alignItems:'center',gap:10,padding:'10px 16px',borderBottom:'1px solid '+C.border,flexShrink:0,background:C.bg1}, children: [
|
|
80
|
-
jsx.jsx('span', { style:{fontWeight:700,fontSize:15,flex:1}, children: '📐 宏测试' }),
|
|
81
|
-
jsx.jsx('button', { onClick:onClose, style:{padding:'5px 12px',borderRadius:6,border:'1px solid '+C.border,background:C.bg3,color:C.text,cursor:'pointer',fontSize:12,fontFamily:'inherit'}, children: '✕' })
|
|
82
|
-
] }),
|
|
83
|
-
// Body
|
|
84
|
-
jsx.jsxs('div', { style:{padding:16,display:'flex',flexDirection:'column',gap:14,overflow:'auto'}, children: [
|
|
85
|
-
// Input
|
|
86
|
-
jsx.jsxs('div', { children: [
|
|
87
|
-
jsx.jsx('div', { style:{fontSize:11,color:C.dim,marginBottom:6}, children: '输入宏语法(支持 random / pick / roll)' }),
|
|
88
|
-
jsx.jsx('textarea', { value:input, onChange:e=>setInput(e.target.value), placeholder:'{[random::选项A::选项B::选项C]}\n{[pick::宝箱::灵石x50::破旧剑谱::神秘丹药]}\n{[roll::2d6+3]}', rows:4, style:{width:'100%',padding:10,borderRadius:8,border:'1px solid '+C.border,background:C.bg3,color:C.text,fontSize:13,fontFamily:'monospace',resize:'vertical',outline:'none',boxSizing:'border-box'} }),
|
|
89
|
-
jsx.jsx('button', { onClick:handleExpand, style:{marginTop:8,padding:'8px 20px',borderRadius:8,border:'none',background:'linear-gradient(135deg,#c5a065,#d4a76a)',color:'#1a1a1a',cursor:'pointer',fontSize:13,fontWeight:700,fontFamily:'inherit',width:'100%'}, children: '⚡ 展开测试' })
|
|
90
|
-
] }),
|
|
91
|
-
// Result
|
|
92
|
-
result && jsx.jsxs('div', { children: [
|
|
93
|
-
jsx.jsxs('div', { style:{display:'flex',alignItems:'center',gap:8,marginBottom:4}, children: [
|
|
94
|
-
jsx.jsx('div', { style:{fontSize:11,color:C.dim,flex:1}, children: '展开结果' }),
|
|
95
|
-
jsx.jsx('button', { onClick:()=>navigator.clipboard.writeText(result), style:{padding:'2px 8px',borderRadius:4,border:'1px solid '+C.border,background:C.bg3,color:C.sub,cursor:'pointer',fontSize:10,fontFamily:'inherit'}, children: '📋 复制' }),
|
|
96
|
-
jsx.jsx('button', { onClick:()=>setInput(result), style:{padding:'2px 8px',borderRadius:4,border:'1px solid '+C.border,background:C.bg3,color:C.sub,cursor:'pointer',fontSize:10,fontFamily:'inherit'}, children: '⬆ 填入' })
|
|
97
|
-
] }),
|
|
98
|
-
jsx.jsx('div', { style:{padding:10,borderRadius:8,border:'1px solid '+C.accent,background:'rgba(74,191,171,0.06)',color:C.text,fontSize:13,fontFamily:'monospace',whiteSpace:'pre-wrap',wordBreak:'break-all',minHeight:20}, children: result })
|
|
99
|
-
] }),
|
|
100
|
-
// Dice quick
|
|
101
|
-
jsx.jsxs('div', { children: [
|
|
102
|
-
jsx.jsx('div', { style:{fontSize:11,color:C.dim,marginBottom:6}, children: '🎲 快捷骰子' }),
|
|
103
|
-
jsx.jsx('div', { style:{display:'flex',flexWrap:'wrap',gap:6}, children: DICE.map(d =>
|
|
104
|
-
jsx.jsx('button', { onClick:()=>quickDice(d), style:{padding:'4px 10px',borderRadius:6,border:'1px solid '+C.border,background:C.bg3,color:C.text,cursor:'pointer',fontSize:12,fontFamily:'monospace'}, children: d }, d)
|
|
105
|
-
) }),
|
|
106
|
-
diceResult && jsx.jsx('div', { style:{marginTop:8,padding:'6px 12px',borderRadius:6,background:'rgba(197,160,101,0.1)',color:C.gold,fontSize:13,fontWeight:600}, children: '结果: '+diceResult })
|
|
107
|
-
] }),
|
|
108
|
-
// Pick cache
|
|
109
|
-
jsx.jsxs('div', { children: [
|
|
110
|
-
jsx.jsxs('div', { style:{display:'flex',alignItems:'center',gap:8,marginBottom:6}, children: [
|
|
111
|
-
jsx.jsx('div', { style:{fontSize:11,color:C.dim,flex:1}, children: '📌 Pick 缓存 ('+picks.length+')' }),
|
|
112
|
-
picks.length>0 && jsx.jsx('button', { onClick:handleClearAll, style:{padding:'2px 8px',borderRadius:4,border:'1px solid '+C.danger,background:'transparent',color:C.danger,cursor:'pointer',fontSize:10,fontFamily:'inherit'}, children: '清除全部' })
|
|
113
|
-
] }),
|
|
114
|
-
picks.length===0 && jsx.jsx('div', { style:{fontSize:11,color:C.dim,fontStyle:'italic'}, children: '暂无缓存(使用 {[pick::key::...]} 后自动出现)' }),
|
|
115
|
-
picks.map(p => jsx.jsxs('div', { key:p.key, style:{display:'flex',alignItems:'center',gap:8,padding:'4px 8px',borderRadius:6,background:C.bg3,marginBottom:4}, children: [
|
|
116
|
-
jsx.jsx('span', { style:{fontSize:11,color:C.gold,fontWeight:600,fontFamily:'monospace',flex:1}, children: p.key }),
|
|
117
|
-
jsx.jsx('span', { style:{fontSize:12,color:C.text}, children: '→ '+p.value }),
|
|
118
|
-
jsx.jsx('button', { onClick:()=>handleReroll(p.key), style:{padding:'2px 8px',borderRadius:4,border:'1px solid '+C.border,background:C.bg1,color:C.sub,cursor:'pointer',fontSize:10,fontFamily:'inherit'}, children: '重抽' })
|
|
119
|
-
] }))
|
|
120
|
-
] }),
|
|
121
|
-
// Syntax help
|
|
122
|
-
jsx.jsxs('details', { style:{fontSize:11}, children: [
|
|
123
|
-
jsx.jsx('summary', { style:{color:C.dim,cursor:'pointer',marginBottom:4}, children: '📖 语法帮助' }),
|
|
124
|
-
jsx.jsx('div', { style:{color:C.sub,lineHeight:1.8,padding:'4px 0'}, children: [
|
|
125
|
-
jsx.jsx('div', { children: '{[random::A::B::C]} → 每次随机选一个' }),
|
|
126
|
-
jsx.jsx('div', { children: '{[pick::key::A::B::C]} → 固定结果,缓存为 key' }),
|
|
127
|
-
jsx.jsx('div', { children: '{[roll::2d6]} → 掷 2 个 6 面骰' }),
|
|
128
|
-
jsx.jsx('div', { children: '{[roll::1d20+3]} → d20 + 3 调整值' }),
|
|
129
|
-
jsx.jsx('div', { style:{marginTop:4,color:C.dim}, children: '在聊天输入框里直接写宏,按 Enter 自动展开后发送给 AI。' })
|
|
130
|
-
] })
|
|
131
|
-
] })
|
|
132
|
-
] })
|
|
133
|
-
] })
|
|
134
|
-
] })
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
let macroVc = null, macroVr = null
|
|
138
|
-
function showMacroPanel() {
|
|
139
|
-
if (macroVc) { macroVc.style.display = ''; return }
|
|
140
|
-
macroVc = document.createElement('div')
|
|
141
|
-
macroVc.setAttribute('data-dsh-macro-panel', '')
|
|
142
|
-
document.body.appendChild(macroVc)
|
|
143
|
-
macroVr = ReactDOM.createRoot(macroVc)
|
|
144
|
-
macroVr.render(jsx.jsx(MacroPanel, { onClose: () => { if (macroVc) macroVc.style.display = 'none' } }))
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// ============ MuvTableView ============
|
|
148
|
-
function MuvTableView({ onClose }) {
|
|
149
|
-
const initial = savedState && savedState.cardName
|
|
150
|
-
? { status:'loaded', cardName:savedState.cardName, schemas:savedState.schemas, initvarData:savedState.initvarData, edits:savedState.edits||{}, generatedBlock:'', error:'', search:'' }
|
|
151
|
-
: { status:'idle', cardName:'', schemas:[], initvarData:{}, edits:{}, generatedBlock:'', error:'', search:'' }
|
|
152
|
-
const [state, setState] = React.useState(initial)
|
|
153
|
-
const [collapsed, setCollapsed] = React.useState(new Set())
|
|
154
|
-
const fileRef = React.useRef(null)
|
|
155
|
-
const hasAutoLoaded = React.useRef(false)
|
|
156
|
-
|
|
157
|
-
// Get the tavern's active card
|
|
158
|
-
const fetchTavernCard = async () => {
|
|
159
|
-
let url = '/api/muv-table/tavern-card'
|
|
160
|
-
if (lastPresetId) url += '?presetId=' + encodeURIComponent(lastPresetId)
|
|
161
|
-
const r = await fetch(url)
|
|
162
|
-
return r.json()
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Auto-load from tavern on first open
|
|
166
|
-
React.useEffect(() => {
|
|
167
|
-
if (!hasAutoLoaded.current && state.status === 'idle') {
|
|
168
|
-
hasAutoLoaded.current = true
|
|
169
|
-
loadFromTavern()
|
|
170
|
-
}
|
|
171
|
-
}, [state.status])
|
|
172
|
-
|
|
173
|
-
// Poll tavern card changes every 2s (debounced 500ms) → auto-resync when session or card changes
|
|
174
|
-
const lastSyncKey = React.useRef('')
|
|
175
|
-
const checkingRef = React.useRef(false)
|
|
176
|
-
React.useEffect(() => {
|
|
177
|
-
if (state.status !== 'loaded' && state.status !== 'idle') return
|
|
178
|
-
const check = async () => {
|
|
179
|
-
if (checkingRef.current) return
|
|
180
|
-
checkingRef.current = true
|
|
181
|
-
try {
|
|
182
|
-
const d = await fetchTavernCard()
|
|
183
|
-
if (d.ok && d.name) {
|
|
184
|
-
const syncKey = d.name + '|' + (d.presetDir || '')
|
|
185
|
-
if (syncKey !== lastSyncKey.current || forceReload) {
|
|
186
|
-
forceReload = false
|
|
187
|
-
lastSyncKey.current = syncKey
|
|
188
|
-
savedState = { cardName:d.name, schemas:d.schemas, initvarData:d.initvarData, edits:{} }; saveSS(savedState)
|
|
189
|
-
setCollapsed(new Set())
|
|
190
|
-
setState({status:'loaded',cardName:d.name,schemas:d.schemas,initvarData:d.initvarData,edits:{},generatedBlock:'',error:'',search:''})
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
} catch (_) {}
|
|
194
|
-
finally { checkingRef.current = false }
|
|
195
|
-
}
|
|
196
|
-
const debouncedCheck = debounce(check, 500)
|
|
197
|
-
check()
|
|
198
|
-
const timer = setInterval(debouncedCheck, 2000)
|
|
199
|
-
return () => clearInterval(timer)
|
|
200
|
-
}, [state.status])
|
|
201
|
-
|
|
202
|
-
const parseCard = async (json) => {
|
|
203
|
-
setState(s=>({...s,status:'loading',error:''}))
|
|
204
|
-
try {
|
|
205
|
-
const r = await fetch('/api/muv-table/parse-card',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(json)})
|
|
206
|
-
const d = await r.json(); if(!d.ok) throw new Error(d.error)
|
|
207
|
-
savedState = { cardName:d.name, schemas:d.schemas, initvarData:d.initvarData, edits:{} }; saveSS(savedState)
|
|
208
|
-
setCollapsed(new Set()); setState({status:'loaded',cardName:d.name,schemas:d.schemas,initvarData:d.initvarData,edits:{},generatedBlock:'',error:'',search:''})
|
|
209
|
-
} catch(e) { setState(s=>({...s,status:'error',error:e.message})) }
|
|
210
|
-
}
|
|
211
|
-
const readFile = (f) => { const r=new FileReader(); r.onload=()=>{ try{parseCard(JSON.parse(r.result))}catch(e){setState(s=>({...s,error:'JSON error: '+e.message}))} }; r.readAsText(f) }
|
|
212
|
-
const handleDrop = (e) => { e.preventDefault(); const f=e.dataTransfer.files[0]; if(f) readFile(f) }
|
|
213
|
-
const loadFromTavern = async () => {
|
|
214
|
-
setState(s=>({...s,status:'loading',error:''}))
|
|
215
|
-
try {
|
|
216
|
-
const d = await fetchTavernCard()
|
|
217
|
-
if(!d.ok) throw new Error(d.error)
|
|
218
|
-
const schemas = d.schemas || []
|
|
219
|
-
savedState = { cardName:d.name, schemas, initvarData:d.initvarData||{}, edits:{} }; saveSS(savedState)
|
|
220
|
-
setCollapsed(new Set()); setState({status:'loaded',cardName:d.name,schemas,initvarData:d.initvarData||{},edits:{},generatedBlock:'',error:'',search:''})
|
|
221
|
-
} catch(e) { setState(s=>({...s,status:'error',error:e.message})) }
|
|
222
|
-
}
|
|
223
|
-
const handleEdit = (path, val) => setState(s=>{ const e={...s.edits,[path]:val}; savedState={...savedState,edits:e}; saveSS(savedState); return {...s,edits:e} })
|
|
224
|
-
const handleGenerate = async () => {
|
|
225
|
-
const edits = Object.entries(state.edits).map(([p,v])=>({path:p,value:v}))
|
|
226
|
-
try {
|
|
227
|
-
const r = await fetch('/api/muv-table/generate',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({originalData:state.initvarData,edits})})
|
|
228
|
-
const d = await r.json(); if(!d.ok) throw new Error(d.error)
|
|
229
|
-
setState(s=>({...s,generatedBlock:d.block}))
|
|
230
|
-
} catch(e) { setState(s=>({...s,error:e.message})) }
|
|
231
|
-
}
|
|
232
|
-
const toggleCollapse = (path) => setCollapsed(c=>{ const n=new Set(c); n.has(path)?n.delete(path):n.add(path); return n })
|
|
233
|
-
const toggleAll = () => setCollapsed(c=>c.size>0?new Set():new Set(state.schemas.map(f=>f.name)))
|
|
234
|
-
const editCount = Object.keys(state.edits).length
|
|
235
|
-
const typeName = { number:'数字', string:'文本', object:'对象', record:'字典' }
|
|
236
|
-
|
|
237
|
-
// ============ render a field node recursively ============
|
|
238
|
-
const renderField = (field, data, prefix, depth) => {
|
|
239
|
-
const path = prefix?prefix+'.'+field.name:field.name
|
|
240
|
-
const val = data?.[field.name]
|
|
241
|
-
const isContainer = field.type==='object' && field.children && field.children.length>0
|
|
242
|
-
const isCollapsed = collapsed.has(path)
|
|
243
|
-
const changed = state.edits[path]!==undefined
|
|
244
|
-
const indent = depth * 16
|
|
245
|
-
|
|
246
|
-
return jsx.jsxs('div', { key:path, children: [
|
|
247
|
-
// Row
|
|
248
|
-
jsx.jsxs('div', {
|
|
249
|
-
onClick: isContainer ? ()=>toggleCollapse(path) : undefined,
|
|
250
|
-
style: {
|
|
251
|
-
display:'flex', alignItems:'center', gap:8, padding:'5px 8px 5px '+(8+indent)+'px',
|
|
252
|
-
cursor: isContainer?'pointer':'default', userSelect:'none',
|
|
253
|
-
borderLeft: changed?'3px solid '+C.gold:'3px solid transparent',
|
|
254
|
-
background: changed?'rgba(197,160,101,0.06)':'transparent',
|
|
255
|
-
borderRadius: '0 6px 6px 0', marginBottom:1,
|
|
256
|
-
transition: 'background 0.12s'
|
|
257
|
-
},
|
|
258
|
-
onMouseEnter: e => { if(isContainer) e.currentTarget.style.background=changed?'rgba(197,160,101,0.12)':C.hover },
|
|
259
|
-
onMouseLeave: e => { e.currentTarget.style.background=changed?'rgba(197,160,101,0.06)':'transparent' },
|
|
260
|
-
children: [
|
|
261
|
-
jsx.jsx('span', { style:{width:14,fontSize:10,color:C.dim,flexShrink:0}, children: isContainer?(isCollapsed?'▸':'▾'):'' }),
|
|
262
|
-
jsx.jsx('span', { style:{fontWeight:isContainer?600:400,color:isContainer?C.text:C.sub,fontSize:13,flex:1,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}, children: field.name }),
|
|
263
|
-
jsx.jsx('span', { style:{fontSize:10,padding:'1px 6px',borderRadius:3,background:field.type==='number'?'rgba(74,191,171,0.15)':'rgba(197,160,101,0.1)',color:field.type==='number'?C.accent:C.sub,flexShrink:0}, children: typeName[field.type]||field.type }),
|
|
264
|
-
!isContainer && jsx.jsx('input', {
|
|
265
|
-
type: field.type==='number'?'number':'text',
|
|
266
|
-
value: state.edits[path]!==undefined?state.edits[path]:(val??''),
|
|
267
|
-
onChange: e => handleEdit(path, e.target.value),
|
|
268
|
-
onClick: e => e.stopPropagation(),
|
|
269
|
-
style: {
|
|
270
|
-
width: field.type==='number'?80:'100%', maxWidth:260,
|
|
271
|
-
background:C.bg3, border:'1px solid '+(changed?C.gold:'var(--dsw-alias-border-l2)'),
|
|
272
|
-
borderRadius:4, padding:'3px 8px', color:C.text, fontSize:12, fontFamily:'inherit', outline:'none',
|
|
273
|
-
flexShrink:1
|
|
274
|
-
}
|
|
275
|
-
})
|
|
276
|
-
]
|
|
277
|
-
}),
|
|
278
|
-
// Children
|
|
279
|
-
isContainer && !isCollapsed && field.children.map(c => renderField(c, val||{}, path, depth+1))
|
|
280
|
-
] })
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// ============ main render ============
|
|
284
|
-
return jsx.jsxs('div', { style:{position:'fixed',inset:0,zIndex:60,background:'rgba(0,0,0,0.5)',display:'flex',alignItems:'center',justifyContent:'center'}, onClick:onClose, children: [
|
|
285
|
-
jsx.jsxs('div', { onClick:e=>e.stopPropagation(), style:{width:'min(92vw,820px)',height:'min(90vh,700px)',background:C.bg,display:'flex',flexDirection:'column',color:C.text,fontFamily:C.font,fontSize:13,borderRadius:12,border:'1px solid '+C.border,boxShadow:'0 20px 60px rgba(0,0,0,0.5)',overflow:'hidden'}, children: [
|
|
286
|
-
// Header
|
|
287
|
-
jsx.jsxs('div', { style:{display:'flex',alignItems:'center',gap:10,padding:'10px 16px',borderBottom:'1px solid '+C.border,flexShrink:0,background:C.bg1}, children: [
|
|
288
|
-
jsx.jsx('span', { style:{fontWeight:700,fontSize:15,flex:1}, children: state.cardName?'📊 '+state.cardName:'📊 MUV 变量表格' }),
|
|
289
|
-
state.status==='loaded' && jsx.jsx('input', { type:'text', placeholder:'🔍 搜索变量...', value:state.search, onChange:e=>setState(s=>({...s,search:e.target.value})), style:{width:160,padding:'4px 10px',borderRadius:6,border:'1px solid '+C.border,background:C.bg3,color:C.text,fontSize:12,fontFamily:'inherit',outline:'none'} }),
|
|
290
|
-
editCount>0 && jsx.jsx('span', { style:{fontSize:11,color:C.sub,background:'rgba(197,160,101,0.12)',padding:'2px 8px',borderRadius:10}, children: editCount+' 修改' }),
|
|
291
|
-
state.status==='loaded' && jsx.jsx('button', { onClick:()=>showMacroPanel(), style:{padding:'5px 10px',borderRadius:6,border:'1px solid '+C.accent,background:'rgba(74,191,171,0.1)',color:C.accent,cursor:'pointer',fontSize:11,fontFamily:'inherit'}, children: '📐 宏' }),
|
|
292
|
-
state.status==='loaded' && jsx.jsx('button', { onClick:loadFromTavern, style:{padding:'5px 8px',borderRadius:6,border:'1px solid '+C.border,background:C.bg3,color:C.text,cursor:'pointer',fontSize:13,fontFamily:'inherit'}, children: '🔄' }),
|
|
293
|
-
state.status==='loaded' && jsx.jsx('button', { onClick:toggleAll, style:{padding:'5px 10px',borderRadius:6,border:'1px solid '+C.border,background:C.bg3,color:C.text,cursor:'pointer',fontSize:11,fontFamily:'inherit'}, children: collapsed.size>0?'📖 展开全部':'📕 折叠全部' }),
|
|
294
|
-
state.status==='loaded' && jsx.jsx('button', { onClick:handleGenerate, style:{padding:'7px 18px',borderRadius:8,border:'none',background:'linear-gradient(135deg,#c5a065,#d4a76a)',color:'#1a1a1a',cursor:'pointer',fontSize:13,fontWeight:700,fontFamily:'inherit',boxShadow:'0 2px 8px rgba(197,160,101,0.25)'}, children: '⚡ 生成 MUV 块' }),
|
|
295
|
-
jsx.jsx('button', { onClick:onClose, style:{padding:'5px 12px',borderRadius:6,border:'1px solid '+C.border,background:C.bg3,color:C.text,cursor:'pointer',fontSize:12,fontFamily:'inherit'}, children: '✕' })
|
|
296
|
-
] }),
|
|
297
|
-
|
|
298
|
-
// Content
|
|
299
|
-
state.status==='idle' && jsx.jsxs('div', { style:{flex:1,display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',gap:16,padding:24}, children: [
|
|
300
|
-
savedState && savedState.cardName && jsx.jsxs('div', { style:{background:C.bg2,border:'1px solid '+C.border,borderRadius:10,padding:'14px 24px',textAlign:'center',maxWidth:400,width:'100%'}, children: [
|
|
301
|
-
jsx.jsx('div', { style:{fontSize:14,fontWeight:600,marginBottom:4}, children: '📌 '+savedState.cardName }),
|
|
302
|
-
jsx.jsx('div', { style:{fontSize:11,color:C.dim,marginBottom:10}, children: savedState.schemas.length+' 个分组 · '+(savedState.edits?Object.keys(savedState.edits).length:0)+' 个修改' }),
|
|
303
|
-
jsx.jsx('button', { onClick:()=>{ setCollapsed(new Set()); setState({status:'loaded',cardName:savedState.cardName,schemas:savedState.schemas,initvarData:savedState.initvarData,edits:savedState.edits||{},generatedBlock:'',error:'',search:''}) }, style:{padding:'6px 20px',borderRadius:8,border:'none',background:'linear-gradient(135deg,#c5a065,#d4a76a)',color:'#1a1a1a',cursor:'pointer',fontSize:13,fontWeight:700,fontFamily:'inherit',boxShadow:'0 2px 8px rgba(197,160,101,0.25)'}, children: '📂 继续编辑' })
|
|
304
|
-
] }),
|
|
305
|
-
jsx.jsxs('div', { onClick:()=>fileRef.current?.click(), onDragOver:e=>e.preventDefault(), onDrop:handleDrop, style:{padding:40,border:'2px dashed '+C.border,borderRadius:12,textAlign:'center',cursor:'pointer',color:C.dim,maxWidth:400,width:'100%'}, children: [
|
|
306
|
-
jsx.jsx('div', { style:{fontSize:28,marginBottom:8}, children: '📁' }),
|
|
307
|
-
jsx.jsx('div', { children: '拖放 MUV 角色卡 JSON 到此处' }),
|
|
308
|
-
jsx.jsx('input', { ref:fileRef, type:'file', accept:'.json', style:{display:'none'}, onChange:e=>{const f=e.target.files[0];if(f)readFile(f)} })
|
|
309
|
-
] }),
|
|
310
|
-
jsx.jsx('button', { onClick:loadFromTavern, style:{padding:'10px 24px',borderRadius:8,border:'1px solid '+C.gold,background:'rgba(197,160,101,0.1)',color:C.gold,cursor:'pointer',fontSize:13,fontWeight:600,fontFamily:'inherit'}, children: '🍺 从酒馆加载当前角色卡' })
|
|
311
|
-
] }),
|
|
312
|
-
state.status==='loading' && jsx.jsx('div', { style:{flex:1,display:'flex',alignItems:'center',justifyContent:'center',color:C.dim}, children: '⏳ 解析中...' }),
|
|
313
|
-
state.error && jsx.jsx('div', { style:{padding:12,color:C.danger}, children: '⚠ '+state.error }),
|
|
314
|
-
|
|
315
|
-
// Tree
|
|
316
|
-
state.status==='loaded' && state.schemas.length===0 && jsx.jsxs('div', { style:{flex:1,display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',gap:12,padding:24,color:C.dim}, children: [
|
|
317
|
-
jsx.jsx('div', { style:{fontSize:14,fontWeight:600,color:C.text}, children: '📋 '+state.cardName }),
|
|
318
|
-
jsx.jsx('div', { style:{fontSize:12}, children: '此卡没有预设初始变量' }),
|
|
319
|
-
jsx.jsx('div', { style:{fontSize:11,textAlign:'center',lineHeight:1.6}, children: '玩起来后 LLM 输出 <UpdateVariable> 块时,变量会自动出现在这里。' }),
|
|
320
|
-
jsx.jsx('div', { style:{fontSize:11,color:C.gold}, children: '也可以手动拖放角色卡 JSON 文件加载初始数据。' })
|
|
321
|
-
] }),
|
|
322
|
-
state.status==='loaded' && state.schemas.length>0 && jsx.jsx('div', { style:{flex:1,overflow:'auto',padding:'8px 12px'}, children:
|
|
323
|
-
state.schemas.filter(f=>!state.search||f.name.includes(state.search)||JSON.stringify(f).includes(state.search)).map(field =>
|
|
324
|
-
jsx.jsxs('div', { key:field.name, style:{marginBottom:10,background:C.bg1,border:'1px solid '+C.border,borderRadius:8,overflow:'hidden'}, children: [
|
|
325
|
-
jsx.jsxs('div', { onClick:()=>toggleCollapse(field.name), style:{display:'flex',alignItems:'center',gap:8,padding:'8px 14px',cursor:'pointer',userSelect:'none',background:collapsed.has(field.name)?C.bg1:C.bg2,borderBottom:collapsed.has(field.name)?'none':'1px solid '+C.border}, onMouseEnter:e=>e.currentTarget.style.background=C.hover, onMouseLeave:e=>e.currentTarget.style.background=collapsed.has(field.name)?C.bg1:C.bg2, children: [
|
|
326
|
-
jsx.jsx('span', { style:{fontSize:12,color:C.dim,width:16}, children: collapsed.has(field.name)?'▸':'▾' }),
|
|
327
|
-
jsx.jsx('span', { style:{fontWeight:700,fontSize:14,flex:1}, children: field.name }),
|
|
328
|
-
jsx.jsx('span', { style:{fontSize:10,padding:'2px 8px',borderRadius:10,background:'rgba(197,160,101,0.12)',color:C.sub}, children: field.children.length+' 项' })
|
|
329
|
-
] }),
|
|
330
|
-
!collapsed.has(field.name) && jsx.jsx('div', { style:{padding:'4px 0'}, children:
|
|
331
|
-
field.children.map(c => renderField(c, state.initvarData[field.name]||{}, field.name, 1))
|
|
332
|
-
})
|
|
333
|
-
] })
|
|
334
|
-
)
|
|
335
|
-
}),
|
|
336
|
-
|
|
337
|
-
// Generated block
|
|
338
|
-
state.generatedBlock && jsx.jsxs('div', { style:{flexShrink:0,borderTop:'2px solid '+C.accent}, children: [
|
|
339
|
-
jsx.jsxs('div', { style:{display:'flex',alignItems:'center',gap:10,padding:'8px 14px',background:C.bg1}, children: [
|
|
340
|
-
jsx.jsx('span', { style:{fontWeight:600,fontSize:12,color:C.accent,flex:1}, children: '✅ MUV 块 ('+state.generatedBlock.split('\n').length+' 行)' }),
|
|
341
|
-
jsx.jsx('button', { onClick:()=>navigator.clipboard.writeText(state.generatedBlock), style:{padding:'4px 12px',borderRadius:4,border:'1px solid '+C.border,background:C.bg3,color:C.text,cursor:'pointer',fontSize:11,fontFamily:'inherit'}, children: '📋 复制' }),
|
|
342
|
-
jsx.jsx('button', { onClick:()=>setState(s=>({...s,generatedBlock:''})), style:{padding:'4px 10px',borderRadius:4,border:'1px solid '+C.border,background:C.bg3,color:C.text,cursor:'pointer',fontSize:11,fontFamily:'inherit'}, children: '✕' })
|
|
343
|
-
] }),
|
|
344
|
-
jsx.jsx('pre', { style:{background:'#0d0e12',padding:12,fontSize:11,fontFamily:'monospace',whiteSpace:'pre-wrap',wordBreak:'break-all',maxHeight:180,overflow:'auto',margin:0,color:'#c0c0c0',borderTop:'1px solid '+C.border}, children: state.generatedBlock })
|
|
345
|
-
] })
|
|
346
|
-
] })
|
|
347
|
-
] })
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// ============ Sidebar entry ============
|
|
351
|
-
function sidebarRoot() { const col=document.querySelector('[data-pane="sidebar"], [class*="sidebarCol"]'); return col?col.querySelector('[class*="logoRow"]')?.parentElement??col.firstElementChild:undefined }
|
|
352
|
-
function newSessionBtn(root) { const n=root.querySelector('button[class*="newSession"]'); if(n)return n; for(const c of root.children) if(c.tagName==='BUTTON') return c }
|
|
353
|
-
function createEntry() {
|
|
354
|
-
const e=document.createElement('button'); e.type='button'; e.dataset.dshMuvEntry=''
|
|
355
|
-
const ex=document.querySelector('[data-dsh-taskboard-entry], [data-dsh-ssh-entry]'); e.className=ex?ex.className:''
|
|
356
|
-
e.setAttribute('aria-label','MUV 表格'); e.innerHTML=`<span style="flex:none;display:inline-flex;justify-content:center;align-items:center">${ICON}</span><span style="text-overflow:ellipsis;overflow:hidden">MUV 表格</span>`
|
|
357
|
-
Object.assign(e.style,{width:'100%',height:'32px',color:'var(--dsw-alias-label-secondary)',cursor:'pointer',whiteSpace:'nowrap',background:'none',border:'none',borderRadius:'8px',alignItems:'center',gap:'8px',padding:'0 12px',fontSize:'13px',display:'flex'})
|
|
358
|
-
e.addEventListener('mouseenter',()=>{e.style.background='var(--dsw-specific-sidebar-nav-item-hover)';e.style.color='var(--dsw-alias-label-primary)'})
|
|
359
|
-
e.addEventListener('mouseleave',()=>{e.style.background='none';e.style.color='var(--dsw-alias-label-secondary)'})
|
|
360
|
-
return e
|
|
361
|
-
}
|
|
362
|
-
function placeEntry(root, entry) {
|
|
363
|
-
const btn=newSessionBtn(root); if(!btn) return false
|
|
364
|
-
if(entry.parentElement!==root){const row=btn.closest('[class*="logoRow"]');const base=row&&row.parentElement===root?row:btn;const family=Array.from(root.children).filter(el=>el instanceof HTMLElement&&el.matches('[data-dsh-taskboard-entry], [data-dsh-ssh-entry]'));const anchor=family.length>0?family[0]:base.nextElementSibling;root.insertBefore(entry,anchor)}
|
|
365
|
-
return true
|
|
366
|
-
}
|
|
367
|
-
let vc=null, vr=null
|
|
368
|
-
function showView(){if(vc){vc.style.display='';return};vc=document.createElement('div');vc.setAttribute('data-dsh-muv-view','');document.body.appendChild(vc);vr=ReactDOM.createRoot(vc);vr.render(jsx.jsx(MuvTableView,{onClose:()=>{if(vc)vc.style.display='none'}}))}
|
|
369
|
-
function mountSidebarEntry(){if(document.querySelector('[data-dsh-muv-entry]'))return()=>{};const e=createEntry();e.addEventListener('click',showView);let root,placed=false;const tp=()=>{if(root&&!root.isConnected){root=undefined;placed=false}if(placed){if(document.body.contains(e))return;root=undefined;placed=false}root??=sidebarRoot();if(!root)return;placed=placeEntry(root,e)};const o=new MutationObserver(tp);o.observe(document.body,{childList:true,subtree:true});tp();return()=>{o.disconnect();e.remove();if(vc)vc.remove();vr=null;vc=null}}
|
|
370
|
-
|
|
371
|
-
// ============ apply ============
|
|
372
|
-
let forceReload = false
|
|
373
|
-
let lastPresetId = ''
|
|
374
|
-
|
|
375
|
-
// ── DOM observer: detect session / preset switches by watching the URL ──
|
|
376
|
-
let _lastPath = window.location.pathname + window.location.search
|
|
377
|
-
function detectSessionChange() {
|
|
378
|
-
try {
|
|
379
|
-
const currentPath = window.location.pathname + window.location.search
|
|
380
|
-
if (currentPath !== _lastPath) {
|
|
381
|
-
_lastPath = currentPath
|
|
382
|
-
document.dispatchEvent(new CustomEvent('tavern-session-changed', {
|
|
383
|
-
detail: { path: currentPath }
|
|
384
|
-
}))
|
|
385
|
-
forceReload = true
|
|
386
|
-
}
|
|
387
|
-
} catch (_) {}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
exports.inject = []
|
|
391
|
-
exports.apply = function () {
|
|
392
|
-
// Listen for tavern preset changes (dispatched by other plugins or DSH internals)
|
|
393
|
-
const onPresetChanged = (e) => {
|
|
394
|
-
forceReload = true
|
|
395
|
-
lastPresetId = e.detail?.presetId || ''
|
|
396
|
-
}
|
|
397
|
-
document.addEventListener('tavern-preset-changed', onPresetChanged)
|
|
398
|
-
|
|
399
|
-
// Listen for tavern session changes
|
|
400
|
-
const onSessionChanged = () => {
|
|
401
|
-
forceReload = true
|
|
402
|
-
}
|
|
403
|
-
document.addEventListener('tavern-session-changed', onSessionChanged)
|
|
404
|
-
|
|
405
|
-
let d=null; const tm=()=>{if(d)return;if(sidebarRoot())d=mountSidebarEntry()}; tm()
|
|
406
|
-
const o=new MutationObserver(() => { tm(); detectSessionChange() })
|
|
407
|
-
o.observe(document.body,{childList:true,subtree:true})
|
|
408
|
-
// Initial detection
|
|
409
|
-
detectSessionChange()
|
|
410
|
-
return () => {
|
|
411
|
-
o.disconnect()
|
|
412
|
-
document.removeEventListener('tavern-preset-changed', onPresetChanged)
|
|
413
|
-
document.removeEventListener('tavern-session-changed', onSessionChanged)
|
|
414
|
-
if (d) d()
|
|
415
|
-
if (macroVc) { macroVc.remove(); macroVc = null; macroVr = null }
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
return module.exports
|
|
419
|
-
}
|
|
1
|
+
// dsh-muv-table client v2: card-based tree view with modern DSH design
|
|
2
|
+
window.__ModuleLoader__.load({
|
|
3
|
+
id: 'dsh-muv-table',
|
|
4
|
+
factory: (require) => {
|
|
5
|
+
var module = { exports: {} }
|
|
6
|
+
var exports = module.exports
|
|
7
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
8
|
+
const React = require('react'), jsx = require('react/jsx-runtime'), ReactDOM = require('react-dom/client')
|
|
9
|
+
|
|
10
|
+
const ICON = `<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"><rect x="1.5" y="3" width="13" height="10" rx="1.5"/><path d="M1.5 6.5h13M6 6.5v6.5M11 6.5v6.5"/></svg>`
|
|
11
|
+
|
|
12
|
+
// ============ persistence ============
|
|
13
|
+
const SK = 'dsh-muv-table-state'
|
|
14
|
+
const loadSS = () => { try { const s=localStorage.getItem(SK); return s?JSON.parse(s):null } catch(_){return null} }
|
|
15
|
+
const saveSS = (s) => { try { localStorage.setItem(SK,JSON.stringify({...s,savedAt:Date.now()})) } catch(_){} }
|
|
16
|
+
let savedState = loadSS()
|
|
17
|
+
// ============ debounce utility ============
|
|
18
|
+
function debounce(fn, delay) {
|
|
19
|
+
let timer = null
|
|
20
|
+
return function (...args) {
|
|
21
|
+
if (timer) clearTimeout(timer)
|
|
22
|
+
timer = setTimeout(() => { timer = null; fn.apply(this, args) }, delay)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ============ color tokens ============
|
|
27
|
+
const C = {
|
|
28
|
+
bg: 'var(--dsw-alias-bg-base)', bg1: 'var(--dsw-alias-bg-layer-1)', bg2: 'var(--dsw-alias-bg-layer-2)', bg3: 'var(--dsw-alias-bg-layer-3)',
|
|
29
|
+
border: 'var(--dsw-alias-border-l2)', borderL: 'var(--dsw-alias-border-l1)',
|
|
30
|
+
text: 'var(--dsw-alias-label-primary)', sub: 'var(--dsw-alias-label-secondary)', dim: 'var(--dsw-alias-label-tertiary)',
|
|
31
|
+
accent: 'var(--dsw-alias-label-accent)', gold: 'var(--dsw-alias-label-accent,#c5a065)', danger: 'var(--dsw-alias-label-danger)',
|
|
32
|
+
hover: 'var(--dsw-specific-sidebar-nav-item-hover)',
|
|
33
|
+
font: 'var(--dsw-font-family)'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ============ MacroPanel ============
|
|
37
|
+
function MacroPanel({ onClose }) {
|
|
38
|
+
const [input, setInput] = React.useState('')
|
|
39
|
+
const [result, setResult] = React.useState('')
|
|
40
|
+
const [picks, setPicks] = React.useState([])
|
|
41
|
+
const [diceResult, setDiceResult] = React.useState('')
|
|
42
|
+
|
|
43
|
+
const refreshPicks = () => {
|
|
44
|
+
if (typeof window._tavernListPicks === 'function') {
|
|
45
|
+
setPicks(window._tavernListPicks())
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
React.useEffect(() => { refreshPicks() }, [])
|
|
49
|
+
|
|
50
|
+
const handleExpand = () => {
|
|
51
|
+
if (typeof window._tavernExpandMacros === 'function') {
|
|
52
|
+
setResult(window._tavernExpandMacros(input))
|
|
53
|
+
} else {
|
|
54
|
+
setResult('⚠ muv-engine 插件未加载')
|
|
55
|
+
}
|
|
56
|
+
refreshPicks()
|
|
57
|
+
}
|
|
58
|
+
const handleReroll = (key) => {
|
|
59
|
+
if (typeof window._tavernRerollPick === 'function') {
|
|
60
|
+
window._tavernRerollPick(key)
|
|
61
|
+
refreshPicks()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const handleClearAll = () => {
|
|
65
|
+
picks.forEach(p => { if (typeof window._tavernRerollPick === 'function') window._tavernRerollPick(p.key) })
|
|
66
|
+
refreshPicks()
|
|
67
|
+
}
|
|
68
|
+
const quickDice = (expr) => {
|
|
69
|
+
if (typeof window._tavernExpandMacros === 'function') {
|
|
70
|
+
setDiceResult(window._tavernExpandMacros('{[roll::'+expr+']}'))
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const DICE = ['d4','d6','d8','d10','d12','d20','2d6','3d6','1d100']
|
|
75
|
+
|
|
76
|
+
return jsx.jsxs('div', { style:{position:'fixed',inset:0,zIndex:70,background:'rgba(0,0,0,0.5)',display:'flex',alignItems:'center',justifyContent:'center'}, onClick:onClose, children: [
|
|
77
|
+
jsx.jsxs('div', { onClick:e=>e.stopPropagation(), style:{width:'min(90vw,520px)',background:C.bg,display:'flex',flexDirection:'column',color:C.text,fontFamily:C.font,fontSize:13,borderRadius:12,border:'1px solid '+C.border,boxShadow:'0 20px 60px rgba(0,0,0,0.5)',overflow:'hidden'}, children: [
|
|
78
|
+
// Header
|
|
79
|
+
jsx.jsxs('div', { style:{display:'flex',alignItems:'center',gap:10,padding:'10px 16px',borderBottom:'1px solid '+C.border,flexShrink:0,background:C.bg1}, children: [
|
|
80
|
+
jsx.jsx('span', { style:{fontWeight:700,fontSize:15,flex:1}, children: '📐 宏测试' }),
|
|
81
|
+
jsx.jsx('button', { onClick:onClose, style:{padding:'5px 12px',borderRadius:6,border:'1px solid '+C.border,background:C.bg3,color:C.text,cursor:'pointer',fontSize:12,fontFamily:'inherit'}, children: '✕' })
|
|
82
|
+
] }),
|
|
83
|
+
// Body
|
|
84
|
+
jsx.jsxs('div', { style:{padding:16,display:'flex',flexDirection:'column',gap:14,overflow:'auto'}, children: [
|
|
85
|
+
// Input
|
|
86
|
+
jsx.jsxs('div', { children: [
|
|
87
|
+
jsx.jsx('div', { style:{fontSize:11,color:C.dim,marginBottom:6}, children: '输入宏语法(支持 random / pick / roll)' }),
|
|
88
|
+
jsx.jsx('textarea', { value:input, onChange:e=>setInput(e.target.value), placeholder:'{[random::选项A::选项B::选项C]}\n{[pick::宝箱::灵石x50::破旧剑谱::神秘丹药]}\n{[roll::2d6+3]}', rows:4, style:{width:'100%',padding:10,borderRadius:8,border:'1px solid '+C.border,background:C.bg3,color:C.text,fontSize:13,fontFamily:'monospace',resize:'vertical',outline:'none',boxSizing:'border-box'} }),
|
|
89
|
+
jsx.jsx('button', { onClick:handleExpand, style:{marginTop:8,padding:'8px 20px',borderRadius:8,border:'none',background:'linear-gradient(135deg,#c5a065,#d4a76a)',color:'#1a1a1a',cursor:'pointer',fontSize:13,fontWeight:700,fontFamily:'inherit',width:'100%'}, children: '⚡ 展开测试' })
|
|
90
|
+
] }),
|
|
91
|
+
// Result
|
|
92
|
+
result && jsx.jsxs('div', { children: [
|
|
93
|
+
jsx.jsxs('div', { style:{display:'flex',alignItems:'center',gap:8,marginBottom:4}, children: [
|
|
94
|
+
jsx.jsx('div', { style:{fontSize:11,color:C.dim,flex:1}, children: '展开结果' }),
|
|
95
|
+
jsx.jsx('button', { onClick:()=>navigator.clipboard.writeText(result), style:{padding:'2px 8px',borderRadius:4,border:'1px solid '+C.border,background:C.bg3,color:C.sub,cursor:'pointer',fontSize:10,fontFamily:'inherit'}, children: '📋 复制' }),
|
|
96
|
+
jsx.jsx('button', { onClick:()=>setInput(result), style:{padding:'2px 8px',borderRadius:4,border:'1px solid '+C.border,background:C.bg3,color:C.sub,cursor:'pointer',fontSize:10,fontFamily:'inherit'}, children: '⬆ 填入' })
|
|
97
|
+
] }),
|
|
98
|
+
jsx.jsx('div', { style:{padding:10,borderRadius:8,border:'1px solid '+C.accent,background:'rgba(74,191,171,0.06)',color:C.text,fontSize:13,fontFamily:'monospace',whiteSpace:'pre-wrap',wordBreak:'break-all',minHeight:20}, children: result })
|
|
99
|
+
] }),
|
|
100
|
+
// Dice quick
|
|
101
|
+
jsx.jsxs('div', { children: [
|
|
102
|
+
jsx.jsx('div', { style:{fontSize:11,color:C.dim,marginBottom:6}, children: '🎲 快捷骰子' }),
|
|
103
|
+
jsx.jsx('div', { style:{display:'flex',flexWrap:'wrap',gap:6}, children: DICE.map(d =>
|
|
104
|
+
jsx.jsx('button', { onClick:()=>quickDice(d), style:{padding:'4px 10px',borderRadius:6,border:'1px solid '+C.border,background:C.bg3,color:C.text,cursor:'pointer',fontSize:12,fontFamily:'monospace'}, children: d }, d)
|
|
105
|
+
) }),
|
|
106
|
+
diceResult && jsx.jsx('div', { style:{marginTop:8,padding:'6px 12px',borderRadius:6,background:'rgba(197,160,101,0.1)',color:C.gold,fontSize:13,fontWeight:600}, children: '结果: '+diceResult })
|
|
107
|
+
] }),
|
|
108
|
+
// Pick cache
|
|
109
|
+
jsx.jsxs('div', { children: [
|
|
110
|
+
jsx.jsxs('div', { style:{display:'flex',alignItems:'center',gap:8,marginBottom:6}, children: [
|
|
111
|
+
jsx.jsx('div', { style:{fontSize:11,color:C.dim,flex:1}, children: '📌 Pick 缓存 ('+picks.length+')' }),
|
|
112
|
+
picks.length>0 && jsx.jsx('button', { onClick:handleClearAll, style:{padding:'2px 8px',borderRadius:4,border:'1px solid '+C.danger,background:'transparent',color:C.danger,cursor:'pointer',fontSize:10,fontFamily:'inherit'}, children: '清除全部' })
|
|
113
|
+
] }),
|
|
114
|
+
picks.length===0 && jsx.jsx('div', { style:{fontSize:11,color:C.dim,fontStyle:'italic'}, children: '暂无缓存(使用 {[pick::key::...]} 后自动出现)' }),
|
|
115
|
+
picks.map(p => jsx.jsxs('div', { key:p.key, style:{display:'flex',alignItems:'center',gap:8,padding:'4px 8px',borderRadius:6,background:C.bg3,marginBottom:4}, children: [
|
|
116
|
+
jsx.jsx('span', { style:{fontSize:11,color:C.gold,fontWeight:600,fontFamily:'monospace',flex:1}, children: p.key }),
|
|
117
|
+
jsx.jsx('span', { style:{fontSize:12,color:C.text}, children: '→ '+p.value }),
|
|
118
|
+
jsx.jsx('button', { onClick:()=>handleReroll(p.key), style:{padding:'2px 8px',borderRadius:4,border:'1px solid '+C.border,background:C.bg1,color:C.sub,cursor:'pointer',fontSize:10,fontFamily:'inherit'}, children: '重抽' })
|
|
119
|
+
] }))
|
|
120
|
+
] }),
|
|
121
|
+
// Syntax help
|
|
122
|
+
jsx.jsxs('details', { style:{fontSize:11}, children: [
|
|
123
|
+
jsx.jsx('summary', { style:{color:C.dim,cursor:'pointer',marginBottom:4}, children: '📖 语法帮助' }),
|
|
124
|
+
jsx.jsx('div', { style:{color:C.sub,lineHeight:1.8,padding:'4px 0'}, children: [
|
|
125
|
+
jsx.jsx('div', { children: '{[random::A::B::C]} → 每次随机选一个' }),
|
|
126
|
+
jsx.jsx('div', { children: '{[pick::key::A::B::C]} → 固定结果,缓存为 key' }),
|
|
127
|
+
jsx.jsx('div', { children: '{[roll::2d6]} → 掷 2 个 6 面骰' }),
|
|
128
|
+
jsx.jsx('div', { children: '{[roll::1d20+3]} → d20 + 3 调整值' }),
|
|
129
|
+
jsx.jsx('div', { style:{marginTop:4,color:C.dim}, children: '在聊天输入框里直接写宏,按 Enter 自动展开后发送给 AI。' })
|
|
130
|
+
] })
|
|
131
|
+
] })
|
|
132
|
+
] })
|
|
133
|
+
] })
|
|
134
|
+
] })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let macroVc = null, macroVr = null
|
|
138
|
+
function showMacroPanel() {
|
|
139
|
+
if (macroVc) { macroVc.style.display = ''; return }
|
|
140
|
+
macroVc = document.createElement('div')
|
|
141
|
+
macroVc.setAttribute('data-dsh-macro-panel', '')
|
|
142
|
+
document.body.appendChild(macroVc)
|
|
143
|
+
macroVr = ReactDOM.createRoot(macroVc)
|
|
144
|
+
macroVr.render(jsx.jsx(MacroPanel, { onClose: () => { if (macroVc) macroVc.style.display = 'none' } }))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ============ MuvTableView ============
|
|
148
|
+
function MuvTableView({ onClose }) {
|
|
149
|
+
const initial = savedState && savedState.cardName
|
|
150
|
+
? { status:'loaded', cardName:savedState.cardName, schemas:savedState.schemas, initvarData:savedState.initvarData, edits:savedState.edits||{}, generatedBlock:'', error:'', search:'' }
|
|
151
|
+
: { status:'idle', cardName:'', schemas:[], initvarData:{}, edits:{}, generatedBlock:'', error:'', search:'' }
|
|
152
|
+
const [state, setState] = React.useState(initial)
|
|
153
|
+
const [collapsed, setCollapsed] = React.useState(new Set())
|
|
154
|
+
const fileRef = React.useRef(null)
|
|
155
|
+
const hasAutoLoaded = React.useRef(false)
|
|
156
|
+
|
|
157
|
+
// Get the tavern's active card
|
|
158
|
+
const fetchTavernCard = async () => {
|
|
159
|
+
let url = '/api/muv-table/tavern-card'
|
|
160
|
+
if (lastPresetId) url += '?presetId=' + encodeURIComponent(lastPresetId)
|
|
161
|
+
const r = await fetch(url)
|
|
162
|
+
return r.json()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Auto-load from tavern on first open
|
|
166
|
+
React.useEffect(() => {
|
|
167
|
+
if (!hasAutoLoaded.current && state.status === 'idle') {
|
|
168
|
+
hasAutoLoaded.current = true
|
|
169
|
+
loadFromTavern()
|
|
170
|
+
}
|
|
171
|
+
}, [state.status])
|
|
172
|
+
|
|
173
|
+
// Poll tavern card changes every 2s (debounced 500ms) → auto-resync when session or card changes
|
|
174
|
+
const lastSyncKey = React.useRef('')
|
|
175
|
+
const checkingRef = React.useRef(false)
|
|
176
|
+
React.useEffect(() => {
|
|
177
|
+
if (state.status !== 'loaded' && state.status !== 'idle') return
|
|
178
|
+
const check = async () => {
|
|
179
|
+
if (checkingRef.current) return
|
|
180
|
+
checkingRef.current = true
|
|
181
|
+
try {
|
|
182
|
+
const d = await fetchTavernCard()
|
|
183
|
+
if (d.ok && d.name) {
|
|
184
|
+
const syncKey = d.name + '|' + (d.presetDir || '')
|
|
185
|
+
if (syncKey !== lastSyncKey.current || forceReload) {
|
|
186
|
+
forceReload = false
|
|
187
|
+
lastSyncKey.current = syncKey
|
|
188
|
+
savedState = { cardName:d.name, schemas:d.schemas, initvarData:d.initvarData, edits:{} }; saveSS(savedState)
|
|
189
|
+
setCollapsed(new Set())
|
|
190
|
+
setState({status:'loaded',cardName:d.name,schemas:d.schemas,initvarData:d.initvarData,edits:{},generatedBlock:'',error:'',search:''})
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} catch (_) {}
|
|
194
|
+
finally { checkingRef.current = false }
|
|
195
|
+
}
|
|
196
|
+
const debouncedCheck = debounce(check, 500)
|
|
197
|
+
check()
|
|
198
|
+
const timer = setInterval(debouncedCheck, 2000)
|
|
199
|
+
return () => clearInterval(timer)
|
|
200
|
+
}, [state.status])
|
|
201
|
+
|
|
202
|
+
const parseCard = async (json) => {
|
|
203
|
+
setState(s=>({...s,status:'loading',error:''}))
|
|
204
|
+
try {
|
|
205
|
+
const r = await fetch('/api/muv-table/parse-card',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(json)})
|
|
206
|
+
const d = await r.json(); if(!d.ok) throw new Error(d.error)
|
|
207
|
+
savedState = { cardName:d.name, schemas:d.schemas, initvarData:d.initvarData, edits:{} }; saveSS(savedState)
|
|
208
|
+
setCollapsed(new Set()); setState({status:'loaded',cardName:d.name,schemas:d.schemas,initvarData:d.initvarData,edits:{},generatedBlock:'',error:'',search:''})
|
|
209
|
+
} catch(e) { setState(s=>({...s,status:'error',error:e.message})) }
|
|
210
|
+
}
|
|
211
|
+
const readFile = (f) => { const r=new FileReader(); r.onload=()=>{ try{parseCard(JSON.parse(r.result))}catch(e){setState(s=>({...s,error:'JSON error: '+e.message}))} }; r.readAsText(f) }
|
|
212
|
+
const handleDrop = (e) => { e.preventDefault(); const f=e.dataTransfer.files[0]; if(f) readFile(f) }
|
|
213
|
+
const loadFromTavern = async () => {
|
|
214
|
+
setState(s=>({...s,status:'loading',error:''}))
|
|
215
|
+
try {
|
|
216
|
+
const d = await fetchTavernCard()
|
|
217
|
+
if(!d.ok) throw new Error(d.error)
|
|
218
|
+
const schemas = d.schemas || []
|
|
219
|
+
savedState = { cardName:d.name, schemas, initvarData:d.initvarData||{}, edits:{} }; saveSS(savedState)
|
|
220
|
+
setCollapsed(new Set()); setState({status:'loaded',cardName:d.name,schemas,initvarData:d.initvarData||{},edits:{},generatedBlock:'',error:'',search:''})
|
|
221
|
+
} catch(e) { setState(s=>({...s,status:'error',error:e.message})) }
|
|
222
|
+
}
|
|
223
|
+
const handleEdit = (path, val) => setState(s=>{ const e={...s.edits,[path]:val}; savedState={...savedState,edits:e}; saveSS(savedState); return {...s,edits:e} })
|
|
224
|
+
const handleGenerate = async () => {
|
|
225
|
+
const edits = Object.entries(state.edits).map(([p,v])=>({path:p,value:v}))
|
|
226
|
+
try {
|
|
227
|
+
const r = await fetch('/api/muv-table/generate',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({originalData:state.initvarData,edits})})
|
|
228
|
+
const d = await r.json(); if(!d.ok) throw new Error(d.error)
|
|
229
|
+
setState(s=>({...s,generatedBlock:d.block}))
|
|
230
|
+
} catch(e) { setState(s=>({...s,error:e.message})) }
|
|
231
|
+
}
|
|
232
|
+
const toggleCollapse = (path) => setCollapsed(c=>{ const n=new Set(c); n.has(path)?n.delete(path):n.add(path); return n })
|
|
233
|
+
const toggleAll = () => setCollapsed(c=>c.size>0?new Set():new Set(state.schemas.map(f=>f.name)))
|
|
234
|
+
const editCount = Object.keys(state.edits).length
|
|
235
|
+
const typeName = { number:'数字', string:'文本', object:'对象', record:'字典' }
|
|
236
|
+
|
|
237
|
+
// ============ render a field node recursively ============
|
|
238
|
+
const renderField = (field, data, prefix, depth) => {
|
|
239
|
+
const path = prefix?prefix+'.'+field.name:field.name
|
|
240
|
+
const val = data?.[field.name]
|
|
241
|
+
const isContainer = field.type==='object' && field.children && field.children.length>0
|
|
242
|
+
const isCollapsed = collapsed.has(path)
|
|
243
|
+
const changed = state.edits[path]!==undefined
|
|
244
|
+
const indent = depth * 16
|
|
245
|
+
|
|
246
|
+
return jsx.jsxs('div', { key:path, children: [
|
|
247
|
+
// Row
|
|
248
|
+
jsx.jsxs('div', {
|
|
249
|
+
onClick: isContainer ? ()=>toggleCollapse(path) : undefined,
|
|
250
|
+
style: {
|
|
251
|
+
display:'flex', alignItems:'center', gap:8, padding:'5px 8px 5px '+(8+indent)+'px',
|
|
252
|
+
cursor: isContainer?'pointer':'default', userSelect:'none',
|
|
253
|
+
borderLeft: changed?'3px solid '+C.gold:'3px solid transparent',
|
|
254
|
+
background: changed?'rgba(197,160,101,0.06)':'transparent',
|
|
255
|
+
borderRadius: '0 6px 6px 0', marginBottom:1,
|
|
256
|
+
transition: 'background 0.12s'
|
|
257
|
+
},
|
|
258
|
+
onMouseEnter: e => { if(isContainer) e.currentTarget.style.background=changed?'rgba(197,160,101,0.12)':C.hover },
|
|
259
|
+
onMouseLeave: e => { e.currentTarget.style.background=changed?'rgba(197,160,101,0.06)':'transparent' },
|
|
260
|
+
children: [
|
|
261
|
+
jsx.jsx('span', { style:{width:14,fontSize:10,color:C.dim,flexShrink:0}, children: isContainer?(isCollapsed?'▸':'▾'):'' }),
|
|
262
|
+
jsx.jsx('span', { style:{fontWeight:isContainer?600:400,color:isContainer?C.text:C.sub,fontSize:13,flex:1,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}, children: field.name }),
|
|
263
|
+
jsx.jsx('span', { style:{fontSize:10,padding:'1px 6px',borderRadius:3,background:field.type==='number'?'rgba(74,191,171,0.15)':'rgba(197,160,101,0.1)',color:field.type==='number'?C.accent:C.sub,flexShrink:0}, children: typeName[field.type]||field.type }),
|
|
264
|
+
!isContainer && jsx.jsx('input', {
|
|
265
|
+
type: field.type==='number'?'number':'text',
|
|
266
|
+
value: state.edits[path]!==undefined?state.edits[path]:(val??''),
|
|
267
|
+
onChange: e => handleEdit(path, e.target.value),
|
|
268
|
+
onClick: e => e.stopPropagation(),
|
|
269
|
+
style: {
|
|
270
|
+
width: field.type==='number'?80:'100%', maxWidth:260,
|
|
271
|
+
background:C.bg3, border:'1px solid '+(changed?C.gold:'var(--dsw-alias-border-l2)'),
|
|
272
|
+
borderRadius:4, padding:'3px 8px', color:C.text, fontSize:12, fontFamily:'inherit', outline:'none',
|
|
273
|
+
flexShrink:1
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
]
|
|
277
|
+
}),
|
|
278
|
+
// Children
|
|
279
|
+
isContainer && !isCollapsed && field.children.map(c => renderField(c, val||{}, path, depth+1))
|
|
280
|
+
] })
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ============ main render ============
|
|
284
|
+
return jsx.jsxs('div', { style:{position:'fixed',inset:0,zIndex:60,background:'rgba(0,0,0,0.5)',display:'flex',alignItems:'center',justifyContent:'center'}, onClick:onClose, children: [
|
|
285
|
+
jsx.jsxs('div', { onClick:e=>e.stopPropagation(), style:{width:'min(92vw,820px)',height:'min(90vh,700px)',background:C.bg,display:'flex',flexDirection:'column',color:C.text,fontFamily:C.font,fontSize:13,borderRadius:12,border:'1px solid '+C.border,boxShadow:'0 20px 60px rgba(0,0,0,0.5)',overflow:'hidden'}, children: [
|
|
286
|
+
// Header
|
|
287
|
+
jsx.jsxs('div', { style:{display:'flex',alignItems:'center',gap:10,padding:'10px 16px',borderBottom:'1px solid '+C.border,flexShrink:0,background:C.bg1}, children: [
|
|
288
|
+
jsx.jsx('span', { style:{fontWeight:700,fontSize:15,flex:1}, children: state.cardName?'📊 '+state.cardName:'📊 MUV 变量表格' }),
|
|
289
|
+
state.status==='loaded' && jsx.jsx('input', { type:'text', placeholder:'🔍 搜索变量...', value:state.search, onChange:e=>setState(s=>({...s,search:e.target.value})), style:{width:160,padding:'4px 10px',borderRadius:6,border:'1px solid '+C.border,background:C.bg3,color:C.text,fontSize:12,fontFamily:'inherit',outline:'none'} }),
|
|
290
|
+
editCount>0 && jsx.jsx('span', { style:{fontSize:11,color:C.sub,background:'rgba(197,160,101,0.12)',padding:'2px 8px',borderRadius:10}, children: editCount+' 修改' }),
|
|
291
|
+
state.status==='loaded' && jsx.jsx('button', { onClick:()=>showMacroPanel(), style:{padding:'5px 10px',borderRadius:6,border:'1px solid '+C.accent,background:'rgba(74,191,171,0.1)',color:C.accent,cursor:'pointer',fontSize:11,fontFamily:'inherit'}, children: '📐 宏' }),
|
|
292
|
+
state.status==='loaded' && jsx.jsx('button', { onClick:loadFromTavern, style:{padding:'5px 8px',borderRadius:6,border:'1px solid '+C.border,background:C.bg3,color:C.text,cursor:'pointer',fontSize:13,fontFamily:'inherit'}, children: '🔄' }),
|
|
293
|
+
state.status==='loaded' && jsx.jsx('button', { onClick:toggleAll, style:{padding:'5px 10px',borderRadius:6,border:'1px solid '+C.border,background:C.bg3,color:C.text,cursor:'pointer',fontSize:11,fontFamily:'inherit'}, children: collapsed.size>0?'📖 展开全部':'📕 折叠全部' }),
|
|
294
|
+
state.status==='loaded' && jsx.jsx('button', { onClick:handleGenerate, style:{padding:'7px 18px',borderRadius:8,border:'none',background:'linear-gradient(135deg,#c5a065,#d4a76a)',color:'#1a1a1a',cursor:'pointer',fontSize:13,fontWeight:700,fontFamily:'inherit',boxShadow:'0 2px 8px rgba(197,160,101,0.25)'}, children: '⚡ 生成 MUV 块' }),
|
|
295
|
+
jsx.jsx('button', { onClick:onClose, style:{padding:'5px 12px',borderRadius:6,border:'1px solid '+C.border,background:C.bg3,color:C.text,cursor:'pointer',fontSize:12,fontFamily:'inherit'}, children: '✕' })
|
|
296
|
+
] }),
|
|
297
|
+
|
|
298
|
+
// Content
|
|
299
|
+
state.status==='idle' && jsx.jsxs('div', { style:{flex:1,display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',gap:16,padding:24}, children: [
|
|
300
|
+
savedState && savedState.cardName && jsx.jsxs('div', { style:{background:C.bg2,border:'1px solid '+C.border,borderRadius:10,padding:'14px 24px',textAlign:'center',maxWidth:400,width:'100%'}, children: [
|
|
301
|
+
jsx.jsx('div', { style:{fontSize:14,fontWeight:600,marginBottom:4}, children: '📌 '+savedState.cardName }),
|
|
302
|
+
jsx.jsx('div', { style:{fontSize:11,color:C.dim,marginBottom:10}, children: savedState.schemas.length+' 个分组 · '+(savedState.edits?Object.keys(savedState.edits).length:0)+' 个修改' }),
|
|
303
|
+
jsx.jsx('button', { onClick:()=>{ setCollapsed(new Set()); setState({status:'loaded',cardName:savedState.cardName,schemas:savedState.schemas,initvarData:savedState.initvarData,edits:savedState.edits||{},generatedBlock:'',error:'',search:''}) }, style:{padding:'6px 20px',borderRadius:8,border:'none',background:'linear-gradient(135deg,#c5a065,#d4a76a)',color:'#1a1a1a',cursor:'pointer',fontSize:13,fontWeight:700,fontFamily:'inherit',boxShadow:'0 2px 8px rgba(197,160,101,0.25)'}, children: '📂 继续编辑' })
|
|
304
|
+
] }),
|
|
305
|
+
jsx.jsxs('div', { onClick:()=>fileRef.current?.click(), onDragOver:e=>e.preventDefault(), onDrop:handleDrop, style:{padding:40,border:'2px dashed '+C.border,borderRadius:12,textAlign:'center',cursor:'pointer',color:C.dim,maxWidth:400,width:'100%'}, children: [
|
|
306
|
+
jsx.jsx('div', { style:{fontSize:28,marginBottom:8}, children: '📁' }),
|
|
307
|
+
jsx.jsx('div', { children: '拖放 MUV 角色卡 JSON 到此处' }),
|
|
308
|
+
jsx.jsx('input', { ref:fileRef, type:'file', accept:'.json', style:{display:'none'}, onChange:e=>{const f=e.target.files[0];if(f)readFile(f)} })
|
|
309
|
+
] }),
|
|
310
|
+
jsx.jsx('button', { onClick:loadFromTavern, style:{padding:'10px 24px',borderRadius:8,border:'1px solid '+C.gold,background:'rgba(197,160,101,0.1)',color:C.gold,cursor:'pointer',fontSize:13,fontWeight:600,fontFamily:'inherit'}, children: '🍺 从酒馆加载当前角色卡' })
|
|
311
|
+
] }),
|
|
312
|
+
state.status==='loading' && jsx.jsx('div', { style:{flex:1,display:'flex',alignItems:'center',justifyContent:'center',color:C.dim}, children: '⏳ 解析中...' }),
|
|
313
|
+
state.error && jsx.jsx('div', { style:{padding:12,color:C.danger}, children: '⚠ '+state.error }),
|
|
314
|
+
|
|
315
|
+
// Tree
|
|
316
|
+
state.status==='loaded' && state.schemas.length===0 && jsx.jsxs('div', { style:{flex:1,display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',gap:12,padding:24,color:C.dim}, children: [
|
|
317
|
+
jsx.jsx('div', { style:{fontSize:14,fontWeight:600,color:C.text}, children: '📋 '+state.cardName }),
|
|
318
|
+
jsx.jsx('div', { style:{fontSize:12}, children: '此卡没有预设初始变量' }),
|
|
319
|
+
jsx.jsx('div', { style:{fontSize:11,textAlign:'center',lineHeight:1.6}, children: '玩起来后 LLM 输出 <UpdateVariable> 块时,变量会自动出现在这里。' }),
|
|
320
|
+
jsx.jsx('div', { style:{fontSize:11,color:C.gold}, children: '也可以手动拖放角色卡 JSON 文件加载初始数据。' })
|
|
321
|
+
] }),
|
|
322
|
+
state.status==='loaded' && state.schemas.length>0 && jsx.jsx('div', { style:{flex:1,overflow:'auto',padding:'8px 12px'}, children:
|
|
323
|
+
state.schemas.filter(f=>!state.search||f.name.includes(state.search)||JSON.stringify(f).includes(state.search)).map(field =>
|
|
324
|
+
jsx.jsxs('div', { key:field.name, style:{marginBottom:10,background:C.bg1,border:'1px solid '+C.border,borderRadius:8,overflow:'hidden'}, children: [
|
|
325
|
+
jsx.jsxs('div', { onClick:()=>toggleCollapse(field.name), style:{display:'flex',alignItems:'center',gap:8,padding:'8px 14px',cursor:'pointer',userSelect:'none',background:collapsed.has(field.name)?C.bg1:C.bg2,borderBottom:collapsed.has(field.name)?'none':'1px solid '+C.border}, onMouseEnter:e=>e.currentTarget.style.background=C.hover, onMouseLeave:e=>e.currentTarget.style.background=collapsed.has(field.name)?C.bg1:C.bg2, children: [
|
|
326
|
+
jsx.jsx('span', { style:{fontSize:12,color:C.dim,width:16}, children: collapsed.has(field.name)?'▸':'▾' }),
|
|
327
|
+
jsx.jsx('span', { style:{fontWeight:700,fontSize:14,flex:1}, children: field.name }),
|
|
328
|
+
jsx.jsx('span', { style:{fontSize:10,padding:'2px 8px',borderRadius:10,background:'rgba(197,160,101,0.12)',color:C.sub}, children: field.children.length+' 项' })
|
|
329
|
+
] }),
|
|
330
|
+
!collapsed.has(field.name) && jsx.jsx('div', { style:{padding:'4px 0'}, children:
|
|
331
|
+
field.children.map(c => renderField(c, state.initvarData[field.name]||{}, field.name, 1))
|
|
332
|
+
})
|
|
333
|
+
] })
|
|
334
|
+
)
|
|
335
|
+
}),
|
|
336
|
+
|
|
337
|
+
// Generated block
|
|
338
|
+
state.generatedBlock && jsx.jsxs('div', { style:{flexShrink:0,borderTop:'2px solid '+C.accent}, children: [
|
|
339
|
+
jsx.jsxs('div', { style:{display:'flex',alignItems:'center',gap:10,padding:'8px 14px',background:C.bg1}, children: [
|
|
340
|
+
jsx.jsx('span', { style:{fontWeight:600,fontSize:12,color:C.accent,flex:1}, children: '✅ MUV 块 ('+state.generatedBlock.split('\n').length+' 行)' }),
|
|
341
|
+
jsx.jsx('button', { onClick:()=>navigator.clipboard.writeText(state.generatedBlock), style:{padding:'4px 12px',borderRadius:4,border:'1px solid '+C.border,background:C.bg3,color:C.text,cursor:'pointer',fontSize:11,fontFamily:'inherit'}, children: '📋 复制' }),
|
|
342
|
+
jsx.jsx('button', { onClick:()=>setState(s=>({...s,generatedBlock:''})), style:{padding:'4px 10px',borderRadius:4,border:'1px solid '+C.border,background:C.bg3,color:C.text,cursor:'pointer',fontSize:11,fontFamily:'inherit'}, children: '✕' })
|
|
343
|
+
] }),
|
|
344
|
+
jsx.jsx('pre', { style:{background:'#0d0e12',padding:12,fontSize:11,fontFamily:'monospace',whiteSpace:'pre-wrap',wordBreak:'break-all',maxHeight:180,overflow:'auto',margin:0,color:'#c0c0c0',borderTop:'1px solid '+C.border}, children: state.generatedBlock })
|
|
345
|
+
] })
|
|
346
|
+
] })
|
|
347
|
+
] })
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ============ Sidebar entry ============
|
|
351
|
+
function sidebarRoot() { const col=document.querySelector('[data-pane="sidebar"], [class*="sidebarCol"]'); return col?col.querySelector('[class*="logoRow"]')?.parentElement??col.firstElementChild:undefined }
|
|
352
|
+
function newSessionBtn(root) { const n=root.querySelector('button[class*="newSession"]'); if(n)return n; for(const c of root.children) if(c.tagName==='BUTTON') return c }
|
|
353
|
+
function createEntry() {
|
|
354
|
+
const e=document.createElement('button'); e.type='button'; e.dataset.dshMuvEntry=''
|
|
355
|
+
const ex=document.querySelector('[data-dsh-taskboard-entry], [data-dsh-ssh-entry]'); e.className=ex?ex.className:''
|
|
356
|
+
e.setAttribute('aria-label','MUV 表格'); e.innerHTML=`<span style="flex:none;display:inline-flex;justify-content:center;align-items:center">${ICON}</span><span style="text-overflow:ellipsis;overflow:hidden">MUV 表格</span>`
|
|
357
|
+
Object.assign(e.style,{width:'100%',height:'32px',color:'var(--dsw-alias-label-secondary)',cursor:'pointer',whiteSpace:'nowrap',background:'none',border:'none',borderRadius:'8px',alignItems:'center',gap:'8px',padding:'0 12px',fontSize:'13px',display:'flex'})
|
|
358
|
+
e.addEventListener('mouseenter',()=>{e.style.background='var(--dsw-specific-sidebar-nav-item-hover)';e.style.color='var(--dsw-alias-label-primary)'})
|
|
359
|
+
e.addEventListener('mouseleave',()=>{e.style.background='none';e.style.color='var(--dsw-alias-label-secondary)'})
|
|
360
|
+
return e
|
|
361
|
+
}
|
|
362
|
+
function placeEntry(root, entry) {
|
|
363
|
+
const btn=newSessionBtn(root); if(!btn) return false
|
|
364
|
+
if(entry.parentElement!==root){const row=btn.closest('[class*="logoRow"]');const base=row&&row.parentElement===root?row:btn;const family=Array.from(root.children).filter(el=>el instanceof HTMLElement&&el.matches('[data-dsh-taskboard-entry], [data-dsh-ssh-entry]'));const anchor=family.length>0?family[0]:base.nextElementSibling;root.insertBefore(entry,anchor)}
|
|
365
|
+
return true
|
|
366
|
+
}
|
|
367
|
+
let vc=null, vr=null
|
|
368
|
+
function showView(){if(vc){vc.style.display='';return};vc=document.createElement('div');vc.setAttribute('data-dsh-muv-view','');document.body.appendChild(vc);vr=ReactDOM.createRoot(vc);vr.render(jsx.jsx(MuvTableView,{onClose:()=>{if(vc)vc.style.display='none'}}))}
|
|
369
|
+
function mountSidebarEntry(){if(document.querySelector('[data-dsh-muv-entry]'))return()=>{};const e=createEntry();e.addEventListener('click',showView);let root,placed=false;const tp=()=>{if(root&&!root.isConnected){root=undefined;placed=false}if(placed){if(document.body.contains(e))return;root=undefined;placed=false}root??=sidebarRoot();if(!root)return;placed=placeEntry(root,e)};const o=new MutationObserver(tp);o.observe(document.body,{childList:true,subtree:true});tp();return()=>{o.disconnect();e.remove();if(vc)vc.remove();vr=null;vc=null}}
|
|
370
|
+
|
|
371
|
+
// ============ apply ============
|
|
372
|
+
let forceReload = false
|
|
373
|
+
let lastPresetId = ''
|
|
374
|
+
|
|
375
|
+
// ── DOM observer: detect session / preset switches by watching the URL ──
|
|
376
|
+
let _lastPath = window.location.pathname + window.location.search
|
|
377
|
+
function detectSessionChange() {
|
|
378
|
+
try {
|
|
379
|
+
const currentPath = window.location.pathname + window.location.search
|
|
380
|
+
if (currentPath !== _lastPath) {
|
|
381
|
+
_lastPath = currentPath
|
|
382
|
+
document.dispatchEvent(new CustomEvent('tavern-session-changed', {
|
|
383
|
+
detail: { path: currentPath }
|
|
384
|
+
}))
|
|
385
|
+
forceReload = true
|
|
386
|
+
}
|
|
387
|
+
} catch (_) {}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
exports.inject = []
|
|
391
|
+
exports.apply = function () {
|
|
392
|
+
// Listen for tavern preset changes (dispatched by other plugins or DSH internals)
|
|
393
|
+
const onPresetChanged = (e) => {
|
|
394
|
+
forceReload = true
|
|
395
|
+
lastPresetId = e.detail?.presetId || ''
|
|
396
|
+
}
|
|
397
|
+
document.addEventListener('tavern-preset-changed', onPresetChanged)
|
|
398
|
+
|
|
399
|
+
// Listen for tavern session changes
|
|
400
|
+
const onSessionChanged = () => {
|
|
401
|
+
forceReload = true
|
|
402
|
+
}
|
|
403
|
+
document.addEventListener('tavern-session-changed', onSessionChanged)
|
|
404
|
+
|
|
405
|
+
let d=null; const tm=()=>{if(d)return;if(sidebarRoot())d=mountSidebarEntry()}; tm()
|
|
406
|
+
const o=new MutationObserver(() => { tm(); detectSessionChange() })
|
|
407
|
+
o.observe(document.body,{childList:true,subtree:true})
|
|
408
|
+
// Initial detection
|
|
409
|
+
detectSessionChange()
|
|
410
|
+
return () => {
|
|
411
|
+
o.disconnect()
|
|
412
|
+
document.removeEventListener('tavern-preset-changed', onPresetChanged)
|
|
413
|
+
document.removeEventListener('tavern-session-changed', onSessionChanged)
|
|
414
|
+
if (d) d()
|
|
415
|
+
if (macroVc) { macroVc.remove(); macroVc = null; macroVr = null }
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return module.exports
|
|
419
|
+
}
|
|
420
420
|
})
|