dsh-muv-table 0.2.1 → 0.2.3

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/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: '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 输出 &lt;UpdateVariable&gt; 块时,变量会自动出现在这里。' }),
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 输出 &lt;UpdateVariable&gt; 块时,变量会自动出现在这里。' }),
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
  })