2025-12-19 02:19:10 +08:00
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
|
|
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
|
|
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
|
|
|
|
<title>剧情总结 · Story Summary</title>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
|
|
:root {
|
|
|
|
|
|
--bg-primary: #fafafa;
|
|
|
|
|
|
--bg-secondary: #ffffff;
|
|
|
|
|
|
--bg-tertiary: #f5f5f5;
|
|
|
|
|
|
--text-primary: #1a1a1a;
|
|
|
|
|
|
--text-secondary: #666666;
|
|
|
|
|
|
--text-muted: #999999;
|
|
|
|
|
|
--border-color: #e5e5e5;
|
|
|
|
|
|
--border-light: #f0f0f0;
|
|
|
|
|
|
--accent: #1a1a1a;
|
|
|
|
|
|
--accent-light: #333333;
|
|
|
|
|
|
--highlight: #ff4444;
|
|
|
|
|
|
--highlight-soft: rgba(255, 68, 68, 0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
body {
|
|
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, sans-serif;
|
|
|
|
|
|
background: var(--bg-primary);
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
|
|
|
}
|
|
|
|
|
|
.summary-container {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
padding: 24px 40px;
|
|
|
|
|
|
max-width: 1800px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
.summary-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
padding-bottom: 24px;
|
|
|
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.header-left h1 { font-size: 2rem; font-weight: 300; letter-spacing: -0.02em; margin-bottom: 4px; }
|
|
|
|
|
|
.header-left h1 span { font-weight: 600; }
|
|
|
|
|
|
.header-subtitle { font-size: 0.875rem; color: var(--text-muted); letter-spacing: 0.05em; text-transform: uppercase; }
|
|
|
|
|
|
.header-stats { display: flex; gap: 48px; text-align: right; }
|
|
|
|
|
|
.stat-item { display: flex; flex-direction: column; }
|
|
|
|
|
|
.stat-value { font-size: 2.5rem; font-weight: 200; line-height: 1; letter-spacing: -0.03em; }
|
|
|
|
|
|
.stat-value .highlight { color: var(--highlight); }
|
|
|
|
|
|
.stat-label { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.1em; margin-top: 4px; }
|
|
|
|
|
|
.controls-bar { display: flex; align-items: center; gap: 16px; padding: 12px 0; margin-bottom: 20px; }
|
|
|
|
|
|
.status-checkbox {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.status-checkbox:hover { border-color: var(--accent); }
|
|
|
|
|
|
.status-checkbox input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--highlight); cursor: pointer; }
|
|
|
|
|
|
.status-checkbox strong { color: var(--highlight); }
|
|
|
|
|
|
.btn {
|
|
|
|
|
|
padding: 12px 28px;
|
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn:hover { border-color: var(--accent); background: var(--bg-tertiary); }
|
|
|
|
|
|
.btn-primary { background: var(--accent); color: white; border-color: var(--accent); }
|
|
|
|
|
|
.btn-primary:hover { background: var(--accent-light); }
|
|
|
|
|
|
.btn-primary:disabled { background: #999; border-color: #999; cursor: not-allowed; opacity: 0.7; }
|
|
|
|
|
|
.btn-icon { padding: 10px 16px; display: flex; align-items: center; gap: 6px; }
|
|
|
|
|
|
.btn-icon svg { width: 16px; height: 16px; }
|
|
|
|
|
|
.btn-sm { padding: 8px 16px; font-size: 0.8125rem; }
|
|
|
|
|
|
.btn-connect { background: var(--accent); color: white; border-color: var(--accent); }
|
|
|
|
|
|
.btn-connect:hover { background: var(--accent-light); }
|
|
|
|
|
|
.btn-connect:disabled { opacity: 0.6; cursor: not-allowed; }
|
|
|
|
|
|
.btn-delete { background: transparent; color: var(--highlight); border-color: var(--highlight); }
|
|
|
|
|
|
.btn-delete:hover { background: var(--highlight-soft); }
|
|
|
|
|
|
.spacer { flex: 1; }
|
|
|
|
|
|
.summary-main { display: grid; grid-template-columns: 1fr 480px; gap: 24px; flex: 1; min-height: 0; }
|
|
|
|
|
|
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
|
|
|
|
|
.section-title { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.15em; color: var(--text-secondary); }
|
|
|
|
|
|
.section-edit-btn {
|
|
|
|
|
|
padding: 4px 12px;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
|
font-size: 0.6875rem;
|
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.05em;
|
|
|
|
|
|
}
|
|
|
|
|
|
.section-edit-btn:hover { border-color: var(--accent); color: var(--text-primary); background: var(--bg-tertiary); }
|
|
|
|
|
|
.section-header-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.section-icon-btn {
|
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.section-icon-btn svg {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
.fullscreen-panel {
|
|
|
|
|
|
width: calc(100vw - 48px);
|
|
|
|
|
|
max-width: none;
|
|
|
|
|
|
height: calc(100vh - 48px);
|
|
|
|
|
|
max-height: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.fullscreen-body {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
#relation-chart-fullscreen {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
touch-action: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.left-content { display: flex; flex-direction: column; gap: 24px; min-height: 0; }
|
|
|
|
|
|
.keywords-section { background: var(--bg-secondary); border: 1px solid var(--border-color); padding: 24px; }
|
|
|
|
|
|
.keywords-cloud { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 16px; }
|
|
|
|
|
|
.keyword-tag {
|
|
|
|
|
|
padding: 8px 20px;
|
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
|
border: 1px solid var(--border-light);
|
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
cursor: default;
|
|
|
|
|
|
}
|
|
|
|
|
|
.keyword-tag.primary { background: var(--accent); color: white; border-color: var(--accent); font-weight: 500; }
|
|
|
|
|
|
.keyword-tag.secondary { background: var(--highlight-soft); border-color: rgba(255, 68, 68, 0.2); color: var(--highlight); }
|
|
|
|
|
|
.keyword-tag:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
|
|
|
|
|
|
.timeline-section {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
max-height: 500px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.timeline-list { flex: 1; overflow-y: auto; padding-right: 8px; min-height: 0; }
|
|
|
|
|
|
.timeline-list::-webkit-scrollbar { width: 4px; }
|
|
|
|
|
|
.timeline-list::-webkit-scrollbar-thumb { background: var(--border-color); }
|
|
|
|
|
|
.timeline-item {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
padding-left: 32px;
|
|
|
|
|
|
padding-bottom: 32px;
|
|
|
|
|
|
border-left: 1px solid var(--border-color);
|
|
|
|
|
|
margin-left: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.timeline-item:last-child { border-left-color: transparent; padding-bottom: 0; }
|
|
|
|
|
|
.timeline-dot {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: -5px;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
width: 9px;
|
|
|
|
|
|
height: 9px;
|
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
|
border: 2px solid var(--text-muted);
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.timeline-item:hover .timeline-dot { border-color: var(--highlight); background: var(--highlight); transform: scale(1.3); }
|
|
|
|
|
|
.timeline-item.critical .timeline-dot { border-color: var(--highlight); background: var(--highlight); }
|
|
|
|
|
|
.timeline-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 8px; }
|
|
|
|
|
|
.timeline-title { font-size: 1rem; font-weight: 500; color: var(--text-primary); }
|
|
|
|
|
|
.timeline-time { font-size: 0.75rem; color: var(--text-muted); font-variant-numeric: tabular-nums; }
|
|
|
|
|
|
.timeline-brief { font-size: 0.875rem; color: var(--text-secondary); line-height: 1.7; margin-bottom: 12px; }
|
|
|
|
|
|
.timeline-meta { display: flex; gap: 16px; font-size: 0.75rem; color: var(--text-muted); }
|
|
|
|
|
|
.timeline-meta .importance { color: var(--highlight); font-weight: 500; }
|
|
|
|
|
|
.right-panel { display: flex; flex-direction: column; gap: 24px; min-height: 0; }
|
|
|
|
|
|
.relations-section {
|
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-height: 320px;
|
|
|
|
|
|
max-height: 400px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
#relation-chart { width: 100%; flex: 1; min-height: 200px; touch-action: none; }
|
|
|
|
|
|
.arcs-section {
|
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
max-height: 400px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.arcs-list { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; padding-right: 8px; }
|
|
|
|
|
|
.arcs-list::-webkit-scrollbar { width: 4px; }
|
|
|
|
|
|
.arcs-list::-webkit-scrollbar-thumb { background: var(--border-color); }
|
|
|
|
|
|
.arc-card {
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
|
border: 1px solid var(--border-light);
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
.arc-card:hover { border-color: var(--accent); box-shadow: 0 4px 20px rgba(0,0,0,0.06); }
|
|
|
|
|
|
.arc-card-active { border-color: var(--highlight); background: var(--highlight-soft); box-shadow: 0 4px 20px rgba(255, 68, 68, 0.15); }
|
|
|
|
|
|
.arc-card-active .arc-name { color: var(--highlight); }
|
|
|
|
|
|
.arc-card-active .arc-progress-inner { background: var(--highlight); }
|
|
|
|
|
|
.arc-header { display: flex; justify-content: flex-start; align-items: center; gap: 12px; margin-bottom: 12px; }
|
|
|
|
|
|
.arc-name { font-size: 0.9375rem; font-weight: 600; }
|
|
|
|
|
|
.arc-phase { font-size: 0.75rem; color: var(--text-muted); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
|
|
.arc-progress { height: 3px; background: var(--border-color); margin-bottom: 12px; overflow: hidden; }
|
|
|
|
|
|
.arc-progress-inner { height: 100%; background: var(--accent); transition: width 0.6s ease-out; }
|
|
|
|
|
|
.arc-info { display: flex; justify-content: space-between; font-size: 0.75rem; color: var(--text-muted); margin-bottom: 8px; }
|
|
|
|
|
|
.arc-beats { font-size: 0.8125rem; color: var(--text-secondary); line-height: 1.6; }
|
|
|
|
|
|
.arc-beat { position: relative; padding-left: 12px; margin-bottom: 4px; }
|
|
|
|
|
|
.arc-beat::before { content: ''; position: absolute; left: 0; top: 8px; width: 4px; height: 4px; background: var(--border-color); border-radius: 50%; }
|
|
|
|
|
|
.empty-state { text-align: center; padding: 40px; color: var(--text-muted); font-size: 0.875rem; }
|
|
|
|
|
|
.modal-overlay { position: fixed; inset: 0; z-index: 10000; display: none; align-items: center; justify-content: center; }
|
|
|
|
|
|
.modal-overlay.active { display: flex; }
|
|
|
|
|
|
.modal-backdrop { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(4px); }
|
|
|
|
|
|
.modal-panel {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-width: 720px;
|
|
|
|
|
|
max-height: 90vh;
|
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
.modal-header { display: flex; justify-content: space-between; align-items: center; padding: 20px 24px; border-bottom: 1px solid var(--border-color); }
|
|
|
|
|
|
.modal-header h2 { font-size: 1rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.1em; }
|
|
|
|
|
|
.modal-close {
|
|
|
|
|
|
width: 32px;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.modal-close:hover { background: var(--bg-tertiary); border-color: var(--accent); }
|
|
|
|
|
|
.modal-close svg { width: 14px; height: 14px; }
|
|
|
|
|
|
.modal-body { flex: 1; overflow-y: auto; padding: 24px; }
|
|
|
|
|
|
.modal-footer { display: flex; justify-content: flex-end; gap: 12px; padding: 16px 24px; border-top: 1px solid var(--border-color); }
|
|
|
|
|
|
.editor-textarea {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
min-height: 300px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
|
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
resize: vertical;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.editor-textarea:focus { border-color: var(--accent); }
|
|
|
|
|
|
.editor-hint { font-size: 0.75rem; color: var(--text-muted); margin-bottom: 12px; line-height: 1.5; }
|
|
|
|
|
|
.editor-error { padding: 12px; background: var(--highlight-soft); border: 1px solid rgba(255, 68, 68, 0.3); color: var(--highlight); font-size: 0.8125rem; margin-top: 12px; display: none; }
|
|
|
|
|
|
.editor-error.visible { display: block; }
|
|
|
|
|
|
.struct-item { border: 1px solid var(--border-color); background: var(--bg-tertiary); padding: 12px; margin-bottom: 8px; display: flex; flex-direction: column; gap: 6px; }
|
|
|
|
|
|
.struct-row { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
|
|
|
|
.struct-row input, .struct-row select, .struct-row textarea {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
padding: 8px 10px;
|
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
transition: border-color 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.struct-row input:focus, .struct-row select:focus, .struct-row textarea:focus { border-color: var(--accent); }
|
|
|
|
|
|
.struct-row textarea { resize: vertical; font-family: inherit; min-height: 60px; }
|
|
|
|
|
|
.struct-actions { display: flex; justify-content: space-between; align-items: center; margin-top: 4px; }
|
|
|
|
|
|
.struct-actions span { font-size: 0.75rem; color: var(--text-muted); }
|
|
|
|
|
|
#editor-structured .btn-sm { padding: 4px 8px; font-size: 0.75rem; }
|
|
|
|
|
|
.settings-section { margin-bottom: 32px; }
|
|
|
|
|
|
.settings-section:last-child { margin-bottom: 0; }
|
|
|
|
|
|
.settings-section-title { font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.15em; color: var(--text-muted); margin-bottom: 16px; padding-bottom: 8px; border-bottom: 1px solid var(--border-light); }
|
|
|
|
|
|
.settings-row { display: flex; gap: 16px; margin-bottom: 16px; flex-wrap: wrap; }
|
|
|
|
|
|
.settings-row:last-child { margin-bottom: 0; }
|
|
|
|
|
|
.settings-field { display: flex; flex-direction: column; gap: 6px; flex: 1; min-width: 200px; }
|
|
|
|
|
|
.settings-field.full { flex: 100%; }
|
|
|
|
|
|
.settings-field label { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
|
|
|
|
.settings-field input, .settings-field select { padding: 10px 14px; background: var(--bg-tertiary); border: 1px solid var(--border-color); font-size: 0.875rem; color: var(--text-primary); outline: none; transition: border-color 0.2s; }
|
|
|
|
|
|
.settings-field input:focus, .settings-field select:focus { border-color: var(--accent); }
|
|
|
|
|
|
.settings-field input[type="password"] { letter-spacing: 0.15em; }
|
|
|
|
|
|
.settings-field-inline { display: flex; align-items: center; gap: 8px; }
|
|
|
|
|
|
.settings-field-inline input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--accent); }
|
|
|
|
|
|
.settings-field-inline label { font-size: 0.8125rem; color: var(--text-secondary); text-transform: none; letter-spacing: 0; }
|
|
|
|
|
|
.settings-hint { font-size: 0.75rem; color: var(--text-muted); margin-top: 4px; }
|
|
|
|
|
|
.settings-btn-row { display: flex; gap: 12px; margin-top: 8px; }
|
|
|
|
|
|
.model-select-wrap { display: none; }
|
|
|
|
|
|
.model-select-wrap.active { display: flex; flex-direction: column; gap: 6px; }
|
|
|
|
|
|
.hidden { display: none !important; }
|
|
|
|
|
|
@media (max-width: 1200px) {
|
|
|
|
|
|
.summary-container { padding: 16px 24px; }
|
|
|
|
|
|
.summary-main { grid-template-columns: 1fr; }
|
|
|
|
|
|
.right-panel { flex-direction: row; }
|
|
|
|
|
|
.relations-section, .arcs-section { flex: 1; min-height: 300px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
html, body { height: auto; overflow-y: auto; -webkit-overflow-scrolling: touch; }
|
|
|
|
|
|
.summary-container { height: auto; min-height: 100vh; overflow: visible; padding: 16px; }
|
|
|
|
|
|
.summary-header { flex-direction: column; gap: 16px; padding-bottom: 16px; margin-bottom: 16px; }
|
|
|
|
|
|
.header-left h1 { font-size: 1.5rem; }
|
|
|
|
|
|
.header-stats { width: 100%; justify-content: space-between; gap: 16px; text-align: center; }
|
|
|
|
|
|
.stat-value { font-size: 1.75rem; }
|
|
|
|
|
|
.stat-label { font-size: 0.625rem; }
|
|
|
|
|
|
.controls-bar { flex-wrap: wrap; gap: 10px; padding: 10px 0; margin-bottom: 16px; }
|
|
|
|
|
|
.spacer { display: none; }
|
|
|
|
|
|
.btn { padding: 10px 20px; font-size: 0.8125rem; }
|
|
|
|
|
|
.btn-icon { padding: 10px 14px; }
|
|
|
|
|
|
.summary-main, .left-content { gap: 16px; overflow: visible; }
|
|
|
|
|
|
.right-panel { flex-direction: column; gap: 16px; }
|
|
|
|
|
|
.timeline-section { padding: 16px; max-height: 400px; }
|
|
|
|
|
|
.relations-section, .arcs-section { min-height: 280px; max-height: 320px; padding: 16px; }
|
|
|
|
|
|
.section-header { margin-bottom: 12px; }
|
|
|
|
|
|
.keywords-section { padding: 16px; }
|
|
|
|
|
|
.keywords-cloud { gap: 8px; margin-top: 12px; }
|
|
|
|
|
|
.keyword-tag { padding: 6px 14px; font-size: 0.8125rem; }
|
|
|
|
|
|
.timeline-item { padding-left: 24px; padding-bottom: 24px; }
|
|
|
|
|
|
.timeline-title { font-size: 0.9375rem; }
|
|
|
|
|
|
.timeline-brief { font-size: 0.8125rem; line-height: 1.6; }
|
|
|
|
|
|
.modal-panel { max-width: 100%; max-height: 100%; height: 100%; border: none; }
|
|
|
|
|
|
.modal-header, .modal-body, .modal-footer { padding: 16px; }
|
|
|
|
|
|
.settings-row { flex-direction: column; gap: 12px; }
|
|
|
|
|
|
.settings-field { min-width: 100%; }
|
|
|
|
|
|
.settings-field input, .settings-field select { padding: 12px 14px; font-size: 1rem; }
|
|
|
|
|
|
.fullscreen-panel {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
border-radius: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
@media (max-width: 480px) {
|
|
|
|
|
|
.summary-container { padding: 12px; }
|
|
|
|
|
|
.summary-header { padding-bottom: 12px; margin-bottom: 12px; }
|
|
|
|
|
|
.header-left h1 { font-size: 1.25rem; }
|
|
|
|
|
|
.header-subtitle { font-size: 0.6875rem; }
|
|
|
|
|
|
.header-stats { gap: 8px; }
|
|
|
|
|
|
.stat-item { flex: 1; }
|
|
|
|
|
|
.stat-value { font-size: 1.5rem; }
|
|
|
|
|
|
.controls-bar { gap: 8px; padding: 8px 0; margin-bottom: 12px; }
|
|
|
|
|
|
.btn { flex: 1; padding: 10px 12px; font-size: 0.75rem; text-align: center; justify-content: center; }
|
|
|
|
|
|
.btn-icon { padding: 10px 12px; }
|
|
|
|
|
|
.btn-icon svg { width: 14px; height: 14px; }
|
|
|
|
|
|
.summary-main, .left-content, .right-panel { gap: 12px; }
|
|
|
|
|
|
.keywords-section, .timeline-section, .relations-section, .arcs-section { padding: 12px; }
|
|
|
|
|
|
.section-title { font-size: 0.6875rem; }
|
|
|
|
|
|
.section-edit-btn { font-size: 0.625rem; padding: 3px 8px; }
|
|
|
|
|
|
.relations-section, .arcs-section { min-height: 240px; }
|
|
|
|
|
|
#relation-chart { min-height: 180px; }
|
|
|
|
|
|
.keywords-cloud { gap: 6px; margin-top: 10px; }
|
|
|
|
|
|
.keyword-tag { padding: 5px 10px; font-size: 0.75rem; }
|
|
|
|
|
|
.timeline-item { padding-left: 20px; padding-bottom: 20px; margin-left: 6px; }
|
|
|
|
|
|
.timeline-dot { width: 7px; height: 7px; left: -4px; }
|
|
|
|
|
|
.timeline-header { flex-direction: column; align-items: flex-start; gap: 2px; }
|
|
|
|
|
|
.timeline-title { font-size: 0.875rem; }
|
|
|
|
|
|
.timeline-time { font-size: 0.6875rem; }
|
|
|
|
|
|
.timeline-brief { font-size: 0.75rem; margin-bottom: 8px; }
|
|
|
|
|
|
.timeline-meta { flex-direction: column; gap: 4px; font-size: 0.6875rem; }
|
|
|
|
|
|
.arc-card { padding: 12px; }
|
|
|
|
|
|
.arc-name { font-size: 0.875rem; }
|
|
|
|
|
|
.arc-phase, .arc-info { font-size: 0.6875rem; }
|
|
|
|
|
|
.arc-beats { font-size: 0.75rem; }
|
|
|
|
|
|
.arc-beat { padding-left: 10px; }
|
|
|
|
|
|
.arc-beat::before { width: 3px; height: 3px; top: 7px; }
|
|
|
|
|
|
.modal-header h2 { font-size: 0.875rem; }
|
|
|
|
|
|
.settings-section-title { font-size: 0.625rem; }
|
|
|
|
|
|
.settings-field label { font-size: 0.6875rem; }
|
|
|
|
|
|
.settings-field-inline label { font-size: 0.75rem; }
|
|
|
|
|
|
.settings-hint { font-size: 0.6875rem; }
|
|
|
|
|
|
.btn-sm { padding: 10px 14px; font-size: 0.75rem; width: 100%; }
|
|
|
|
|
|
.editor-textarea { min-height: 200px; font-size: 0.75rem; }
|
|
|
|
|
|
}
|
|
|
|
|
|
@media (hover: none) and (pointer: coarse) {
|
|
|
|
|
|
.btn { min-height: 44px; }
|
|
|
|
|
|
.keyword-tag { min-height: 36px; display: flex; align-items: center; }
|
|
|
|
|
|
.keyword-tag:hover { transform: none; }
|
|
|
|
|
|
.timeline-item:hover .timeline-dot { transform: none; }
|
|
|
|
|
|
.arc-card:hover { box-shadow: none; }
|
|
|
|
|
|
.modal-close { width: 44px; height: 44px; }
|
|
|
|
|
|
.settings-field input, .settings-field select { min-height: 44px; }
|
|
|
|
|
|
.settings-field-inline input[type="checkbox"] { width: 22px; height: 22px; }
|
|
|
|
|
|
.section-edit-btn { min-height: 32px; padding: 6px 12px; }
|
|
|
|
|
|
}
|
|
|
|
|
|
.phase-toast {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
bottom: 80px;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
|
max-width: 85vw;
|
|
|
|
|
|
padding: 12px 20px;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.85);
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
z-index: 10001;
|
|
|
|
|
|
animation: toastIn 0.2s ease;
|
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
}
|
|
|
|
|
|
@keyframes toastIn {
|
|
|
|
|
|
from { opacity: 0; transform: translateX(-50%) translateY(10px); }
|
|
|
|
|
|
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
|
|
|
|
}
|
2025-12-21 01:47:38 +08:00
|
|
|
|
.stat-warning {
|
|
|
|
|
|
font-size: 0.625rem;
|
|
|
|
|
|
color: #ff9800;
|
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
#keep-visible-count {
|
|
|
|
|
|
width: 32px;
|
|
|
|
|
|
padding: 2px 4px;
|
|
|
|
|
|
margin: 0 2px;
|
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
|
font-size: inherit;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: var(--highlight);
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
#keep-visible-count:focus {
|
|
|
|
|
|
border-color: var(--accent);
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
}
|
2025-12-19 02:19:10 +08:00
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div class="summary-container">
|
|
|
|
|
|
<header class="summary-header">
|
|
|
|
|
|
<div class="header-left">
|
|
|
|
|
|
<h1>剧情<span>总结</span></h1>
|
|
|
|
|
|
<div class="header-subtitle">Story Summary · Timeline · Character Arcs</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="header-stats">
|
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
|
<div class="stat-value" id="stat-events">0</div>
|
|
|
|
|
|
<div class="stat-label">已记录事件</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
|
<div class="stat-value" id="stat-summarized">0</div>
|
|
|
|
|
|
<div class="stat-label">已总结楼层</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
|
<div class="stat-value"><span class="highlight" id="stat-pending">0</span></div>
|
|
|
|
|
|
<div class="stat-label">待总结</div>
|
2025-12-21 01:47:38 +08:00
|
|
|
|
<div class="stat-warning hidden" id="pending-warning">再删1条将回滚</div>
|
2025-12-19 02:19:10 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
<div class="controls-bar">
|
|
|
|
|
|
<label class="status-checkbox">
|
|
|
|
|
|
<input type="checkbox" id="hide-summarized">
|
2025-12-21 01:47:38 +08:00
|
|
|
|
<span>聊天时隐藏已总结 · <strong id="summarized-count">0</strong> 楼(保留
|
|
|
|
|
|
<input type="number" id="keep-visible-count" min="0" max="50" value="3">
|
|
|
|
|
|
楼)</span>
|
|
|
|
|
|
</label>
|
2025-12-19 02:19:10 +08:00
|
|
|
|
<span class="spacer"></span>
|
|
|
|
|
|
<button class="btn btn-icon" id="btn-settings">
|
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
|
|
|
<circle cx="12" cy="12" r="3"></circle>
|
|
|
|
|
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
设置
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn" id="btn-clear">清空</button>
|
|
|
|
|
|
<button class="btn btn-primary" id="btn-generate">总结</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<main class="summary-main">
|
|
|
|
|
|
<div class="left-content">
|
|
|
|
|
|
<section class="keywords-section">
|
|
|
|
|
|
<div class="section-header">
|
|
|
|
|
|
<div class="section-title">核心关键词</div>
|
|
|
|
|
|
<button class="section-edit-btn" data-section="keywords">编辑</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="keywords-cloud" id="keywords-cloud"></div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
<section class="timeline-section">
|
|
|
|
|
|
<div class="section-header">
|
|
|
|
|
|
<div class="section-title">剧情时间线</div>
|
|
|
|
|
|
<button class="section-edit-btn" data-section="events">编辑</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="timeline-list" id="timeline-list"></div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="right-panel">
|
|
|
|
|
|
<section class="relations-section">
|
|
|
|
|
|
<div class="section-header">
|
|
|
|
|
|
<div class="section-title">人物关系</div>
|
|
|
|
|
|
<div class="section-header-actions">
|
|
|
|
|
|
<button class="section-edit-btn section-icon-btn" id="btn-fullscreen-relations" title="全屏查看">
|
|
|
|
|
|
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
|
|
|
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="section-edit-btn" data-section="characters">编辑</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="relation-chart"></div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
<section class="arcs-section">
|
|
|
|
|
|
<div class="section-header">
|
|
|
|
|
|
<div class="section-title">角色弧光</div>
|
|
|
|
|
|
<button class="section-edit-btn" data-section="arcs">编辑</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="arcs-list" id="arcs-list"></div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-overlay" id="editor-modal">
|
|
|
|
|
|
<div class="modal-backdrop" id="editor-backdrop"></div>
|
|
|
|
|
|
<div class="modal-panel">
|
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
|
<h2 id="editor-title">编辑</h2>
|
|
|
|
|
|
<button class="modal-close" id="editor-close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
|
<div class="editor-hint" id="editor-hint"></div>
|
|
|
|
|
|
<div id="editor-structured" class="hidden"></div>
|
|
|
|
|
|
<textarea class="editor-textarea" id="editor-textarea"></textarea>
|
|
|
|
|
|
<div class="editor-error" id="editor-error"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
|
<button class="btn" id="editor-cancel">取消</button>
|
|
|
|
|
|
<button class="btn btn-primary" id="editor-save">保存</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-overlay" id="settings-modal">
|
|
|
|
|
|
<div class="modal-backdrop" id="settings-backdrop"></div>
|
|
|
|
|
|
<div class="modal-panel">
|
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
|
<h2>设置</h2>
|
|
|
|
|
|
<button class="modal-close" id="settings-close"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
|
<div class="settings-section">
|
|
|
|
|
|
<div class="settings-section-title">API 配置</div>
|
|
|
|
|
|
<div class="settings-row">
|
|
|
|
|
|
<div class="settings-field">
|
|
|
|
|
|
<label>渠道</label>
|
|
|
|
|
|
<select id="api-provider">
|
|
|
|
|
|
<option value="st">酒馆主 API(沿用当前)</option>
|
|
|
|
|
|
<option value="openai">OpenAI 兼容</option>
|
|
|
|
|
|
<option value="google">Google (Gemini)</option>
|
|
|
|
|
|
<option value="claude">Claude (Anthropic)</option>
|
|
|
|
|
|
<option value="deepseek">DeepSeek</option>
|
|
|
|
|
|
<option value="cohere">Cohere</option>
|
|
|
|
|
|
<option value="custom">自定义</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="settings-row hidden" id="api-url-row">
|
|
|
|
|
|
<div class="settings-field full">
|
|
|
|
|
|
<label>API URL</label>
|
|
|
|
|
|
<input type="text" id="api-url" placeholder="https://api.openai.com 或代理地址">
|
|
|
|
|
|
<div class="settings-hint">不同渠道默认端点:OpenAI 用 /v1,Gemini 用 /v1beta,Claude 用 /v1</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="settings-row hidden" id="api-key-row">
|
|
|
|
|
|
<div class="settings-field full">
|
|
|
|
|
|
<label>API KEY</label>
|
|
|
|
|
|
<input type="password" id="api-key" placeholder="仅保存在本地,不会上传">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="settings-row hidden" id="api-model-manual-row">
|
|
|
|
|
|
<div class="settings-field full">
|
|
|
|
|
|
<label>模型</label>
|
|
|
|
|
|
<input type="text" id="api-model-text" placeholder="如 gemini-1.5-pro、claude-3-haiku">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="settings-row hidden" id="api-model-select-row">
|
|
|
|
|
|
<div class="settings-field full">
|
|
|
|
|
|
<label>可用模型</label>
|
|
|
|
|
|
<select id="api-model-select">
|
|
|
|
|
|
<option value="">请先拉取模型列表</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="settings-btn-row hidden" id="api-connect-row">
|
|
|
|
|
|
<button class="btn btn-sm btn-connect" id="btn-connect">连接 / 拉取模型列表</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="settings-section">
|
|
|
|
|
|
<div class="settings-section-title">生成参数</div>
|
|
|
|
|
|
<div class="settings-row">
|
|
|
|
|
|
<div class="settings-field"><label>Temperature</label><input type="number" id="gen-temp" step="0.01" min="0" max="2" placeholder="未设置"></div>
|
|
|
|
|
|
<div class="settings-field"><label>Top P</label><input type="number" id="gen-top-p" step="0.01" min="0" max="1" placeholder="未设置"></div>
|
|
|
|
|
|
<div class="settings-field"><label>Top K</label><input type="number" id="gen-top-k" step="1" min="1" placeholder="未设置"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="settings-row">
|
|
|
|
|
|
<div class="settings-field"><label>存在惩罚</label><input type="number" id="gen-presence" step="0.01" min="-2" max="2" placeholder="未设置"></div>
|
|
|
|
|
|
<div class="settings-field"><label>频率惩罚</label><input type="number" id="gen-frequency" step="0.01" min="-2" max="2" placeholder="未设置"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="settings-section">
|
|
|
|
|
|
<div class="settings-section-title">自动触发</div>
|
|
|
|
|
|
<div class="settings-row">
|
|
|
|
|
|
<div class="settings-field"><label>总结间隔(楼)</label><input type="number" id="trigger-interval" min="5" step="5" value="20"></div>
|
|
|
|
|
|
<div class="settings-field">
|
|
|
|
|
|
<label>触发时机</label>
|
|
|
|
|
|
<select id="trigger-timing">
|
|
|
|
|
|
<option value="after_ai">AI 回复后</option>
|
|
|
|
|
|
<option value="before_user">用户发送前</option>
|
|
|
|
|
|
<option value="manual">仅手动</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="settings-row">
|
|
|
|
|
|
<div class="settings-field-inline">
|
|
|
|
|
|
<input type="checkbox" id="trigger-enabled">
|
|
|
|
|
|
<label for="trigger-enabled">启用自动总结</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
|
<button class="btn" id="settings-cancel">取消</button>
|
|
|
|
|
|
<button class="btn btn-primary" id="settings-save">保存</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-overlay" id="relations-fullscreen-modal">
|
|
|
|
|
|
<div class="modal-backdrop" id="relations-fullscreen-backdrop"></div>
|
|
|
|
|
|
<div class="modal-panel fullscreen-panel">
|
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
|
<h2>人物关系图</h2>
|
|
|
|
|
|
<button class="modal-close" id="relations-fullscreen-close">
|
|
|
|
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
|
|
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
|
|
|
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-body fullscreen-body">
|
|
|
|
|
|
<div id="relation-chart-fullscreen"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
|
|
|
|
|
|
<script>
|
|
|
|
|
|
const config = {
|
|
|
|
|
|
api: { provider: 'st', url: '', key: '', model: '', modelCache: [] },
|
|
|
|
|
|
gen: { temperature: null, top_p: null, top_k: null, presence_penalty: null, frequency_penalty: null },
|
|
|
|
|
|
trigger: { enabled: false, interval: 20, timing: 'after_ai' }
|
|
|
|
|
|
};
|
|
|
|
|
|
let summaryData = { keywords: [], events: [], characters: { main: [], relationships: [] }, arcs: [] };
|
|
|
|
|
|
let localGenerating = false;
|
|
|
|
|
|
let relationChart = null;
|
|
|
|
|
|
let relationChartFullscreen = null;
|
|
|
|
|
|
let currentEditSection = null;
|
|
|
|
|
|
const providerDefaults = {
|
|
|
|
|
|
st: { url: '', needKey: false, canFetch: false, needManualModel: false },
|
|
|
|
|
|
openai: { url: 'https://api.openai.com', needKey: true, canFetch: true, needManualModel: false },
|
|
|
|
|
|
google: { url: 'https://generativelanguage.googleapis.com', needKey: true, canFetch: false, needManualModel: true },
|
|
|
|
|
|
claude: { url: 'https://api.anthropic.com', needKey: true, canFetch: false, needManualModel: true },
|
|
|
|
|
|
deepseek: { url: 'https://api.deepseek.com', needKey: true, canFetch: true, needManualModel: false },
|
|
|
|
|
|
cohere: { url: 'https://api.cohere.ai', needKey: true, canFetch: false, needManualModel: true },
|
|
|
|
|
|
custom: { url: '', needKey: true, canFetch: true, needManualModel: false }
|
|
|
|
|
|
};
|
|
|
|
|
|
const sectionMeta = {
|
|
|
|
|
|
keywords: { title: '编辑关键词', hint: '每行一个关键词,格式:关键词|权重(核心/重要/一般)' },
|
|
|
|
|
|
events: { title: '编辑事件时间线', hint: '编辑时,每个事件要素都应完整' },
|
|
|
|
|
|
characters: { title: '编辑人物关系', hint: '编辑时,每个要素都应完整' },
|
|
|
|
|
|
arcs: { title: '编辑角色弧光', hint: '编辑时,每个要素都应完整' }
|
|
|
|
|
|
};
|
|
|
|
|
|
function getCharName(c) { return typeof c === 'string' ? c : c.name; }
|
|
|
|
|
|
function preserveAddedAt(newItem, oldItem) { if (oldItem?._addedAt != null) newItem._addedAt = oldItem._addedAt; return newItem; }
|
|
|
|
|
|
function loadConfig() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const saved = localStorage.getItem('summary_panel_config');
|
2025-12-21 01:47:38 +08:00
|
|
|
|
if (saved) {
|
|
|
|
|
|
const p = JSON.parse(saved);
|
|
|
|
|
|
Object.assign(config.api, p.api || {});
|
|
|
|
|
|
Object.assign(config.gen, p.gen || {});
|
|
|
|
|
|
Object.assign(config.trigger, p.trigger || {});
|
|
|
|
|
|
if (config.trigger.timing === 'manual' && config.trigger.enabled) {
|
|
|
|
|
|
config.trigger.enabled = false;
|
|
|
|
|
|
saveConfig();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-19 02:19:10 +08:00
|
|
|
|
} catch {}
|
|
|
|
|
|
}
|
|
|
|
|
|
function saveConfig() { try { localStorage.setItem('summary_panel_config', JSON.stringify(config)); } catch {} }
|
|
|
|
|
|
function renderKeywords(keywords) {
|
|
|
|
|
|
summaryData.keywords = keywords || [];
|
|
|
|
|
|
const wc = { '核心': 'primary', '重要': 'secondary', high: 'primary', medium: 'secondary' };
|
|
|
|
|
|
document.getElementById('keywords-cloud').innerHTML = keywords.map(k => `<span class="keyword-tag ${wc[k.weight] || wc[k.level] || ''}">${k.text}</span>`).join('') || '<div class="empty-state">暂无关键词</div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
function renderTimeline(events) {
|
|
|
|
|
|
summaryData.events = events || [];
|
|
|
|
|
|
const c = document.getElementById('timeline-list');
|
|
|
|
|
|
if (!events?.length) { c.innerHTML = '<div class="empty-state">暂无事件记录</div>'; return; }
|
|
|
|
|
|
c.innerHTML = events.map(ev => {
|
|
|
|
|
|
const tl = ev.timeLabel || '';
|
|
|
|
|
|
const p = ev.participants || ev.characters || [];
|
|
|
|
|
|
const isCritical = ev.weight === '核心' || ev.weight === '主线';
|
|
|
|
|
|
const typeLabel = ev.type || '';
|
|
|
|
|
|
const weightLabel = ev.weight || '';
|
|
|
|
|
|
return `<div class="timeline-item ${isCritical ? 'critical' : ''}"><div class="timeline-dot"></div><div class="timeline-header"><div class="timeline-title">${ev.title || ''}</div><div class="timeline-time">${tl}</div></div><div class="timeline-brief">${ev.summary || ev.brief || ''}</div><div class="timeline-meta"><span>人物:${p.join('、') || '—'}</span><span class="importance">${typeLabel}${typeLabel && weightLabel ? ' · ' : ''}${weightLabel}</span></div></div>`;
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let allNodes = [];
|
|
|
|
|
|
let allLinks = [];
|
|
|
|
|
|
|
|
|
|
|
|
function openRelationsFullscreen() {
|
|
|
|
|
|
const modal = document.getElementById('relations-fullscreen-modal');
|
|
|
|
|
|
modal.classList.add('active');
|
|
|
|
|
|
|
|
|
|
|
|
const dom = document.getElementById('relation-chart-fullscreen');
|
|
|
|
|
|
if (!relationChartFullscreen) {
|
|
|
|
|
|
relationChartFullscreen = echarts.init(dom);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (allNodes.length === 0) {
|
|
|
|
|
|
relationChartFullscreen.clear();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
relationChartFullscreen.setOption({
|
|
|
|
|
|
tooltip: { show: false },
|
|
|
|
|
|
series: [{
|
|
|
|
|
|
type: 'graph',
|
|
|
|
|
|
layout: 'force',
|
|
|
|
|
|
roam: true,
|
|
|
|
|
|
draggable: true,
|
|
|
|
|
|
data: allNodes.map(n => ({
|
|
|
|
|
|
...n,
|
|
|
|
|
|
symbolSize: n.symbolSize * 1.5,
|
|
|
|
|
|
label: { ...n.label, fontSize: 14 }
|
|
|
|
|
|
})),
|
|
|
|
|
|
links: allLinks.map(l => ({
|
|
|
|
|
|
...l,
|
|
|
|
|
|
label: { ...l.label, fontSize: 12 }
|
|
|
|
|
|
})),
|
|
|
|
|
|
force: {
|
|
|
|
|
|
repulsion: 600,
|
|
|
|
|
|
edgeLength: [80, 200],
|
|
|
|
|
|
gravity: 0.08,
|
|
|
|
|
|
friction: 0.6
|
|
|
|
|
|
},
|
|
|
|
|
|
label: { show: true },
|
|
|
|
|
|
emphasis: { focus: 'adjacency' }
|
|
|
|
|
|
}]
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => relationChartFullscreen.resize(), 100);
|
|
|
|
|
|
window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type: 'FULLSCREEN_OPENED' }, '*');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeRelationsFullscreen() {
|
|
|
|
|
|
document.getElementById('relations-fullscreen-modal').classList.remove('active');
|
|
|
|
|
|
window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type: 'FULLSCREEN_CLOSED' }, '*');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderRelations(data) {
|
|
|
|
|
|
summaryData.characters = data || { main: [], relationships: [] };
|
|
|
|
|
|
const dom = document.getElementById('relation-chart');
|
|
|
|
|
|
if (!relationChart) {
|
|
|
|
|
|
relationChart = echarts.init(dom);
|
|
|
|
|
|
}
|
|
|
|
|
|
const mainChars = (data?.main || []).map(getCharName);
|
|
|
|
|
|
const rels = data?.relationships || [];
|
|
|
|
|
|
const allNames = new Set(mainChars);
|
|
|
|
|
|
rels.forEach(r => { if (r.from) allNames.add(r.from); if (r.to) allNames.add(r.to); });
|
|
|
|
|
|
|
|
|
|
|
|
const degrees = {};
|
|
|
|
|
|
rels.forEach(r => {
|
|
|
|
|
|
degrees[r.from] = (degrees[r.from] || 0) + 1;
|
|
|
|
|
|
degrees[r.to] = (degrees[r.to] || 0) + 1;
|
|
|
|
|
|
});
|
|
|
|
|
|
allNodes = Array.from(allNames).map(name => {
|
|
|
|
|
|
const degree = degrees[name] || 0;
|
|
|
|
|
|
const isMain = mainChars.includes(name);
|
|
|
|
|
|
return {
|
|
|
|
|
|
id: name,
|
|
|
|
|
|
name: name,
|
|
|
|
|
|
symbol: 'circle',
|
|
|
|
|
|
symbolSize: Math.min(45, Math.max(20, degree * 3 + 15)),
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
color: isMain ? '#ff4444' : (degree > 2 ? '#333' : '#999'),
|
|
|
|
|
|
borderColor: '#fff',
|
|
|
|
|
|
borderWidth: 1
|
|
|
|
|
|
},
|
|
|
|
|
|
label: {
|
|
|
|
|
|
show: true,
|
|
|
|
|
|
position: 'right',
|
|
|
|
|
|
color: '#333',
|
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
|
fontWeight: isMain ? 'bold' : 'normal'
|
|
|
|
|
|
},
|
|
|
|
|
|
degree: degree
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
allLinks = rels.map(r => ({
|
|
|
|
|
|
source: r.from,
|
|
|
|
|
|
target: r.to,
|
|
|
|
|
|
value: r.label || r.type,
|
|
|
|
|
|
lineStyle: {
|
|
|
|
|
|
width: 1.5,
|
|
|
|
|
|
color: '#bbb',
|
|
|
|
|
|
curveness: 0.1
|
|
|
|
|
|
},
|
|
|
|
|
|
label: {
|
|
|
|
|
|
show: true,
|
|
|
|
|
|
formatter: r.label,
|
|
|
|
|
|
fontSize: 10,
|
|
|
|
|
|
color: '#000000',
|
|
|
|
|
|
opacity: 1,
|
|
|
|
|
|
fontFamily: 'sans-serif',
|
|
|
|
|
|
backgroundColor: '#ffffff',
|
|
|
|
|
|
padding: [2, 4],
|
|
|
|
|
|
borderRadius: 3,
|
|
|
|
|
|
borderColor: '#ccc',
|
|
|
|
|
|
borderWidth: 0.5
|
|
|
|
|
|
}
|
|
|
|
|
|
}));
|
|
|
|
|
|
if (!allNodes.length) { relationChart.clear(); return; }
|
|
|
|
|
|
const updateChart = (nodes, links) => {
|
|
|
|
|
|
relationChart.setOption({
|
|
|
|
|
|
tooltip: { show: false },
|
|
|
|
|
|
|
|
|
|
|
|
series: [{
|
|
|
|
|
|
type: 'graph',
|
|
|
|
|
|
layout: 'force',
|
|
|
|
|
|
roam: true,
|
|
|
|
|
|
draggable: true,
|
|
|
|
|
|
data: nodes,
|
|
|
|
|
|
links: links,
|
|
|
|
|
|
force: {
|
|
|
|
|
|
repulsion: 400,
|
|
|
|
|
|
edgeLength: [60, 150],
|
|
|
|
|
|
gravity: 0.1,
|
|
|
|
|
|
friction: 0.6
|
|
|
|
|
|
},
|
|
|
|
|
|
label: { show: true },
|
|
|
|
|
|
emphasis: { focus: 'adjacency' }
|
|
|
|
|
|
}]
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
updateChart(allNodes, allLinks);
|
|
|
|
|
|
relationChart.off('click');
|
|
|
|
|
|
relationChart.on('click', params => {
|
|
|
|
|
|
if (params.dataType === 'node') {
|
|
|
|
|
|
const centerId = params.data.id;
|
|
|
|
|
|
highlightArc(centerId);
|
|
|
|
|
|
|
|
|
|
|
|
const relatedLinks = allLinks.filter(l => l.source === centerId || l.target === centerId);
|
|
|
|
|
|
const relatedNodeIds = new Set([centerId]);
|
|
|
|
|
|
relatedLinks.forEach(l => {
|
|
|
|
|
|
relatedNodeIds.add(l.source);
|
|
|
|
|
|
relatedNodeIds.add(l.target);
|
|
|
|
|
|
});
|
|
|
|
|
|
const relatedNodes = allNodes.filter(n => relatedNodeIds.has(n.id));
|
|
|
|
|
|
|
|
|
|
|
|
updateChart(relatedNodes, relatedLinks);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
relationChart.getZr().on('click', params => {
|
|
|
|
|
|
if (!params.target) {
|
|
|
|
|
|
updateChart(allNodes, allLinks);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function highlightArc(charId) {
|
|
|
|
|
|
const c = document.getElementById('arcs-list');
|
|
|
|
|
|
c.querySelectorAll('.arc-card').forEach(card => {
|
|
|
|
|
|
card.classList.toggle('arc-card-active', card.dataset.characterId === charId);
|
|
|
|
|
|
if (card.dataset.characterId === charId) card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
function renderArcs(arcs) {
|
|
|
|
|
|
summaryData.arcs = arcs || [];
|
|
|
|
|
|
const c = document.getElementById('arcs-list');
|
|
|
|
|
|
if (!arcs?.length) { c.innerHTML = '<div class="empty-state">暂无角色数据</div>'; return; }
|
|
|
|
|
|
const truncate = (str, len) => str && str.length > len ? str.slice(0, len) + '…' : (str || '');
|
|
|
|
|
|
c.innerHTML = arcs.map(arc => {
|
|
|
|
|
|
const beats = (arc.moments || arc.beats || []).map(m => typeof m === 'string' ? m : m.text);
|
|
|
|
|
|
const fullPhase = arc.trajectory || arc.phase || '';
|
|
|
|
|
|
return `<div class="arc-card" data-character-id="${arc.id || arc.name}">
|
|
|
|
|
|
<div class="arc-header">
|
|
|
|
|
|
<span class="arc-name">${arc.name || '角色'}</span>
|
|
|
|
|
|
<span class="arc-phase" data-full="${fullPhase.replace(/"/g, '"')}">${truncate(fullPhase, 20)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="arc-progress"><div class="arc-progress-inner" style="width:${(arc.progress || 0) * 100}%"></div></div>
|
|
|
|
|
|
<div class="arc-info"><span>弧光进度</span><span>${Math.round((arc.progress || 0) * 100)}%</span></div>
|
|
|
|
|
|
<div class="arc-beats">${beats.map(b => `<div class="arc-beat">${b}</div>`).join('')}</div>
|
|
|
|
|
|
</div>`;
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
c.querySelectorAll('.arc-phase').forEach(el => {
|
|
|
|
|
|
el.style.cursor = 'pointer';
|
|
|
|
|
|
el.addEventListener('click', e => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
const full = el.dataset.full;
|
|
|
|
|
|
if (full && full.length > 20) {
|
|
|
|
|
|
document.querySelectorAll('.phase-toast').forEach(t => t.remove());
|
|
|
|
|
|
const toast = document.createElement('div');
|
|
|
|
|
|
toast.className = 'phase-toast';
|
|
|
|
|
|
toast.textContent = full;
|
|
|
|
|
|
document.body.appendChild(toast);
|
|
|
|
|
|
setTimeout(() => toast.remove(), 3000);
|
|
|
|
|
|
toast.addEventListener('click', () => toast.remove());
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
c.querySelectorAll('.arc-card').forEach(card => {
|
|
|
|
|
|
card.addEventListener('click', () => {
|
|
|
|
|
|
c.querySelectorAll('.arc-card').forEach(x => x.classList.remove('arc-card-active'));
|
|
|
|
|
|
card.classList.add('arc-card-active');
|
|
|
|
|
|
if (relationChart) {
|
|
|
|
|
|
const opt = relationChart.getOption();
|
|
|
|
|
|
const idx = opt?.series?.[0]?.data?.findIndex(n => n.id === card.dataset.characterId || n.name === card.dataset.characterId);
|
|
|
|
|
|
if (idx >= 0) relationChart.dispatchAction({ type: 'highlight', seriesIndex: 0, dataIndex: idx });
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-12-21 01:47:38 +08:00
|
|
|
|
function updateStats(s) {
|
|
|
|
|
|
if (!s) return;
|
|
|
|
|
|
document.getElementById('stat-summarized').textContent = s.summarizedUpTo ?? 0;
|
|
|
|
|
|
document.getElementById('stat-events').textContent = s.eventsCount ?? 0;
|
|
|
|
|
|
|
|
|
|
|
|
const pending = s.pendingFloors ?? 0;
|
|
|
|
|
|
document.getElementById('stat-pending').textContent = pending;
|
|
|
|
|
|
document.getElementById('pending-warning').classList.toggle('hidden', pending !== -1);
|
|
|
|
|
|
}
|
2025-12-19 02:19:10 +08:00
|
|
|
|
const editorModal = document.getElementById('editor-modal');
|
|
|
|
|
|
const editorTextarea = document.getElementById('editor-textarea');
|
|
|
|
|
|
const editorError = document.getElementById('editor-error');
|
|
|
|
|
|
const editorStructured = document.getElementById('editor-structured');
|
|
|
|
|
|
function createDeleteBtn() { const b = document.createElement('button'); b.type = 'button'; b.className = 'btn btn-sm btn-delete'; b.textContent = '删除'; return b; }
|
|
|
|
|
|
function renderEventsEditor(events) {
|
|
|
|
|
|
const list = events?.length ? events : [{ id: 'evt-1', title: '', timeLabel: '', summary: '', participants: [], type: '日常', weight: '点睛' }];
|
|
|
|
|
|
const evtMap = new Map((summaryData.events || []).map(e => [e.id, e]));
|
|
|
|
|
|
let maxId = 0;
|
|
|
|
|
|
list.forEach(e => { const m = e.id?.match(/evt-(\d+)/); if (m) maxId = Math.max(maxId, parseInt(m[1])); });
|
|
|
|
|
|
editorStructured.innerHTML = list.map((ev, idx) => {
|
|
|
|
|
|
const id = ev.id || `evt-${maxId + idx + 1}`;
|
|
|
|
|
|
return `<div class="struct-item event-item" data-id="${id}"><div class="struct-row"><input type="text" class="event-title" placeholder="事件标题" value="${ev.title || ''}"><input type="text" class="event-time" placeholder="时间标签" value="${ev.timeLabel || ''}"></div><div class="struct-row"><textarea class="event-summary" rows="2" placeholder="一句话描述">${ev.summary || ''}</textarea></div><div class="struct-row"><input type="text" class="event-participants" placeholder="人物(顿号分隔)" value="${(ev.participants || []).join('、')}"></div><div class="struct-row"><select class="event-type">${['相遇','冲突','揭示','抉择','羁绊','转变','收束','日常'].map(t => `<option value="${t}" ${ev.type === t ? 'selected' : ''}>${t}</option>`).join('')}</select><select class="event-weight">${['核心','主线','转折','点睛','氛围'].map(t => `<option value="${t}" ${ev.weight === t ? 'selected' : ''}>${t}</option>`).join('')}</select></div><div class="struct-actions"><span>ID:${id}</span></div></div>`;
|
|
|
|
|
|
}).join('') + '<div style="margin-top:8px;"><button type="button" class="btn btn-sm" id="event-add">+ 新增事件</button></div>';
|
|
|
|
|
|
editorStructured.querySelectorAll('.event-item').forEach(item => {
|
|
|
|
|
|
const del = createDeleteBtn();
|
|
|
|
|
|
item.querySelector('.struct-actions').appendChild(del);
|
|
|
|
|
|
del.addEventListener('click', () => item.remove());
|
|
|
|
|
|
});
|
|
|
|
|
|
document.getElementById('event-add')?.addEventListener('click', () => {
|
|
|
|
|
|
const items = editorStructured.querySelectorAll('.event-item');
|
|
|
|
|
|
let newMax = maxId;
|
|
|
|
|
|
items.forEach(it => { const m = it.dataset.id?.match(/evt-(\d+)/); if (m) newMax = Math.max(newMax, parseInt(m[1])); });
|
|
|
|
|
|
const newId = `evt-${newMax + 1}`;
|
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
|
div.className = 'struct-item event-item';
|
|
|
|
|
|
div.dataset.id = newId;
|
|
|
|
|
|
div.innerHTML = `<div class="struct-row"><input type="text" class="event-title" placeholder="事件标题"><input type="text" class="event-time" placeholder="时间标签"></div><div class="struct-row"><textarea class="event-summary" rows="2" placeholder="一句话描述"></textarea></div><div class="struct-row"><input type="text" class="event-participants" placeholder="人物(顿号分隔)"></div><div class="struct-row"><select class="event-type">${['相遇','冲突','揭示','抉择','羁绊','转变','收束','日常'].map(t => `<option value="${t}">${t}</option>`).join('')}</select><select class="event-weight">${['核心','主线','转折','点睛','氛围'].map(t => `<option value="${t}">${t}</option>`).join('')}</select></div><div class="struct-actions"><span>ID:${newId}</span></div>`;
|
|
|
|
|
|
const del = createDeleteBtn();
|
|
|
|
|
|
div.querySelector('.struct-actions').appendChild(del);
|
|
|
|
|
|
del.addEventListener('click', () => div.remove());
|
|
|
|
|
|
editorStructured.insertBefore(div, document.getElementById('event-add').parentElement);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
function renderCharactersEditor(data) {
|
|
|
|
|
|
const d = data || { main: [], relationships: [] };
|
|
|
|
|
|
const main = (d.main || []).map(getCharName);
|
|
|
|
|
|
const rels = d.relationships || [];
|
|
|
|
|
|
editorStructured.innerHTML = `<div class="struct-item"><div class="struct-row"><strong>主要角色</strong></div><div id="char-main-list">${(main.length ? main : ['']).map(name => `<div class="struct-row char-main-item"><input type="text" class="char-main-name" placeholder="角色名" value="${name || ''}"></div>`).join('')}</div><div style="margin-top:8px;"><button type="button" class="btn btn-sm" id="char-main-add">+ 新增主要角色</button></div></div><div class="struct-item"><div class="struct-row"><strong>人物关系</strong></div><div id="char-rel-list">${(rels.length ? rels : [{ from: '', to: '', label: '', trend: '不变' }]).map((r, i) => `<div class="struct-row char-rel-item" data-idx="${i}"><input type="text" class="char-rel-from" placeholder="角色 A" value="${r.from || ''}"><input type="text" class="char-rel-to" placeholder="角色 B" value="${r.to || ''}"><input type="text" class="char-rel-label" placeholder="关系" value="${r.label || ''}"><select class="char-rel-trend">${['亲近','疏远','不变','新建','破裂'].map(t => `<option value="${t}" ${r.trend === t ? 'selected' : ''}>${t}</option>`).join('')}</select></div>`).join('')}</div><div style="margin-top:8px;"><button type="button" class="btn btn-sm" id="char-rel-add">+ 新增关系</button></div></div>`;
|
|
|
|
|
|
editorStructured.querySelectorAll('.char-main-item').forEach(item => {
|
|
|
|
|
|
const del = createDeleteBtn();
|
|
|
|
|
|
item.appendChild(del);
|
|
|
|
|
|
del.addEventListener('click', () => item.remove());
|
|
|
|
|
|
});
|
|
|
|
|
|
document.getElementById('char-main-add')?.addEventListener('click', () => {
|
|
|
|
|
|
const list = document.getElementById('char-main-list');
|
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
|
div.className = 'struct-row char-main-item';
|
|
|
|
|
|
div.innerHTML = '<input type="text" class="char-main-name" placeholder="角色名">';
|
|
|
|
|
|
const del = createDeleteBtn();
|
|
|
|
|
|
div.appendChild(del);
|
|
|
|
|
|
del.addEventListener('click', () => div.remove());
|
|
|
|
|
|
list.appendChild(div);
|
|
|
|
|
|
});
|
|
|
|
|
|
editorStructured.querySelectorAll('.char-rel-item').forEach(item => {
|
|
|
|
|
|
const del = createDeleteBtn();
|
|
|
|
|
|
item.appendChild(del);
|
|
|
|
|
|
del.addEventListener('click', () => item.remove());
|
|
|
|
|
|
});
|
|
|
|
|
|
document.getElementById('char-rel-add')?.addEventListener('click', () => {
|
|
|
|
|
|
const list = document.getElementById('char-rel-list');
|
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
|
div.className = 'struct-row char-rel-item';
|
|
|
|
|
|
div.innerHTML = `<input type="text" class="char-rel-from" placeholder="角色 A"><input type="text" class="char-rel-to" placeholder="角色 B"><input type="text" class="char-rel-label" placeholder="关系"><select class="char-rel-trend">${['亲近','疏远','不变','新建','破裂'].map(t => `<option value="${t}">${t}</option>`).join('')}</select>`;
|
|
|
|
|
|
const del = createDeleteBtn();
|
|
|
|
|
|
div.appendChild(del);
|
|
|
|
|
|
del.addEventListener('click', () => div.remove());
|
|
|
|
|
|
list.appendChild(div);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
function renderArcsEditor(arcs) {
|
|
|
|
|
|
const list = arcs?.length ? arcs : [{ name: '', trajectory: '', progress: 0, moments: [] }];
|
|
|
|
|
|
editorStructured.innerHTML = `<div id="arc-list">${list.map((arc, idx) => {
|
|
|
|
|
|
const momentsText = (arc.moments || []).map(m => typeof m === 'string' ? m : m.text).join('\n');
|
|
|
|
|
|
return `<div class="struct-item arc-item" data-index="${idx}"><div class="struct-row"><input type="text" class="arc-name" placeholder="角色名" value="${arc.name || ''}"></div><div class="struct-row"><textarea class="arc-trajectory" rows="2" placeholder="当前状态描述">${arc.trajectory || ''}</textarea></div><div class="struct-row"><label style="font-size:0.75rem;color:var(--text-muted);">进度:<input type="number" class="arc-progress" min="0" max="100" value="${Math.round((arc.progress || 0) * 100)}" style="width:64px;display:inline-block;"> %</label></div><div class="struct-row"><textarea class="arc-moments" rows="3" placeholder="关键时刻,一行一个">${momentsText}</textarea></div><div class="struct-actions"><span>角色弧光 ${idx + 1}</span></div></div>`;
|
|
|
|
|
|
}).join('')}</div><div style="margin-top:8px;"><button type="button" class="btn btn-sm" id="arc-add">+ 新增角色弧光</button></div>`;
|
|
|
|
|
|
editorStructured.querySelectorAll('.arc-item').forEach(item => {
|
|
|
|
|
|
const del = createDeleteBtn();
|
|
|
|
|
|
item.querySelector('.struct-actions').appendChild(del);
|
|
|
|
|
|
del.addEventListener('click', () => item.remove());
|
|
|
|
|
|
});
|
|
|
|
|
|
document.getElementById('arc-add')?.addEventListener('click', () => {
|
|
|
|
|
|
const listEl = document.getElementById('arc-list');
|
|
|
|
|
|
const idx = listEl.querySelectorAll('.arc-item').length;
|
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
|
div.className = 'struct-item arc-item';
|
|
|
|
|
|
div.dataset.index = idx;
|
|
|
|
|
|
div.innerHTML = `<div class="struct-row"><input type="text" class="arc-name" placeholder="角色名"></div><div class="struct-row"><textarea class="arc-trajectory" rows="2" placeholder="当前状态描述"></textarea></div><div class="struct-row"><label style="font-size:0.75rem;color:var(--text-muted);">进度:<input type="number" class="arc-progress" min="0" max="100" value="0" style="width:64px;display:inline-block;"> %</label></div><div class="struct-row"><textarea class="arc-moments" rows="3" placeholder="关键时刻,一行一个"></textarea></div><div class="struct-actions"><span>角色弧光 ${idx + 1}</span></div>`;
|
|
|
|
|
|
const del = createDeleteBtn();
|
|
|
|
|
|
div.querySelector('.struct-actions').appendChild(del);
|
|
|
|
|
|
del.addEventListener('click', () => div.remove());
|
|
|
|
|
|
listEl.appendChild(div);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
function openEditor(section) {
|
|
|
|
|
|
currentEditSection = section;
|
|
|
|
|
|
const meta = sectionMeta[section];
|
|
|
|
|
|
document.getElementById('editor-title').textContent = meta.title;
|
|
|
|
|
|
document.getElementById('editor-hint').textContent = meta.hint;
|
|
|
|
|
|
editorError.classList.remove('visible');
|
|
|
|
|
|
editorError.textContent = '';
|
|
|
|
|
|
editorStructured.classList.add('hidden');
|
|
|
|
|
|
editorTextarea.classList.remove('hidden');
|
|
|
|
|
|
if (section === 'keywords') {
|
|
|
|
|
|
editorTextarea.value = summaryData.keywords.map(k => `${k.text}|${k.weight || '一般'}`).join('\n');
|
|
|
|
|
|
} else if (section === 'events') {
|
|
|
|
|
|
editorTextarea.classList.add('hidden');
|
|
|
|
|
|
editorStructured.classList.remove('hidden');
|
|
|
|
|
|
renderEventsEditor(summaryData.events || []);
|
|
|
|
|
|
} else if (section === 'characters') {
|
|
|
|
|
|
editorTextarea.classList.add('hidden');
|
|
|
|
|
|
editorStructured.classList.remove('hidden');
|
|
|
|
|
|
renderCharactersEditor(summaryData.characters || { main: [], relationships: [] });
|
|
|
|
|
|
} else if (section === 'arcs') {
|
|
|
|
|
|
editorTextarea.classList.add('hidden');
|
|
|
|
|
|
editorStructured.classList.remove('hidden');
|
|
|
|
|
|
renderArcsEditor(summaryData.arcs || []);
|
|
|
|
|
|
}
|
|
|
|
|
|
editorModal.classList.add('active');
|
|
|
|
|
|
window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type: 'EDITOR_OPENED' }, '*');
|
|
|
|
|
|
}
|
|
|
|
|
|
function closeEditor() { editorModal.classList.remove('active'); currentEditSection = null; window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type: 'EDITOR_CLOSED' }, '*'); }
|
|
|
|
|
|
function saveEditor() {
|
|
|
|
|
|
const section = currentEditSection;
|
|
|
|
|
|
let parsed;
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (section === 'keywords') {
|
|
|
|
|
|
const oldMap = new Map((summaryData.keywords || []).map(k => [k.text, k]));
|
|
|
|
|
|
parsed = editorTextarea.value.trim().split('\n').filter(l => l.trim()).map(line => {
|
|
|
|
|
|
const [text, weight] = line.split('|').map(s => s.trim());
|
|
|
|
|
|
return preserveAddedAt({ text: text || '', weight: weight || '一般' }, oldMap.get(text));
|
|
|
|
|
|
});
|
|
|
|
|
|
} else if (section === 'events') {
|
|
|
|
|
|
const oldMap = new Map((summaryData.events || []).map(e => [e.id, e]));
|
|
|
|
|
|
parsed = Array.from(editorStructured.querySelectorAll('.event-item')).map(item => {
|
|
|
|
|
|
const id = item.dataset.id;
|
|
|
|
|
|
const title = item.querySelector('.event-title').value.trim();
|
|
|
|
|
|
const timeLabel = item.querySelector('.event-time').value.trim();
|
|
|
|
|
|
const summary = item.querySelector('.event-summary').value.trim();
|
|
|
|
|
|
const participants = item.querySelector('.event-participants').value.trim().split(/[,、,]/).map(s => s.trim()).filter(Boolean);
|
|
|
|
|
|
const type = item.querySelector('.event-type').value;
|
|
|
|
|
|
const weight = item.querySelector('.event-weight').value;
|
|
|
|
|
|
return preserveAddedAt({ id, title, timeLabel, summary, participants, type, weight }, oldMap.get(id));
|
|
|
|
|
|
}).filter(ev => ev.title || ev.summary);
|
|
|
|
|
|
} else if (section === 'characters') {
|
|
|
|
|
|
const oldMainMap = new Map((summaryData.characters?.main || []).map(m => [getCharName(m), m]));
|
|
|
|
|
|
const mainNames = Array.from(editorStructured.querySelectorAll('.char-main-name')).map(i => i.value.trim()).filter(Boolean);
|
|
|
|
|
|
const main = mainNames.map(name => preserveAddedAt({ name }, oldMainMap.get(name)));
|
|
|
|
|
|
const oldRelMap = new Map((summaryData.characters?.relationships || []).map(r => [`${r.from}->${r.to}`, r]));
|
|
|
|
|
|
const rels = Array.from(editorStructured.querySelectorAll('.char-rel-item')).map(item => {
|
|
|
|
|
|
const from = item.querySelector('.char-rel-from').value.trim();
|
|
|
|
|
|
const to = item.querySelector('.char-rel-to').value.trim();
|
|
|
|
|
|
const label = item.querySelector('.char-rel-label').value.trim();
|
|
|
|
|
|
const trend = item.querySelector('.char-rel-trend').value;
|
|
|
|
|
|
return preserveAddedAt({ from, to, label, trend }, oldRelMap.get(`${from}->${to}`));
|
|
|
|
|
|
}).filter(r => r.from && r.to);
|
|
|
|
|
|
parsed = { main, relationships: rels };
|
|
|
|
|
|
} else if (section === 'arcs') {
|
|
|
|
|
|
const oldArcMap = new Map((summaryData.arcs || []).map(a => [a.name, a]));
|
|
|
|
|
|
parsed = Array.from(editorStructured.querySelectorAll('.arc-item')).map(item => {
|
|
|
|
|
|
const name = item.querySelector('.arc-name').value.trim();
|
|
|
|
|
|
const trajectory = item.querySelector('.arc-trajectory').value.trim();
|
|
|
|
|
|
let progress = parseFloat(item.querySelector('.arc-progress').value || '0') / 100;
|
|
|
|
|
|
progress = Math.max(0, Math.min(1, progress || 0));
|
|
|
|
|
|
const momentsRaw = item.querySelector('.arc-moments').value.trim();
|
|
|
|
|
|
const oldArc = oldArcMap.get(name);
|
|
|
|
|
|
const oldMomentMap = new Map((oldArc?.moments || []).map(m => [typeof m === 'string' ? m : m.text, m]));
|
|
|
|
|
|
const moments = momentsRaw ? momentsRaw.split('\n').map(s => s.trim()).filter(Boolean).map(text => preserveAddedAt({ text }, oldMomentMap.get(text))) : [];
|
|
|
|
|
|
return preserveAddedAt({ name, trajectory, progress, moments }, oldArc);
|
|
|
|
|
|
}).filter(a => a.name || a.trajectory || a.moments?.length);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
editorError.textContent = `格式错误: ${e.message}`;
|
|
|
|
|
|
editorError.classList.add('visible');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type: 'UPDATE_SECTION', section, data: parsed }, '*');
|
|
|
|
|
|
if (section === 'keywords') renderKeywords(parsed);
|
|
|
|
|
|
else if (section === 'events') {
|
|
|
|
|
|
renderTimeline(parsed);
|
|
|
|
|
|
document.getElementById('stat-events').textContent = parsed.length;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (section === 'characters') renderRelations(parsed);
|
|
|
|
|
|
else if (section === 'arcs') renderArcs(parsed);
|
|
|
|
|
|
closeEditor();
|
|
|
|
|
|
}
|
|
|
|
|
|
document.querySelectorAll('.section-edit-btn[data-section]').forEach(btn => btn.addEventListener('click', () => openEditor(btn.dataset.section)));
|
|
|
|
|
|
document.getElementById('editor-backdrop').addEventListener('click', closeEditor);
|
|
|
|
|
|
document.getElementById('editor-close').addEventListener('click', closeEditor);
|
|
|
|
|
|
document.getElementById('editor-cancel').addEventListener('click', closeEditor);
|
|
|
|
|
|
document.getElementById('editor-save').addEventListener('click', saveEditor);
|
|
|
|
|
|
const settingsModal = document.getElementById('settings-modal');
|
|
|
|
|
|
let tempConfig = null;
|
|
|
|
|
|
function updateProviderUI(provider) {
|
|
|
|
|
|
const pv = providerDefaults[provider] || providerDefaults.custom;
|
|
|
|
|
|
const isSt = provider === 'st';
|
|
|
|
|
|
document.getElementById('api-url-row').classList.toggle('hidden', isSt);
|
|
|
|
|
|
document.getElementById('api-key-row').classList.toggle('hidden', !pv.needKey);
|
|
|
|
|
|
document.getElementById('api-model-manual-row').classList.toggle('hidden', isSt || !pv.needManualModel);
|
|
|
|
|
|
const hasCache = config.api.modelCache.length > 0;
|
|
|
|
|
|
document.getElementById('api-model-select-row').classList.toggle('hidden', isSt || pv.needManualModel || !hasCache);
|
|
|
|
|
|
document.getElementById('api-connect-row').classList.toggle('hidden', isSt || !pv.canFetch);
|
|
|
|
|
|
const urlInput = document.getElementById('api-url');
|
|
|
|
|
|
if (!urlInput.value && pv.url) urlInput.value = pv.url;
|
|
|
|
|
|
}
|
|
|
|
|
|
function openSettings() {
|
|
|
|
|
|
tempConfig = JSON.parse(JSON.stringify(config));
|
|
|
|
|
|
document.getElementById('api-provider').value = config.api.provider;
|
|
|
|
|
|
document.getElementById('api-url').value = config.api.url;
|
|
|
|
|
|
document.getElementById('api-key').value = config.api.key;
|
|
|
|
|
|
document.getElementById('api-model-text').value = config.api.model;
|
|
|
|
|
|
document.getElementById('gen-temp').value = config.gen.temperature ?? '';
|
|
|
|
|
|
document.getElementById('gen-top-p').value = config.gen.top_p ?? '';
|
|
|
|
|
|
document.getElementById('gen-top-k').value = config.gen.top_k ?? '';
|
|
|
|
|
|
document.getElementById('gen-presence').value = config.gen.presence_penalty ?? '';
|
|
|
|
|
|
document.getElementById('gen-frequency').value = config.gen.frequency_penalty ?? '';
|
|
|
|
|
|
document.getElementById('trigger-enabled').checked = config.trigger.enabled;
|
|
|
|
|
|
document.getElementById('trigger-interval').value = config.trigger.interval;
|
|
|
|
|
|
document.getElementById('trigger-timing').value = config.trigger.timing;
|
2025-12-21 01:47:38 +08:00
|
|
|
|
|
|
|
|
|
|
const enabledCheckbox = document.getElementById('trigger-enabled');
|
|
|
|
|
|
if (config.trigger.timing === 'manual') {
|
|
|
|
|
|
enabledCheckbox.checked = false;
|
|
|
|
|
|
enabledCheckbox.disabled = true;
|
|
|
|
|
|
enabledCheckbox.parentElement.style.opacity = '0.5';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
enabledCheckbox.disabled = false;
|
|
|
|
|
|
enabledCheckbox.parentElement.style.opacity = '1';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-19 02:19:10 +08:00
|
|
|
|
if (config.api.modelCache.length > 0) {
|
|
|
|
|
|
const sel = document.getElementById('api-model-select');
|
|
|
|
|
|
sel.innerHTML = config.api.modelCache.map(m => `<option value="${m}" ${m === config.api.model ? 'selected' : ''}>${m}</option>`).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
updateProviderUI(config.api.provider);
|
|
|
|
|
|
settingsModal.classList.add('active');
|
|
|
|
|
|
window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type: 'SETTINGS_OPENED' }, '*');
|
|
|
|
|
|
}
|
|
|
|
|
|
function closeSettings(save) {
|
|
|
|
|
|
if (save) {
|
|
|
|
|
|
const pn = id => { const v = document.getElementById(id).value; return v === '' ? null : parseFloat(v); };
|
|
|
|
|
|
const provider = document.getElementById('api-provider').value;
|
|
|
|
|
|
const pv = providerDefaults[provider] || providerDefaults.custom;
|
|
|
|
|
|
config.api.provider = provider;
|
|
|
|
|
|
config.api.url = document.getElementById('api-url').value;
|
|
|
|
|
|
config.api.key = document.getElementById('api-key').value;
|
|
|
|
|
|
if (provider === 'st') {
|
|
|
|
|
|
config.api.model = '';
|
|
|
|
|
|
} else if (pv.needManualModel) {
|
|
|
|
|
|
config.api.model = document.getElementById('api-model-text').value;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
config.api.model = document.getElementById('api-model-select').value;
|
|
|
|
|
|
}
|
|
|
|
|
|
config.gen.temperature = pn('gen-temp');
|
|
|
|
|
|
config.gen.top_p = pn('gen-top-p');
|
|
|
|
|
|
config.gen.top_k = pn('gen-top-k');
|
|
|
|
|
|
config.gen.presence_penalty = pn('gen-presence');
|
|
|
|
|
|
config.gen.frequency_penalty = pn('gen-frequency');
|
2025-12-21 01:47:38 +08:00
|
|
|
|
|
|
|
|
|
|
const timing = document.getElementById('trigger-timing').value;
|
|
|
|
|
|
config.trigger.timing = timing;
|
|
|
|
|
|
config.trigger.enabled = (timing === 'manual') ? false : document.getElementById('trigger-enabled').checked;
|
2025-12-19 02:19:10 +08:00
|
|
|
|
config.trigger.interval = parseInt(document.getElementById('trigger-interval').value) || 20;
|
2025-12-21 01:47:38 +08:00
|
|
|
|
|
2025-12-19 02:19:10 +08:00
|
|
|
|
saveConfig();
|
|
|
|
|
|
}
|
|
|
|
|
|
tempConfig = null;
|
|
|
|
|
|
settingsModal.classList.remove('active');
|
|
|
|
|
|
window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type: 'SETTINGS_CLOSED' }, '*');
|
|
|
|
|
|
}
|
|
|
|
|
|
async function fetchModels() {
|
|
|
|
|
|
const btn = document.getElementById('btn-connect');
|
|
|
|
|
|
const provider = document.getElementById('api-provider').value;
|
|
|
|
|
|
if (!providerDefaults[provider]?.canFetch) { alert('当前渠道不支持自动拉取模型'); return; }
|
|
|
|
|
|
let baseUrl = document.getElementById('api-url').value.trim().replace(/\/+$/, '');
|
|
|
|
|
|
const apiKey = document.getElementById('api-key').value.trim();
|
|
|
|
|
|
if (!apiKey) { alert('请先填写 API KEY'); return; }
|
|
|
|
|
|
btn.disabled = true;
|
|
|
|
|
|
btn.textContent = '连接中...';
|
|
|
|
|
|
try {
|
|
|
|
|
|
const tryFetch = async url => {
|
|
|
|
|
|
const res = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}`, Accept: 'application/json' } });
|
|
|
|
|
|
return res.ok ? (await res.json())?.data?.map(m => m?.id).filter(Boolean) || null : null;
|
|
|
|
|
|
};
|
|
|
|
|
|
if (baseUrl.endsWith('/v1')) baseUrl = baseUrl.slice(0, -3);
|
|
|
|
|
|
let models = await tryFetch(`${baseUrl}/v1/models`);
|
|
|
|
|
|
if (!models) models = await tryFetch(`${baseUrl}/models`);
|
|
|
|
|
|
if (!models?.length) throw new Error('未获取到模型列表');
|
|
|
|
|
|
config.api.modelCache = [...new Set(models)];
|
|
|
|
|
|
const sel = document.getElementById('api-model-select');
|
|
|
|
|
|
sel.innerHTML = config.api.modelCache.map(m => `<option value="${m}">${m}</option>`).join('');
|
|
|
|
|
|
document.getElementById('api-model-select-row').classList.remove('hidden');
|
|
|
|
|
|
if (!config.api.model && models.length) {
|
|
|
|
|
|
config.api.model = models[0];
|
|
|
|
|
|
sel.value = models[0];
|
|
|
|
|
|
} else if (config.api.model) {
|
|
|
|
|
|
sel.value = config.api.model;
|
|
|
|
|
|
}
|
|
|
|
|
|
saveConfig();
|
|
|
|
|
|
alert(`成功获取 ${models.length} 个模型`);
|
|
|
|
|
|
} catch (e) { alert('连接失败:' + (e.message || '请检查 URL 和 KEY')); }
|
|
|
|
|
|
finally { btn.disabled = false; btn.textContent = '连接 / 拉取模型列表'; }
|
|
|
|
|
|
}
|
|
|
|
|
|
document.getElementById('btn-settings').addEventListener('click', openSettings);
|
|
|
|
|
|
document.getElementById('settings-backdrop').addEventListener('click', () => closeSettings(false));
|
|
|
|
|
|
document.getElementById('settings-close').addEventListener('click', () => closeSettings(false));
|
|
|
|
|
|
document.getElementById('settings-cancel').addEventListener('click', () => closeSettings(false));
|
|
|
|
|
|
document.getElementById('settings-save').addEventListener('click', () => closeSettings(true));
|
|
|
|
|
|
document.getElementById('api-provider').addEventListener('change', e => {
|
|
|
|
|
|
const provider = e.target.value;
|
|
|
|
|
|
const pv = providerDefaults[provider];
|
|
|
|
|
|
document.getElementById('api-url').value = '';
|
|
|
|
|
|
if (!pv.canFetch) {
|
|
|
|
|
|
config.api.modelCache = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
updateProviderUI(provider);
|
|
|
|
|
|
});
|
|
|
|
|
|
document.getElementById('btn-connect').addEventListener('click', fetchModels);
|
|
|
|
|
|
document.getElementById('api-model-select').addEventListener('change', e => { config.api.model = e.target.value; });
|
|
|
|
|
|
document.getElementById('btn-clear').addEventListener('click', () => { window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type: 'REQUEST_CLEAR' }, '*'); });
|
|
|
|
|
|
document.getElementById('btn-generate').addEventListener('click', () => {
|
|
|
|
|
|
const btn = document.getElementById('btn-generate');
|
|
|
|
|
|
if (!localGenerating) {
|
|
|
|
|
|
localGenerating = true;
|
|
|
|
|
|
btn.textContent = '停止';
|
|
|
|
|
|
window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type: 'REQUEST_GENERATE', config: { api: config.api, gen: config.gen, trigger: config.trigger } }, '*');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
localGenerating = false;
|
|
|
|
|
|
btn.textContent = '总结';
|
|
|
|
|
|
window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type: 'REQUEST_CANCEL' }, '*');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
window.addEventListener('message', event => {
|
|
|
|
|
|
const data = event.data;
|
|
|
|
|
|
if (!data || data.source !== 'LittleWhiteBox') return;
|
|
|
|
|
|
const btn = document.getElementById('btn-generate');
|
|
|
|
|
|
switch (data.type) {
|
|
|
|
|
|
case 'GENERATION_STATE':
|
|
|
|
|
|
localGenerating = !!data.isGenerating;
|
|
|
|
|
|
btn.textContent = localGenerating ? '停止' : '总结';
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'SUMMARY_BASE_DATA':
|
|
|
|
|
|
if (data.stats) {
|
|
|
|
|
|
updateStats(data.stats);
|
|
|
|
|
|
document.getElementById('summarized-count').textContent = data.stats.hiddenCount ?? 0;
|
|
|
|
|
|
}
|
2025-12-21 01:47:38 +08:00
|
|
|
|
if (data.hideSummarized !== undefined) {
|
|
|
|
|
|
document.getElementById('hide-summarized').checked = data.hideSummarized;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (data.keepVisibleCount !== undefined) {
|
|
|
|
|
|
document.getElementById('keep-visible-count').value = data.keepVisibleCount;
|
|
|
|
|
|
}
|
2025-12-19 02:19:10 +08:00
|
|
|
|
break;
|
|
|
|
|
|
case 'SUMMARY_FULL_DATA':
|
|
|
|
|
|
if (data.payload) {
|
|
|
|
|
|
const p = data.payload;
|
|
|
|
|
|
if (p.keywords) renderKeywords(p.keywords);
|
|
|
|
|
|
if (p.events) renderTimeline(p.events);
|
|
|
|
|
|
if (p.characters) renderRelations(p.characters);
|
|
|
|
|
|
if (p.arcs) renderArcs(p.arcs);
|
|
|
|
|
|
document.getElementById('stat-events').textContent = p.events?.length || 0;
|
|
|
|
|
|
if (p.lastSummarizedMesId != null) document.getElementById('stat-summarized').textContent = p.lastSummarizedMesId + 1;
|
|
|
|
|
|
if (p.stats) updateStats(p.stats);
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'SUMMARY_ERROR':
|
|
|
|
|
|
console.error('Summary error:', data.message);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'SUMMARY_CLEARED':
|
|
|
|
|
|
const total = data.payload?.totalFloors || 0;
|
|
|
|
|
|
document.getElementById('stat-events').textContent = 0;
|
|
|
|
|
|
document.getElementById('stat-summarized').textContent = 0;
|
|
|
|
|
|
document.getElementById('stat-pending').textContent = total;
|
|
|
|
|
|
document.getElementById('summarized-count').textContent = 0;
|
|
|
|
|
|
summaryData = { keywords: [], events: [], characters: { main: [], relationships: [] }, arcs: [] };
|
|
|
|
|
|
renderKeywords([]);
|
|
|
|
|
|
renderTimeline([]);
|
|
|
|
|
|
renderRelations(null);
|
|
|
|
|
|
renderArcs([]);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
|
loadConfig();
|
|
|
|
|
|
document.getElementById('stat-events').textContent = '—';
|
|
|
|
|
|
document.getElementById('stat-summarized').textContent = '—';
|
|
|
|
|
|
document.getElementById('stat-pending').textContent = '—';
|
|
|
|
|
|
document.getElementById('summarized-count').textContent = '0';
|
|
|
|
|
|
renderKeywords([]);
|
|
|
|
|
|
renderTimeline([]);
|
|
|
|
|
|
renderArcs([]);
|
2025-12-21 01:47:38 +08:00
|
|
|
|
|
2025-12-19 02:19:10 +08:00
|
|
|
|
document.getElementById('hide-summarized').addEventListener('change', e => {
|
|
|
|
|
|
window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type: 'TOGGLE_HIDE_SUMMARIZED', enabled: e.target.checked }, '*');
|
|
|
|
|
|
});
|
2025-12-21 01:47:38 +08:00
|
|
|
|
|
|
|
|
|
|
document.getElementById('keep-visible-count').addEventListener('change', e => {
|
|
|
|
|
|
const count = Math.max(0, Math.min(50, parseInt(e.target.value) || 3));
|
|
|
|
|
|
e.target.value = count;
|
|
|
|
|
|
window.parent.postMessage({
|
|
|
|
|
|
source: 'LittleWhiteBox-StoryFrame',
|
|
|
|
|
|
type: 'UPDATE_KEEP_VISIBLE',
|
|
|
|
|
|
count: count
|
|
|
|
|
|
}, '*');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-12-19 02:19:10 +08:00
|
|
|
|
document.getElementById('btn-fullscreen-relations').addEventListener('click', openRelationsFullscreen);
|
|
|
|
|
|
document.getElementById('relations-fullscreen-backdrop').addEventListener('click', closeRelationsFullscreen);
|
|
|
|
|
|
document.getElementById('relations-fullscreen-close').addEventListener('click', closeRelationsFullscreen);
|
|
|
|
|
|
|
2025-12-21 01:47:38 +08:00
|
|
|
|
document.getElementById('trigger-timing').addEventListener('change', e => {
|
|
|
|
|
|
const timing = e.target.value;
|
|
|
|
|
|
const enabledCheckbox = document.getElementById('trigger-enabled');
|
|
|
|
|
|
if (timing === 'manual') {
|
|
|
|
|
|
enabledCheckbox.checked = false;
|
|
|
|
|
|
enabledCheckbox.disabled = true;
|
|
|
|
|
|
enabledCheckbox.parentElement.style.opacity = '0.5';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
enabledCheckbox.disabled = false;
|
|
|
|
|
|
enabledCheckbox.parentElement.style.opacity = '1';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-12-19 02:19:10 +08:00
|
|
|
|
window.addEventListener('resize', () => {
|
|
|
|
|
|
relationChart?.resize();
|
|
|
|
|
|
relationChartFullscreen?.resize();
|
|
|
|
|
|
});
|
2025-12-21 01:47:38 +08:00
|
|
|
|
|
2025-12-19 02:19:10 +08:00
|
|
|
|
window.parent.postMessage({ source: 'LittleWhiteBox-StoryFrame', type: 'FRAME_READY' }, '*');
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|
|
|
|
|
|
}
|