Files
LittleWhiteBox/modules/story-outline/story-outline.html

526 lines
103 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" />
<title>小白板</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
<style>
*{margin:0;padding:0;box-sizing:border-box}:root{--bg:#FDFDF9;--bg2:#FFFFFF;--bg3:#F0F0EE;--c:#1C1917;--c2:#57534E;--c3:#78716C;--bd:#E5E5E4;--r4:8px;--r6:12px;--r8:16px;--shadow-sm:0 1px 3px rgba(0,0,0,0.1);--shadow:0 4px 6px -1px rgba(0,0,0,0.1),0 2px 4px -1px rgba(0,0,0,0.06);--shadow-lg:0 20px 25px -5px rgba(0,0,0,0.1),0 10px 10px -5px rgba(0,0,0,0.04)}html,body{width:100%;height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:var(--bg);color:var(--c)}.fc,.fcc{display:flex;align-items:center}.fcc{justify-content:center}.g4{gap:4px}.g6{gap:6px}.g10{gap:10px}.g20{gap:20px}.p12{padding:12px}.r6{border-radius:var(--r6)}.bd{border:1px solid var(--bd)}.bg3{background:var(--bg3)}.fs12{font-size:12px}.fw6{font-weight:600}.c2{color:var(--c2)}.usn{user-select:none}.f1{flex:1}.jsb{justify-content:space-between}.mt6{margin-top:6px}.mt8{margin-top:8px}.mt10{margin-top:10px}.mt12{margin-top:12px}.mt16{margin-top:16px}.mb4{margin-bottom:4px}.mb8{margin-bottom:8px}.mb10{margin-bottom:10px}.mb12{margin-bottom:12px}.lh15{line-height:1.5}.lh165{line-height:1.65}.w100{width:100px}.w140{width:140px}.btn{padding:8px 16px;background:var(--bg2);font-size:13px;font-weight:500;cursor:pointer;border:1px solid var(--bd);border-radius:var(--r4);color:var(--c);transition:all .2s;box-shadow:var(--shadow-sm);display:inline-flex;align-items:center;justify-content:center;gap:6px}.btn:hover{background:#fafafa;border-color:#dcdcdc;box-shadow:var(--shadow);transform:translateY(-1px)}.btn:active{transform:translateY(0)}.btn:disabled{opacity:.5;cursor:not-allowed;box-shadow:none;transform:none}.btn-p{background:var(--c);color:#fff;border-color:var(--c)}.btn-p:hover{background:#2a2a28;color:#fff;border-color:#2a2a28}.btn-due{background:#ffe1e1 !important;border-color:#ff9b9b !important;color:#7a1f1f !important}.btn-s{padding:6px 12px;font-size:12px}.btn-c,.btn-add{padding:0;border-radius:50%;flex-shrink:0}.btn-c{width:32px;height:32px;background:var(--bg3);border-color:var(--bd);color:var(--c2)}.btn-c:hover{background:#e7e5e4;color:var(--c)}.btn-add{width:36px;height:36px;background:var(--bg2);color:var(--c);box-shadow:var(--shadow-sm)}.fold{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r6);margin-bottom:8px;overflow:hidden;box-shadow:var(--shadow-sm);transition:box-shadow .2s}.fold:hover{box-shadow:var(--shadow)}.fold-h{padding:12px 16px;cursor:pointer;display:flex;justify-content:space-between;align-items:center}.fold-a{color:var(--c3);font-size:10px;transition:transform .15s}.fold.exp .fold-a{transform:rotate(180deg)}.fold-b{max-height:0;overflow:hidden;transition:max-height .2s cubic-bezier(0,1,0,1)}.fold.exp .fold-b{max-height:500px;transition:max-height .3s ease-in-out}.side-nav-wrap{position:fixed;left:12px;top:50%;transform:translateY(-50%);z-index:500;display:flex;flex-direction:column;gap:8px}.side-glass{background:rgba(255,255,255,.8);backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,0.5);box-shadow:var(--shadow)}.side-nav,.side-menu{display:flex;flex-direction:column;border-radius:24px}.side-nav{gap:6px;padding:8px 6px}.side-menu{position:relative;padding:6px 6px;align-items:center}.side-menu-btn,.nav-i{display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--c3);transition:all .2s}.side-menu-btn{width:32px;height:32px;font-size:12px;border-radius:10px}.nav-i{width:36px;height:36px;border-radius:50%}.nav-i i{font-size:14px}.side-menu-btn:hover,.nav-i:hover{background:var(--bg3);color:var(--c)}.side-menu-btn.act,.nav-i.act{background:var(--c);color:#fff;box-shadow:var(--shadow-sm)}.side-menu-panel{position:absolute;left:100%;top:50%;transform:translateY(-50%);margin-left:12px;display:none;flex-direction:column;gap:6px;padding:8px;background:var(--bg2);border-radius:var(--r6);border:1px solid var(--bd);white-space:nowrap;z-index:600;box-shadow:var(--shadow)}.side-menu-panel.show{display:flex}.side-menu-panel .btn{font-size:11px;padding:6px 12px;width:100%;justify-content:flex-start}.toolbar{height:52px;background:var(--bg2);border-bottom:1px solid var(--bd);padding:0 16px 0 64px;position:relative;z-index:200;box-shadow:var(--shadow-sm)}.toolbar-t{font-size:16px;font-weight:700;margin-right:auto;letter-spacing:-0.5px;color:var(--c)}.toolbar-t span{font-weight:400;color:var(--c3);margin-left:8px;font-size:12px;letter-spacing:0}.main-wrap{width:100%;height:calc(100% - 52px);position:relative}.page{position:absolute;inset:0;background:var(--bg);display:none;overflow:hidden}.page.act{display:block}.page-pad{padding:24px 24px 24px 72px;overflow-y:auto;height:100%}.banner{width:100%;height:180px;border-radius:var(--r8);overflow:hidden;margin-bottom:24px;position:relative;background:var(--bg3);box-shadow:var(--shadow-sm)}.banner img{width:100%;height:100%;object-fit:cover}.banner-ov{position:absolute;inset:0;background:linear-gradient(transparent 40%,rgba(0,0,0,.6));display:flex;align-items:flex-end;padding:20px}.banner-ov div{color:#fff;font-size:14px;font-weight:500;text-shadow:0 2px 4px rgba(0,0,0,0.3)}.sec-t{font-size:13px;color:var(--c3);margin-bottom:12px;font-weight:600;letter-spacing:0.5px;text-transform:uppercase}.sec-hd{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}.sec-hd .sec-t{margin-bottom:0}.news-sec{margin-bottom:32px}.news-t{font-size:14px;font-weight:600;color:var(--c)}.news-time{font-size:11px;color:var(--c3)}.fold.exp .news-b{padding:0 16px 16px}.news-b p{font-size:13px;color:var(--c2);line-height:1.7}.user-guide{padding:20px;background:var(--bg2);border-radius:var(--r6);border:1px solid var(--bd);margin-bottom:24px;box-shadow:var(--shadow-sm)}.user-guide-state{font-size:14px;font-weight:500;margin-bottom:12px;color:var(--c)}.user-guide-actions{display:flex;flex-direction:column;gap:8px}.user-guide-action{padding:10px 14px;background:var(--bg);border:1px solid var(--bd);border-radius:var(--r4);font-size:13px;color:var(--c2);cursor:pointer;transition:all .2s}.user-guide-action:hover{background:var(--bg2);border-color:var(--c3)}#mapWrap{width:100%;height:100%;background:var(--bg);cursor:grab;overflow:hidden;position:relative;touch-action:none}#inner,#lines{position:absolute;width:4000px;height:4000px;transform-origin:0 0}#lines{pointer-events:none}.item{position:absolute;padding:8px 16px;background:var(--bg2);border:1px solid var(--bd);border-radius:20px;font-size:12px;font-weight:600;white-space:nowrap;cursor:pointer;user-select:none;box-shadow:var(--shadow-sm);color:var(--c);transition:transform .1s,border-color .1s}.item.node-main{background:var(--c);color:#fff;border-color:var(--c);z-index:10}.item.node-home{background:#fff;color:var(--c);border:2px solid var(--c);z-index:11}.item.node-sub{background:var(--bg2)}.item:hover{transform:scale(1.05);z-index:20}.item.hl{border-color:var(--c);box-shadow:0 0 0 3px rgba(68,64,60,0.2);z-index:20}.map-act{position:absolute;top:16px;right:16px;z-index:100}#btn-goto{display:none;background:var(--c);color:#fff;border:none;box-shadow:var(--shadow)}#btn-goto.show{display:flex}.map-lbl{position:absolute;top:16px;left:72px;z-index:100;background:var(--bg2);border:1px solid var(--bd);border-radius:20px;padding:8px 16px;font-size:12px;color:var(--c);cursor:pointer;display:flex;align-items:center;box-shadow:var(--shadow-sm)}.map-lbl i:first-child{margin-right:8px;color:var(--c3)}.map-lbl .fa-chevron-down{margin-left:8px;font-size:9px;color:var(--c3)}.map-lbl-sel{position:absolute;opacity:0;cursor:pointer;inset:0;border:none;background:transparent;-webkit-appearance:none;appearance:none}.panel{position:fixed;background:var(--bg2);padding:12px 16px;border-radius:var(--r6);font-size:12px;color:var(--c2);border:1px solid var(--bd);box-shadow:var(--shadow)}#zoom-ind{bottom:20px;right:20px;font-family:monospace}#tip{bottom:20px;left:72px;max-width:280px;line-height:1.6;max-height:220px;overflow-y:auto;display:none}#tip .desc{display:none}#tip .info-w{display:block}#tip.show{display:block}.loc-lk{color:var(--c);font-weight:600;cursor:pointer;border-bottom:1px dashed var(--c3);transition:color .2s}.loc-lk:hover{color:#000;border-bottom-style:solid}.local-map-title{font-size:15px;font-weight:700;color:var(--c);margin-bottom:12px;padding-bottom:10px;border-bottom:1px solid var(--bd)}.info-h{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}.info-t{font-weight:700;color:var(--c);font-size:14px}.info-bk{font-size:11px;color:var(--c3);cursor:pointer;padding:4px 8px;border-radius:4px;background:var(--bg3)}.info-c{color:var(--c2);font-size:13px;line-height:1.6}.comm-hd{display:flex;align-items:center;gap:12px;margin-bottom:20px}.comm-tabs{display:flex;gap:4px;background:var(--bg3);padding:4px;border-radius:var(--r6)}.comm-tab{padding:8px 20px;text-align:center;border-radius:var(--r4);cursor:pointer;font-size:12px;font-weight:600;color:var(--c3);transition:all .2s;border:none}.comm-tab:first-child{border-radius:var(--r4)}.comm-tab:last-child{border-radius:var(--r4);border-left:none}.comm-tab.act{background:var(--bg2);color:var(--c);box-shadow:var(--shadow-sm)}.comm-sec{display:none}.comm-sec.act{display:block}.ct-hd{gap:12px}.ct-av,.chat-av{width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:600;font-size:14px;flex-shrink:0;box-shadow:var(--shadow-sm)}.ct-info{flex:1;min-width:0}.ct-name{font-size:14px;font-weight:600;color:var(--c)}.ct-st{font-size:12px;color:var(--c3);margin-top:2px}.ct-det{padding:0 16px 16px}.ct-info-text{font-size:13px;color:var(--c2);line-height:1.7;margin:0 0 16px;background:var(--bg);padding:10px;border-radius:var(--r4)}.ct-acts{display:flex;gap:10px}.ct-acts .btn{flex:1;justify-content:center;font-size:12px;height:32px}.empty{text-align:center;padding:60px 20px;color:var(--c3);font-size:13px;background:var(--bg3);border-radius:var(--r6);border:1px dashed var(--bd)}.modal{position:fixed;inset:0;z-index:10000;display:none;justify-content:center;align-items:center;backdrop-filter:blur(2px);transition:all .3s}.modal.act{display:flex}.modal-bd{position:absolute;inset:0;background:rgba(0,0,0,.15)}.modal-p{position:relative;width:90%;max-width:700px;max-height:85vh;background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r8);overflow:hidden;display:flex;flex-direction:column;box-shadow:var(--shadow-lg)}.modal-p.sm{max-width:400px}.modal-p.lg{max-width:800px}.modal-hd,.modal-ft{padding:16px 20px;background:var(--bg2)}.modal-hd{justify-content:space-between;border-bottom:1px solid var(--bd)}.modal-hd h2{font-size:16px;font-weight:700;color:var(--c)}.modal-ft{justify-content:flex-end;gap:12px;border-top:1px solid var(--bd);background:#fafaf9}.modal-x{width:32px;height:32px;background:transparent;cursor:pointer;border:none;border-radius:50%;color:var(--c3);font-size:16px}.modal-x:hover{background:var(--bg3);color:var(--c)}.modal-by{flex:1;overflow-y:auto;padding:24px}.form-g{margin-bottom:20px}.form-l{display:block;font-size:12px;font-weight:600;margin-bottom:8px;color:var(--c2);text-transform:uppercase;letter-spacing:0.5px}.form-in{width:100%;padding:10px 14px;border:1px solid var(--bd);border-radius:var(--r4);font-size:14px;outline:none;background:var(--bg);color:var(--c);transition:all .2s;box-shadow:inset 0 1px 2px rgba(0,0,0,0.03)}.form-in:focus{background:#fff;border-color:var(--c3);box-shadow:0 0 0 3px rgba(0,0,0,0.05)}.form-ta{min-height:100px;resize:vertical;font-family:inherit;line-height:1.6}.goto-d{font-size:14px;color:var(--c);font-weight:500;padding:12px 16px;background:var(--bg3);border-radius:var(--r4);margin-bottom:16px;border:1px solid var(--bd)}.ed-ta{width:100%;min-height:120px;padding:16px;background:var(--bg);border:1px solid var(--bd);border-radius:var(--r4);font-family:"SF Mono",Monaco,Consolas,monospace;font-size:12px;line-height:1.6;color:var(--c);resize:vertical;outline:0;overflow:auto}.ed-ta:focus{background:#fff;border-color:var(--c3)}.ed-preview{margin-top:12px;padding:16px;background:var(--bg3);border:1px solid var(--bd);border-radius:var(--r4);font-size:11px;font-family:"SF Mono",Monaco,Consolas,monospace;white-space:pre-wrap;display:none;color:var(--c2)}.ed-err{padding:12px;background:#fef2f2;border:1px solid #fecaca;border-radius:var(--r4);color:#b91c1c;font-size:13px;margin-top:12px;display:none}.ed-err.vis{display:block}.chat{position:fixed;top:0;right:-420px;width:380px;height:100%;background:var(--bg2);border-left:1px solid var(--bd);z-index:600;display:flex;flex-direction:column;box-shadow:var(--shadow-lg);transition:right .3s cubic-bezier(0.16,1,0.3,1)}.chat.act{right:0}.chat-hd{padding:16px 20px;border-bottom:1px solid var(--bd);display:flex;align-items:center;gap:14px;flex-shrink:0}.chat-t{flex:1;min-width:0}.chat-nm{font-size:15px;font-weight:700;color:var(--c)}.chat-st{font-size:12px;color:var(--c3);margin-top:2px}.chat-hd-acts{display:flex;align-items:center;gap:6px}.chat-compress,.chat-clr,.chat-x{width:36px;height:36px;border:none;background:transparent;cursor:pointer;border-radius:50%;display:flex;align-items:center;justify-content:center;color:var(--c3);transition:background .2s}.chat-compress:hover,.chat-clr:hover,.chat-x:hover{background:var(--bg3);color:var(--c)}.chat-compress:disabled{opacity:.4;cursor:not-allowed}.chat-divider{width:100%;text-align:center;padding:12px 0;color:var(--c3);font-size:11px;position:relative}.chat-divider:before,.chat-divider:after{content:"";position:absolute;top:50%;width:30%;height:1px;background:var(--bd)}.chat-divider:before{left:0}.chat-divider:after{right:0}.chat-msgs{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:16px;background:var(--bg)}.chat-msg{max-width:85%;padding:12px 16px;border-radius:18px;font-size:14px;line-height:1.6;white-space:pre-wrap;box-shadow:var(--shadow-sm)}.chat-msg.sent{align-self:flex-end;background:var(--c);color:#fff;border-bottom-right-radius:4px}.chat-msg.recv{align-self:flex-start;background:var(--bg2);border-bottom-left-radius:4px;border:1px solid var(--bd);color:var(--c)}.chat-msg.typing{font-style:italic;opacity:.7;background:transparent;box-shadow:none;border:none;padding:0}.chat-in-w{padding:16px;border-top:1px solid var(--bd);display:flex;gap:12px;flex-shrink:0;background:var(--bg2)}.chat-in{flex:1;padding:12px 16px;border:1px solid var(--bd);border-radius:24px;font-size:14px;outline:none;background:var(--bg);transition:all .2s}.chat-in:focus{border-color:var(--c);background:#fff;box-shadow:var(--shadow-sm)}.chat-in:disabled,.chat-send:disabled{opacity:.5;cursor:not-allowed}.chat-send{width:40px;height:40px;border:none;background:var(--c);color:#fff;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:var(--shadow-sm);transition:transform .2s}.chat-send:hover{transform:scale(1.05)}.chat-emp{text-align:center;color:var(--c3);font-size:13px;padding:60px 20px}.loc-list{max-height:300px;overflow-y:auto}.loc-i{padding:14px 16px;border:1px solid var(--bd);border-radius:var(--r6);margin-bottom:10px;cursor:pointer;background:var(--bg2);transition:all .2s}.loc-i:hover{border-color:var(--c3);transform:translateY(-1px)}.loc-i.sel{border-color:var(--c);background:var(--c);color:#fff;box-shadow:var(--shadow)}.loc-i.sel .loc-i-info{color:rgba(255,255,255,0.8)}.loc-i-nm{font-size:14px;font-weight:600}.loc-i-info{font-size:12px;opacity:.7;margin-top:4px}.mob-pop{position:fixed;bottom:0;left:0;right:0;background:var(--bg2);border-top:1px solid var(--bd);border-radius:20px 20px 0 0;z-index:101;display:none;flex-direction:column;box-shadow:var(--shadow-lg)}.mob-pop.act{display:flex}.pop-hd{padding:12px;cursor:grab;touch-action:none;flex-shrink:0}.pop-handle{width:40px;height:5px;background:var(--bd);border-radius:3px;margin:0 auto}.pop-ct{flex:1;overflow-y:auto;padding:0 24px 24px;-webkit-overflow-scrolling:touch}.pop-desc{display:none}.pop-info{display:block}.pop-info-h{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.pop-info-t{font-weight:700;font-size:16px;color:var(--c)}.pop-info-bk{font-size:12px;color:var(--c3);cursor:pointer;padding:6px 12px;border-radius:var(--r4);background:var(--bg3)}.pop-h-ind{position:absolute;top:50%;right:8px;transform:translateY(-50%);display:flex;flex-direction:column;gap:4px;opacity:0}.mob-pop.drag .pop-h-ind{opacity:1}.pop-h-ind span{width:4px;height:10px;background:var(--bd);border-radius:2px}.pop-h-ind span.act{background:var(--c)}.side-pop{position:fixed;top:44px;right:0;bottom:0;background:var(--bg2);border-left:1px solid var(--bd);z-index:90;display:none;width:12px;transition:width 0.3s cubic-bezier(0.4,0,0.2,1);overflow:hidden}.side-pop.show{display:flex}.side-pop.act{width:332px;box-shadow:var(--shadow-lg)}.side-pop-handle{width:12px;background:var(--bg3);cursor:pointer;flex-shrink:0;display:flex;align-items:center;justify-content:center;touch-action:none;border-right:1px solid var(--bd);transition:background 0.2s}.side-pop-handle:hover{background:var(--bd)}.side-pop-bar{width:3px;height:36px;background:var(--bd);border-radius:2px}.side-pop-ct{flex:1;overflow-y:auto;overflow-x:hidden;padding:16px;margin-bottom:30vh;min-width:0}.side-pop-hd{font-size:11px;color:var(--c3);margin-bottom:10px}.side-pop-desc{font-size:13px;color:var(--c2);line-height:1.7;margin-top:2em}.set-row{display:flex;align-items:center;gap:8px;margin-bottom:10px}.set-row .form-in{flex:1}.set-row .btn{flex-shrink:0}.set-hint{font-size:11px;color:var(--c3);margin-top:4px}.set-test{display:flex;align-items:center;gap:8px;margin-top:8px}.set-test-res{font-size:12px;padding:6px 10px;border-radius:var(--r4);display:none}.set-test-res.ok{display:block;background:#dcfce7;color:#166534;border:1px solid #86efac}.set-test-res.err{display:block;background:#fef2f2;color:#b91c1c;border:1px solid #fecaca}.warn{color:#f80}.settings-body{height:480px;overflow-y:auto;background:var(--bg2);padding:24px}.settings-nav{display:flex;align-items:center;gap:4px;background:var(--bg3);padding:4px;border-radius:var(--r6);height:40px}.set-nav-item{padding:0 16px;height:32px;background:transparent;border:none;border-radius:var(--r4);cursor:pointer;font-size:13px;font-weight:500;color:var(--c3);display:flex;align-items:center;gap:8px;transition:all .2s;white-space:nowrap}.set-nav-item:hover{color:var(--c2)}.set-nav-item.act{background:var(--bg2);color:var(--c);box-shadow:var(--shadow-sm);font-weight:600}.set-tab-page{display:none;animation:fadeIn .3s}.set-tab-page.act{display:block}.set-group{background:#fff;border:1px solid var(--bd);border-radius:var(--r6);padding:20px;margin-bottom:20px;box-shadow:var(--shadow-sm)}.set-group-t{font-size:14px;font-weight:700;color:var(--c);margin-bottom:16px;display:flex;align-items:center;gap:8px;padding-bottom:10px;border-bottom:1px dashed var(--bd)}.tips-box{background:var(--bg3);padding:10px 12px;border-radius:var(--r4);font-size:12px;color:var(--c2);line-height:1.5;margin-top:8px}.btn.w100{width:100%}#data-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:12px}.data-item{display:flex;align-items:center;gap:10px;padding:10px 12px;background:var(--bg);border:1px solid var(--bd);border-radius:var(--r6);cursor:pointer;transition:all .2s;position:relative;min-height:60px}.data-item:hover{border-color:var(--c3);transform:translateY(-1px);box-shadow:var(--shadow-sm)}.data-item.sel{background:#fff;border-color:var(--c);box-shadow:0 0 0 1px var(--c) inset}.data-ck{width:20px;height:20px;border:1px solid var(--bd);background:var(--bg3);border-radius:4px;display:flex;align-items:center;justify-content:center;color:transparent;font-size:10px;flex-shrink:0}.data-item.sel .data-ck{background:var(--c);border-color:var(--c);color:#fff}.data-info{flex:1;min-width:0}.data-nm{font-size:13px;font-weight:600;color:var(--c);margin-bottom:2px}.data-desc{font-size:11px;color:var(--c3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.data-edit{width:28px;height:28px;border-radius:4px;border:1px solid transparent;background:transparent;color:var(--c3);display:flex;align-items:center;justify-content:center;font-size:12px;transition:all .2s;cursor:pointer}.data-edit:hover{background:var(--bg3);color:var(--c);border-color:var(--bd)}@keyframes fadeIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}@media (max-width:600px){.modal-p.lg{width:100%;height:100%;max-height:100%;border-radius:0}.settings-body{height:auto;flex:1;padding:16px}.modal-hd{padding:10px 16px}.set-nav-item{padding:0 10px;font-size:12px}.set-nav-item i{display:none}#data-list{grid-template-columns:1fr}.set-row{flex-wrap:wrap}.mt-mob-8{margin-top:8px}}.data-item{display:flex;align-items:flex-start;gap:10px;padding:12px;background:var(--bg);border:1px solid var(--bd);border-radius:var(--r6);margin-bottom:8px;cursor:pointer}.data-item.sel{border-color:var(--c);background:rgba(34,34,34,.05)}.data-ck{width:18px;height:18px;border:2px solid var(--bd);border-radius:var(--r4);display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:2px}.data-item.sel .data-ck{background:var(--c);border-color:var(--c);color:#fff}.data-ck i{font-size:10px;opacity:0}.data-item.sel .data-ck i{opacity:1}.data-info{flex:1;min-width:0}.data-nm{font-size:13px;font-weight:500;margin-bottom:4px}.data-desc{font-size:11px;color:var(--c3);line-height:1.4}.data-edit{width:28px;height:28px;border:1px solid var(--bd);border-radius:var(--r4);background:var(--bg2);cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;color:var(--c3)}#set-model-list{display:none;margin-top:8px}#world-gen-status,#world-sim-status{display:none;color:#4a9}#adv-u1,#adv-a1,#adv-u2,#adv-a2{height:85px}#adv-json{min-height:140px}#data-edit-ta{min-height:300px}#res-msg{margin-bottom:10px;line-height:1.5}#res-record-box{display:none}#res-action{display:none}#res-record{max-height:200px;overflow-y:auto;background:var(--bg3);padding:8px;border-radius:var(--r4);font-size:12px;white-space:pre-wrap;word-break:break-all}.adv-modal .modal-p{max-width:900px;height:80vh;max-height:800px;display:flex;flex-direction:column;padding:0;background:var(--bg2);border-radius:var(--r8);box-shadow:var(--shadow-lg);border:1px solid var(--bd)}.adv-modal .modal-hd{padding:16px 24px;border-bottom:1px solid var(--bd);background:var(--bg2);flex-shrink:0}.adv-modal .modal-hd h2{font-size:18px;font-weight:700;color:var(--c);letter-spacing:-0.5px}.adv-layout{display:flex;flex:1;overflow:hidden;height:100%}.adv-sidebar{width:260px;background:var(--bg3);border-right:1px solid var(--bd);padding:20px;overflow-y:auto;display:flex;flex-direction:column;gap:24px;flex-shrink:0}.adv-sidebar .form-g{margin-bottom:0}.adv-sidebar .set-hint{font-size:12px;color:var(--c3);line-height:1.6}.adv-main{flex:1;padding:24px 32px;overflow-y:auto;background:var(--bg2);display:flex;flex-direction:column;gap:32px;min-width:0}.adv-var-group-title{font-size:12px;font-weight:700;color:var(--c2);text-transform:uppercase;letter-spacing:0.5px;margin:16px 0 8px 0;padding-bottom:4px;border-bottom:1px solid var(--bd)}.adv-var-item{font-size:12px;color:var(--c2);padding:4px 0;display:flex;flex-direction:column;gap:2px}.adv-var-item code{font-family:"SF Mono",Monaco,Consolas,monospace;background:rgba(0,0,0,0.06);padding:2px 5px;border-radius:4px;color:var(--c);font-size:11px;align-self:flex-start;border:1px solid rgba(0,0,0,0.05)}.adv-var-desc{color:var(--c3);font-size:11px;line-height:1.4;padding-left:2px}.adv-section{background:var(--bg);border:1px solid var(--bd);border-radius:var(--r6);padding:20px;box-shadow:var(--shadow-sm)}.adv-h-title{font-size:14px;font-weight:700;color:var(--c);margin-bottom:16px;display:flex;align-items:center;gap:8px}.adv-h-title::before{content:'';width:4px;height:16px;background:var(--c);border-radius:2px;display:block}.uaua-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}.ed-ta-adv{font-family:"SF Mono",Monaco,Consolas,monospace;font-size:12px;line-height:1.5;padding:12px;border:1px solid var(--bd);border-radius:var(--r4);background:#fff;width:100%;resize:vertical;min-height:120px;transition:border-color 0.2s;outline:none}.ed-ta-adv:focus{border-color:var(--c);box-shadow:0 0 0 2px rgba(0,0,0,0.05)}.adv-tag-list{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px}.adv-tag{font-size:10px;padding:4px 8px;background:var(--bg2);border:1px solid var(--bd);border-radius:12px;color:var(--c2);cursor:pointer;transition:all 0.2s;user-select:none}.adv-tag:hover{border-color:var(--c3);color:var(--c);background:#fff}@media(max-width:768px){.adv-layout{flex-direction:column;overflow-y:auto}.adv-sidebar{width:100%;height:auto;border-right:none;border-bottom:1px solid var(--bd);padding:16px}.adv-main{overflow:visible;padding:16px}.uaua-grid{grid-template-columns:1fr}}@media(max-width:550px){.chat{width:100%;right:-100%}.side-pop{bottom:0}.side-nav-wrap{left:6px}.side-nav{padding:6px 4px}.side-menu{padding:5px 4px}.nav-i{width:28px;height:28px}.nav-i i{font-size:11px}.toolbar{padding:0 12px 0 56px}.toolbar-t span{display:none}.btn{padding:6px 10px;font-size:11px}.modal-p{max-width:100%;max-height:100%;border-radius:0}#tip{display:none !important}.page-pad{padding:14px 14px 14px 48px}.map-lbl{left:48px}}
</style></style>
</head>
<body>
<!-- Side Nav -->
<div class="side-nav-wrap">
<div class="side-menu side-glass">
<div class="side-menu-btn" id="btn-side-menu-toggle" title="快捷操作"><i class="fa-solid fa-ellipsis"></i></div>
<div class="side-menu-panel" id="side-menu-panel">
<button class="btn btn-s fc g4" id="btn-gen-local-map"><i class="fa-solid fa-plus"></i>局部地图</button>
<button class="btn btn-s fc g4" id="btn-simulate"><i class="fa-solid fa-rotate"></i>世界推演</button>
<button class="btn btn-s fc g4" id="btn-gen-local-scene"><i
class="fa-solid fa-feather-pointed"></i>局部剧情</button>
</div>
</div>
<nav class="side-nav side-glass">
<div class="nav-i" data-p="world"><i class="fa-solid fa-earth-americas"></i></div>
<div class="nav-i act" data-p="map"><i class="fa-solid fa-map"></i></div>
<div class="nav-i" data-p="comm"><i class="fa-solid fa-phone"></i></div>
</nav>
</div>
<!-- Toolbar -->
<div class="toolbar fc g10 usn">
<div class="toolbar-t">小白板<span>预测试</span></div>
<button class="btn btn-s fc g6" id="btn-deduce"><i class="fa-solid fa-wand-magic-sparkles"></i>生成</button>
<button class="btn btn-s fc g6" id="btn-settings"><i class="fa-solid fa-gear"></i>设置</button>
<button class="btn btn-c fcc" id="btn-close"></button>
</div>
<!-- Main -->
<div class="main-wrap">
<!-- World -->
<div class="page page-pad" id="page-world">
<div class="banner">
<img src="https://picsum.photos/800/300" alt="" />
<div class="banner-ov">
<div>探索未知的世界...</div>
</div>
</div>
<div class="news-sec">
<div class="sec-hd">
<h3 class="sec-t">最新消息</h3>
<button class="btn btn-add fcc" id="btn-refresh-world-news" title="只刷新世界新闻"><i
class="fa-solid fa-rotate"></i></button>
</div>
<div id="news-list"></div>
</div>
<div class="user-guide" id="user-guide">
<h3 class="sec-t">当前状态</h3>
<div class="user-guide-state" id="ug-state">尚未生成世界数据...</div>
<h3 class="sec-t mt12">行动指南</h3>
<div class="user-guide-actions" id="ug-actions">
<div class="user-guide-action">等待世界生成...</div>
</div>
</div>
</div>
<!-- Map -->
<div class="page act" id="page-map">
<div id="mapWrap">
<div class="map-lbl" id="map-lbl">
<i class="fa-solid fa-map-location-dot"></i>
<span id="map-lbl-t">大地图</span>
<i class="fa-solid fa-chevron-down"></i>
<select id="map-lbl-select" class="map-lbl-sel"></select>
</div>
<div class="map-act">
<button class="btn btn-s btn-p fc g6" id="btn-goto"><i class="fa-solid fa-location-arrow"></i><span
id="goto-t">前往</span></button>
</div>
<div id="inner"><svg id="lines"></svg></div>
</div>
<div class="panel" id="zoom-ind">100%</div>
<div class="panel" id="tip">
<div class="desc" id="desc"></div>
<div class="info-w">
<div class="info-h">
<div class="info-t" id="info-t"></div>
<div class="info-bk" id="info-bk">← 返回</div>
</div>
<div class="info-c" id="info-c"></div>
</div>
</div>
</div>
<!-- Contacts -->
<div class="page page-pad" id="page-comm">
<div class="comm-hd">
<div class="comm-tabs">
<div class="comm-tab act" data-t="stranger">陌路人</div>
<div class="comm-tab" data-t="contact">联络人</div>
</div>
<button class="btn btn-add fcc" id="btn-refresh-strangers" title="微信摇一摇,寻找遇过的陌生人"><i
class="fa-solid fa-street-view"></i></button>
<button class="btn btn-add fcc" id="btn-add-ct"><i class="fa-solid fa-plus"></i></button>
</div>
<div id="sec-stranger" class="comm-sec act"></div>
<div id="sec-contact" class="comm-sec"></div>
</div>
</div>
<!-- Chat -->
<div class="chat" id="chat">
<div class="chat-hd">
<div class="chat-av" id="chat-av"></div>
<div class="chat-t">
<div class="chat-nm" id="chat-nm"></div>
<div class="chat-st" id="chat-st"></div>
</div>
<div class="chat-hd-acts">
<button class="chat-compress" id="chat-compress" title="压缩总结"><i
class="fa-solid fa-compress"></i></button>
<button class="chat-clr" id="chat-clr" title="清空记录"><i class="fa-solid fa-trash"></i></button>
<button class="chat-x" id="chat-x"><i class="fa-solid fa-xmark"></i></button>
</div>
</div>
<div class="chat-msgs" id="chat-msgs"></div>
<div class="chat-in-w">
<button class="chat-send" id="chat-back" title="回退"><i class="fa-solid fa-rotate-left"></i></button>
<input type="text" class="chat-in" id="chat-in" placeholder="输入消息..." />
<button class="chat-send" id="chat-send"><i class="fa-solid fa-paper-plane"></i></button>
</div>
</div>
<!-- Right Panel -->
<div class="side-pop" id="side-pop">
<div class="side-pop-handle" id="side-pop-handle">
<div class="side-pop-bar"></div>
</div>
<div class="side-pop-ct">
<div class="side-pop-hd fc jsb">
<span>场景描述</span>
<button class="btn btn-s fc g4" id="btn-refresh-local-map"><i
class="fa-solid fa-rotate-right"></i>刷新</button>
</div>
<div class="side-pop-desc" id="side-desc"></div>
</div>
</div>
<!-- Mobile Bottom Sheet -->
<div class="mob-pop" id="mob-pop">
<div class="pop-h-ind"><span data-l="2"></span><span data-l="1"></span><span data-l="0"></span></div>
<div class="pop-hd" id="pop-hd">
<div class="pop-handle"></div>
</div>
<div class="pop-ct">
<div class="pop-desc" id="mob-desc"></div>
<div class="pop-info">
<div class="pop-info-h">
<div class="pop-info-t" id="mob-info-t"></div>
<div class="pop-info-bk" id="mob-info-bk">← 返回</div>
</div>
<div id="mob-info-c"></div>
</div>
</div>
</div>
<!-- Settings Modal (Redesigned) -->
<div class="modal" id="m-settings">
<div class="modal-bd"></div>
<div class="modal-p lg settings-modal">
<div class="modal-hd fc">
<nav class="settings-nav">
<button class="set-nav-item act" onclick="clickTab(this, 'tab-general')"><i
class="fa-solid fa-sliders"></i> <span class="nav-txt">常规</span></button>
<button class="set-nav-item" onclick="clickTab(this, 'tab-conn')"><i class="fa-solid fa-link"></i>
<span class="nav-txt">连接</span></button>
<button class="set-nav-item" onclick="clickTab(this, 'tab-data')"><i
class="fa-solid fa-database"></i> <span class="nav-txt">数据</span></button>
</nav>
<button class="modal-x fcc"></button>
</div>
<div class="settings-body">
<!-- Content -->
<div class="settings-content">
<!-- Tab: General -->
<div class="set-tab-page act" id="tab-general">
<div class="set-group">
<div class="set-group-t">运行模式</div>
<div class="form-g mb4">
<select class="form-in" id="set-mode">
<option value="story">故事模式 (Story Mode)</option>
<option value="assist">辅助模式 (Assist Mode)</option>
</select>
<div class="tips-box"><i class="fa-solid fa-circle-info"></i>
故事模式包含完整的世界模拟;辅助模式仅用于生成辅助地图/剧情。</div>
</div>
</div>
<div class="set-group">
<div class="set-group-t">剧情控制</div>
<div class="set-row g20">
<div class="form-g f1">
<label class="form-l">当前阶段 (Stage)</label>
<input type="number" class="form-in" id="set-stage" min="0" max="10" value="0" />
</div>
<div class="form-g f1">
<label class="form-l">偏离分数 (Deviation)</label>
<input type="number" class="form-in" id="set-deviation" min="0" max="100"
value="0" />
</div>
</div>
<div class="form-g">
<label class="form-l">推演倒计时 (Sim Countdown)</label>
<div class="fc g10">
<input type="number" class="form-in f1" id="set-sim-target" value="5" />
<span class="fs12 c2">行动数</span>
</div>
<div class="set-hint">当数值≤0时提醒进行世界推演。</div>
</div>
</div>
<div class="set-group">
<div class="set-group-t">历史记录</div>
<div class="set-test">
<label class="form-l">上下文楼层数</label>
<input type="number" class="form-in w100" id="set-history-count" min="0" max="200"
value="50" />
<label class="fc g4 fs12 c2 usn" style="margin-left:auto">
<input type="checkbox" id="set-use-stream" /> <span style="font-weight:600">启用流式
(Streaming)</span>
</label>
</div>
<div class="set-test-res" id="test-res"></div>
</div>
</div>
<!-- Tab: Connection -->
<div class="set-tab-page" id="tab-conn">
<div class="set-group">
<div class="set-group-t">API 设置</div>
<div class="form-g">
<label class="form-l">API 端点 (URL)</label>
<input type="text" class="form-in" id="set-api-url" placeholder="默认使用 SillyTavern 设置" />
</div>
<div class="form-g">
<label class="form-l">API 密钥 (Key)</label>
<input type="password" class="form-in" id="set-api-key" placeholder="如需覆盖请填写" />
</div>
</div>
<div class="set-group">
<div class="set-group-t">模型选择 (Model)</div>
<div class="set-row g10">
<input type="text" class="form-in f1" id="set-model" placeholder="Model ID" />
<button class="btn btn-s" id="btn-fetch-models">获取列表</button>
</div>
<select class="form-in mt6" id="set-model-list"></select>
<div class="mt10">
<button class="btn btn-s btn-p w100" id="btn-test-conn">测试连接</button>
</div>
</div>
</div>
<!-- Tab: Data -->
<div class="set-tab-page" id="tab-data">
<div class="set-group">
<div class="set-group-t">NPC 生成设置</div>
<div class="set-row g20">
<div class="form-g f1">
<label class="form-l">插入位置</label>
<select class="form-in" id="set-npc-position">
<option value="0">Char 前</option>
<option value="1">Char 后</option>
<option value="2">AN 前</option>
<option value="3">AN 后</option>
<option value="5">EM 前</option>
<option value="6">EM 后</option>
</select>
</div>
<div class="form-g f1">
<label class="form-l">条目顺序</label>
<input type="number" class="form-in" id="set-npc-order" min="0" max="1000"
value="100" />
</div>
</div>
</div>
<div class="set-group">
<div class="set-group-t">预设数据 (Presets)</div>
<div class="set-hint mb12">勾选要写入预设的数据块:</div>
<div id="data-list" class="data-grid"></div>
</div>
<div class="set-group">
<div class="set-group-t">高级模板</div>
<div class="set-hint mb10">自定义提示词与JSON格式。</div>
<button class="btn btn-s w100" id="btn-adv-prompts"><i class="fa-solid fa-pen"></i>
编辑高级模板</button>
</div>
</div>
</div>
</div>
<div class="modal-ft fc">
<button class="btn btn-s m-cancel">取消</button>
<button class="btn btn-s btn-p" id="set-save">保存设置</button>
</div>
</div>
</div>
<!-- Advanced Prompts Modal -->
<div class="modal adv-modal" id="m-adv-prompts">
<div class="modal-bd"></div>
<div class="modal-p">
<div class="modal-hd fc">
<h2>高级设置 · 模板编辑</h2><button class="modal-x fcc"></button>
</div>
<div class="adv-layout">
<!-- Sidebar -->
<div class="adv-sidebar">
<div class="form-g">
<label class="form-l">选择模板</label>
<select class="form-in" id="adv-key"></select>
<div class="set-hint mt8">优先级:角色卡 &gt; 原保存 &gt; 默认。</div>
</div>
</div>
<!-- Main Content -->
<div class="adv-main">
<div class="adv-section">
<div class="adv-h-title">UAUA 提示词 (JS Function String)</div>
<div class="uaua-grid">
<div class="form-g"><label class="form-l">User 1 (u1)</label><textarea class="ed-ta-adv"
id="adv-u1"></textarea></div>
<div class="form-g"><label class="form-l">Assistant 1 (a1)</label><textarea
class="ed-ta-adv" id="adv-a1"></textarea></div>
<div class="form-g"><label class="form-l">User 2 (u2)</label><textarea class="ed-ta-adv"
id="adv-u2"></textarea></div>
<div class="form-g"><label class="form-l">Assistant 2 (a2)</label><textarea
class="ed-ta-adv" id="adv-a2"></textarea></div>
</div>
</div>
<div class="adv-section" id="adv-json-wrap">
<div class="adv-h-title">JSON Template</div>
<textarea class="ed-ta-adv" id="adv-json" style="min-height:300px;font-size:13px"></textarea>
</div>
</div>
</div>
<div class="modal-ft fc">
<div class="fc g10 f1">
<button class="btn btn-s" id="adv-reset"><i class="fa-solid fa-rotate-left"></i> 重置</button>
<select class="form-in w140" id="adv-scope" title="选择来源">
<option value="character">角色</option>
<option value="global">通用</option>
</select>
</div>
<button class="btn btn-s" id="adv-save-global">通用保存</button>
<button class="btn btn-s btn-p" id="adv-save-char">角色卡保存</button>
</div>
</div>
</div>
<!-- Data Edit Modal -->
<div class="modal" id="m-data-edit">
<div class="modal-bd"></div>
<div class="modal-p">
<div class="modal-hd fc">
<h2 id="data-edit-title">编辑数据</h2><button class="modal-x fcc"></button>
</div>
<div class="modal-by">
<textarea class="ed-ta" id="data-edit-ta"></textarea>
<pre class="ed-preview" id="data-edit-preview"></pre>
<div class="ed-err" id="data-edit-err"></div>
</div>
<div class="modal-ft fc"><button class="btn btn-s m-cancel">取消</button><button class="btn btn-s btn-p"
id="data-edit-save">保存</button></div>
</div>
</div>
<!-- Goto Modal -->
<div class="modal" id="m-goto">
<div class="modal-bd"></div>
<div class="modal-p sm">
<div class="modal-hd fc">
<h2>确认前往</h2><button class="modal-x fcc"></button>
</div>
<div class="modal-by">
<div class="goto-d" id="goto-d"></div>
<div class="form-g"><label class="form-l">任务目标(可选)</label><textarea class="form-in form-ta"
id="goto-task" placeholder="描述你要做什么..."></textarea></div>
</div>
<div class="modal-ft fc"><button class="btn btn-s m-cancel">取消</button><button class="btn btn-s btn-p"
id="goto-ok">确认前往</button></div>
</div>
</div>
<!-- Invite Modal -->
<div class="modal" id="m-invite">
<div class="modal-bd"></div>
<div class="modal-p sm">
<div class="modal-hd fc">
<h2>邀请前往</h2><button class="modal-x fcc"></button>
</div>
<div class="modal-by">
<div class="goto-d" id="inv-t"></div>
<div class="form-g"><label class="form-l">选择地点</label>
<div class="loc-list" id="loc-list"></div>
</div>
</div>
<div class="modal-ft fc"><button class="btn btn-s m-cancel">取消</button><button class="btn btn-s btn-p"
id="inv-ok">发送邀请</button></div>
</div>
</div>
<!-- World Gen Modal -->
<div class="modal" id="m-world-gen">
<div class="modal-bd"></div>
<div class="modal-p">
<div class="modal-hd fc">
<h2>世界生成</h2><button class="modal-x fcc"></button>
</div>
<div class="modal-by">
<div class="form-g">
<label class="form-l">玩家特殊需求(可选)</label>
<textarea class="form-in form-ta" id="world-gen-req" rows="4"
placeholder="例如:希望有更多悬疑元素、增加一个神秘组织..."></textarea>
<div class="set-hint">AI 会根据当前世界观和你的需求生成完整的世界数据</div>
</div>
<div id="world-gen-status" class="set-hint"></div>
</div>
<div class="modal-ft fc"><button class="btn btn-s m-cancel">取消</button><button class="btn btn-s btn-p"
id="world-gen-ok"><i class="fa-solid fa-wand-magic-sparkles"></i> 开始生成</button></div>
</div>
</div>
<!-- World Sim Modal -->
<div class="modal" id="m-world-sim">
<div class="modal-bd"></div>
<div class="modal-p">
<div class="modal-hd fc">
<h2>世界推演</h2><button class="modal-x fcc"></button>
</div>
<div class="modal-by">
<div class="form-g">
<div class="set-hint mb12">
<strong>推演模式</strong>:根据玩家行为和时间流逝,演化世界状态。
<ul class="lh15" style="margin:8px 0;padding-left:20px;font-size:12px">
<li>L1/L2 将大幅更新</li>
<li>L3/L4 适度调整</li>
<li>L5 保持不变</li>
<li>Stage 将推进到下一阶段</li>
</ul>
</div>
<div class="set-hint warn">⚠️ 此操作会覆盖当前世界数据,建议先备份</div>
</div>
<div id="world-sim-status" class="set-hint"></div>
</div>
<div class="modal-ft fc"><button class="btn btn-s m-cancel">取消</button><button class="btn btn-s btn-p"
id="world-sim-ok"><i class="fa-solid fa-rotate"></i> 开始推演</button></div>
</div>
</div>
<!-- Add Contact Modal -->
<div class="modal" id="m-add-ct">
<div class="modal-bd"></div>
<div class="modal-p sm">
<div class="modal-hd fc">
<h2>添加联络人</h2><button class="modal-x fcc"></button>
</div>
<div class="modal-by">
<div class="form-g">
<label class="form-l">UID世界书条目UID</label>
<div class="set-row">
<input type="text" class="form-in" id="add-uid" placeholder="请输入世界书条目UID" />
<button class="btn btn-s" id="btn-check-uid"><i class="fa-solid fa-search"></i> 检查</button>
</div>
<div class="set-hint">输入角色卡绑定世界书的条目UID</div>
</div>
<div class="form-g" id="name-select-group" style="display:none">
<label class="form-l">选择名字</label>
<select class="form-in" id="add-name">
<option value="">-- 请选择 --</option>
</select>
</div>
<div id="uid-check-err" class="ed-err"></div>
</div>
<div class="modal-ft fc"><button class="btn btn-s m-cancel">取消</button><button class="btn btn-s btn-p"
id="add-ct-ok" disabled>确定</button></div>
</div>
</div>
<!-- Result Modal -->
<div class="modal" id="m-result">
<div class="modal-bd"></div>
<div class="modal-p sm">
<div class="modal-hd fc">
<h2 id="res-title">操作结果</h2><button class="modal-x fcc"></button>
</div>
<div class="modal-by">
<div id="res-msg"></div>
<div id="res-record-box">
<div class="set-hint mb4">详细记录:</div>
<pre id="res-record"></pre>
</div>
</div>
<div class="modal-ft fc">
<button class="btn btn-s" id="res-action"></button>
<button class="btn btn-s btn-p m-cancel">确定</button>
</div>
</div>
</div>
<script>
const D={stage:0,deviationScore:0,simulationTarget:5,meta:{truth:null,onion_layers:null,timeline:null,user_guide:null},world:{},maps:{outdoor:{nodes:[]},indoor:null},sceneSetup:null,contacts:{strangers:[],contacts:[{name:"{{characterName}}",avatar:"",color:"#555",location:"在线",info:"角色卡联络人",online:!0,worldbookUid:"__CHARACTER_CARD__",messages:[],summarizedCount:0}]}};let charSmsHistory={messages:[],summarizedCount:0,summaries:{}};const $=t=>document.getElementById(t),$$=t=>document.querySelectorAll(t),isMob=()=>innerWidth<=550,escHtml=t=>t.replace(/[&<>"']/g,(t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[t]))),h=t=>escHtml(String(t??"")),stripXml=t=>t?t.replace(/<(\w+)[^>]*>[\s\S]*?<\/\1>/g,"").replace(/<[^>]+\/?>/g,"").trim():"",parseLinks=t=>h(t).replace(/\*\*([^*]+)\*\*/g,'<span class="loc-lk" data-loc="$1">$1</span>'),PARENT_ORIGIN=(()=>{try{return new URL(document.referrer).origin}catch{return window.location.origin}})(),post=(t,e={})=>parent.postMessage({source:"LittleWhiteBox-OutlineFrame",type:t,...e},PARENT_ORIGIN),syncSimDueUI=()=>{const t=(Number(D.simulationTarget)||0)<=0;$("btn-simulate")?.classList.toggle("btn-due",t),$("world-sim-ok")?.classList.toggle("btn-due",t)},BtnState={load:(t,e)=>{t.disabled=!0,t._o=t.innerHTML,t.innerHTML=`<i class="fa-solid fa-spinner fa-spin"></i> ${e}`},reset:(t,e)=>{t.disabled=!1,t.innerHTML=e||t._o}},Req={_p:{},_id:0,create(t){const e=t+"_"+ ++this._id;return this._p[t]={id:e},e},get(t){return this._p[t]},match(t){const e=t?.split("_")[0];return this._p[e]?.id===t?this._p[e]:null},set(t,e){Object.assign(this._p[t]||{},e)},clear(t){delete this._p[t]}},openM=t=>$(t).classList.add("act"),closeM=t=>$(t).classList.remove("act"),dirMap={north:[0,-1],south:[0,1],east:[1,0],west:[-1,0],northeast:[1,-1],northwest:[-1,-1],southeast:[1,1],southwest:[-1,1]};let playerLocation="未知",curNode=null,drag=!1,scale=1,offX=0,offY=0,seed=123456789,nodes=[],lines=[],anim=!1,chatTgt=null,invTgt=null,selLoc=null,smsGen=!1,selectedMapValue="current";const inner=$("inner"),svg=$("lines"),mapWrap=$("mapWrap"),popup=$("mob-pop"),chat=$("chat"),sidePop=$("side-pop"),rand=()=>(seed=(9301*seed+49297)%233280)/233280,getCurInside=()=>D.maps?.indoor?.[playerLocation]||null,snaps=()=>[$("pop-hd")?.offsetHeight||0,.3*innerHeight,.65*innerHeight];let popH=0,popDrag=!1,popSY=0,popSH=0,popLv=1;const inds=popup.querySelectorAll(".pop-h-ind span"),setPopH=t=>{const e=snaps();popH=Math.max(e[0],Math.min(.85*innerHeight,t)),popup.style.height=popH+"px",popLv=e.map(((t,e)=>[e,Math.abs(popH-t)])).sort(((t,e)=>t[1]-e[1]))[0][0],inds.forEach(((t,e)=>t.classList.toggle("act",e===popLv)))},snapTo=t=>{popLv=Math.max(0,Math.min(2,t)),setPopH(snaps()[popLv])},openPop=(t=1)=>{popup.classList.add("act"),snapTo(t)};$("side-pop-handle").onclick=()=>sidePop.classList.toggle("act"),$("pop-hd").onmousedown=t=>{popDrag=!0,popSY=t.clientY,popSH=popH||snaps()[1],popup.classList.add("drag"),t.preventDefault()},$("pop-hd").ontouchstart=t=>{t.preventDefault(),popDrag=!0,popSY=t.touches[0].clientY,popSH=popH||snaps()[1],popup.classList.add("drag")},document.onmousemove=t=>{popDrag&&setPopH(popSH+popSY-t.clientY)},document.ontouchmove=t=>{popDrag&&t.touches.length&&(t.preventDefault(),setPopH(popSH+popSY-t.touches[0].clientY))};const endDrag=()=>{popDrag&&(popDrag=!1,popup.classList.remove("drag"),snapTo(popLv))};document.onmouseup=endDrag,document.ontouchend=endDrag,document.ontouchcancel=endDrag;const bindLinks=t=>t.querySelectorAll(".loc-lk").forEach((t=>t.onclick=e=>{e.stopPropagation();const s=t.dataset.loc,o=nodes.find((t=>t.name===s));if(o)return panTo(o),void showInfo(o);const a=getCurInside(),n=a?.nodes?.find((t=>t.name===s));n&&($("info-t").textContent=n.name,$("info-c").textContent=n.info||"暂无信息...",$("tip").classList.add("show"),$("btn-goto").classList.remove("show"),isMob()&&($("mob-info-t").textContent=n.name,$("mob-info-c").textContent=n.info||"暂无信息...",popup.classList.contains("act")||openPop(1)))})),bindFold=t=>t.querySelector(".fold-h").onclick=()=>t.classList.toggle("exp");$$(".modal-bd,.modal-x,.m-cancel").forEach((t=>t.onclick=()=>t.closest(".modal").classList.remove("act")));const openChat=t=>{chatTgt=t,$("chat-av").textContent=t.avatar,$("chat-av").style.background=t.color,$("chat-nm").textContent=t.name,$("chat-st").textContent=t.online?"● 在线":t.location,!t.worldbookUid||t.messages&&t.messages.length?renderMsgs():post("LOAD_SMS_HISTORY",{worldbookUid:t.worldbookUid}),chat.classList.add("act"),$("chat-in").focus()},closeChat=()=>{chat.classList.remove("act"),chatTgt=null,smsGen=!1},renderMsgs=()=>{if(!chatTgt)return;const t=chatTgt.messages||[],e=chatTgt.summarizedCount||0;let s="";t.length?t.forEach(((t,o)=>{e>0&&o===e&&(s+='<div class="chat-divider">—— 以上为已总结消息 ——</div>'),s+=`<div class="chat-msg ${"sent"===t.type?"sent":"recv"}${t.typing?" typing":""}"><div>${escHtml(stripXml(t.text))}</div></div>`})):s='<div class="chat-emp">暂无消息,开始聊天吧</div>',$("chat-msgs").innerHTML=s,$("chat-msgs").scrollTop=$("chat-msgs").scrollHeight,$("chat-compress").disabled=t.length-e<2||smsGen,$("chat-back").disabled=!t.length||smsGen},sendMsg=()=>{const t=$("chat-in").value.trim();if(!t||!chatTgt||smsGen)return;chatTgt.messages=chatTgt.messages||[],chatTgt.messages.push({type:"sent",text:t}),$("chat-in").value="",renderMsgs(),smsGen=!0,$("chat-in").disabled=$("chat-send").disabled=$("chat-back").disabled=!0,chatTgt.messages.push({type:"received",text:"正在输入...",typing:!0}),renderMsgs();const e=Req.create("sms");Req.set("sms",{tgt:chatTgt}),post("SEND_SMS",{requestId:e,contactName:chatTgt.name,worldbookUid:chatTgt.worldbookUid,userMessage:t,chatHistory:chatTgt.messages.filter((t=>!t.typing)).slice(-20),summarizedCount:chatTgt.summarizedCount||0})},isCharCardContact=t=>"__CHARACTER_CARD__"===t?.worldbookUid,contactsForSave=()=>(D.contacts.contacts||[]).filter((t=>!isCharCardContact(t))),saveCt=()=>post("SAVE_CONTACTS",{contacts:contactsForSave(),strangers:D.contacts.strangers}),saveChat=t=>post("SAVE_SMS_HISTORY",{worldbookUid:t.worldbookUid,contactName:t.name,messages:t.messages.filter((t=>!t.typing)),summarizedCount:t.summarizedCount||0});$("chat-x").onclick=closeChat,$("chat-back").onclick=()=>{if(!chatTgt||smsGen)return;const t=chatTgt.messages||[];if(t.length){for(t.pop();t[t.length-1]?.typing;)t.pop();chatTgt.summarizedCount=Math.min(chatTgt.summarizedCount||0,t.length),renderMsgs(),saveCt(),chatTgt.worldbookUid&&saveChat(chatTgt)}},$("chat-clr").onclick=()=>{chatTgt&&confirm(`确定要清空与 ${chatTgt.name} 的所有聊天记录吗?`)&&(chatTgt.messages=[],chatTgt.summarizedCount=0,renderMsgs(),saveCt(),chatTgt.worldbookUid&&saveChat(chatTgt))},$("chat-compress").onclick=()=>{if(!chatTgt||smsGen)return;const t=chatTgt.messages||[],e=chatTgt.summarizedCount||0,s=t.slice(e);if(s.length<2)return void alert("至少需要2条未总结的消息才能压缩");if(!confirm(`确定要压缩总结 ${s.length} 条消息吗?`))return;smsGen=!0,$("chat-compress").disabled=$("chat-in").disabled=$("chat-send").disabled=$("chat-back").disabled=!0;const o=Req.create("compress");Req.set("compress",{tgt:chatTgt}),post("COMPRESS_SMS",{requestId:o,contactName:chatTgt.name,worldbookUid:chatTgt.worldbookUid,messages:t.filter((t=>!t.typing)),summarizedCount:e})},$("chat-send").onclick=sendMsg;const chatIn=$("chat-in");["keydown","keypress","keyup"].forEach((t=>chatIn.addEventListener(t,(t=>t.stopPropagation())))),chatIn.addEventListener("keydown",(t=>{"Enter"!==t.key||t.shiftKey||(t.preventDefault(),sendMsg())}));const openInv=t=>{invTgt=t,selLoc=null,$("inv-t").textContent=`邀请:${t.name}`,$("loc-list").innerHTML=D.maps.outdoor.nodes.map((t=>`<div class="loc-i" data-n="${h(t.name)}"><div class="loc-i-nm">${h(t.name)}</div><div class="loc-i-info">${h(t.info||"")}</div></div>`)).join(""),$$("#loc-list .loc-i").forEach((t=>t.onclick=()=>{$$("#loc-list .loc-i").forEach((t=>t.classList.remove("sel"))),t.classList.add("sel"),selLoc=t.dataset.n})),openM("m-invite")};$("inv-ok").onclick=()=>{if(!selLoc||!invTgt)return;const t=$("inv-ok");BtnState.load(t,"询问中...");const e=Req.create("invite"),s=canonicalLoc(selLoc);Req.set("invite",{contact:invTgt,loc:s,btn:t}),post("SEND_INVITE",{requestId:e,contactName:invTgt.name,contactUid:invTgt.worldbookUid,targetLocation:s,smsHistory:(invTgt.messages||[]).map((t=>"sent"===t.type?`{{user}}: ${t.text}`:`${invTgt.name}: ${t.text}`)).join("\n")})};let addCtState={uid:"",name:"",keys:[]};const resetAddCt=()=>{addCtState={uid:"",name:"",keys:[]},$("add-uid").value="",$("add-name").value="",$("add-name").innerHTML='<option value="">-- 请选择 --</option>',$("name-select-group").style.display="none",$("uid-check-err").classList.remove("vis"),$("add-ct-ok").disabled=!0,BtnState.reset($("btn-check-uid"),'<i class="fa-solid fa-search"></i> 检查')},showUidErr=t=>{$("uid-check-err").textContent=t,$("uid-check-err").classList.add("vis")};$("btn-check-uid").onclick=()=>{const t=$("add-uid").value.trim();t?($("uid-check-err").classList.remove("vis"),BtnState.load($("btn-check-uid"),"检查中"),$("name-select-group").style.display="none",$("add-ct-ok").disabled=!0,addCtState.uid=t,post("CHECK_WORLDBOOK_UID",{uid:t,requestId:Req.create("uidck")})):showUidErr("请输入UID")},$("btn-add-ct").onclick=()=>{resetAddCt(),openM("m-add-ct")},$("add-ct-ok").onclick=()=>{const t=addCtState.uid||$("add-uid").value.trim(),e=addCtState.name||$("add-name").value.trim();t&&e&&(D.contacts.contacts.some((e=>e.worldbookUid===t))?showUidErr("该联络人已存在"):(D.contacts.contacts.push({name:e,avatar:e[0],color:"#"+Math.floor(16777215*Math.random()).toString(16).padStart(6,"0"),location:"未知",worldbookUid:t,messages:[]}),saveCt(),closeM("m-add-ct"),render()))};const genAddCt=(t,e,s)=>{BtnState.load(s,"检查中");const o=Req.create("stgwb");Req.set("stgwb",{name:t,info:e,btn:s}),post("CHECK_STRANGER_WORLDBOOK",{requestId:o,strangerName:t})};function canonicalLoc(t){return String(t||"").trim().replace(/^\u90ae\u8f6e/,"")}$("btn-refresh-strangers").onclick=()=>{const t=$("btn-refresh-strangers");BtnState.load(t,"");const e=Req.create("extract");Req.set("extract",{btn:t}),post("EXTRACT_STRANGERS",{requestId:e,existingContacts:D.contacts.contacts,existingStrangers:D.contacts.strangers})},$("world-gen-ok").onclick=()=>{const t=$("world-gen-ok"),e=$("world-gen-status");BtnState.load(t,"生成中"),e.style.display="block",e.textContent="正在生成世界数据,请稍候...",post("GENERATE_WORLD",{requestId:Req.create("wgen"),playerRequests:$("world-gen-req").value.trim()})},$("world-sim-ok").onclick=()=>{if(!D.meta&&!D.timeline&&!D.maps?.outdoor)return void alert("请先生成世界数据,再进行推演");const t=$("world-sim-ok"),e=$("world-sim-status");BtnState.load(t,"推演中"),e.style.display="block",e.style.color="#4a9",e.textContent="正在分析玩家行为并推演世界变化...",post("SIMULATE_WORLD",{requestId:id,currentData:JSON.stringify({meta:D.meta,timeline:D.timeline,world:D.world,maps:D.maps},null,2)})},$("btn-deduce").onclick=()=>openM("m-world-gen"),$("btn-simulate").onclick=()=>openM("m-world-sim"),$("btn-side-menu-toggle").onclick=()=>{const t=$("side-menu-panel"),e=$("btn-side-menu-toggle");t.classList.toggle("show"),e.classList.toggle("act",t.classList.contains("show"))},document.addEventListener("click",(t=>{t.target.closest(".side-menu")||($("side-menu-panel")?.classList.remove("show"),$("btn-side-menu-toggle")?.classList.remove("act"))}));const getWaitingContacts=t=>{const e=canonicalLoc(t);return e?(D.contacts.contacts||[]).filter((t=>t?.waitingAt&&canonicalLoc(t.waitingAt)===e)):[]},finishTravel=(t,e)=>{playerLocation=t,selectedMapValue="current";const s=getWaitingContacts(t);s.forEach((t=>delete t.waitingAt)),saveAll(),render(),hideInfo();const o=$("goto-task")?.value;let a=`{{user}}离开了${e||"上一地点"},来到${t}${o?"意图:"+o:""}`;s.length&&(a+=` ${s.map((t=>t.name)).join("、")}已经在这里等你了。`),post("EXECUTE_SLASH_COMMAND",{command:`/sendas name="剧情任务" ${a}`})};$("goto-ok").onclick=()=>{if(!curNode)return;const t=$("goto-ok"),e=D.maps?.outdoor?.nodes?.find((t=>t.name===playerLocation)),s={name:playerLocation,info:e?.info||""},o=curNode.name,a=D.maps?.indoor?.[o];if(a?.description)return BtnState.reset(t,"确认前往"),closeM("m-goto"),finishTravel(o,s.name),void switchMapView("current");let n="main"===curNode.type?"main":"home"===curNode.type?"home":"sub";const r=Req.create("scene");Req.set("scene",{node:curNode,prev:s.name}),BtnState.load(t,"生成中"),post("SCENE_SWITCH",{requestId:r,prevLocationName:s.name,prevLocationInfo:s.info,targetLocationName:curNode.name,targetLocationType:n,targetLocationInfo:curNode.data?.info||"",playerAction:$("goto-task").value||""})},$("btn-gen-local-map").onclick=()=>{const t=$("btn-gen-local-map");BtnState.load(t,"生成中");const e=Req.create("localmap");Req.set("localmap",{btn:t}),post("GENERATE_LOCAL_MAP",{requestId:e,outdoorDescription:D.maps?.outdoor?.description||""})},$("btn-refresh-local-map").onclick=()=>{if(!playerLocation)return void showResultModal("提示","请先生成世界数据",!0);const t=$("btn-refresh-local-map"),e=D.maps?.indoor?.[playerLocation],s=e;if(!s)return void showResultModal("提示","当前区域没有局部地图,请先生成",!0);BtnState.load(t,"刷新中");const o=Req.create("localmaprf");Req.set("localmaprf",{btn:t,loc:playerLocation}),post("REFRESH_LOCAL_MAP",{requestId:o,locationName:playerLocation,currentLocalMap:s,outdoorDescription:D.maps?.outdoor?.description||""})},$("btn-gen-local-scene").onclick=()=>{if(!playerLocation||"未知"===playerLocation)return void showResultModal("提示","请先生成世界数据",!0);const t=$("btn-gen-local-scene");BtnState.load(t,"生成中");const e=D.maps?.outdoor?.nodes?.find((t=>t.name===playerLocation)),s=D.maps?.indoor?.[playerLocation],o=s?.description||e?.info||e?.data?.info||"",a=Req.create("localscene");Req.set("localscene",{btn:t,loc:playerLocation}),post("GENERATE_LOCAL_SCENE",{requestId:a,locationName:playerLocation,locationInfo:o})},$("btn-refresh-world-news").onclick=()=>{const t=$("btn-refresh-world-news");if(!t)return;t.disabled=!0,t._o=t._o??t.innerHTML,t.innerHTML='<i class="fa-solid fa-rotate fa-spin"></i>';const e=Req.create("newsrf");Req.set("newsrf",{btn:t}),post("REFRESH_WORLD_NEWS",{requestId:e})};const saveAll=()=>post("SAVE_ALL_DATA",{allData:{meta:D.meta,world:D.world,outdoor:D.maps.outdoor,indoor:D.maps.indoor,sceneSetup:D.sceneSetup,strangers:D.contacts.strangers,contacts:contactsForSave()},playerLocation}),dataKeys=[["meta","大纲","核心真相、洋葱结构、时间线、用户指南",()=>D.meta,t=>D.meta=t],["world","世界资讯","世界新闻等信息",()=>D.world,t=>D.world=t],["outdoor","大地图","室外区域的地点和路线",()=>D.maps.outdoor,t=>D.maps.outdoor=t],["indoor","局部地图","隐藏的室内/局部场景地图",()=>D.maps.indoor,t=>D.maps.indoor=t],["sceneSetup","区域剧情","当前区域的 Side Story",()=>D.sceneSetup,t=>D.sceneSetup=t],["characterContactSms","角色卡短信","角色卡联络人的短信记录",()=>({messages:charSmsHistory?.messages||[],summarizedCount:charSmsHistory?.summarizedCount||0,summaries:charSmsHistory?.summaries||{}}),t=>{t&&"object"==typeof t&&(charSmsHistory={messages:charSmsHistory?.messages||[],summarizedCount:charSmsHistory?.summarizedCount||0,...t||{}})}],["strangers","陌路人","已遇见但未建立联系的角色",()=>D.contacts.strangers,t=>D.contacts.strangers=t],["contacts","联络人","已添加的联系人",()=>contactsForSave(),t=>{const e=(D.contacts.contacts||[]).find(isCharCardContact);D.contacts.contacts=(e?[e]:[]).concat(Array.isArray(t)?t:[])}]];let gSet={apiUrl:"",apiKey:"",model:"",mode:"assist"},dataCk={},editCtx=null,commSet={historyCount:50,npcPosition:0,npcOrder:100,stream:!1},promptDefaults={jsonTemplates:{},promptSources:{}},promptStores={global:{jsonTemplates:{},promptSources:{}},character:{jsonTemplates:{},promptSources:{}}};const reqSet=()=>post("GET_SETTINGS"),renderDataList=()=>{$("data-list").innerHTML=dataKeys.map((([t,e,s])=>`<div class="data-item ${dataCk[t]?"sel":""}" data-k="${t}"><div class="data-ck"><i class="fa-solid fa-check"></i></div><div class="data-info"><div class="data-nm">${e}</div><div class="data-desc">${s}</div></div><button class="data-edit" data-k="${t}" title="编辑"><i class="fa-solid fa-pen"></i></button></div>`)).join(""),$$("#data-list .data-item").forEach((t=>t.onclick=e=>{if(e.target.closest(".data-edit"))return;const s=t.dataset.k;dataCk[s]=!dataCk[s],t.classList.toggle("sel",dataCk[s])})),$$("#data-list .data-edit").forEach((t=>t.onclick=e=>{e.stopPropagation(),openDataEdit(t.dataset.k)}))},ADV_PROMPT_ITEMS=[["sms","短信回复"],["invite","邀请回复"],["npc","NPC 生成"],["stranger","提取陌路人"],["worldGenStep1","大纲生成"],["worldGenStep2","世界生成"],["worldSim","世界推演(故事模式)"],["worldSimAssist","世界推演(辅助模式)"],["worldNewsRefresh","世界新闻刷新"],["sceneSwitch","场景切换"],["localMapGen","局部地图生成"],["localMapRefresh","局部地图刷新"],["localSceneGen","局部剧情生成"],["summary","总结压缩"]],advHasJsonTemplate=t=>{const e=promptDefaults?.jsonTemplates||{};return Object.prototype.hasOwnProperty.call(e,t)},advGetScope=()=>"global"===$("adv-scope")?.value?"global":"character",advStoreHasKey=(t,e)=>{const s=("global"===t?promptStores.global:promptStores.character)||{};return!(!Object.prototype.hasOwnProperty.call(s?.promptSources||{},e)&&!Object.prototype.hasOwnProperty.call(s?.jsonTemplates||{},e))},advGetPromptObj=(t,e="character")=>{const s=promptDefaults?.promptSources||{},o=promptStores?.global?.promptSources||{},a=promptStores?.character?.promptSources||{},n=("global"===e?o[t]||s[t]:a[t]||o[t]||s[t])||{};return{u1:"string"==typeof n.u1?n.u1:"",a1:"string"==typeof n.a1?n.a1:"",u2:"string"==typeof n.u2?n.u2:"",a2:"string"==typeof n.a2?n.a2:""}},advNormalizeDisplayText=t=>{let e=String(t??"").replace(/\r\n/g,"\n").replace(/\r/g,"\n");return!e.includes("\n")&&e.includes("\\n")&&(e=e.replaceAll("\\n","\n")),e.includes("\\t")&&(e=e.replaceAll("\\t","\t")),e.includes("\\`")&&(e=e.replaceAll("\\`","`")),e},advNormalizeSaveText=t=>String(t??"").replace(/\r\n/g,"\n").replace(/\r/g,"\n"),advGetJsonTemplate=(t,e="character")=>{const s=promptDefaults?.jsonTemplates||{},o=promptStores?.global?.jsonTemplates||{},a=promptStores?.character?.jsonTemplates||{};if(!(advHasJsonTemplate(t)||Object.prototype.hasOwnProperty.call(o,t)||Object.prototype.hasOwnProperty.call(a,t)))return"";const n="global"===e?Object.prototype.hasOwnProperty.call(o,t)?o[t]:s[t]:Object.prototype.hasOwnProperty.call(a,t)?a[t]:Object.prototype.hasOwnProperty.call(o,t)?o[t]:s[t];return"string"==typeof n?n:""},advApplyToUI=(t,e="character")=>{const s=advGetPromptObj(t,e),o=(t,e)=>{const s=$(t);s&&(s.value=advNormalizeDisplayText(e))};o("adv-u1",s.u1),o("adv-a1",s.a1),o("adv-u2",s.u2),o("adv-a2",s.a2),$("adv-json-wrap").style.display="";const a=advGetJsonTemplate(t,e),n=$("adv-json");n&&(n.value=String(a??"")),advUpdateVarHelp(t)},advUpdateVarHelp=t=>{$$(".adv-vars-group").forEach((e=>{const s=String(e.dataset.advFor||"").split(",").map((t=>t.trim())).filter(Boolean);e.style.display=!s.length||s.includes(t)?"":"none"}))},advBuildEdits=()=>{const t=t=>advNormalizeSaveText($(t)?.value??"");return{prompt:{u1:t("adv-u1"),a1:t("adv-a1"),u2:t("adv-u2"),a2:t("adv-a2")},jsonTemplate:advNormalizeSaveText($("adv-json")?.value??"")}},advInit=()=>{const t=$("adv-key");if(!t||t._inited)return;t._inited=!0,t.innerHTML=ADV_PROMPT_ITEMS.map((([t,e])=>`<option value="${t}">${e} (${t})</option>`)).join(""),t.onchange=()=>advApplyToUI(t.value,advGetScope());const e=$("adv-scope");e&&!e._inited&&(e._inited=!0,e.onchange=()=>{const t=$("adv-key")?.value;t&&advApplyToUI(t,advGetScope())})},advOpen=()=>{advInit();const t=$("adv-key"),e=t?.value||ADV_PROMPT_ITEMS[0]?.[0];if(e){const t=$("adv-scope");t&&(t.value=advStoreHasKey("character",e)?"character":"global"),advApplyToUI(e,advGetScope())}openM("m-adv-prompts")},advSaveTo=t=>{advInit();const e=$("adv-key")?.value;if(!e)return;const{prompt:s,jsonTemplate:o}=advBuildEdits();post("SAVE_PROMPTS",{scope:t,key:e,prompt:s,jsonTemplate:o}),closeM("m-adv-prompts")},advReset=()=>{advInit();const t=$("adv-key")?.value;t&&(post("SAVE_PROMPTS",{scope:advGetScope(),key:t,reset:!0}),closeM("m-adv-prompts"))},parseJsonLoose=t=>{const e=String(t??"").trim();if(!e)throw new Error("空内容");try{return JSON.parse(e)}catch{}const s=e.match(/```[^\n]*\n([\s\S]*?)\n```/);if(s?.[1]){const t=s[1].trim();try{return JSON.parse(t)}catch{}}const o=(t,s)=>{const o=e.indexOf(t),a=e.lastIndexOf(s);return-1===o||-1===a||a<=o?null:e.slice(o,a+1)},a=o("{","}")??o("[","]");return a?JSON.parse(a):JSON.parse(e)},updateEditPreview=()=>{const t=$("data-edit-preview");t&&(t.style.display="none",t.textContent="")},setEditContent=(t,e)=>{$("data-edit-title").textContent=t,$("data-edit-ta").value=e,$("data-edit-err").classList.remove("vis"),updateEditPreview(),openM("m-data-edit")},openDataEdit=t=>{const e=dataKeys.find((([e])=>e===t));e&&(editCtx={type:"characterContactSms"===t?"charSms":"data",key:t},setEditContent(`编辑 - ${e[1]}`,JSON.stringify(e[3](),null,2)))};$("data-edit-save").onclick=()=>{if(editCtx)try{const t=parseJsonLoose($("data-edit-ta").value);if("data"===editCtx.type){const e=dataKeys.find((([t])=>t===editCtx.key));if(!e)return;e[4](t),render(),saveAll()}else if("charSms"===editCtx.type){const e=t?.summaries??t;if(!e||"object"!=typeof e||Array.isArray(e))throw new Error("需要 summaries 对象");charSmsHistory.summaries=e,post("SAVE_CHAR_SMS_HISTORY",{summaries:e})}closeM("m-data-edit"),editCtx=null}catch(t){$("data-edit-err").textContent=`JSON错误: ${t.message}`,$("data-edit-err").classList.add("vis")}},$("data-edit-ta").addEventListener("input",updateEditPreview);const showTestRes=(t,e)=>{const s=$("test-res");s.textContent=e,s.className="set-test-res "+(t?"ok":"err")},showResultModal=(t,e,s=!1,o=null)=>{$("res-title").textContent=t,$("res-title").style.color=s?"#ff6b6b":"",$("res-msg").textContent=e;const a=$("res-record-box"),n=$("res-record");o?(a.style.display="block",n.textContent="object"==typeof o?JSON.stringify(o,null,2):String(o)):(a.style.display="none",n.textContent="");const r=$("res-action");r.style.display="none",r.textContent="",r.onclick=null,openM("m-result")};function render(){const t=D.world?.news||[];$("news-list").innerHTML=t.length?t.map((t=>`<div class="fold"><div class="fold-h"><div><div class="news-t">${h(t.title)}</div><div class="news-time">${h(t.time||"")}</div></div><i class="fa-solid fa-chevron-down fold-a"></i></div><div class="fold-b news-b"><p>${h(t.content)}</p></div></div>`)).join(""):'<div class="empty">暂无新闻</div>',$$("#news-list .fold").forEach(bindFold);const e=D.meta?.user_guide;e&&($("ug-state").textContent=e.current_state||"未知状态",$("ug-actions").innerHTML=(e.guides||[]).map(((t,e)=>`<div class="user-guide-action" data-idx="${e}">${e+1}. ${h(t)}</div>`)).join("")||'<div class="user-guide-action">暂无行动指南</div>');const s=(t,e)=>(t||[]).length?t.map((t=>`<div class="fold" data-name="${h(t.name||"")}" data-info="${h(t.info||"")}" data-uid="${h(t.worldbookUid||"")}"><div class="fold-h ct-hd fc"><div class="ct-av" style="background:${h(t.color||"")}">${h(t.avatar||"")}</div><div class="ct-info"><div class="ct-name">${h(t.name||"")}</div><div class="ct-st">${t.online?"● 在线":h(t.location)}</div></div><i class="fa-solid fa-chevron-down fold-a"></i></div><div class="fold-b"><div class="ct-det">${t.info?`<div class="ct-info-text">${h(t.info)}</div>`:""}<div class="ct-acts">${e?`<button class="btn btn-s btn-p fc add-btn" data-name="${h(t.name||"")}" data-info="${h(t.info||"")}"><i class="fa-solid fa-user-plus"></i> 添加</button><button class="btn btn-s fc ignore-btn" data-name="${h(t.name||"")}"><i class="fa-solid fa-eye-slash"></i> 忽略</button>`:`<button class="btn btn-s fc msg-btn" data-uid="${h(t.worldbookUid||"")}"><i class="fa-solid fa-message"></i> 短信</button><button class="btn btn-s btn-p fc inv-btn" data-uid="${h(t.worldbookUid||"")}"><i class="fa-solid fa-paper-plane"></i> 邀请</button>`}</div></div></div></div>`)).join(""):'<div class="empty">暂无</div>';if($("sec-stranger").innerHTML=s(D.contacts.strangers,!0),$("sec-contact").innerHTML=s(D.contacts.contacts,!1),$$(".comm-sec .fold").forEach(bindFold),$$(".add-btn").forEach((t=>t.onclick=e=>{e.stopPropagation(),genAddCt(t.dataset.name,t.dataset.info||"",t)})),$$(".ignore-btn").forEach((t=>t.onclick=e=>{e.stopPropagation();const s=D.contacts.strangers.findIndex((e=>e.name===t.dataset.name));s>-1&&(D.contacts.strangers.splice(s,1),saveCt(),render())})),$$(".msg-btn").forEach((t=>t.onclick=e=>{e.stopPropagation();const s=D.contacts.contacts.find((e=>e.worldbookUid===t.dataset.uid));s&&openChat(s)})),$$(".inv-btn").forEach((t=>t.onclick=e=>{e.stopPropagation();const s=D.contacts.contacts.find((e=>e.worldbookUid===t.dataset.uid));s&&openInv(s)})),"current"===selectedMapValue){const t=getCurInside();$("side-desc").innerHTML=t?.description?`<div class="local-map-title">📍 ${h(playerLocation)}</div>`+parseLinks(t.description):parseLinks(D.maps?.outdoor?.description||"")}else $("side-desc").innerHTML=parseLinks(D.maps?.outdoor?.description||"");bindLinks($("side-desc")),$("mob-desc").innerHTML="",renderMapSelector(),renderMap()}function renderMap(){const t=D.maps?.outdoor;if(seed=123456789,inner.querySelectorAll(".item").forEach((t=>t.remove())),svg.innerHTML="",nodes=[],lines=[],!t?.nodes?.length)return;t.nodes.forEach(((t,e)=>{const s=dirMap[t.position]||[0,0],o=Math.hypot(s[0],s[1])||1,a=120*(t.distant||1);nodes.push({id:"n"+e,name:t.name,type:t.type||"sub",pos:t.position,distant:t.distant||1,x:s[0]/o*a,y:s[1]/o*a,data:t})}));const e={};nodes.forEach((t=>(e[t.pos]=e[t.pos]||[]).push(t)));for(let t in e){const s=e[t];if(s.length<=1)continue;const o=dirMap[t]||[0,0],a=Math.hypot(o[0],o[1])||1,n=-o[1]/a,r=o[0]/a,i=(s.length-1)/2;s.sort(((t,e)=>t.distant-e.distant)).forEach(((t,e)=>{t.x+=n*(e-i)*50,t.y+=r*(e-i)*50}))}for(let t=0;t<15;t++)for(let t=0;t<nodes.length;t++)for(let e=t+1;e<nodes.length;e++){const s=nodes[t],o=nodes[e],a=s.x-o.x,n=s.y-o.y,r=Math.hypot(a,n);if(r<80&&r>0){const t=(80-r)/r*.5;s.x+=a*t,s.y+=n*t,o.x-=a*t,o.y-=n*t}}nodes.forEach((t=>{const e=document.createElement("div");e.className=`item node-${t.type}`,e.id=t.id,e.textContent="home"===t.type?"🏠 "+t.name:t.name,e.style.cssText=`left:${t.x+2e3}px;top:${t.y+2e3}px`,e.onclick=e=>{e.stopPropagation(),curNode?.id===t.id?hideInfo():showInfo(t)},inner.appendChild(e)}));for(let t in e){const s=e[t].sort(((t,e)=>t.distant-e.distant));for(let t=0;t<s.length-1;t++)lines.push([s[t],s[t+1]])}const s=Object.values(e).map((t=>t.sort(((t,e)=>t.distant-e.distant))[0])).sort(((t,e)=>Math.atan2(t.y,t.x)-Math.atan2(e.y,e.x)));s.forEach(((t,e)=>lines.push([t,s[(e+1)%s.length]])));const o=(t,e)=>lines.some((([s,o])=>s===t&&o===e||s===e&&o===t));for(let t=0,e=0;e<Math.floor(nodes.length/5)&&t<200;t++){const t=nodes[Math.floor(rand()*nodes.length)],s=nodes[Math.floor(rand()*nodes.length)];t===s||o(t,s)||(lines.push([t,s]),e++)}drawLines()}function drawLines(){svg.innerHTML="";const t=mapWrap.getBoundingClientRect();lines.forEach((([e,s])=>{const o=$(e.id),a=$(s.id);if(!o||!a)return;const n=o.getBoundingClientRect(),r=a.getBoundingClientRect(),i=document.createElementNS("http://www.w3.org/2000/svg","line");i.setAttribute("x1",(n.left+n.width/2-t.left)/scale-offX/scale),i.setAttribute("y1",(n.top+n.height/2-t.top)/scale-offY/scale),i.setAttribute("x2",(r.left+r.width/2-t.left)/scale-offX/scale),i.setAttribute("y2",(r.top+r.height/2-t.top)/scale-offY/scale);const c="main"===e.type&&"main"===s.type||"home"===e.type||"home"===s.type;i.setAttribute("stroke",c?"#333":"#aaa"),i.setAttribute("stroke-width",c?"2":"1"),c||i.setAttribute("stroke-dasharray","4 3"),svg.appendChild(i)}))}$("btn-settings").onclick=()=>{reqSet(),$("set-api-url").value=gSet.apiUrl||"",$("set-api-key").value=gSet.apiKey||"",$("set-model").value=gSet.model||"",$("set-model-list").style.display="none",$("test-res").className="set-test-res",$("set-stage").value=D.stage||0,$("set-deviation").value=D.deviationScore||0,$("set-sim-target").value=D.simulationTarget??5,$("set-mode").value=gSet.mode||"story",$("set-history-count").value=commSet.historyCount||50,$("set-use-stream").checked=!!commSet.stream,$("set-npc-position").value=commSet.npcPosition||0,$("set-npc-order").value=commSet.npcOrder||100,renderDataList(),syncSimDueUI(),openM("m-settings")},$("btn-adv-prompts").onclick=()=>advOpen(),$("adv-save-global").onclick=()=>advSaveTo("global"),$("adv-save-char").onclick=()=>advSaveTo("character"),$("adv-reset").onclick=()=>advReset(),$("btn-fetch-models").onclick=()=>{BtnState.load($("btn-fetch-models"),"加载"),post("FETCH_MODELS",{apiUrl:$("set-api-url").value.trim(),apiKey:$("set-api-key").value.trim()})},$("btn-test-conn").onclick=()=>{$("test-res").className="set-test-res",BtnState.load($("btn-test-conn"),"测试"),post("TEST_CONNECTION",{apiUrl:$("set-api-url").value.trim(),apiKey:$("set-api-key").value.trim(),model:$("set-model").value.trim()})},$("set-save").onclick=()=>{gSet={apiUrl:$("set-api-url").value.trim(),apiKey:$("set-api-key").value.trim(),model:$("set-model").value.trim(),mode:$("set-mode").value||"story"},D.stage=Math.max(0,Math.min(10,parseInt($("set-stage").value,10)||0)),D.deviationScore=Math.max(0,Math.min(100,parseInt($("set-deviation").value,10)||0)),D.simulationTarget=parseInt($("set-sim-target").value,10),Number.isNaN(D.simulationTarget)&&(D.simulationTarget=5),commSet={historyCount:Math.max(0,Math.min(200,parseInt($("set-history-count").value,10)||50)),stream:!!$("set-use-stream").checked,npcPosition:parseInt($("set-npc-position").value,10)||0,npcOrder:Math.max(0,Math.min(1e3,parseInt($("set-npc-order").value,10)||100))};const t={};dataKeys.forEach((([e,,,s])=>{dataCk[e]&&(t[e]=s())})),syncSimDueUI(),post("SAVE_SETTINGS",{globalSettings:gSet,commSettings:commSet,stage:D.stage,deviationScore:D.deviationScore,simulationTarget:D.simulationTarget,playerLocation,dataChecked:dataCk,outlineData:t,allData:{meta:D.meta,timeline:D.timeline,world:D.world,outdoor:D.maps.outdoor,indoor:D.maps.indoor,strangers:D.contacts.strangers,contacts:contactsForSave()}}),closeM("m-settings")},$("btn-close").onclick=()=>post("CLOSE_PANEL"),window.addEventListener("message",(t=>{if(t.origin!==PARENT_ORIGIN||t.source!==parent)return;if("LittleWhiteBox"!==t.data?.source)return;const e=t.data,s=e.type;if("LOAD_SETTINGS"===s){if(e.globalSettings&&(gSet=e.globalSettings),void 0!==e.stage&&(D.stage=e.stage),void 0!==e.deviationScore&&(D.deviationScore=e.deviationScore),void 0!==e.simulationTarget&&(D.simulationTarget=e.simulationTarget),e.playerLocation&&(playerLocation=e.playerLocation),e.commSettings&&(commSet={historyCount:e.commSettings.historyCount??50,npcPosition:e.commSettings.npcPosition??0,npcOrder:e.commSettings.npcOrder??100,stream:!!e.commSettings.stream}),e.dataChecked&&(dataCk=e.dataChecked),e.promptConfig&&(promptDefaults=e.promptConfig.defaults||promptDefaults,e.promptConfig.stores?(promptStores.global=e.promptConfig.stores.global||{jsonTemplates:{},promptSources:{}},promptStores.character=e.promptConfig.stores.character||{jsonTemplates:{},promptSources:{}}):(promptStores.global=e.promptConfig.current||{jsonTemplates:{},promptSources:{}},promptStores.character=promptStores.character||{jsonTemplates:{},promptSources:{}})),e.outlineData){const t=e.outlineData;t.meta&&(D.meta=t.meta),t.world&&(D.world=t.world),t.outdoor&&(D.maps.outdoor=t.outdoor),t.indoor&&(D.maps.indoor=t.indoor),t.sceneSetup&&(D.sceneSetup=t.sceneSetup),t.strangers&&(D.contacts.strangers=t.strangers),t.contacts&&(D.contacts.contacts=t.contacts)}{const t=e.characterContactSmsHistory||{};charSmsHistory={messages:Array.isArray(t.messages)?t.messages:[],summarizedCount:t.summarizedCount||0,summaries:t.summaries||{}}}let t=D.contacts.contacts.find((t=>"__CHARACTER_CARD__"===t.worldbookUid));t||(t=D.contacts.contacts.find((t=>!t.worldbookUid&&"炒饭智能"===t.name)),t?(t.worldbookUid="__CHARACTER_CARD__",t.info="角色卡联络人",t.location="在线",t.online=!0):(D.contacts.contacts.unshift({name:e.characterCardName||"{{characterName}}",avatar:"",color:"#555",location:"在线",info:"角色卡联络人",online:!0,worldbookUid:"__CHARACTER_CARD__",messages:[],summarizedCount:0}),t=D.contacts.contacts[0])),t&&e.characterCardName&&(t.name=e.characterCardName,t.avatar=(e.characterCardName||"")[0]||t.avatar||""),render(),syncSimDueUI(),$("m-settings").classList.contains("act")&&($("set-api-url").value=gSet.apiUrl||"",$("set-api-key").value=gSet.apiKey||"",$("set-model").value=gSet.model||"",$("set-stage").value=D.stage,$("set-deviation").value=D.deviationScore,$("set-sim-target").value=D.simulationTarget??5,$("set-mode").value=gSet.mode||"story",$("set-history-count").value=commSet.historyCount,$("set-use-stream").checked=!!commSet.stream,$("set-npc-position").value=commSet.npcPosition,$("set-npc-order").value=commSet.npcOrder,renderDataList())}else if("PROMPT_CONFIG_UPDATED"===s){if(e.promptConfig&&(promptDefaults=e.promptConfig.defaults||promptDefaults,e.promptConfig.stores?(promptStores.global=e.promptConfig.stores.global||{jsonTemplates:{},promptSources:{}},promptStores.character=e.promptConfig.stores.character||{jsonTemplates:{},promptSources:{}}):(promptStores.global=e.promptConfig.current||{jsonTemplates:{},promptSources:{}},promptStores.character=promptStores.character||{jsonTemplates:{},promptSources:{}}),$("m-adv-prompts").classList.contains("act"))){const t=$("adv-key")?.value;t&&advApplyToUI(t,advGetScope())}}else if("FETCH_MODELS_RESULT"===s){BtnState.reset($("btn-fetch-models"),"获取");const t=$("set-model-list");if(e.error)return t.style.display="none",void showTestRes(!1,"获取模型失败: "+e.error);if(!e.models?.length)return t.style.display="none",void showTestRes(!1,"未找到可用模型");t.innerHTML='<option value="">-- 选择模型 --</option>'+e.models.map((t=>`<option value="${t}">${t}</option>`)).join(""),t.style.display="block",t.onchange=()=>{t.value&&($("set-model").value=t.value)},showTestRes(!0,`找到 ${e.models.length} 个模型`)}else if("TEST_CONN_RESULT"===s)BtnState.reset($("btn-test-conn"),"测试连接"),showTestRes(e.success,e.message);else if("CHECK_WORLDBOOK_UID_RESULT"===s){if(BtnState.reset($("btn-check-uid"),'<i class="fa-solid fa-search"></i> 检查'),!Req.match(e.requestId))return;if(e.error)return void showUidErr(e.error);if(!e.primaryKeys?.length)return void showUidErr("该条目没有主要关键字");addCtState.keys=e.primaryKeys;const t=$("add-name");t.innerHTML='<option value="">-- 请选择 --</option>'+e.primaryKeys.map((t=>`<option value="${t}">${t}</option>`)).join(""),t.onchange=()=>{addCtState.name=t.value,$("add-ct-ok").disabled=!t.value},$("name-select-group").style.display="block",1===e.primaryKeys.length&&(addCtState.name=e.primaryKeys[0],t.value=addCtState.name,$("add-ct-ok").disabled=!1)}else if("SMS_RESULT"===s){const t=Req.get("sms");if(!t||t.id!==e.requestId)return;if(Req.clear("sms"),smsGen=!1,$("chat-in").disabled=$("chat-send").disabled=$("chat-back").disabled=!1,!chatTgt)return;chatTgt.messages=chatTgt.messages.filter((t=>!t.typing)),e.error?chatTgt.messages.push({type:"received",text:`[错误] ${e.error}`}):e.reply&&chatTgt.messages.push({type:"received",text:stripXml(e.reply)}),renderMsgs(),saveCt(),chatTgt.worldbookUid&&saveChat(chatTgt)}else if("SMS_STREAM"===s){const t=Req.get("sms");if(!t||t.id!==e.requestId||!chatTgt)return;const s=chatTgt.messages.find((t=>t.typing));s&&e.text&&(s.text=e.text,renderMsgs())}else if("LOAD_SMS_HISTORY_RESULT"===s){if(!chatTgt||chatTgt.worldbookUid!==e.worldbookUid)return;e.messages?.length&&(chatTgt.messages=e.messages,chatTgt.summarizedCount=e.summarizedCount||0,saveCt()),renderMsgs()}else if("COMPRESS_SMS_RESULT"===s){const t=Req.get("compress");if(!t||t.id!==e.requestId)return;if(Req.clear("compress"),smsGen=!1,$("chat-compress").disabled=$("chat-in").disabled=$("chat-send").disabled=$("chat-back").disabled=!1,!chatTgt)return;if(e.error)return void alert(`压缩失败: ${e.error}`);void 0!==e.newSummarizedCount&&(chatTgt.summarizedCount=e.newSummarizedCount,renderMsgs(),saveCt())}else if("CHECK_STRANGER_WORLDBOOK_RESULT"===s){const t=Req.get("stgwb");if(!t||t.id!==e.requestId)return;const{name:s,info:o,btn:a}=t;if(Req.clear("stgwb"),e.found&&e.worldbookUid){BtnState.reset(a,'<i class="fa-solid fa-user-plus"></i> 添加');const t=D.contacts.strangers.findIndex((t=>t.name===s));if(t>-1){const s=D.contacts.strangers.splice(t,1)[0];D.contacts.contacts.push({name:s.name,avatar:s.avatar||s.name[0],color:s.color||"#"+Math.floor(16777215*Math.random()).toString(16).padStart(6,"0"),location:s.location||"未知",info:s.info||"",worldbookUid:e.worldbookUid,messages:[]}),saveCt(),render()}}else{BtnState.load(a,"生成中");const t=Req.create("npcgen");Req.set("npcgen",{name:s,info:o,btn:a}),post("GENERATE_NPC",{requestId:t,strangerName:s,strangerInfo:o})}}else if("GENERATE_NPC_RESULT"===s){const t=Req.get("npcgen");if(!t||t.id!==e.requestId)return;const{name:s,btn:o}=t;if(Req.clear("npcgen"),BtnState.reset(o,'<i class="fa-solid fa-user-plus"></i> 添加'),e.error)return void showResultModal("生成角色失败","生成 NPC 失败",!0,e.error);if(e.success&&e.worldbookUid){const t=D.contacts.strangers.findIndex((t=>t.name===s));if(t>-1){const o=D.contacts.strangers.splice(t,1)[0],a=e.npcData||{};D.contacts.contacts.push({name:a.name||o.name,avatar:(a.name||o.name)[0],color:o.color||"#"+Math.floor(16777215*Math.random()).toString(16).padStart(6,"0"),location:o.location||"未知",info:a.intro||o.info||"",worldbookUid:e.worldbookUid,messages:[]}),saveCt(),render(),showResultModal("生成成功",`NPC ${s} 已生成并添加到联络人`,!1,e.npcData)}}}else if("EXTRACT_STRANGERS_RESULT"===s){const t=Req.get("extract");if(!t||t.id!==e.requestId)return;const{btn:s}=t;if(Req.clear("extract"),BtnState.reset(s,'<i class="fa-solid fa-rotate"></i>'),e.error)return void showResultModal("提取失败","提取陌路人失败",!0,e.error);if(e.success&&Array.isArray(e.strangers)){if(!e.strangers.length)return void showResultModal("提取结果","没有发现新的陌路人");const t=[...D.contacts.contacts.map((t=>t.name)),...D.contacts.strangers.map((t=>t.name))],s=e.strangers.filter((e=>!t.includes(e.name)));if(!s.length)return void showResultModal("提取结果","提取到的角色都已存在");D.contacts.strangers=D.contacts.strangers.concat(s),saveCt(),render(),showResultModal("提取成功",`成功提取 ${s.length} 个新陌路人`,!1,s)}}else if("GENERATE_WORLD_STATUS"===s){if(!Req.match(e.requestId))return;const t=$("world-gen-status");t.style.display="block",t.style.color="#4a9",t.textContent=e.message}else if("GENERATE_WORLD_RESULT"===s){if(!Req.match(e.requestId))return;Req.clear("wgen");const t=$("world-gen-ok"),s=$("world-gen-status");if(BtnState.reset(t,'<i class="fa-solid fa-wand-magic-sparkles"></i> 开始生成'),e.error){if(s.style.display="none",showResultModal("生成失败","世界生成失败",!0,e.error),String(e.error||"").includes("Step 2")){const e=$("res-action");e.style.display="inline-block",e.textContent="重试 Step2",e.onclick=()=>{closeM("m-result");const e=Req.create("wgen");BtnState.load(t,"重试中"),s.style.display="block",s.style.color="#4a9",s.textContent="准备重试 Step 2/2...",post("RETRY_WORLD_GEN_STEP2",{requestId:e})}}return}if(e.success&&e.worldData){s.style.color="#4a9",s.textContent="生成成功!正在应用数据...";const t=e.worldData;if(t.meta&&(D.meta=t.meta),t.world&&(D.world=t.world),t.maps?.outdoor&&(D.maps.outdoor=t.maps.outdoor),D.stage=0,D.deviationScore=0,t.playerLocation)playerLocation=t.playerLocation;else{const t=D.maps?.outdoor?.nodes?.find((t=>"home"===t.type));playerLocation=t?.name||D.maps?.outdoor?.nodes?.[0]?.name||"未知"}t.maps?.inside&&playerLocation&&(D.maps.indoor=D.maps.indoor||{},D.maps.indoor[playerLocation]=t.maps.inside),selectedMapValue="current",saveAll(),render(),setTimeout((()=>{closeM("m-world-gen"),s.style.display="none",$("world-gen-req").value="",showResultModal("生成成功","世界数据生成完成Stage 和 Deviation 已重置为 0",!1,e.worldData)}),500)}}else if("SIMULATE_WORLD_RESULT"===s){if(!e.isAuto&&!Req.match(e.requestId))return;if(!e.isAuto){Req.clear("wsim");const t=$("world-sim-ok"),s=$("world-sim-status");if(BtnState.reset(t,'<i class="fa-solid fa-rotate"></i> 开始推演'),e.error)return s.style.display="none",void showResultModal("推演失败","世界推演失败",!0,e.error)}if(e.success&&e.simData){if(!e.isAuto){const t=$("world-sim-status");t.style.color="#4a9",t.textContent="推演成功!正在应用数据..."}const t=e.simData;t.meta&&(D.meta=t.meta),t.world&&(D.world=t.world),t.maps?.outdoor&&(D.maps.outdoor=t.maps.outdoor),D.stage=(D.stage||0)+1,saveAll(),render(),e.isAuto||setTimeout((()=>{closeM("m-world-sim"),$("world-sim-status").style.display="none",showResultModal("推演成功",`世界推演完成Stage 已推进到 ${D.stage}`,!1,e.simData)}),500)}}else if("REFRESH_WORLD_NEWS_RESULT"===s){const t=Req.get("newsrf");if(!t||t.id!==e.requestId)return;const{btn:s}=t;if(Req.clear("newsrf"),s&&(s.disabled=!1,s.innerHTML=s._o??'<i class="fa-solid fa-rotate"></i>'),e.error)return void showResultModal("刷新失败","世界新闻刷新失败",!0,e.error);e.success&&Array.isArray(e.news)&&(D.world=D.world||{},D.world.news=e.news,saveAll(),render(),showResultModal("刷新成功",`已更新世界新闻(${e.news.length} 条)`,!1,e.news))}else if("SCENE_SWITCH_RESULT"===s){const t=Req.get("scene");if(!t||t.id!==e.requestId)return;const{node:s,prev:o}=t;if(Req.clear("scene"),BtnState.reset($("goto-ok"),"确认前往"),closeM("m-goto"),e.error)return void showResultModal("切换失败","场景切换失败",!0,e.error);if(e.success&&e.sceneData){const t=e.sceneData;if("number"==typeof t.newScore&&(D.deviationScore=t.newScore),t.localMap&&(D.maps.indoor=D.maps.indoor||{},D.maps.indoor[s.name]=t.localMap),t.strangers?.length){const e=new Set((D.contacts.strangers||[]).map((t=>t.name))),s=t.strangers.filter((t=>!e.has(t.name)));D.contacts.strangers=[...D.contacts.strangers||[],...s]}finishTravel(s.name,o),0!==t.scoreDelta&&showResultModal("切换成功",`场景切换完成!\n偏差值变化: ${t.scoreDelta>0?"+":""}${t.scoreDelta} (当前: ${t.newScore})`,!1,e.sceneData)}}else if("REFRESH_LOCAL_MAP_RESULT"===s){const t=Req.get("localmaprf");if(!t||t.id!==e.requestId)return;const{btn:s,loc:o}=t;if(Req.clear("localmaprf"),BtnState.reset(s,'<i class="fa-solid fa-rotate-right"></i>刷新'),e.error)return void showResultModal("刷新失败","刷新局部地图失败",!0,e.error);if(e.success&&e.localMapData){const t=e.localMapData,s=t.name||o||playerLocation||"当前位置";D.maps.indoor=D.maps.indoor||{},D.maps.indoor[s]=t,playerLocation=s,selectedMapValue="current",saveAll(),render(),t.description&&($("side-desc").innerHTML=`<div class="local-map-title">📍 ${h(s)}</div>`+parseLinks(t.description),bindLinks($("side-desc"))),showResultModal("刷新成功",`局部地图已刷新!当前位置: ${s}`,!1,e.localMapData)}}else if("GENERATE_LOCAL_SCENE_RESULT"===s){const t=Req.get("localscene");if(!t||t.id!==e.requestId)return;const{btn:s,loc:o}=t;if(Req.clear("localscene"),BtnState.reset(s,'<i class="fa-solid fa-feather-pointed"></i>局部剧情'),e.error)return void showResultModal("生成失败","局部剧情生成失败",!0,e.error);if(e.success&&e.sceneSetup){D.sceneSetup={...D.sceneSetup||{},...e.sceneSetup||{}},saveAll(),render();const t=String(e.introduce||"").replace(/^\s*(?:\/?\s*(?:sendas|as)\s+name\s*=\s*(?:"[^"]*"|'[^']*'|\S+)\s+)/i,"").replace(/\s+/g," ").trim();t&&post("EXECUTE_SLASH_COMMAND",{command:`/sendas name="剧情任务" ${t}`}),showResultModal("生成成功",`局部剧情已生成:${o||playerLocation}`,!1,e.sceneSetup)}}else if("SEND_INVITE_RESULT"===s){const t=Req.get("invite");if(!t||t.id!==e.requestId)return;const{contact:s,loc:o,btn:a}=t;if(Req.clear("invite"),BtnState.reset(a,"发送邀请"),e.error)return void showResultModal("邀请失败","邀请发送失败",!0,e.error);if(e.success&&e.inviteData){const t=e.inviteData,a=canonicalLoc(t.targetLocation||o||""),n=D.contacts.contacts.find((t=>t&&s&&t.worldbookUid&&s.worldbookUid&&t.worldbookUid===s.worldbookUid))||D.contacts.contacts.find((t=>t&&s&&t.name===s.name))||s;if(n.messages=n.messages||[],n.messages.push({type:"sent",text:`我邀请你前往「${a}`}),n.messages.push({type:"received",text:t.reply}),t.accepted){const e=canonicalLoc(playerLocation);n.location=a,e&&a&&e===a?(delete n.waitingAt,post("EXECUTE_SLASH_COMMAND",{command:`/sendas name="剧情任务" ${n.name}过来了。`})):n.waitingAt=a,showResultModal("邀请成功",`${n.name} 接受了邀请!\n回复: ${t.reply}`,!1,t)}else showResultModal("邀请被拒",`${n.name} 拒绝了邀请。\n回复: ${t.reply}`,!1,t);saveAll(),n.worldbookUid&&saveChat(n),closeM("m-invite"),render(),openChat(n)}}else if("GENERATE_LOCAL_MAP_RESULT"===s){const t=Req.get("localmap");if(!t||t.id!==e.requestId)return;const{btn:s}=t;if(Req.clear("localmap"),BtnState.reset(s,'<i class="fa-solid fa-plus"></i>局部地图'),e.error)return void showResultModal("生成失败","局部地图生成失败",!0,e.error);if(e.success&&e.localMapData){const t=e.localMapData,s=t.name||"当前位置";D.sceneSetup=null,D.maps.indoor=D.maps.indoor||{},D.maps.indoor[s]=t,playerLocation=s,selectedMapValue="current",saveAll(),render(),t.description&&($("side-desc").innerHTML=`<div class="local-map-title">📍 ${h(s)}</div>`+parseLinks(t.description),bindLinks($("side-desc"))),showResultModal("生成成功",`局部地图生成完成!当前位置: ${s}`,!1,t)}}}));const updateTf=()=>{inner.style.transform=`translate(${offX}px,${offY}px) scale(${scale})`,$("zoom-ind").textContent=Math.round(100*scale)+"%",requestAnimationFrame(drawLines)},initPos=()=>{offX=mapWrap.clientWidth/2-2e3,offY=mapWrap.clientHeight/2-2e3,scale=1,updateTf()};function panTo(t,e=350){if(!t||anim)return;if(!$(t.id))return;const s=-(t.x+2e3)*scale+mapWrap.clientWidth/2,o=-(t.y+2e3)*scale+.25*mapWrap.clientHeight,a=offX,n=offY,r=performance.now();anim=!0,function t(i){const c=Math.min((i-r)/e,1),l=1-Math.pow(1-c,3);offX=a+(s-a)*l,offY=n+(o-n)*l,updateTf(),c<1?requestAnimationFrame(t):anim=!1}(r)}function showInfo(t){if(!t?.data)return;curNode=t,inner.querySelectorAll(".item").forEach((t=>t.classList.remove("hl"))),$(t.id)?.classList.add("hl");const e=t.name===playerLocation;$("btn-goto").classList.toggle("show",!e),e||($("goto-t").textContent=`前往 ${t.name}`);const s=D.maps?.indoor?.[t.name];e&&s?.description?($("side-desc").innerHTML=`<div class="local-map-title">📍 ${h(t.name)}</div>`+parseLinks(s.description),bindLinks($("side-desc"))):($("side-desc").innerHTML=parseLinks(D.maps?.outdoor?.description||""),bindLinks($("side-desc"))),isMob()?($("mob-info-t").textContent=t.name,$("mob-info-c").textContent=e?s?.description||t.data.info||"暂无信息...":t.data.info||"暂无信息...",popup.classList.contains("act")||openPop(1)):($("info-t").textContent=t.name,$("info-c").textContent=t.data.info||"暂无信息...",$("tip").classList.add("show"))}const hideInfo=()=>{curNode=null,inner.querySelectorAll(".item").forEach((t=>t.classList.remove("hl"))),$("btn-goto").classList.remove("show"),$("tip").classList.remove("show")};function renderMapSelector(){const t=$("map-lbl-select");t.innerHTML='<option value="overview">🗺️ 大地图</option>';const e=D.maps?.outdoor?.nodes?.findIndex((t=>t.name===playerLocation)),s=D.maps?.indoor&&D.maps.indoor[playerLocation];if((e>=0||s)&&(t.innerHTML+=`<option value="current">📍 ${h(playerLocation)}(你)</option>`),t.innerHTML+="<option disabled>──────────</option>",D.maps?.outdoor?.nodes?.length&&D.maps.outdoor.nodes.forEach(((e,s)=>{e.name!==playerLocation&&(t.innerHTML+=`<option value="node:${s}">${h(e.name)}</option>`)})),D.maps?.indoor){const e=Object.keys(D.maps.indoor).filter((t=>t!==playerLocation&&!D.maps?.outdoor?.nodes?.some((e=>e.name===t))));e.length&&(t.innerHTML+="<option disabled>── 隐藏地图 ──</option>",e.forEach((e=>t.innerHTML+=`<option value="indoor:${h(e)}">🏠 ${h(e)}</option>`)))}t.value=selectedMapValue,updateMapLabel()}function updateMapLabel(){const t=$("map-lbl-select").value;if("overview"===t)$("map-lbl-t").textContent="大地图";else if("current"===t)$("map-lbl-t").textContent=playerLocation+"(你)";else if(t.startsWith("node:")){const e=parseInt(t.split(":")[1]);$("map-lbl-t").textContent=D.maps?.outdoor?.nodes?.[e]?.name||"未知"}else t.startsWith("indoor:")&&($("map-lbl-t").textContent=t.replace("indoor:",""))}function switchMapView(t){if(selectedMapValue=t,hideInfo(),"overview"===t)$("btn-goto").classList.remove("show"),$("side-desc").innerHTML=parseLinks(D.maps?.outdoor?.description||""),bindLinks($("side-desc")),initPos();else if("current"===t){$("btn-goto").classList.remove("show");const t=getCurInside();t?.description?($("side-desc").innerHTML=`<div class="local-map-title">📍 ${h(playerLocation)}</div>`+parseLinks(t.description),bindLinks($("side-desc"))):($("side-desc").innerHTML=parseLinks(D.maps?.outdoor?.description||""),bindLinks($("side-desc")));const e=nodes.find((t=>t.name===playerLocation));e&&(panTo(e),showInfo(e))}else if(t.startsWith("node:")){const e=parseInt(t.split(":")[1]),s=D.maps?.outdoor?.nodes?.[e];$("side-desc").innerHTML=parseLinks(D.maps?.outdoor?.description||""),bindLinks($("side-desc"));const o=nodes.find((t=>t.name===s?.name));o&&(panTo(o),showInfo(o))}else if(t.startsWith("indoor:")){const e=t.replace("indoor:",""),s=D.maps?.indoor?.[e];e!==playerLocation?($("side-desc").innerHTML=parseLinks(D.maps?.outdoor?.description||""),bindLinks($("side-desc")),curNode={name:e,type:"sub",data:{info:stripXml(s?.description||"")},isIndoor:!0},$("btn-goto").classList.add("show"),$("goto-t").textContent=`前往 ${e}`):($("btn-goto").classList.remove("show"),s?.description&&($("side-desc").innerHTML=`<div class="local-map-title">🏠 ${h(e)}</div>`+parseLinks(s.description),bindLinks($("side-desc"))))}updateMapLabel()}$("info-bk").onclick=hideInfo,$("mob-info-bk").onclick=()=>popup.classList.remove("act"),$("map-lbl-select").onchange=t=>switchMapView(t.target.value);let sx,sy,lastDist=0,lastCX=0,lastCY=0;mapWrap.onmousedown=t=>{anim||t.target.closest(".map-act,.map-lbl")||(t.target.classList.contains("item")||hideInfo(),drag=!0,sx=t.clientX,sy=t.clientY,mapWrap.style.cursor="grabbing")},mapWrap.onmousemove=t=>{drag&&(offX+=t.clientX-sx,offY+=t.clientY-sy,sx=t.clientX,sy=t.clientY,updateTf())},mapWrap.onmouseup=mapWrap.onmouseleave=()=>{drag=!1,mapWrap.style.cursor="grab"},mapWrap.onwheel=t=>{if(anim)return;t.preventDefault();const e=Math.max(.3,Math.min(3,scale+(t.deltaY>0?-.1:.1))),s=mapWrap.getBoundingClientRect(),o=t.clientX-s.left,a=t.clientY-s.top,n=e/scale;offX=o-(o-offX)*n,offY=a-(a-offY)*n,scale=e,updateTf()},mapWrap.ontouchstart=t=>{if(anim||t.target.closest(".map-act,.map-lbl"))return;const e=t.target.closest(".item");e?e._ts={x:t.touches[0].clientX,y:t.touches[0].clientY,t:Date.now()}:(hideInfo(),1===t.touches.length?(drag=!0,sx=t.touches[0].clientX,sy=t.touches[0].clientY):2===t.touches.length&&(drag=!1,lastDist=Math.hypot(t.touches[0].clientX-t.touches[1].clientX,t.touches[0].clientY-t.touches[1].clientY),lastCX=(t.touches[0].clientX+t.touches[1].clientX)/2,lastCY=(t.touches[0].clientY+t.touches[1].clientY)/2))},mapWrap.ontouchmove=t=>{const e=t.target.closest(".item");if(e&&e._ts)Math.hypot(t.touches[0].clientX-e._ts.x,t.touches[0].clientY-e._ts.y)>10&&(delete e._ts,drag=!0,sx=t.touches[0].clientX,sy=t.touches[0].clientY);else if(1===t.touches.length&&drag)t.preventDefault(),offX+=t.touches[0].clientX-sx,offY+=t.touches[0].clientY-sy,sx=t.touches[0].clientX,sy=t.touches[0].clientY,updateTf();else if(2===t.touches.length){t.preventDefault();const e=Math.hypot(t.touches[0].clientX-t.touches[1].clientX,t.touches[0].clientY-t.touches[1].clientY),s=(t.touches[0].clientX+t.touches[1].clientX)/2,o=(t.touches[0].clientY+t.touches[1].clientY)/2,a=Math.max(.3,Math.min(3,scale*(e/lastDist))),n=mapWrap.getBoundingClientRect(),r=s-n.left,i=o-n.top,c=a/scale;offX=r-(r-offX)*c,offY=i-(i-offY)*c,offX+=s-lastCX,offY+=o-lastCY,scale=a,lastDist=e,lastCX=s,lastCY=o,updateTf()}},mapWrap.ontouchend=t=>{const e=t.target.closest(".item");if(e&&e._ts){const s=Date.now()-e._ts.t;if(delete e._ts,s<300){const s=nodes.find((t=>t.id===e.id));s&&(t.preventDefault(),curNode?.id===s.id?hideInfo():showInfo(s))}}drag=!1},$$(".nav-i").forEach((t=>t.onclick=()=>{$$(".nav-i").forEach((t=>t.classList.remove("act"))),$$(".page").forEach((t=>t.classList.remove("act"))),t.classList.add("act"),$(`page-${t.dataset.p}`).classList.add("act");const e="map"===t.dataset.p;sidePop.classList.toggle("show",e),isMob()&&(e?openPop(1):popup.classList.remove("act")),e&&setTimeout((()=>{initPos(),drawLines()}),50)})),$$(".comm-tab").forEach((t=>t.onclick=()=>{$$(".comm-tab").forEach((t=>t.classList.remove("act"))),$$(".comm-sec").forEach((t=>t.classList.remove("act"))),t.classList.add("act"),$(`sec-${t.dataset.t}`).classList.add("act")})),$("btn-goto").onclick=t=>{t.stopPropagation(),curNode&&($("goto-d").textContent=`目的地:${curNode.name}`,$("goto-task").value="",openM("m-goto"))},addEventListener("resize",(()=>requestAnimationFrame(drawLines))),window.clickTab=(t,e)=>{const s=t.closest(".settings-modal");if(!s)return;s.querySelectorAll(".set-nav-item").forEach((t=>t.classList.remove("act"))),t.classList.add("act"),s.querySelectorAll(".set-tab-page").forEach((t=>t.classList.remove("act")));const o=document.getElementById(e);o&&(o.classList.add("act"),o.style.animation="none",o.offsetHeight,o.style.animation=null)},document.addEventListener("DOMContentLoaded",(()=>{render(),initPos(),sidePop.classList.add("show"),sidePop.classList.add("act"),isMob()&&openPop(1),post("FRAME_READY"),setTimeout((()=>{"current"===selectedMapValue&&switchMapView("current")}),100)}));
</script></script>
</body>
</html>