5598 lines
217 KiB
HTML
5598 lines
217 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}TODO 看板 - {{ todo_board.title }} - LandPPT{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
<style>
|
||
/* Outline view specific styles */
|
||
.outline-card {
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.outline-card:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||
}
|
||
|
||
.slide-number {
|
||
background: #3498db;
|
||
color: white;
|
||
border-radius: 50%;
|
||
width: 24px;
|
||
height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 0.8em;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.slide-number.large {
|
||
width: 32px;
|
||
height: 32px;
|
||
font-size: 1em;
|
||
}
|
||
|
||
.slide-type-tag {
|
||
background: #e8f4fd;
|
||
color: #3498db;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 0.8em;
|
||
}
|
||
|
||
.content-points {
|
||
margin: 0;
|
||
padding-left: 20px;
|
||
color: #555;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.content-points li {
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
/* Modal styles */
|
||
.modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0,0,0,0.5);
|
||
z-index: 1000;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
}
|
||
|
||
/* Button hover effects */
|
||
.btn:hover {
|
||
opacity: 0.9;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
/* Animation for loading states */
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* 脉冲动画 */
|
||
@keyframes pulse {
|
||
0% { transform: scale(1); opacity: 1; }
|
||
50% { transform: scale(1.1); opacity: 0.7; }
|
||
100% { transform: scale(1); opacity: 1; }
|
||
}
|
||
|
||
/* 波浪动画 */
|
||
@keyframes wave {
|
||
0%, 60%, 100% { transform: initial; }
|
||
30% { transform: translateY(-15px); }
|
||
}
|
||
|
||
/* 渐变背景动画 */
|
||
@keyframes gradientShift {
|
||
0% { background-position: 0% 50%; }
|
||
50% { background-position: 100% 50%; }
|
||
100% { background-position: 0% 50%; }
|
||
}
|
||
|
||
/* 打字机效果 */
|
||
@keyframes typing {
|
||
from { width: 0; }
|
||
to { width: 100%; }
|
||
}
|
||
|
||
/* 闪烁光标 */
|
||
@keyframes blink {
|
||
0%, 50% { opacity: 1; }
|
||
51%, 100% { opacity: 0; }
|
||
}
|
||
|
||
/* 进度条动画 */
|
||
@keyframes progressBar {
|
||
0% { width: 0%; }
|
||
25% { width: 30%; }
|
||
50% { width: 60%; }
|
||
75% { width: 85%; }
|
||
100% { width: 100%; }
|
||
}
|
||
|
||
/* 浮动动画 */
|
||
@keyframes float {
|
||
0%, 100% { transform: translateY(0px); }
|
||
50% { transform: translateY(-10px); }
|
||
}
|
||
|
||
/* 等待动画容器样式 */
|
||
.loading-container {
|
||
text-align: center;
|
||
padding: 30px 20px;
|
||
background: linear-gradient(-45deg, #f8f9fa, #e9ecef, #f8f9fa, #e9ecef);
|
||
background-size: 400% 400%;
|
||
animation: gradientShift 4s ease infinite;
|
||
border-radius: 15px;
|
||
margin: 15px 0;
|
||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||
max-height: 500px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.loading-icon {
|
||
font-size: 2.5em;
|
||
color: #3498db;
|
||
margin-bottom: 15px;
|
||
animation: pulse 2s ease-in-out infinite;
|
||
}
|
||
|
||
.loading-title {
|
||
font-size: 1.2em;
|
||
font-weight: 600;
|
||
color: #2c3e50;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
/* 创意AI大脑思考动画 */
|
||
.brain-container {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
margin: 20px 0;
|
||
height: 180px;
|
||
perspective: 1000px;
|
||
position: relative;
|
||
}
|
||
|
||
.ai-brain {
|
||
position: relative;
|
||
width: 120px;
|
||
height: 120px;
|
||
animation: brainFloat 4s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes brainFloat {
|
||
0%, 100% {
|
||
transform: translateY(0px) scale(1);
|
||
}
|
||
50% {
|
||
transform: translateY(-10px) scale(1.05);
|
||
}
|
||
}
|
||
|
||
.brain-core {
|
||
position: absolute;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
||
border-radius: 50%;
|
||
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.brain-core::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 20%;
|
||
left: 20%;
|
||
width: 60%;
|
||
height: 60%;
|
||
background: radial-gradient(circle, rgba(255,255,255,0.2) 0%, transparent 70%);
|
||
border-radius: 50%;
|
||
animation: brainPulse 2s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes brainPulse {
|
||
0%, 100% {
|
||
opacity: 0.3;
|
||
transform: scale(1);
|
||
}
|
||
50% {
|
||
opacity: 0.7;
|
||
transform: scale(1.1);
|
||
}
|
||
}
|
||
|
||
.brain-icon {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
font-size: 48px;
|
||
color: white;
|
||
text-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
||
animation: iconGlow 3s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes iconGlow {
|
||
0%, 100% {
|
||
text-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
||
}
|
||
50% {
|
||
text-shadow: 0 2px 20px rgba(255,255,255,0.5), 0 0 30px rgba(102, 126, 234, 0.8);
|
||
}
|
||
}
|
||
|
||
/* 思维连接线动画 */
|
||
.neural-network {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.neural-line {
|
||
position: absolute;
|
||
background: linear-gradient(90deg, transparent 0%, rgba(102, 126, 234, 0.6) 50%, transparent 100%);
|
||
border-radius: 2px;
|
||
opacity: 0;
|
||
animation: neuralPulse 3s ease-in-out infinite;
|
||
}
|
||
|
||
.neural-line:nth-child(1) {
|
||
top: 30%;
|
||
left: -20%;
|
||
width: 60px;
|
||
height: 2px;
|
||
transform: rotate(45deg);
|
||
animation-delay: 0s;
|
||
}
|
||
|
||
.neural-line:nth-child(2) {
|
||
top: 60%;
|
||
right: -20%;
|
||
width: 50px;
|
||
height: 2px;
|
||
transform: rotate(-30deg);
|
||
animation-delay: 0.5s;
|
||
}
|
||
|
||
.neural-line:nth-child(3) {
|
||
bottom: 20%;
|
||
left: -15%;
|
||
width: 45px;
|
||
height: 2px;
|
||
transform: rotate(-45deg);
|
||
animation-delay: 1s;
|
||
}
|
||
|
||
.neural-line:nth-child(4) {
|
||
top: 20%;
|
||
right: -15%;
|
||
width: 55px;
|
||
height: 2px;
|
||
transform: rotate(60deg);
|
||
animation-delay: 1.5s;
|
||
}
|
||
|
||
@keyframes neuralPulse {
|
||
0%, 70% {
|
||
opacity: 0;
|
||
transform: scale(0.5) rotate(var(--rotation, 0deg));
|
||
}
|
||
10%, 60% {
|
||
opacity: 1;
|
||
transform: scale(1) rotate(var(--rotation, 0deg));
|
||
}
|
||
100% {
|
||
opacity: 0;
|
||
transform: scale(0.5) rotate(var(--rotation, 0deg));
|
||
}
|
||
}
|
||
|
||
/* 思考气泡动画 */
|
||
.thought-bubbles {
|
||
position: absolute;
|
||
top: -20px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 200px;
|
||
height: 60px;
|
||
}
|
||
|
||
.thought-bubble {
|
||
position: absolute;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
border-radius: 50%;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
opacity: 0;
|
||
animation: bubbleFloat 4s ease-in-out infinite;
|
||
}
|
||
|
||
.thought-bubble:nth-child(1) {
|
||
width: 12px;
|
||
height: 12px;
|
||
bottom: 0;
|
||
left: 45%;
|
||
animation-delay: 0s;
|
||
}
|
||
|
||
.thought-bubble:nth-child(2) {
|
||
width: 18px;
|
||
height: 18px;
|
||
bottom: 15px;
|
||
left: 35%;
|
||
animation-delay: 0.3s;
|
||
}
|
||
|
||
.thought-bubble:nth-child(3) {
|
||
width: 24px;
|
||
height: 24px;
|
||
bottom: 35px;
|
||
left: 25%;
|
||
animation-delay: 0.6s;
|
||
}
|
||
|
||
@keyframes bubbleFloat {
|
||
0%, 80% {
|
||
opacity: 0;
|
||
transform: translateY(20px) scale(0.5);
|
||
}
|
||
10%, 70% {
|
||
opacity: 1;
|
||
transform: translateY(0px) scale(1);
|
||
}
|
||
100% {
|
||
opacity: 0;
|
||
transform: translateY(-10px) scale(0.8);
|
||
}
|
||
}
|
||
|
||
/* 进度环动画 */
|
||
.progress-ring {
|
||
position: absolute;
|
||
top: -10px;
|
||
left: -10px;
|
||
width: 140px;
|
||
height: 140px;
|
||
}
|
||
|
||
.progress-ring-circle {
|
||
fill: none;
|
||
stroke: rgba(102, 126, 234, 0.3);
|
||
stroke-width: 3;
|
||
stroke-linecap: round;
|
||
transform-origin: 50% 50%;
|
||
transform: rotate(-90deg);
|
||
animation: progressRotate 8s linear infinite;
|
||
}
|
||
|
||
.progress-ring-progress {
|
||
fill: none;
|
||
stroke: #667eea;
|
||
stroke-width: 3;
|
||
stroke-linecap: round;
|
||
stroke-dasharray: 440;
|
||
stroke-dashoffset: 440;
|
||
transform-origin: 50% 50%;
|
||
transform: rotate(-90deg);
|
||
animation: progressFill 8s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes progressRotate {
|
||
0% {
|
||
transform: rotate(-90deg);
|
||
}
|
||
100% {
|
||
transform: rotate(270deg);
|
||
}
|
||
}
|
||
|
||
@keyframes progressFill {
|
||
0%, 20% {
|
||
stroke-dashoffset: 440;
|
||
}
|
||
80%, 100% {
|
||
stroke-dashoffset: 0;
|
||
}
|
||
}
|
||
|
||
/* 文字生成效果 */
|
||
.text-generation {
|
||
position: absolute;
|
||
bottom: -40px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 200px;
|
||
text-align: center;
|
||
}
|
||
|
||
.generating-text {
|
||
color: #667eea;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
opacity: 0;
|
||
animation: textFade 4s ease-in-out infinite;
|
||
}
|
||
|
||
.generating-dots {
|
||
color: #667eea;
|
||
font-size: 16px;
|
||
letter-spacing: 2px;
|
||
animation: dotsAnimation 2s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes textFade {
|
||
0%, 20% {
|
||
opacity: 0;
|
||
transform: translateY(10px);
|
||
}
|
||
30%, 70% {
|
||
opacity: 1;
|
||
transform: translateY(0px);
|
||
}
|
||
80%, 100% {
|
||
opacity: 0;
|
||
transform: translateY(-10px);
|
||
}
|
||
}
|
||
|
||
@keyframes dotsAnimation {
|
||
0%, 20% {
|
||
opacity: 0.3;
|
||
}
|
||
50% {
|
||
opacity: 1;
|
||
}
|
||
100% {
|
||
opacity: 0.3;
|
||
}
|
||
}
|
||
|
||
/* 完成状态动画 */
|
||
.ai-brain.completed {
|
||
animation: brainComplete 2s ease-out forwards;
|
||
}
|
||
|
||
.ai-brain.completed .brain-core {
|
||
background: linear-gradient(135deg, #27ae60 0%, #2ecc71 50%, #58d68d 100%);
|
||
animation: completionGlow 1.5s ease-out;
|
||
}
|
||
|
||
.ai-brain.completed .brain-icon::before {
|
||
content: '✓';
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
font-size: 32px;
|
||
color: white;
|
||
animation: checkmarkAppear 1s ease-out;
|
||
}
|
||
|
||
@keyframes brainComplete {
|
||
0% {
|
||
transform: translateY(0px) scale(1);
|
||
}
|
||
50% {
|
||
transform: translateY(-20px) scale(1.2);
|
||
}
|
||
100% {
|
||
transform: translateY(-5px) scale(1.1);
|
||
}
|
||
}
|
||
|
||
@keyframes completionGlow {
|
||
0% {
|
||
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
|
||
}
|
||
50% {
|
||
box-shadow: 0 12px 48px rgba(39, 174, 96, 0.6);
|
||
}
|
||
100% {
|
||
box-shadow: 0 10px 40px rgba(39, 174, 96, 0.4);
|
||
}
|
||
}
|
||
|
||
@keyframes checkmarkAppear {
|
||
0% {
|
||
opacity: 0;
|
||
transform: translate(-50%, -50%) scale(0.3);
|
||
}
|
||
50% {
|
||
opacity: 1;
|
||
transform: translate(-50%, -50%) scale(1.3);
|
||
}
|
||
100% {
|
||
opacity: 1;
|
||
transform: translate(-50%, -50%) scale(1);
|
||
}
|
||
}
|
||
|
||
.progress-container {
|
||
width: 100%;
|
||
height: 6px;
|
||
background: #e9ecef;
|
||
border-radius: 3px;
|
||
margin: 20px 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.progress-bar {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #3498db, #2ecc71);
|
||
border-radius: 3px;
|
||
animation: progressBar 15s ease-in-out infinite;
|
||
}
|
||
|
||
.loading-tips {
|
||
margin-top: 20px;
|
||
padding: 12px;
|
||
background: rgba(52, 152, 219, 0.1);
|
||
border-radius: 8px;
|
||
border-left: 4px solid #3498db;
|
||
}
|
||
|
||
.loading-tips h5 {
|
||
color: #2c3e50;
|
||
margin-bottom: 8px;
|
||
font-size: 1em;
|
||
}
|
||
|
||
.loading-tips p {
|
||
color: #666;
|
||
margin: 0;
|
||
font-size: 0.9em;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
@keyframes blink {
|
||
0%, 50% { opacity: 1; }
|
||
51%, 100% { opacity: 0; }
|
||
}
|
||
|
||
/* 表单美化 */
|
||
.form-group {
|
||
margin-bottom: 25px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
color: #2c3e50;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.form-group input,
|
||
.form-group select,
|
||
.form-group textarea {
|
||
width: 100%;
|
||
padding: 12px 16px;
|
||
border: 2px solid #e9ecef;
|
||
border-radius: 10px;
|
||
font-size: 14px;
|
||
transition: all 0.3s ease;
|
||
background: white;
|
||
}
|
||
|
||
.form-group input:focus,
|
||
.form-group select:focus,
|
||
.form-group textarea:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
/* 按钮美化 */
|
||
.btn {
|
||
padding: 12px 24px;
|
||
border-radius: 10px;
|
||
font-weight: 600;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
border: none;
|
||
cursor: pointer;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
text-decoration: none;
|
||
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 20px rgba(0,0,0,0.15);
|
||
}
|
||
|
||
.btn-primary {
|
||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||
color: white;
|
||
}
|
||
|
||
.btn-success {
|
||
background: linear-gradient(135deg, #27ae60, #229954);
|
||
color: white;
|
||
}
|
||
|
||
.btn-outline-primary {
|
||
background: transparent;
|
||
color: #667eea;
|
||
border: 2px solid #667eea;
|
||
}
|
||
|
||
.btn-outline-primary:hover {
|
||
background: #667eea;
|
||
color: white;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: linear-gradient(135deg, #95a5a6, #7f8c8d);
|
||
color: white;
|
||
}
|
||
|
||
/* 小尺寸按钮 */
|
||
.btn-sm {
|
||
padding: 8px 16px;
|
||
font-size: 13px;
|
||
gap: 6px;
|
||
}
|
||
|
||
.btn-xs {
|
||
padding: 6px 12px;
|
||
font-size: 12px;
|
||
gap: 4px;
|
||
}
|
||
|
||
/* 大纲相关按钮样式 */
|
||
.btn-outline-warning {
|
||
background: transparent;
|
||
color: #f39c12;
|
||
border: 2px solid #f39c12;
|
||
}
|
||
|
||
.btn-outline-warning:hover {
|
||
background: #f39c12;
|
||
color: white;
|
||
}
|
||
|
||
.btn-info {
|
||
background: linear-gradient(135deg, #3498db, #2980b9);
|
||
color: white;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.scenarios-hero {
|
||
padding: 30px 15px;
|
||
margin: -20px -15px 30px -15px;
|
||
}
|
||
|
||
.scenarios-hero h2 {
|
||
font-size: 1.8em;
|
||
}
|
||
|
||
#requirements-section {
|
||
margin: 15px;
|
||
padding: 25px 20px;
|
||
}
|
||
|
||
.btn-group-custom {
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.btn-group-custom a {
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<!-- 页面头部美化 -->
|
||
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px 0; margin: -20px -20px 30px -20px; text-align: center; position: relative; overflow: hidden;">
|
||
<div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><defs><pattern id=\"dots\" width=\"20\" height=\"20\" patternUnits=\"userSpaceOnUse\"><circle cx=\"10\" cy=\"10\" r=\"1\" fill=\"white\" opacity=\"0.1\"/></pattern></defs><rect width=\"100\" height=\"100\" fill=\"url(%23dots)\"/></svg></div>
|
||
<div style="position: relative; z-index: 1;">
|
||
<h2 style="font-size: 2em; font-weight: 700; margin-bottom: 12px; text-shadow: 0 2px 10px rgba(0,0,0,0.3);">📋 {{ todo_board.title }}</h2>
|
||
<p style="opacity: 0.9; font-size: 1em;">项目ID: <code style="background: rgba(255,255,255,0.2); padding: 4px 10px; border-radius: 6px; font-family: 'Consolas', monospace; font-size: 0.9em;">{{ todo_board.task_id }}</code></p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Requirements Confirmation Section -->
|
||
{% set requirements_stage = todo_board.stages | selectattr('id', 'equalto', 'requirements_confirmation') | first %}
|
||
{% if requirements_stage and requirements_stage.status == 'pending' %}
|
||
<div id="requirements-section" style="max-width: 900px; margin: 15px auto; background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); border-radius: 16px; padding: 30px; box-shadow: 0 10px 30px rgba(0,0,0,0.08); border: 1px solid rgba(102, 126, 234, 0.15);">
|
||
<div style="text-align: center; margin-bottom: 25px;">
|
||
<div style="display: inline-block; background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 12px 20px; border-radius: 40px; margin-bottom: 15px; box-shadow: 0 6px 20px rgba(102, 126, 234, 0.25);">
|
||
<h3 style="margin: 0; font-weight: 600; font-size: 1.1em;">📝 需求确认</h3>
|
||
</div>
|
||
<p style="color: #7f8c8d; margin-bottom: 0; font-size: 1em; line-height: 1.5;">请确认以下信息,AI将根据您的确认生成定制化的PPT内容</p>
|
||
</div>
|
||
|
||
<!-- Loading indicator for requirements form -->
|
||
<div id="ai-loading" class="loading-container" style="display: block;">
|
||
<div class="loading-icon">
|
||
<i class="fas fa-cog"></i>
|
||
</div>
|
||
<div class="loading-title">正在准备需求表单</div>
|
||
<div class="progress-container">
|
||
<div class="progress-bar" style="animation-duration: 2s;"></div>
|
||
</div>
|
||
<div class="loading-tips">
|
||
<h5><i class="fas fa-info-circle"></i> 提示</h5>
|
||
<p>请稍候,系统正在为您准备项目需求确认表单...</p>
|
||
</div>
|
||
</div>
|
||
|
||
<form id="requirements-form" style="text-align: left; display: none;" enctype="multipart/form-data">
|
||
|
||
<!-- Content Source Selection -->
|
||
<div style="margin-bottom: 20px;">
|
||
<label style="display: block; margin-bottom: 8px; color: #2c3e50; font-weight: bold;">内容来源选择</label>
|
||
<div style="display: flex; gap: 15px; margin-bottom: 15px;">
|
||
<label style="display: flex; align-items: center; cursor: pointer;">
|
||
<input type="radio" name="content_source" value="manual" checked onchange="toggleContentSourceTodo()" style="margin-right: 8px;">
|
||
<span>手动输入主题</span>
|
||
</label>
|
||
<label style="display: flex; align-items: center; cursor: pointer;">
|
||
<input type="radio" name="content_source" value="file" onchange="toggleContentSourceTodo()" style="margin-right: 8px;">
|
||
<span>从文件生成</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- File Upload Section (hidden by default) -->
|
||
<div id="file-upload-section-todo" style="display: none; margin-bottom: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef;">
|
||
<label style="display: block; margin-bottom: 8px; color: #2c3e50; font-weight: bold;">📁 上传文件 (支持多文件)</label>
|
||
<input type="file" id="file_upload_todo" name="file_upload" accept=".pdf,.docx,.txt,.md,.jpg,.jpeg,.png,.xlsx,.csv"
|
||
multiple
|
||
style="width: 100%; padding: 8px; border: 2px dashed #3498db; background: #f8f9fa; border-radius: 6px;">
|
||
<small style="color: #7f8c8d; display: block; margin-top: 5px;">
|
||
📌 支持同时选择多个文件 | 支持 PDF、DOCX、TXT、MD 等格式 | 单个文件最大 100MB
|
||
</small>
|
||
|
||
<!-- Multiple Files List Display -->
|
||
<div id="selected-files-list-todo" style="margin-top: 10px; display: none;">
|
||
<div style="background: white; padding: 10px; border-radius: 6px; border: 1px solid #e0e0e0;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||
<strong style="color: #2c3e50;">已选择的文件:</strong>
|
||
<button type="button" onclick="clearAllFiles()"
|
||
style="padding: 4px 12px; background: #e74c3c; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px;">
|
||
清除所有
|
||
</button>
|
||
</div>
|
||
<div id="files-list-container-todo" style="max-height: 200px; overflow-y: auto;">
|
||
<!-- Files will be listed here -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- File Processing Options -->
|
||
<div id="file-processing-options-todo" style="margin-top: 15px; padding: 15px; background: white; border-radius: 8px; border: 1px solid #e9ecef; display: none;">
|
||
<h6 style="margin-bottom: 10px; color: #2c3e50;">文件处理选项</h6>
|
||
|
||
<div style="display: flex; flex-wrap: wrap; gap: 15px;">
|
||
<!-- PDF专用处理方式选项 -->
|
||
<div id="pdf-processing-mode-todo" style="display: none; flex: 1; min-width: 200px;">
|
||
<label style="display: block; margin-bottom: 5px; color: #2c3e50; font-weight: normal;">处理方式:</label>
|
||
<select name="file_processing_mode" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
|
||
<option value="markitdown">标准处理 (MarkItDown)</option>
|
||
<option value="magic_pdf">高质量处理 (Mineru)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 通用解析深度选项 -->
|
||
<div style="flex: 1; min-width: 200px;">
|
||
<label style="display: block; margin-bottom: 5px; color: #2c3e50; font-weight: normal;">解析深度:</label>
|
||
<select name="content_analysis_depth" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
|
||
<option value="fast" selected>快速解析</option>
|
||
<option value="standard">标准解析</option>
|
||
<option value="deep">深度解析</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px;">
|
||
<div>
|
||
<label style="display: block; margin-bottom: 8px; color: #2c3e50; font-weight: bold;">主题 (Topic)</label>
|
||
<input type="text" id="topic" name="topic" value="{{ todo_board.title.split(' - ')[0] if ' - ' in todo_board.title else todo_board.title }}"
|
||
style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px;" required>
|
||
<small style="color: #7f8c8d; font-size: 12px; margin-top: 5px; display: block;">文件上传时可留空,将自动从文件提取标题</small>
|
||
</div>
|
||
<div>
|
||
<label style="display: block; margin-bottom: 8px; color: #2c3e50; font-weight: bold;">目标受众 (Target Audience)</label>
|
||
<div style="margin-bottom: 10px;">
|
||
<select id="audience_type" name="audience_type" style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px;" onchange="toggleCustomAudience()" required>
|
||
<option value="">请选择目标受众</option>
|
||
<option value="企业管理层">企业管理层</option>
|
||
<option value="技术团队">技术团队</option>
|
||
<option value="销售团队">销售团队</option>
|
||
<option value="学生群体">学生群体</option>
|
||
<option value="学术研究者">学术研究者</option>
|
||
<option value="投资人">投资人</option>
|
||
<option value="客户群体">客户群体</option>
|
||
<option value="培训学员">培训学员</option>
|
||
<option value="项目团队">项目团队</option>
|
||
<option value="行业专家">行业专家</option>
|
||
<option value="普通大众">普通大众</option>
|
||
<option value="自定义">自定义受众</option>
|
||
</select>
|
||
</div>
|
||
<!-- 自定义受众输入框 -->
|
||
<div id="custom-audience-section" style="display: none;">
|
||
<input type="text" id="custom_audience" name="custom_audience"
|
||
placeholder="请描述您的目标受众,例如:初级程序员、产品经理、高中生等..."
|
||
style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px;">
|
||
<small style="color: #7f8c8d; font-size: 12px; margin-top: 5px; display: block;">详细描述您的目标受众特征,AI将据此调整内容深度和表达方式</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
|
||
|
||
|
||
<div style="margin-bottom: 25px;">
|
||
<label style="display: block; margin-bottom: 8px; color: #2c3e50; font-weight: bold;">PPT页数设置 (Page Count)</label>
|
||
<p style="font-size: 12px; color: #7f8c8d; margin-bottom: 15px;">选择PPT的页数生成方式:</p>
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;">
|
||
<div class="page-count-option active" data-mode="ai_decide" style="border: 2px solid #3498db; border-radius: 10px; padding: 15px; cursor: pointer; text-align: center; transition: all 0.3s ease;">
|
||
<div style="font-size: 24px; margin-bottom: 8px;">🤖</div>
|
||
<h5 style="color: #2c3e50; margin-bottom: 5px;">AI智能决定</h5>
|
||
<p style="font-size: 12px; color: #7f8c8d;">AI根据内容深度和逻辑结构自主决定最合适的页数</p>
|
||
</div>
|
||
<div class="page-count-option" data-mode="custom_range" style="border: 2px solid #ddd; border-radius: 10px; padding: 15px; cursor: pointer; text-align: center; transition: all 0.3s ease;">
|
||
<div style="font-size: 24px; margin-bottom: 8px;">📊</div>
|
||
<h5 style="color: #2c3e50; margin-bottom: 5px;">自定义范围</h5>
|
||
<p style="font-size: 12px; color: #7f8c8d;">在指定范围内生成PPT页数</p>
|
||
</div>
|
||
</div>
|
||
<input type="hidden" id="page_count_mode" name="page_count_mode" value="ai_decide">
|
||
|
||
<!-- Custom range section (hidden by default) -->
|
||
<div id="custom-range-section" style="display: none; margin-top: 15px; padding: 15px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef;">
|
||
<div style="display: flex; gap: 15px; align-items: center;">
|
||
<div style="flex: 1;">
|
||
<label style="display: block; margin-bottom: 5px; color: #2c3e50; font-weight: bold;">最少页数</label>
|
||
<input type="number" id="min_pages" name="min_pages" value="8" min="5" max="50"
|
||
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 6px;">
|
||
</div>
|
||
<div style="flex: 1;">
|
||
<label style="display: block; margin-bottom: 5px; color: #2c3e50; font-weight: bold;">最多页数</label>
|
||
<input type="number" id="max_pages" name="max_pages" value="15" min="5" max="50"
|
||
style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 6px;">
|
||
</div>
|
||
</div>
|
||
<p style="font-size: 12px; color: #7f8c8d; margin-top: 10px; margin-bottom: 0;">
|
||
建议范围:5-50页,AI会在此范围内生成最合适的页数
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-bottom: 25px;">
|
||
<label style="display: block; margin-bottom: 8px; color: #2c3e50; font-weight: bold;">PPT风格 (Style)</label>
|
||
<p style="font-size: 12px; color: #7f8c8d; margin-bottom: 15px;">选择适合您内容的PPT风格:</p>
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; margin-bottom: 15px;">
|
||
<div class="style-option" data-style="general" style="border: 2px solid #ddd; border-radius: 10px; padding: 15px; cursor: pointer; text-align: center; transition: all 0.3s ease;">
|
||
<div style="font-size: 24px; margin-bottom: 8px;">📋</div>
|
||
<h5 style="color: #2c3e50; margin-bottom: 5px;">通用场景</h5>
|
||
<p style="font-size: 12px; color: #7f8c8d;">适用于商务汇报、学术演讲等通用场景</p>
|
||
</div>
|
||
<div class="style-option" data-style="keynote" style="border: 2px solid #ddd; border-radius: 10px; padding: 15px; cursor: pointer; text-align: center; transition: all 0.3s ease;">
|
||
<div style="font-size: 24px; margin-bottom: 8px;">🎯</div>
|
||
<h5 style="color: #2c3e50; margin-bottom: 5px;">发布会</h5>
|
||
<p style="font-size: 12px; color: #7f8c8d;">Apple风格发布会,卡片式布局,科技感强</p>
|
||
</div>
|
||
<div class="style-option" data-style="custom" style="border: 2px solid #ddd; border-radius: 10px; padding: 15px; cursor: pointer; text-align: center; transition: all 0.3s ease;">
|
||
<div style="font-size: 24px; margin-bottom: 8px;">🎨</div>
|
||
<h5 style="color: #2c3e50; margin-bottom: 5px;">自定义风格</h5>
|
||
<p style="font-size: 12px; color: #7f8c8d;">使用自定义提示词定制独特风格</p>
|
||
</div>
|
||
</div>
|
||
<input type="hidden" id="ppt_style" name="ppt_style" value="general" required>
|
||
|
||
<!-- Custom style prompt input (hidden by default) -->
|
||
<div id="custom-style-section" style="display: none; margin-top: 15px; padding: 15px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef;">
|
||
<label style="display: block; margin-bottom: 8px; color: #2c3e50; font-weight: bold;">自定义风格提示词</label>
|
||
<textarea id="custom_style_prompt" name="custom_style_prompt"
|
||
style="width: 100%; height: 120px; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; resize: vertical;"
|
||
placeholder="请输入您希望的PPT风格描述,例如:简约现代风格,使用蓝色主题,卡片式布局..."></textarea>
|
||
<p style="font-size: 12px; color: #7f8c8d; margin-top: 5px;">详细描述您期望的PPT风格,AI将根据您的描述生成相应的设计</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="text-align: center;">
|
||
<button type="submit" id="confirm-requirements-btn"
|
||
style="background: #27ae60; color: white; border: none; padding: 15px 30px; border-radius: 8px; font-size: 16px; cursor: pointer; font-weight: bold;">
|
||
🚀 确认需求并跳转到大纲生成
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Project Actions -->
|
||
<div style="text-align: center; margin-bottom: 40px;">
|
||
<div style="display: inline-flex; gap: 12px; flex-wrap: wrap; justify-content: center; background: white; padding: 20px; border-radius: 16px; box-shadow: 0 8px 25px rgba(0,0,0,0.08);">
|
||
<a href="/projects/{{ todo_board.task_id }}" style="background: linear-gradient(135deg, #3498db, #2980b9); color: white; text-decoration: none; padding: 10px 20px; border-radius: 10px; font-weight: 600; transition: all 0.3s ease; box-shadow: 0 3px 12px rgba(52, 152, 219, 0.25); display: flex; align-items: center; gap: 6px; font-size: 0.9em;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 5px 16px rgba(52, 152, 219, 0.35)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 3px 12px rgba(52, 152, 219, 0.25)'">
|
||
📊 项目详情
|
||
</a>
|
||
|
||
{% if todo_board.overall_progress >= 100 %}
|
||
<a href="/projects/{{ todo_board.task_id }}/preview" target="_blank" style="background: linear-gradient(135deg, #27ae60, #229954); color: white; text-decoration: none; padding: 10px 20px; border-radius: 10px; font-weight: 600; transition: all 0.3s ease; box-shadow: 0 3px 12px rgba(39, 174, 96, 0.25); display: flex; align-items: center; gap: 6px; font-size: 0.9em;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 5px 16px rgba(39, 174, 96, 0.35)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 3px 12px rgba(39, 174, 96, 0.25)'">
|
||
🔍 预览 PPT
|
||
</a>
|
||
<a href="/projects/{{ todo_board.task_id }}/edit" target="_blank" style="background: linear-gradient(135deg, #e74c3c, #c0392b); color: white; text-decoration: none; padding: 10px 20px; border-radius: 10px; font-weight: 600; transition: all 0.3s ease; box-shadow: 0 3px 12px rgba(231, 76, 60, 0.25); display: flex; align-items: center; gap: 6px; font-size: 0.9em;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 5px 16px rgba(231, 76, 60, 0.35)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 3px 12px rgba(231, 76, 60, 0.25)'">
|
||
✏️ 编辑 PPT
|
||
</a>
|
||
{% endif %}
|
||
|
||
<a href="/projects" style="background: linear-gradient(135deg, #95a5a6, #7f8c8d); color: white; text-decoration: none; padding: 10px 20px; border-radius: 10px; font-weight: 600; transition: all 0.3s ease; box-shadow: 0 3px 12px rgba(149, 165, 166, 0.25); display: flex; align-items: center; gap: 6px; font-size: 0.9em;" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 5px 16px rgba(149, 165, 166, 0.35)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 3px 12px rgba(149, 165, 166, 0.25)'">
|
||
📋 返回项目列表
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Task execution is now handled directly in the stage cards -->
|
||
|
||
<style>
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
@keyframes blink {
|
||
0%, 50% { opacity: 1; }
|
||
51%, 100% { opacity: 0; }
|
||
}
|
||
|
||
.todo-stage {
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.todo-stage:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
||
}
|
||
|
||
.btn {
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.btn:hover {
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.btn-stream:hover {
|
||
background: #2980b9 !important;
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.task-item {
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.task-item.active {
|
||
background: #e8f4fd !important;
|
||
border-left: 4px solid #3498db;
|
||
}
|
||
|
||
.output-cursor {
|
||
display: inline-block;
|
||
}
|
||
|
||
.output-cursor.hidden {
|
||
display: none;
|
||
}
|
||
|
||
.style-option {
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.style-option:hover {
|
||
border-color: #3498db !important;
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.2);
|
||
}
|
||
|
||
.style-option.selected {
|
||
border-color: #3498db !important;
|
||
background: #e8f4fd !important;
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 2px 10px rgba(52, 152, 219, 0.3);
|
||
}
|
||
|
||
.page-count-option {
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.page-count-option:hover {
|
||
border-color: #3498db !important;
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.2);
|
||
}
|
||
|
||
.page-count-option.active {
|
||
border-color: #3498db !important;
|
||
background: #e8f4fd !important;
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 2px 10px rgba(52, 152, 219, 0.3);
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
<script>
|
||
let currentProjectId = '{{ todo_board.task_id }}';
|
||
let outlineGenerationStarted = false; // 防止重复调用大纲生成
|
||
|
||
// Page count selection functionality
|
||
function initializePageCountSelection() {
|
||
const pageCountOptions = document.querySelectorAll('.page-count-option');
|
||
const pageCountModeInput = document.getElementById('page_count_mode');
|
||
const customRangeSection = document.getElementById('custom-range-section');
|
||
|
||
pageCountOptions.forEach(option => {
|
||
option.addEventListener('click', function() {
|
||
// Remove active class from all options
|
||
pageCountOptions.forEach(opt => opt.classList.remove('active'));
|
||
|
||
// Add active class to clicked option
|
||
this.classList.add('active');
|
||
|
||
// Update hidden input value
|
||
const selectedMode = this.getAttribute('data-mode');
|
||
pageCountModeInput.value = selectedMode;
|
||
|
||
// Show/hide custom range section
|
||
if (selectedMode === 'custom_range') {
|
||
customRangeSection.style.display = 'block';
|
||
} else {
|
||
customRangeSection.style.display = 'none';
|
||
}
|
||
|
||
// Update visual styles
|
||
updatePageCountStyles();
|
||
});
|
||
});
|
||
|
||
// Set default selection
|
||
const defaultOption = document.querySelector('.page-count-option[data-mode="ai_decide"]');
|
||
if (defaultOption) {
|
||
defaultOption.classList.add('active');
|
||
}
|
||
|
||
// Initialize visual styles
|
||
updatePageCountStyles();
|
||
}
|
||
|
||
// Update page count option visual styles
|
||
function updatePageCountStyles() {
|
||
const pageCountOptions = document.querySelectorAll('.page-count-option');
|
||
pageCountOptions.forEach(option => {
|
||
if (option.classList.contains('active')) {
|
||
option.style.borderColor = '#3498db';
|
||
option.style.background = '#e8f4fd';
|
||
} else {
|
||
option.style.borderColor = '#ddd';
|
||
option.style.background = 'white';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Style selection functionality
|
||
function initializeStyleSelection() {
|
||
const styleOptions = document.querySelectorAll('.style-option');
|
||
const pptStyleInput = document.getElementById('ppt_style');
|
||
const customStyleSection = document.getElementById('custom-style-section');
|
||
|
||
styleOptions.forEach(option => {
|
||
option.addEventListener('click', function() {
|
||
// Remove selected class from all options
|
||
styleOptions.forEach(opt => opt.classList.remove('selected'));
|
||
|
||
// Add selected class to clicked option
|
||
this.classList.add('selected');
|
||
|
||
// Update hidden input value
|
||
const selectedStyle = this.getAttribute('data-style');
|
||
pptStyleInput.value = selectedStyle;
|
||
|
||
// Show/hide custom style section
|
||
if (selectedStyle === 'custom') {
|
||
customStyleSection.style.display = 'block';
|
||
} else {
|
||
customStyleSection.style.display = 'none';
|
||
}
|
||
});
|
||
});
|
||
|
||
// Set default selection
|
||
const defaultOption = document.querySelector('.style-option[data-style="general"]');
|
||
if (defaultOption) {
|
||
defaultOption.classList.add('selected');
|
||
}
|
||
}
|
||
|
||
// Initialize outline display for completed stages
|
||
function initializeOutlineDisplay() {
|
||
// Check if outline generation stage is completed and has content
|
||
const outlineStage = document.querySelector('[data-stage-id="outline_generation"]');
|
||
if (outlineStage) {
|
||
const statusIcon = outlineStage.querySelector('.stage-status-icon');
|
||
if (statusIcon && statusIcon.textContent === '✓') {
|
||
// Stage is completed, ensure output area is visible
|
||
const outputDiv = document.getElementById('outline-output-outline_generation');
|
||
if (outputDiv) {
|
||
outputDiv.style.display = 'block';
|
||
|
||
// Hide cursor for completed stage
|
||
const cursorDiv = document.getElementById('outline-cursor-outline_generation');
|
||
if (cursorDiv) {
|
||
cursorDiv.style.display = 'none';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// AI大脑思考动画控制函数
|
||
let brainAnimationInterval = null;
|
||
let activeBrains = new Set();
|
||
|
||
function startBrainAnimation(prefix = '') {
|
||
const brainId = prefix ? `loading-brain-${prefix}` : 'loading-brain';
|
||
const brain = document.getElementById(brainId);
|
||
|
||
if (!brain) return;
|
||
|
||
// 添加到活动大脑集合
|
||
activeBrains.add(prefix);
|
||
|
||
// 启动大脑思考状态
|
||
brain.classList.remove('completed');
|
||
|
||
// 创建动态文字效果
|
||
function updateThinkingText() {
|
||
if (!activeBrains.has(prefix)) return;
|
||
|
||
const textElement = brain.querySelector('.generating-text');
|
||
if (textElement) {
|
||
const texts = [
|
||
'正在分析内容结构',
|
||
'正在构建逻辑框架',
|
||
'正在优化大纲层次',
|
||
'正在完善内容要点',
|
||
'正在生成PPT大纲'
|
||
];
|
||
|
||
const randomText = texts[Math.floor(Math.random() * texts.length)];
|
||
textElement.textContent = randomText;
|
||
}
|
||
}
|
||
|
||
// 每3秒更新思考文字
|
||
updateThinkingText();
|
||
const textInterval = setInterval(updateThinkingText, 3000);
|
||
|
||
// 存储定时器引用
|
||
if (!brainAnimationInterval) {
|
||
brainAnimationInterval = {};
|
||
}
|
||
brainAnimationInterval[prefix] = textInterval;
|
||
}
|
||
|
||
function stopBrainAnimation(prefix = '') {
|
||
const brainId = prefix ? `loading-brain-${prefix}` : 'loading-brain';
|
||
const brain = document.getElementById(brainId);
|
||
|
||
// 从活动大脑集合中移除
|
||
activeBrains.delete(prefix);
|
||
|
||
if (brain) {
|
||
brain.classList.add('completed');
|
||
|
||
// 更新完成文字
|
||
const textElement = brain.querySelector('.generating-text');
|
||
if (textElement) {
|
||
textElement.textContent = '大纲生成完成';
|
||
}
|
||
|
||
const dotsElement = brain.querySelector('.generating-dots');
|
||
if (dotsElement) {
|
||
dotsElement.textContent = '✓';
|
||
}
|
||
}
|
||
|
||
// 清除文字更新定时器
|
||
if (brainAnimationInterval && brainAnimationInterval[prefix]) {
|
||
clearInterval(brainAnimationInterval[prefix]);
|
||
delete brainAnimationInterval[prefix];
|
||
}
|
||
}
|
||
|
||
// 兼容旧的函数名
|
||
function startLoadingAnimation(prefix = '') {
|
||
startBrainAnimation(prefix);
|
||
}
|
||
|
||
function stopLoadingAnimation(prefix = '') {
|
||
stopBrainAnimation(prefix);
|
||
}
|
||
|
||
// 兼容漏斗动画函数名
|
||
function startFunnelAnimation(prefix = '') {
|
||
startBrainAnimation(prefix);
|
||
}
|
||
|
||
function stopFunnelAnimation(prefix = '') {
|
||
stopBrainAnimation(prefix);
|
||
}
|
||
|
||
// 兼容书本动画函数名
|
||
function startBookAnimation(prefix = '') {
|
||
startBrainAnimation(prefix);
|
||
}
|
||
|
||
function stopBookAnimation(prefix = '') {
|
||
stopBrainAnimation(prefix);
|
||
}
|
||
|
||
|
||
|
||
// 显示成功完成动画
|
||
function showBrainCompletion(prefix = '') {
|
||
const brainId = prefix ? `loading-brain-${prefix}` : 'loading-brain';
|
||
const brain = document.getElementById(brainId);
|
||
|
||
if (brain) {
|
||
// 停止思考动画并显示完成状态
|
||
stopBrainAnimation(prefix);
|
||
brain.classList.add('completed');
|
||
|
||
// 2秒后隐藏整个动画容器
|
||
setTimeout(() => {
|
||
if (brain.parentNode && brain.parentNode.parentNode) {
|
||
brain.parentNode.parentNode.style.display = 'none';
|
||
}
|
||
}, 2000);
|
||
}
|
||
}
|
||
|
||
// 兼容旧的函数名
|
||
function showBookCompletion(prefix = '') {
|
||
showBrainCompletion(prefix);
|
||
}
|
||
|
||
function showFunnelCompletion(prefix = '') {
|
||
showBrainCompletion(prefix);
|
||
}
|
||
|
||
// Handle requirements form submission and AI suggestions loading
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// Initialize outline display if already completed
|
||
initializeOutlineDisplay();
|
||
|
||
// Initialize page count selection
|
||
initializePageCountSelection();
|
||
|
||
// Initialize style selection
|
||
initializeStyleSelection();
|
||
|
||
// Check if we should auto-start outline generation
|
||
checkAutoStartOutline();
|
||
|
||
// 直接显示需求表单,不再加载AI建议
|
||
showRequirementsForm();
|
||
|
||
const requirementsForm = document.getElementById('requirements-form');
|
||
if (requirementsForm) {
|
||
requirementsForm.addEventListener('submit', async function(e) {
|
||
e.preventDefault();
|
||
|
||
const formData = new FormData(requirementsForm);
|
||
const confirmBtn = document.getElementById('confirm-requirements-btn');
|
||
|
||
const requirementsSection = document.getElementById('requirements-section');
|
||
if (requirementsSection) {
|
||
requirementsSection.style.display = 'none';
|
||
}
|
||
|
||
|
||
showOutlineSection();
|
||
|
||
|
||
try {
|
||
const response = await fetch(`/projects/${currentProjectId}/confirm-requirements`, {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (response.ok && result.status === 'success') {
|
||
|
||
setTimeout(() => {
|
||
const contentSource = document.querySelector('input[name="content_source"]:checked');
|
||
if (contentSource && contentSource.value === 'file') {
|
||
|
||
startFileOutlineGeneration();
|
||
} else {
|
||
|
||
startOutlineGenerationNew();
|
||
}
|
||
}, 1000);
|
||
} else {
|
||
console.error('Requirements confirmation failed:', result.message);
|
||
|
||
alert('需求确认失败: ' + (result.message || '未知错误'));
|
||
|
||
const requirementsSection = document.getElementById('requirements-section');
|
||
const outlineSection = document.getElementById('outline-section');
|
||
if (requirementsSection) requirementsSection.style.display = 'block';
|
||
if (outlineSection) outlineSection.style.display = 'none';
|
||
}
|
||
} catch (error) {
|
||
console.error('Error confirming requirements:', error);
|
||
alert('需求确认失败: ' + error.message);
|
||
|
||
const requirementsSection = document.getElementById('requirements-section');
|
||
const outlineSection = document.getElementById('outline-section');
|
||
if (requirementsSection) requirementsSection.style.display = 'block';
|
||
if (outlineSection) outlineSection.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// Check if we should auto-start outline generation
|
||
function checkAutoStartOutline() {
|
||
// 防止重复调用
|
||
if (outlineGenerationStarted) {
|
||
console.log('Outline generation already started, skipping auto-start check...');
|
||
return;
|
||
}
|
||
|
||
// Check if outline section is visible and outline generation should start
|
||
const outlineSection = document.getElementById('outline-section');
|
||
if (outlineSection && outlineSection.style.display !== 'none') {
|
||
// Check if outline generation is in running state
|
||
const outlineCursor = document.getElementById('outline-cursor');
|
||
if (outlineCursor && outlineCursor.style.display !== 'none') {
|
||
// Auto-start outline generation
|
||
setTimeout(() => {
|
||
startOutlineGenerationNew();
|
||
}, 500);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Show outline section
|
||
function showOutlineSection() {
|
||
// Create and show outline section if it doesn't exist
|
||
let outlineSection = document.getElementById('outline-section');
|
||
if (!outlineSection) {
|
||
outlineSection = document.createElement('div');
|
||
outlineSection.id = 'outline-section';
|
||
outlineSection.style.cssText = 'max-width: 1200px; margin: 20px auto; background: white; border-radius: 15px; padding: 30px; box-shadow: 0 4px 20px rgba(0,0,0,0.1);';
|
||
|
||
outlineSection.innerHTML = `
|
||
<div style="text-align: center; margin-bottom: 30px;">
|
||
<h3 style="color: #2c3e50; margin-bottom: 10px;">
|
||
<i class="fas fa-brain"></i> PPT 大纲生成
|
||
</h3>
|
||
<p style="color: #7f8c8d;">AI正在为您生成专业的PPT大纲,您可以实时查看并编辑</p>
|
||
</div>
|
||
|
||
<div style="background: #f8f9fa; border-radius: 10px; padding: 20px; border: 2px solid #e9ecef;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
|
||
<h5 style="color: #2c3e50; margin: 0;">
|
||
<i class="fas fa-list-alt"></i> 大纲内容
|
||
<span id="outline-status" style="color: #f39c12; font-size: 0.8em; margin-left: 10px; display: none;">
|
||
</span>
|
||
</h5>
|
||
|
||
<div style="display: flex; align-items: center; gap: 10px;">
|
||
<!-- View Toggle Buttons - Always show -->
|
||
<div style="display: flex; background: #e9ecef; border-radius: 5px; padding: 2px;">
|
||
<button id="json-view-btn-new" onclick="switchOutlineViewNew('json')"
|
||
style="background: #3498db; color: white; border: none; padding: 5px 10px; border-radius: 3px; font-size: 11px; cursor: pointer; transition: all 0.3s ease;">
|
||
<i class="fas fa-code"></i> JSON
|
||
</button>
|
||
<button id="outline-view-btn-new" onclick="switchOutlineViewNew('outline')"
|
||
style="background: transparent; color: #6c757d; border: none; padding: 5px 10px; border-radius: 3px; font-size: 11px; cursor: pointer; transition: all 0.3s ease;">
|
||
<i class="fas fa-list-ul"></i> 大纲视图
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Action Buttons -->
|
||
<div id="outline-actions" style="display: none;">
|
||
<button onclick="regenerateOutlineNew()" class="btn btn-sm btn-outline-warning" style="margin-right: 8px;">
|
||
<i class="fas fa-sync"></i> 重新生成大纲
|
||
</button>
|
||
<button onclick="editOutlineNew()" class="btn btn-sm btn-outline-primary" style="margin-right: 8px;">
|
||
<i class="fas fa-edit"></i> 编辑大纲
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- JSON View (default) -->
|
||
<div id="outline-content-display" style="background: white; border: 1px solid #dee2e6; border-radius: 8px; padding: 25px; min-height: 400px; max-height: 600px; overflow-y: auto; font-family: 'Microsoft YaHei', Arial, sans-serif; line-height: 1.8; font-size: 14px; display: block; position: relative;">
|
||
<div id="outline-placeholder" class="loading-container">
|
||
<div class="loading-icon">
|
||
<i class="fas fa-brain"></i>
|
||
</div>
|
||
<div class="loading-title">AI正在生成PPT大纲</div>
|
||
|
||
<div class="brain-container">
|
||
<div class="ai-brain" id="loading-brain-dynamic">
|
||
<div class="brain-core">
|
||
<div class="brain-icon">🧠</div>
|
||
</div>
|
||
<svg class="progress-ring" width="140" height="140">
|
||
<circle class="progress-ring-circle" cx="70" cy="70" r="70"></circle>
|
||
<circle class="progress-ring-progress" cx="70" cy="70" r="70"></circle>
|
||
</svg>
|
||
<div class="neural-network">
|
||
<div class="neural-line" style="--rotation: 45deg;"></div>
|
||
<div class="neural-line" style="--rotation: -30deg;"></div>
|
||
<div class="neural-line" style="--rotation: -45deg;"></div>
|
||
<div class="neural-line" style="--rotation: 60deg;"></div>
|
||
</div>
|
||
<div class="thought-bubbles">
|
||
<div class="thought-bubble"></div>
|
||
<div class="thought-bubble"></div>
|
||
<div class="thought-bubble"></div>
|
||
</div>
|
||
<div class="text-generation">
|
||
<div class="generating-text">正在分析内容结构</div>
|
||
<div class="generating-dots">• • •</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="loading-tips">
|
||
<h5><i class="fas fa-lightbulb"></i> 小贴士</h5>
|
||
<p>AI正在根据您的需求智能生成PPT大纲,包括标题、内容要点和逻辑结构。生成完成后您可以实时编辑和调整。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 光标元素移到内容容器内部,使用绝对定位,默认隐藏 -->
|
||
<div id="outline-cursor" style="position: absolute; bottom: 20px; right: 20px; display: none; width: 2px; height: 16px; background: #3498db; animation: blink 1s infinite; pointer-events: none;"></div>
|
||
</div>
|
||
|
||
<!-- Outline View -->
|
||
<div id="outline-view-new" style="display: none;">
|
||
<!-- Outline Toolbar -->
|
||
<div id="outline-toolbar-new" style="background: #f8f9fa; border: 1px solid #dee2e6; border-bottom: none; border-radius: 8px 8px 0 0; padding: 10px; display: flex; align-items: center; justify-content: space-between;">
|
||
<div style="display: flex; align-items: center; gap: 10px;">
|
||
<span style="color: #2c3e50; font-weight: 500;">
|
||
<i class="fas fa-list-ul"></i> PPT大纲预览
|
||
</span>
|
||
<span style="color: #7f8c8d; font-size: 12px;">支持简洁视图和详细视图切换</span>
|
||
</div>
|
||
<div style="display: flex; gap: 8px;">
|
||
<button onclick="toggleOutlineViewModeNew()" class="btn btn-xs btn-info" title="切换视图模式">
|
||
<i class="fas fa-eye"></i> <span id="viewToggleTextNew">详细视图</span>
|
||
</button>
|
||
<button onclick="editOutlineNew()" class="btn btn-xs btn-primary" title="修改大纲">
|
||
<i class="fas fa-edit"></i> 修改大纲
|
||
</button>
|
||
<button onclick="exportOutlineJSONNew()" class="btn btn-xs btn-success" title="导出JSON">
|
||
<i class="fas fa-download"></i> 导出JSON
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="outline-container-new" style="background: white; border: 1px solid #dee2e6; border-radius: 0 0 8px 8px; height: 500px; overflow-y: auto; position: relative;">
|
||
<div id="outline-content-new" style="width: 100%; height: 100%; padding: 20px;">
|
||
<!-- 大纲内容将在这里显示 -->
|
||
</div>
|
||
<div id="outline-loading-new" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; color: #7f8c8d; display: none;">
|
||
<i class="fas fa-spinner fa-spin fa-2x" style="margin-bottom: 15px;"></i>
|
||
<p>正在加载大纲...</p>
|
||
</div>
|
||
<div id="outline-error-new" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; color: #e74c3c; display: none;">
|
||
<i class="fas fa-exclamation-triangle fa-2x" style="margin-bottom: 15px;"></i>
|
||
<p>大纲加载失败,请检查数据格式</p>
|
||
</div>
|
||
<div id="outline-empty-new" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; color: #7f8c8d; display: block;">
|
||
<i class="fas fa-list-ul fa-3x" style="margin-bottom: 20px; opacity: 0.3;"></i>
|
||
<h4 style="color: #95a5a6; margin-bottom: 15px;">大纲视图</h4>
|
||
<p style="margin-bottom: 20px;">大纲生成完成后,结构化内容将在这里显示</p>
|
||
<p style="font-size: 14px; color: #bdc3c7;">支持简洁视图和详细视图切换,可编辑大纲内容</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
|
||
<div id="outline-edit-area" style="display: none; margin-top: 20px;">
|
||
<h6 style="color: #2c3e50; margin-bottom: 15px;">
|
||
<i class="fas fa-code"></i> 编辑大纲JSON
|
||
</h6>
|
||
<div style="background: #f8f9fa; padding: 10px; border-radius: 5px; margin-bottom: 10px; font-size: 12px; color: #6c757d;">
|
||
<i class="fas fa-info-circle"></i>
|
||
请编辑JSON格式的大纲。确保JSON格式正确,包含title和slides数组。
|
||
</div>
|
||
<textarea id="outline-editor"
|
||
style="width: 100%; height: 400px; padding: 15px; border: 1px solid #ddd; border-radius: 8px;
|
||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 13px; line-height: 1.4;
|
||
resize: vertical; background: #f8f9fa;"
|
||
placeholder='请输入JSON格式的大纲,例如:
|
||
{
|
||
"title": "PPT标题",
|
||
"slides": [
|
||
{
|
||
"page_number": 1,
|
||
"title": "页面标题",
|
||
"content_points": ["要点1", "要点2"],
|
||
"slide_type": "title"
|
||
}
|
||
]
|
||
}'></textarea>
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 15px;">
|
||
<div style="font-size: 12px; color: #6c757d;">
|
||
<i class="fas fa-lightbulb"></i>
|
||
提示:slide_type可选值:title, content, agenda, thankyou
|
||
</div>
|
||
<div>
|
||
<button onclick="validateJSON()" class="btn btn-sm btn-info" style="margin-right: 8px;">
|
||
<i class="fas fa-check-circle"></i> 验证JSON
|
||
</button>
|
||
<button onclick="cancelEditOutlineNew()" class="btn btn-sm btn-secondary" style="margin-right: 8px;">
|
||
<i class="fas fa-times"></i> 取消
|
||
</button>
|
||
<button onclick="saveOutlineEditNew()" class="btn btn-sm btn-primary">
|
||
<i class="fas fa-save"></i> 保存修改
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// Insert after requirements section or at the beginning
|
||
const requirementsSection = document.getElementById('requirements-section');
|
||
if (requirementsSection) {
|
||
requirementsSection.parentNode.insertBefore(outlineSection, requirementsSection.nextSibling);
|
||
} else {
|
||
const container = document.querySelector('div[style*="text-align: center"]');
|
||
if (container) {
|
||
container.appendChild(outlineSection);
|
||
}
|
||
}
|
||
}
|
||
|
||
outlineSection.style.display = 'block';
|
||
|
||
// 启动等待动画
|
||
setTimeout(() => {
|
||
startFunnelAnimation('dynamic');
|
||
}, 100);
|
||
}
|
||
|
||
|
||
function showRequirementsForm() {
|
||
try {
|
||
// Hide loading indicator
|
||
const loadingElement = document.getElementById('ai-loading');
|
||
if (loadingElement) {
|
||
loadingElement.style.display = 'none';
|
||
}
|
||
|
||
// Show form
|
||
const formElement = document.getElementById('requirements-form');
|
||
if (formElement) {
|
||
formElement.style.display = 'block';
|
||
}
|
||
|
||
|
||
} catch (error) {
|
||
console.error('Error showing requirements form:', error);
|
||
|
||
// Hide loading and show form with default options
|
||
const loadingElement = document.getElementById('ai-loading');
|
||
if (loadingElement) {
|
||
loadingElement.style.display = 'none';
|
||
}
|
||
|
||
const formElement = document.getElementById('requirements-form');
|
||
if (formElement) {
|
||
formElement.style.display = 'block';
|
||
}
|
||
}
|
||
}
|
||
|
||
// Populate checkbox options
|
||
function populateCheckboxOptions(containerId, options, name) {
|
||
const container = document.getElementById(containerId);
|
||
container.innerHTML = '';
|
||
|
||
options.forEach(option => {
|
||
const div = document.createElement('div');
|
||
div.style.cssText = 'padding: 10px; border: 1px solid #ddd; border-radius: 6px; background: white; cursor: pointer; transition: all 0.3s ease;';
|
||
|
||
const checkbox = document.createElement('input');
|
||
checkbox.type = 'checkbox';
|
||
checkbox.name = name;
|
||
checkbox.value = option;
|
||
checkbox.id = `${name}_${options.indexOf(option)}`;
|
||
checkbox.style.marginRight = '8px';
|
||
|
||
const label = document.createElement('label');
|
||
label.htmlFor = checkbox.id;
|
||
label.textContent = option;
|
||
label.style.cursor = 'pointer';
|
||
|
||
div.appendChild(checkbox);
|
||
div.appendChild(label);
|
||
|
||
// Add click handler for the entire div
|
||
div.addEventListener('click', function(e) {
|
||
if (e.target !== checkbox) {
|
||
checkbox.checked = !checkbox.checked;
|
||
}
|
||
updateCheckboxStyle(div, checkbox.checked);
|
||
});
|
||
|
||
// Add change handler for checkbox
|
||
checkbox.addEventListener('change', function() {
|
||
updateCheckboxStyle(div, this.checked);
|
||
});
|
||
|
||
container.appendChild(div);
|
||
});
|
||
}
|
||
|
||
// Populate radio options
|
||
function populateRadioOptions(containerId, options, name) {
|
||
const container = document.getElementById(containerId);
|
||
container.innerHTML = '';
|
||
|
||
options.forEach(option => {
|
||
const div = document.createElement('div');
|
||
div.style.cssText = 'padding: 10px 15px; border: 1px solid #ddd; border-radius: 6px; background: white; cursor: pointer; transition: all 0.3s ease;';
|
||
|
||
const radio = document.createElement('input');
|
||
radio.type = 'radio';
|
||
radio.name = name;
|
||
radio.value = option;
|
||
radio.id = `${name}_${options.indexOf(option)}`;
|
||
radio.style.marginRight = '8px';
|
||
|
||
const label = document.createElement('label');
|
||
label.htmlFor = radio.id;
|
||
label.textContent = option;
|
||
label.style.cursor = 'pointer';
|
||
|
||
div.appendChild(radio);
|
||
div.appendChild(label);
|
||
|
||
// Add click handler for the entire div
|
||
div.addEventListener('click', function(e) {
|
||
if (e.target !== radio) {
|
||
radio.checked = true;
|
||
}
|
||
updateRadioStyles(name);
|
||
});
|
||
|
||
// Add change handler for radio
|
||
radio.addEventListener('change', function() {
|
||
updateRadioStyles(name);
|
||
});
|
||
|
||
container.appendChild(div);
|
||
});
|
||
}
|
||
|
||
// Update checkbox visual style
|
||
function updateCheckboxStyle(div, checked) {
|
||
if (checked) {
|
||
div.style.background = '#e8f4fd';
|
||
div.style.borderColor = '#3498db';
|
||
div.style.transform = 'scale(1.02)';
|
||
} else {
|
||
div.style.background = 'white';
|
||
div.style.borderColor = '#ddd';
|
||
div.style.transform = 'scale(1)';
|
||
}
|
||
}
|
||
|
||
// Update radio button visual styles
|
||
function updateRadioStyles(name) {
|
||
const radios = document.querySelectorAll(`input[name="${name}"]`);
|
||
radios.forEach(radio => {
|
||
const div = radio.parentElement;
|
||
if (radio.checked) {
|
||
div.style.background = '#e8f4fd';
|
||
div.style.borderColor = '#3498db';
|
||
div.style.transform = 'scale(1.02)';
|
||
} else {
|
||
div.style.background = 'white';
|
||
div.style.borderColor = '#ddd';
|
||
div.style.transform = 'scale(1)';
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
// Outline editing functions
|
||
function editOutline(stageId) {
|
||
const outlineContent = document.getElementById(`outline-content-${stageId}`);
|
||
const outlineEdit = document.getElementById(`outline-edit-${stageId}`);
|
||
const outlineEditor = document.getElementById(`outline-editor-${stageId}`);
|
||
|
||
if (outlineContent && outlineEdit && outlineEditor) {
|
||
// Copy current content to editor
|
||
outlineEditor.value = outlineContent.textContent;
|
||
|
||
// Hide content, show editor
|
||
outlineContent.parentElement.style.display = 'none';
|
||
outlineEdit.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
function cancelEditOutline(stageId) {
|
||
const outlineContent = document.getElementById(`outline-content-${stageId}`);
|
||
const outlineEdit = document.getElementById(`outline-edit-${stageId}`);
|
||
|
||
if (outlineContent && outlineEdit) {
|
||
// Show content, hide editor
|
||
outlineContent.parentElement.style.display = 'block';
|
||
outlineEdit.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
|
||
|
||
// 重试文件大纲生成函数
|
||
function retryFileOutlineGeneration() {
|
||
console.log('Retrying file outline generation');
|
||
|
||
// 重置状态
|
||
outlineGenerationStarted = false;
|
||
|
||
// 清除错误内容,但保留placeholder div
|
||
const contentDiv = document.getElementById('outline-content-display');
|
||
if (contentDiv) {
|
||
// 只清除非placeholder的子元素
|
||
const children = Array.from(contentDiv.children);
|
||
children.forEach(child => {
|
||
if (child.id !== 'outline-placeholder') {
|
||
child.remove();
|
||
}
|
||
});
|
||
}
|
||
|
||
// 重置placeholder div状态
|
||
const placeholderDiv = document.getElementById('outline-placeholder');
|
||
if (placeholderDiv) {
|
||
console.log('Resetting placeholder div for file retry');
|
||
placeholderDiv.style.display = 'none';
|
||
placeholderDiv.className = '';
|
||
placeholderDiv.innerHTML = '';
|
||
} else {
|
||
console.log('Placeholder div not found, creating new one for file retry');
|
||
// 如果找不到,创建一个新的
|
||
const newPlaceholderDiv = document.createElement('div');
|
||
newPlaceholderDiv.id = 'outline-placeholder';
|
||
newPlaceholderDiv.className = 'loading-container';
|
||
contentDiv.appendChild(newPlaceholderDiv);
|
||
}
|
||
|
||
// 隐藏操作按钮
|
||
const actionsDiv = document.getElementById('outline-actions');
|
||
if (actionsDiv) {
|
||
actionsDiv.style.display = 'none';
|
||
}
|
||
|
||
// 调用生成函数
|
||
startFileOutlineGeneration();
|
||
}
|
||
|
||
// File outline generation function for file uploads (non-streaming)
|
||
async function startFileOutlineGeneration() {
|
||
console.log('Starting file outline generation (non-streaming)');
|
||
|
||
// 防止重复调用
|
||
if (outlineGenerationStarted) {
|
||
console.log('Outline generation already started, skipping...');
|
||
return;
|
||
}
|
||
|
||
const contentDiv = document.getElementById('outline-content-display');
|
||
const cursorDiv = document.getElementById('outline-cursor');
|
||
const statusDiv = document.getElementById('outline-status');
|
||
const placeholderDiv = document.getElementById('outline-placeholder');
|
||
|
||
if (!contentDiv) {
|
||
console.error('Outline content div not found');
|
||
return;
|
||
}
|
||
|
||
// 标记为已开始
|
||
outlineGenerationStarted = true;
|
||
|
||
// 显示增强的加载状态
|
||
if (placeholderDiv) {
|
||
placeholderDiv.innerHTML = `
|
||
<div class="loading-icon">
|
||
<i class="fas fa-file-alt"></i>
|
||
</div>
|
||
<div class="loading-title">正在从文件生成PPT大纲</div>
|
||
|
||
<div class="brain-container">
|
||
<div class="ai-brain" id="loading-brain-file">
|
||
<div class="brain-core">
|
||
<div class="brain-icon">📄</div>
|
||
</div>
|
||
<svg class="progress-ring" width="140" height="140">
|
||
<circle class="progress-ring-circle" cx="70" cy="70" r="70"></circle>
|
||
<circle class="progress-ring-progress" cx="70" cy="70" r="70"></circle>
|
||
</svg>
|
||
<div class="neural-network">
|
||
<div class="neural-line" style="--rotation: 45deg;"></div>
|
||
<div class="neural-line" style="--rotation: -30deg;"></div>
|
||
<div class="neural-line" style="--rotation: -45deg;"></div>
|
||
<div class="neural-line" style="--rotation: 60deg;"></div>
|
||
</div>
|
||
<div class="thought-bubbles">
|
||
<div class="thought-bubble"></div>
|
||
<div class="thought-bubble"></div>
|
||
<div class="thought-bubble"></div>
|
||
</div>
|
||
<div class="text-generation">
|
||
<div class="generating-text">正在解析文件内容</div>
|
||
<div class="generating-dots">• • •</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="loading-tips">
|
||
<h5><i class="fas fa-lightbulb"></i> 小贴士</h5>
|
||
<p>AI正在智能分析您上传的文件内容,提取关键信息并生成结构化的PPT大纲。</p>
|
||
</div>
|
||
`;
|
||
placeholderDiv.className = 'loading-container';
|
||
placeholderDiv.style.display = 'block';
|
||
|
||
// 启动文件处理动画
|
||
startBrainAnimation('file');
|
||
}
|
||
|
||
// 隐藏光标(文件生成不需要流式效果)
|
||
if (cursorDiv) {
|
||
cursorDiv.style.display = 'none';
|
||
}
|
||
|
||
try {
|
||
// 调用文件大纲生成接口(非流式)
|
||
const response = await fetch(`/projects/${currentProjectId}/generate-file-outline`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.error || result.status === "error") {
|
||
// 停止加载动画
|
||
stopBookAnimation('file');
|
||
|
||
// 显示错误信息和重新生成按钮,但保留placeholder div
|
||
// 先确保placeholder div存在
|
||
let placeholderDiv = document.getElementById('outline-placeholder');
|
||
if (!placeholderDiv) {
|
||
placeholderDiv = document.createElement('div');
|
||
placeholderDiv.id = 'outline-placeholder';
|
||
placeholderDiv.className = 'loading-container';
|
||
contentDiv.appendChild(placeholderDiv);
|
||
}
|
||
|
||
// 清除其他内容,只保留placeholder
|
||
const children = Array.from(contentDiv.children);
|
||
children.forEach(child => {
|
||
if (child.id !== 'outline-placeholder') {
|
||
child.remove();
|
||
}
|
||
});
|
||
|
||
// 在placeholder前添加错误信息
|
||
const errorDiv = document.createElement('div');
|
||
errorDiv.innerHTML = `
|
||
<div style="color: #e74c3c; text-align: center; padding: 20px;">
|
||
<i class="fas fa-exclamation-triangle" style="font-size: 24px; margin-bottom: 10px;"></i>
|
||
<div style="margin-bottom: 15px;">文件大纲生成失败</div>
|
||
<div style="font-size: 14px; margin-bottom: 20px; color: #7f8c8d;">${result.error || result.message || '未知错误'}</div>
|
||
<button onclick="retryFileOutlineGeneration()" class="btn btn-warning btn-sm">
|
||
<i class="fas fa-sync"></i> 重新生成大纲
|
||
</button>
|
||
</div>
|
||
`;
|
||
contentDiv.insertBefore(errorDiv, placeholderDiv);
|
||
if (placeholderDiv) placeholderDiv.style.display = 'none';
|
||
|
||
// 重置标志,允许重试
|
||
outlineGenerationStarted = false;
|
||
return;
|
||
}
|
||
|
||
// 停止加载动画
|
||
stopBookAnimation('file');
|
||
|
||
// 隐藏加载状态
|
||
if (placeholderDiv) {
|
||
placeholderDiv.style.display = 'none';
|
||
}
|
||
|
||
// 一次性显示完整的大纲内容
|
||
if (result.outline_content) {
|
||
// 格式化JSON内容
|
||
let formattedContent;
|
||
try {
|
||
const parsed = JSON.parse(result.outline_content);
|
||
formattedContent = JSON.stringify(parsed, null, 2);
|
||
} catch (e) {
|
||
formattedContent = result.outline_content;
|
||
}
|
||
|
||
// 显示格式化的内容 - 使用textContent而不是innerHTML来避免HTML转义问题
|
||
const preElement = document.createElement('pre');
|
||
preElement.style.cssText = 'margin: 0; font-family: "Consolas", "Monaco", "Courier New", monospace; font-size: 13px; white-space: pre-wrap; word-wrap: break-word;';
|
||
preElement.textContent = formattedContent;
|
||
contentDiv.innerHTML = '';
|
||
contentDiv.appendChild(preElement);
|
||
}
|
||
|
||
// 重置标志,允许下次生成
|
||
outlineGenerationStarted = false;
|
||
|
||
// Show action buttons
|
||
const actionsDiv = document.getElementById('outline-actions');
|
||
if (actionsDiv) {
|
||
actionsDiv.style.display = 'block';
|
||
}
|
||
|
||
// 尝试解析并渲染大纲预览
|
||
try {
|
||
const parsedOutline = JSON.parse(result.outline_content);
|
||
if (parsedOutline && parsedOutline.slides) {
|
||
renderOutlinePreview(parsedOutline);
|
||
}
|
||
} catch (e) {
|
||
console.log('大纲内容正在生成中,暂时无法解析为JSON');
|
||
}
|
||
|
||
// Show start PPT generation button
|
||
showStartPPTButton();
|
||
|
||
} catch (error) {
|
||
console.error('Error generating file outline:', error);
|
||
|
||
// 停止加载动画
|
||
stopFunnelAnimation('file');
|
||
|
||
// 显示连接错误和重新生成按钮,但保留placeholder div
|
||
// 先确保placeholder div存在
|
||
let placeholderDiv = document.getElementById('outline-placeholder');
|
||
if (!placeholderDiv) {
|
||
placeholderDiv = document.createElement('div');
|
||
placeholderDiv.id = 'outline-placeholder';
|
||
placeholderDiv.className = 'loading-container';
|
||
contentDiv.appendChild(placeholderDiv);
|
||
}
|
||
|
||
// 清除其他内容,只保留placeholder
|
||
const children = Array.from(contentDiv.children);
|
||
children.forEach(child => {
|
||
if (child.id !== 'outline-placeholder') {
|
||
child.remove();
|
||
}
|
||
});
|
||
|
||
// 在placeholder前添加错误信息
|
||
const errorDiv = document.createElement('div');
|
||
errorDiv.innerHTML = `
|
||
<div style="color: #e74c3c; text-align: center; padding: 20px;">
|
||
<i class="fas fa-exclamation-triangle" style="font-size: 24px; margin-bottom: 10px;"></i>
|
||
<div style="margin-bottom: 15px;">连接失败</div>
|
||
<div style="font-size: 14px; margin-bottom: 20px; color: #7f8c8d;">连接错误: ${error.message}</div>
|
||
<button onclick="retryFileOutlineGeneration()" class="btn btn-warning btn-sm">
|
||
<i class="fas fa-sync"></i> 重新生成大纲
|
||
</button>
|
||
</div>
|
||
`;
|
||
contentDiv.insertBefore(errorDiv, placeholderDiv);
|
||
if (placeholderDiv) placeholderDiv.style.display = 'none';
|
||
|
||
// 重置标志,允许重试
|
||
outlineGenerationStarted = false;
|
||
}
|
||
}
|
||
|
||
// 重试大纲生成函数
|
||
function retryOutlineGeneration() {
|
||
console.log('Retrying outline generation');
|
||
|
||
// 重置状态
|
||
outlineGenerationStarted = false;
|
||
|
||
// 清除错误内容,但保留placeholder div
|
||
const contentDiv = document.getElementById('outline-content-display');
|
||
if (contentDiv) {
|
||
// 只清除非placeholder的子元素
|
||
const children = Array.from(contentDiv.children);
|
||
children.forEach(child => {
|
||
if (child.id !== 'outline-placeholder') {
|
||
child.remove();
|
||
}
|
||
});
|
||
}
|
||
|
||
// 重置placeholder div状态
|
||
const placeholderDiv = document.getElementById('outline-placeholder');
|
||
if (placeholderDiv) {
|
||
console.log('Resetting placeholder div for retry');
|
||
placeholderDiv.style.display = 'none';
|
||
placeholderDiv.className = '';
|
||
placeholderDiv.innerHTML = '';
|
||
} else {
|
||
console.log('Placeholder div not found, creating new one');
|
||
// 如果找不到,创建一个新的
|
||
const newPlaceholderDiv = document.createElement('div');
|
||
newPlaceholderDiv.id = 'outline-placeholder';
|
||
newPlaceholderDiv.className = 'loading-container';
|
||
contentDiv.appendChild(newPlaceholderDiv);
|
||
}
|
||
|
||
// 隐藏操作按钮
|
||
const actionsDiv = document.getElementById('outline-actions');
|
||
if (actionsDiv) {
|
||
actionsDiv.style.display = 'none';
|
||
}
|
||
|
||
// 调用生成函数
|
||
startOutlineGenerationNew();
|
||
}
|
||
|
||
// New outline generation function for the new UI (non-streaming)
|
||
async function startOutlineGenerationNew() {
|
||
console.log('Starting new outline generation (non-streaming)');
|
||
|
||
// 防止重复调用
|
||
if (outlineGenerationStarted) {
|
||
console.log('Outline generation already started, skipping...');
|
||
return;
|
||
}
|
||
|
||
const contentDiv = document.getElementById('outline-content-display');
|
||
const cursorDiv = document.getElementById('outline-cursor');
|
||
const statusDiv = document.getElementById('outline-status');
|
||
const placeholderDiv = document.getElementById('outline-placeholder');
|
||
|
||
if (!contentDiv) {
|
||
console.error('Outline content div not found');
|
||
return;
|
||
}
|
||
|
||
// 标记为已开始
|
||
outlineGenerationStarted = true;
|
||
|
||
// 显示增强的加载状态
|
||
if (placeholderDiv) {
|
||
console.log('Setting up placeholder div for outline generation');
|
||
placeholderDiv.innerHTML = `
|
||
<div class="loading-icon">
|
||
<i class="fas fa-brain"></i>
|
||
</div>
|
||
<div class="loading-title">AI正在生成PPT大纲</div>
|
||
|
||
<div class="brain-container">
|
||
<div class="ai-brain" id="loading-brain-stream">
|
||
<div class="brain-core">
|
||
<div class="brain-icon">🧠</div>
|
||
</div>
|
||
<svg class="progress-ring" width="140" height="140">
|
||
<circle class="progress-ring-circle" cx="70" cy="70" r="70"></circle>
|
||
<circle class="progress-ring-progress" cx="70" cy="70" r="70"></circle>
|
||
</svg>
|
||
<div class="neural-network">
|
||
<div class="neural-line" style="--rotation: 45deg;"></div>
|
||
<div class="neural-line" style="--rotation: -30deg;"></div>
|
||
<div class="neural-line" style="--rotation: -45deg;"></div>
|
||
<div class="neural-line" style="--rotation: 60deg;"></div>
|
||
</div>
|
||
<div class="thought-bubbles">
|
||
<div class="thought-bubble"></div>
|
||
<div class="thought-bubble"></div>
|
||
<div class="thought-bubble"></div>
|
||
</div>
|
||
<div class="text-generation">
|
||
<div class="generating-text">正在生成PPT大纲</div>
|
||
<div class="generating-dots">• • •</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="loading-tips">
|
||
<h5><i class="fas fa-lightbulb"></i> 小贴士</h5>
|
||
<p>AI正在根据您的需求智能生成PPT大纲,包括标题、内容要点和逻辑结构。生成完成后将一次性显示完整大纲,您可以编辑和调整。</p>
|
||
</div>
|
||
`;
|
||
placeholderDiv.className = 'loading-container';
|
||
placeholderDiv.style.display = 'block';
|
||
console.log('Placeholder div set up, starting brain animation');
|
||
|
||
// 启动动画
|
||
startBrainAnimation('stream');
|
||
console.log('Brain animation started');
|
||
} else {
|
||
console.error('Placeholder div not found!');
|
||
}
|
||
|
||
// 隐藏光标元素(非流式生成不需要)
|
||
if (cursorDiv) {
|
||
cursorDiv.style.display = 'none';
|
||
}
|
||
|
||
// Hide status div during generation
|
||
if (statusDiv) {
|
||
statusDiv.style.display = 'none';
|
||
}
|
||
|
||
try {
|
||
// 调用非流式大纲生成接口
|
||
const response = await fetch(`/projects/${currentProjectId}/generate-outline`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.error || result.status === "error") {
|
||
// 停止加载动画
|
||
stopBookAnimation('stream');
|
||
|
||
// 显示错误信息和重新生成按钮,但保留placeholder div
|
||
// 先确保placeholder div存在
|
||
let errorPlaceholderDiv = document.getElementById('outline-placeholder');
|
||
if (!errorPlaceholderDiv) {
|
||
errorPlaceholderDiv = document.createElement('div');
|
||
errorPlaceholderDiv.id = 'outline-placeholder';
|
||
errorPlaceholderDiv.className = 'loading-container';
|
||
contentDiv.appendChild(errorPlaceholderDiv);
|
||
}
|
||
|
||
// 隐藏加载状态
|
||
errorPlaceholderDiv.style.display = 'none';
|
||
|
||
// 清除其他内容,只保留placeholder
|
||
const children = Array.from(contentDiv.children);
|
||
children.forEach(child => {
|
||
if (child.id !== 'outline-placeholder') {
|
||
child.remove();
|
||
}
|
||
});
|
||
|
||
// 在placeholder前添加错误信息
|
||
const errorDiv = document.createElement('div');
|
||
errorDiv.innerHTML = `
|
||
<div style="color: #e74c3c; text-align: center; padding: 20px;">
|
||
<i class="fas fa-exclamation-triangle" style="font-size: 24px; margin-bottom: 10px;"></i>
|
||
<div style="margin-bottom: 15px;">大纲生成失败</div>
|
||
<div style="font-size: 14px; margin-bottom: 20px; color: #7f8c8d;">${result.error || result.message || '未知错误'}</div>
|
||
<button onclick="retryOutlineGeneration()" class="btn btn-warning btn-sm">
|
||
<i class="fas fa-sync"></i> 重新生成大纲
|
||
</button>
|
||
</div>
|
||
`;
|
||
contentDiv.insertBefore(errorDiv, errorPlaceholderDiv);
|
||
if (cursorDiv) cursorDiv.style.display = 'none';
|
||
|
||
// 重置标志,允许重试
|
||
outlineGenerationStarted = false;
|
||
return;
|
||
}
|
||
|
||
// 停止加载动画
|
||
stopBookAnimation('stream');
|
||
|
||
// 隐藏加载状态
|
||
if (placeholderDiv) {
|
||
placeholderDiv.style.display = 'none';
|
||
}
|
||
|
||
// 一次性显示完整的大纲内容
|
||
if (result.outline_content) {
|
||
// 格式化JSON内容
|
||
let formattedContent;
|
||
try {
|
||
const parsed = JSON.parse(result.outline_content);
|
||
formattedContent = JSON.stringify(parsed, null, 2);
|
||
} catch (e) {
|
||
formattedContent = result.outline_content;
|
||
}
|
||
|
||
// 显示格式化的内容
|
||
const preElement = document.createElement('pre');
|
||
preElement.style.cssText = 'margin: 0; font-family: "Consolas", "Monaco", "Courier New", monospace; font-size: 13px; white-space: pre-wrap; word-wrap: break-word;';
|
||
preElement.textContent = formattedContent;
|
||
contentDiv.innerHTML = '';
|
||
contentDiv.appendChild(preElement);
|
||
}
|
||
|
||
// 重置标志,允许下次生成
|
||
outlineGenerationStarted = false;
|
||
|
||
// Show action buttons
|
||
const actionsDiv = document.getElementById('outline-actions');
|
||
if (actionsDiv) {
|
||
actionsDiv.style.display = 'block';
|
||
}
|
||
|
||
// Generate outline view if currently in outline view
|
||
if (currentViewNew === 'outline') {
|
||
setTimeout(() => {
|
||
renderOutlineViewNew();
|
||
}, 500);
|
||
}
|
||
|
||
// Show start PPT generation button
|
||
showStartPPTButton();
|
||
} catch (error) {
|
||
console.error('Error streaming outline generation:', error);
|
||
|
||
// 停止加载动画
|
||
stopFunnelAnimation('stream');
|
||
|
||
// 隐藏加载状态
|
||
if (placeholderDiv) {
|
||
placeholderDiv.style.display = 'none';
|
||
}
|
||
|
||
// 显示连接错误和重新生成按钮,但保留placeholder div
|
||
// 先确保placeholder div存在
|
||
let placeholderDiv = document.getElementById('outline-placeholder');
|
||
if (!placeholderDiv) {
|
||
placeholderDiv = document.createElement('div');
|
||
placeholderDiv.id = 'outline-placeholder';
|
||
placeholderDiv.className = 'loading-container';
|
||
contentDiv.appendChild(placeholderDiv);
|
||
}
|
||
|
||
// 清除其他内容,只保留placeholder
|
||
const children = Array.from(contentDiv.children);
|
||
children.forEach(child => {
|
||
if (child.id !== 'outline-placeholder') {
|
||
child.remove();
|
||
}
|
||
});
|
||
|
||
// 在placeholder前添加错误信息
|
||
const errorDiv = document.createElement('div');
|
||
errorDiv.innerHTML = `
|
||
<div style="color: #e74c3c; text-align: center; padding: 20px;">
|
||
<i class="fas fa-exclamation-triangle" style="font-size: 24px; margin-bottom: 10px;"></i>
|
||
<div style="margin-bottom: 15px;">连接失败</div>
|
||
<div style="font-size: 14px; margin-bottom: 20px; color: #7f8c8d;">连接错误: ${error.message}</div>
|
||
<button onclick="retryOutlineGeneration()" class="btn btn-warning btn-sm">
|
||
<i class="fas fa-sync"></i> 重新生成大纲
|
||
</button>
|
||
</div>
|
||
`;
|
||
contentDiv.insertBefore(errorDiv, placeholderDiv);
|
||
if (cursorDiv) cursorDiv.style.display = 'none';
|
||
// Keep status div hidden
|
||
|
||
// 重置标志,允许重试
|
||
outlineGenerationStarted = false;
|
||
}
|
||
}
|
||
|
||
// 显示自定义需求输入弹窗
|
||
function showCustomRequirementsModal() {
|
||
return new Promise((resolve) => {
|
||
// 创建遮罩层
|
||
const overlay = document.createElement('div');
|
||
overlay.style.cssText = `
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 10000;
|
||
`;
|
||
|
||
// 创建弹窗
|
||
const modal = document.createElement('div');
|
||
modal.style.cssText = `
|
||
background: white;
|
||
border-radius: 8px;
|
||
padding: 24px;
|
||
max-width: 500px;
|
||
width: 90%;
|
||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||
`;
|
||
|
||
modal.innerHTML = `
|
||
<h4 style="margin-top: 0; margin-bottom: 16px; color: #333;">
|
||
<i class="fas fa-magic" style="color: #3498db; margin-right: 8px;"></i>
|
||
重新生成大纲
|
||
</h4>
|
||
<p style="margin-bottom: 16px; color: #666; font-size: 14px;">
|
||
您可以输入额外的自定义需求,或者直接点击“生成”使用原有设置。
|
||
</p>
|
||
<textarea
|
||
id="custom-requirements-input"
|
||
placeholder="例如:\n- 需要更多图表和数据展示\n- 增加案例分析部分\n- 突出创新点和亮点"
|
||
style="
|
||
width: 100%;
|
||
height: 120px;
|
||
padding: 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
resize: vertical;
|
||
font-family: inherit;
|
||
box-sizing: border-box;
|
||
"
|
||
></textarea>
|
||
<div style="margin-top: 20px; display: flex; gap: 12px; justify-content: flex-end;">
|
||
<button
|
||
id="modal-cancel-btn"
|
||
style="
|
||
padding: 10px 20px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
background: white;
|
||
color: #666;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
"
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
id="modal-confirm-btn"
|
||
style="
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
background: #3498db;
|
||
color: white;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
"
|
||
>
|
||
<i class="fas fa-sync" style="margin-right: 6px;"></i>
|
||
生成
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
overlay.appendChild(modal);
|
||
document.body.appendChild(overlay);
|
||
|
||
// 获取元素
|
||
const textarea = modal.querySelector('#custom-requirements-input');
|
||
const cancelBtn = modal.querySelector('#modal-cancel-btn');
|
||
const confirmBtn = modal.querySelector('#modal-confirm-btn');
|
||
|
||
// 自动聚焦到输入框
|
||
setTimeout(() => textarea.focus(), 100);
|
||
|
||
// 取消按钮
|
||
cancelBtn.onclick = () => {
|
||
document.body.removeChild(overlay);
|
||
resolve(null);
|
||
};
|
||
|
||
// 确认按钮
|
||
confirmBtn.onclick = () => {
|
||
const value = textarea.value.trim();
|
||
document.body.removeChild(overlay);
|
||
resolve(value);
|
||
};
|
||
|
||
// 点击遮罩层关闭
|
||
overlay.onclick = (e) => {
|
||
if (e.target === overlay) {
|
||
document.body.removeChild(overlay);
|
||
resolve(null);
|
||
}
|
||
};
|
||
|
||
// 键盘事件
|
||
textarea.onkeydown = (e) => {
|
||
// Ctrl/Cmd + Enter 提交
|
||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||
e.preventDefault();
|
||
confirmBtn.click();
|
||
}
|
||
// Escape 取消
|
||
if (e.key === 'Escape') {
|
||
e.preventDefault();
|
||
cancelBtn.click();
|
||
}
|
||
};
|
||
});
|
||
}
|
||
|
||
// 重新生成大纲函数
|
||
async function regenerateOutlineNew() {
|
||
console.log('Starting outline regeneration');
|
||
|
||
// 使用自定义弹窗让用户输入额外需求
|
||
const customRequirements = await showCustomRequirementsModal();
|
||
|
||
// 如果用户点击取消,返回 null
|
||
if (customRequirements === null) {
|
||
return;
|
||
}
|
||
|
||
// 隐藏操作按钮
|
||
const actionsDiv = document.getElementById('outline-actions');
|
||
if (actionsDiv) {
|
||
actionsDiv.style.display = 'none';
|
||
}
|
||
|
||
// 隐藏开始PPT生成按钮
|
||
hideStartPPTButton();
|
||
|
||
// 确保切换到JSON视图以显示生成过程
|
||
switchOutlineViewNew('json');
|
||
|
||
// 清空当前内容并显示加载状态
|
||
const contentDiv = document.getElementById('outline-content-display');
|
||
|
||
// 先获取placeholder div引用,然后清空其他内容
|
||
let placeholderDiv = document.getElementById('outline-placeholder');
|
||
|
||
if (contentDiv) {
|
||
// 只清除非placeholder的子元素
|
||
const children = Array.from(contentDiv.children);
|
||
children.forEach(child => {
|
||
if (child.id !== 'outline-placeholder') {
|
||
child.remove();
|
||
}
|
||
});
|
||
|
||
// 如果placeholder div不存在,创建一个新的
|
||
if (!placeholderDiv) {
|
||
placeholderDiv = document.createElement('div');
|
||
placeholderDiv.id = 'outline-placeholder';
|
||
placeholderDiv.className = 'loading-container';
|
||
contentDiv.appendChild(placeholderDiv);
|
||
}
|
||
}
|
||
|
||
if (placeholderDiv) {
|
||
placeholderDiv.style.display = 'block';
|
||
placeholderDiv.innerHTML = `
|
||
<div class="loading-icon">
|
||
<i class="fas fa-brain"></i>
|
||
</div>
|
||
<div class="loading-title">正在重新生成大纲</div>
|
||
|
||
<div class="brain-container">
|
||
<div class="ai-brain" id="loading-brain-regenerate">
|
||
<div class="brain-core">
|
||
<div class="brain-icon">🧠</div>
|
||
</div>
|
||
<svg class="progress-ring" width="140" height="140">
|
||
<circle class="progress-ring-circle" cx="70" cy="70" r="70"></circle>
|
||
<circle class="progress-ring-progress" cx="70" cy="70" r="70"></circle>
|
||
</svg>
|
||
<div class="neural-network">
|
||
<div class="neural-line" style="--rotation: 45deg;"></div>
|
||
<div class="neural-line" style="--rotation: -30deg;"></div>
|
||
<div class="neural-line" style="--rotation: -45deg;"></div>
|
||
<div class="neural-line" style="--rotation: 60deg;"></div>
|
||
</div>
|
||
<div class="thought-bubbles">
|
||
<div class="thought-bubble"></div>
|
||
<div class="thought-bubble"></div>
|
||
<div class="thought-bubble"></div>
|
||
</div>
|
||
<div class="text-generation">
|
||
<div class="generating-text">正在重新分析内容</div>
|
||
<div class="generating-dots">• • •</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="loading-tips">
|
||
<h5><i class="fas fa-lightbulb"></i> 小贴士</h5>
|
||
<p>AI正在重新分析您的需求并生成全新的PPT大纲,请稍候...</p>
|
||
</div>
|
||
`;
|
||
|
||
// 启动重新生成动画
|
||
setTimeout(() => {
|
||
startBrainAnimation('regenerate');
|
||
}, 100);
|
||
}
|
||
|
||
// 重置生成标志
|
||
outlineGenerationStarted = false;
|
||
|
||
// 重置大纲生成阶段状态(不触发页面重新加载)
|
||
try {
|
||
await updateStageStatusSilent('outline_generation', 'running', 0);
|
||
} catch (error) {
|
||
console.warn('Failed to update stage status:', error);
|
||
}
|
||
|
||
// 调用专门的重新生成大纲接口,带上自定义需求
|
||
try {
|
||
console.log('Calling regenerate outline API with custom requirements:', customRequirements);
|
||
const response = await fetch(`/projects/${currentProjectId}/regenerate-outline`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
custom_requirements: customRequirements || ''
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.error || result.status === "error") {
|
||
// 显示错误信息
|
||
if (contentDiv) {
|
||
contentDiv.innerHTML = `<div style="color: #e74c3c; text-align: center; padding: 20px;">
|
||
<i class="fas fa-exclamation-triangle"></i> 错误: ${result.error || result.message || '未知错误'}
|
||
</div>`;
|
||
}
|
||
stopBrainAnimation('regenerate');
|
||
if (placeholderDiv) placeholderDiv.style.display = 'none';
|
||
|
||
// 重新显示操作按钮
|
||
if (actionsDiv) {
|
||
actionsDiv.style.display = 'block';
|
||
}
|
||
|
||
// 重置标志,允许重试
|
||
outlineGenerationStarted = false;
|
||
return;
|
||
}
|
||
|
||
// 停止重新生成动画并隐藏加载状态
|
||
stopBrainAnimation('regenerate');
|
||
if (placeholderDiv) {
|
||
placeholderDiv.style.display = 'none';
|
||
}
|
||
|
||
// 显示生成的大纲内容
|
||
if (contentDiv && result.outline_content) {
|
||
const preElement = document.createElement('pre');
|
||
preElement.style.cssText = 'white-space: pre-wrap; word-wrap: break-word; font-family: "Courier New", monospace; font-size: 12px; line-height: 1.4; margin: 0; padding: 0; background: transparent; border: none;';
|
||
preElement.textContent = result.outline_content;
|
||
contentDiv.appendChild(preElement);
|
||
}
|
||
|
||
// 重置标志,允许下次生成
|
||
outlineGenerationStarted = false;
|
||
|
||
// Show action buttons
|
||
if (actionsDiv) {
|
||
actionsDiv.style.display = 'block';
|
||
}
|
||
|
||
// Generate outline view if currently in outline view
|
||
if (currentViewNew === 'outline') {
|
||
setTimeout(() => {
|
||
renderOutlineViewNew();
|
||
}, 500);
|
||
}
|
||
|
||
// Show start PPT generation button
|
||
showStartPPTButton();
|
||
} catch (error) {
|
||
console.error('Error during outline regeneration:', error);
|
||
|
||
// 显示错误信息
|
||
if (contentDiv) {
|
||
contentDiv.innerHTML = `<div style="color: #e74c3c; text-align: center; padding: 20px;">
|
||
<i class="fas fa-exclamation-triangle"></i> 重新生成失败: ${error.message}
|
||
</div>`;
|
||
}
|
||
|
||
stopBrainAnimation('regenerate');
|
||
if (placeholderDiv) {
|
||
placeholderDiv.style.display = 'none';
|
||
}
|
||
|
||
// 重新显示操作按钮
|
||
if (actionsDiv) {
|
||
actionsDiv.style.display = 'block';
|
||
}
|
||
|
||
// 重置标志,允许重试
|
||
outlineGenerationStarted = false;
|
||
}
|
||
}
|
||
|
||
// New outline editing functions for JSON
|
||
function editOutlineNew() {
|
||
const contentDiv = document.getElementById('outline-content-display');
|
||
const editArea = document.getElementById('outline-edit-area');
|
||
const editor = document.getElementById('outline-editor');
|
||
|
||
if (contentDiv && editArea && editor) {
|
||
// Get current JSON content - 优先从pre元素获取
|
||
let jsonContent = '';
|
||
const preElement = contentDiv.querySelector('pre');
|
||
if (preElement) {
|
||
jsonContent = preElement.textContent || preElement.innerText || '';
|
||
} else {
|
||
// 如果没有pre元素,从整个div获取内容
|
||
jsonContent = contentDiv.textContent || contentDiv.innerText || '';
|
||
}
|
||
|
||
// Try to parse and reformat the JSON for better editing
|
||
try {
|
||
const parsed = JSON.parse(jsonContent);
|
||
jsonContent = JSON.stringify(parsed, null, 2);
|
||
} catch (e) {
|
||
console.warn('Content is not valid JSON, using as-is:', e);
|
||
}
|
||
|
||
editor.value = jsonContent;
|
||
|
||
// Hide content, show editor
|
||
contentDiv.style.display = 'none';
|
||
editArea.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
function cancelEditOutlineNew() {
|
||
const contentDiv = document.getElementById('outline-content-display');
|
||
const editArea = document.getElementById('outline-edit-area');
|
||
|
||
if (contentDiv && editArea) {
|
||
// Show content, hide editor
|
||
contentDiv.style.display = 'block';
|
||
editArea.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// JSON validation function
|
||
function validateJSON() {
|
||
const editor = document.getElementById('outline-editor');
|
||
if (!editor) return;
|
||
|
||
try {
|
||
const jsonData = JSON.parse(editor.value);
|
||
|
||
// Validate required structure
|
||
if (!jsonData.title) {
|
||
throw new Error('缺少必需字段: title');
|
||
}
|
||
if (!jsonData.slides || !Array.isArray(jsonData.slides)) {
|
||
throw new Error('缺少必需字段: slides (必须是数组)');
|
||
}
|
||
|
||
// Validate each slide
|
||
for (let i = 0; i < jsonData.slides.length; i++) {
|
||
const slide = jsonData.slides[i];
|
||
if (!slide.title) {
|
||
throw new Error(`第${i+1}个幻灯片缺少title字段`);
|
||
}
|
||
if (!slide.content_points || !Array.isArray(slide.content_points)) {
|
||
throw new Error(`第${i+1}个幻灯片缺少content_points字段 (必须是数组)`);
|
||
}
|
||
if (!slide.slide_type) {
|
||
throw new Error(`第${i+1}个幻灯片缺少slide_type字段`);
|
||
}
|
||
}
|
||
|
||
// Format and update editor content
|
||
editor.value = JSON.stringify(jsonData, null, 2);
|
||
|
||
alert('✅ JSON格式验证通过!');
|
||
return true;
|
||
|
||
} catch (error) {
|
||
alert('❌ JSON格式错误: ' + error.message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function saveOutlineEditNew() {
|
||
const editor = document.getElementById('outline-editor');
|
||
const contentDiv = document.getElementById('outline-content-display');
|
||
|
||
if (!editor || !contentDiv) return;
|
||
|
||
// Validate JSON before saving
|
||
if (!validateJSON()) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/projects/${currentProjectId}/update-outline`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
outline_content: editor.value
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
// Update display content with formatted JSON - 使用textContent避免HTML转义问题
|
||
const preElement = document.createElement('pre');
|
||
preElement.style.cssText = 'margin: 0; font-family: "Consolas", "Monaco", "Courier New", monospace; font-size: 13px; white-space: pre-wrap; word-wrap: break-word;';
|
||
preElement.textContent = editor.value;
|
||
contentDiv.innerHTML = '';
|
||
contentDiv.appendChild(preElement);
|
||
|
||
// Hide editor, show content
|
||
cancelEditOutlineNew();
|
||
|
||
// Show success message
|
||
alert('✅ 大纲已更新');
|
||
|
||
// 立即更新大纲视图,确保使用最新的数据
|
||
setTimeout(() => {
|
||
// 强制刷新大纲视图,使用编辑器中的最新内容
|
||
try {
|
||
const parsedOutline = JSON.parse(editor.value);
|
||
|
||
// 如果当前是大纲视图,重新渲染
|
||
if (currentView === 'outline') {
|
||
renderOutlineView();
|
||
}
|
||
|
||
// 如果当前是新的大纲视图,强制使用最新数据重新渲染
|
||
if (currentViewNew === 'outline') {
|
||
// 直接传递解析后的大纲数据,避免从DOM重新读取
|
||
renderOutlineViewNewWithData(parsedOutline);
|
||
}
|
||
} catch (parseError) {
|
||
console.error('Failed to parse updated outline:', parseError);
|
||
// 如果解析失败,仍然尝试常规渲染
|
||
if (currentView === 'outline') {
|
||
renderOutlineView();
|
||
}
|
||
if (currentViewNew === 'outline') {
|
||
renderOutlineViewNew();
|
||
}
|
||
}
|
||
}, 100); // 减少延迟,立即更新
|
||
} else {
|
||
alert('❌ 保存失败,请重试');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error saving outline:', error);
|
||
alert('❌ 保存失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
function confirmOutlineNew() {
|
||
if (confirm('确认大纲内容?')) {
|
||
// Mark outline as confirmed
|
||
fetch(`/projects/${currentProjectId}/confirm-outline`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
}).then(response => {
|
||
if (response.ok) {
|
||
// Hide edit/confirm buttons and show start PPT button
|
||
const actionsDiv = document.getElementById('outline-actions');
|
||
if (actionsDiv) {
|
||
actionsDiv.style.display = 'none';
|
||
}
|
||
|
||
// Show start PPT generation button
|
||
showStartPPTButton();
|
||
} else {
|
||
alert('确认失败,请重试');
|
||
}
|
||
}).catch(error => {
|
||
console.error('Error confirming outline:', error);
|
||
alert('确认失败: ' + error.message);
|
||
});
|
||
}
|
||
}
|
||
|
||
|
||
|
||
// Show start PPT generation button
|
||
function showStartPPTButton() {
|
||
// Check if button already exists
|
||
let startPPTButton = document.getElementById('start-ppt-button');
|
||
|
||
if (!startPPTButton) {
|
||
// Create start PPT button only if it doesn't exist
|
||
const outlineSection = document.getElementById('outline-section');
|
||
if (outlineSection) {
|
||
startPPTButton = document.createElement('div');
|
||
startPPTButton.id = 'start-ppt-button';
|
||
startPPTButton.style.cssText = 'text-align: center; margin-top: 30px; padding: 20px; background: #e8f5e8; border-radius: 10px; border: 2px solid #27ae60;';
|
||
|
||
startPPTButton.innerHTML = `
|
||
<button onclick="startPPTGenerationFromOutline()" class="btn btn-success btn-lg" style="padding: 15px 30px; font-size: 18px;">
|
||
<i class="fas fa-rocket"></i> 开始生成PPT
|
||
</button>
|
||
`;
|
||
|
||
// Insert after outline section
|
||
outlineSection.parentNode.insertBefore(startPPTButton, outlineSection.nextSibling);
|
||
}
|
||
}
|
||
|
||
if (startPPTButton) {
|
||
startPPTButton.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
// Hide start PPT generation button
|
||
function hideStartPPTButton() {
|
||
const startPPTButton = document.getElementById('start-ppt-button');
|
||
if (startPPTButton) {
|
||
startPPTButton.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Start PPT generation from outline
|
||
function startPPTGenerationFromOutline() {
|
||
|
||
if (confirm('确认开始生成PPT?这将跳转到模板选择页面。')) {
|
||
// Redirect to template selection page
|
||
window.location.href = `/projects/${currentProjectId}/template-selection`;
|
||
}
|
||
}
|
||
|
||
// 重试流式大纲生成函数
|
||
function retryStreamingOutlineGeneration() {
|
||
console.log('Retrying streaming outline generation');
|
||
|
||
// 重置状态 - 这里不需要重置outlineGenerationStarted,因为流式生成有自己的逻辑
|
||
|
||
// 清除错误内容
|
||
const stageId = 'outline_generation';
|
||
const contentDiv = document.getElementById(`outline-content-${stageId}`);
|
||
if (contentDiv) {
|
||
contentDiv.innerHTML = '';
|
||
console.log('Cleared streaming outline content for retry');
|
||
} else {
|
||
console.error('Streaming outline content div not found!');
|
||
}
|
||
|
||
// 调用生成函数
|
||
startOutlineGeneration();
|
||
}
|
||
|
||
// Start outline generation with streaming
|
||
async function startOutlineGeneration() {
|
||
console.log('Starting outline generation with streaming');
|
||
|
||
const stageId = 'outline_generation';
|
||
const outputDiv = document.getElementById(`outline-output-${stageId}`);
|
||
const contentDiv = document.getElementById(`outline-content-${stageId}`);
|
||
const cursorDiv = document.getElementById(`outline-cursor-${stageId}`);
|
||
|
||
if (!outputDiv || !contentDiv || !cursorDiv) {
|
||
console.error('Outline output elements not found');
|
||
return;
|
||
}
|
||
|
||
// Show output area
|
||
outputDiv.style.display = 'block';
|
||
contentDiv.textContent = '';
|
||
cursorDiv.style.display = 'inline-block';
|
||
|
||
// Update stage status to running
|
||
const stageElement = document.querySelector(`[data-stage-id="${stageId}"]`);
|
||
if (stageElement) {
|
||
const statusIcon = stageElement.querySelector('.stage-status-icon');
|
||
if (statusIcon) {
|
||
statusIcon.innerHTML = '<div style="display: inline-block; width: 12px; height: 12px; border: 2px solid #f3f3f3; border-top: 2px solid #f39c12; border-radius: 50%; animation: spin 1s linear infinite;"></div>';
|
||
}
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/projects/${currentProjectId}/outline-stream`);
|
||
const reader = response.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
|
||
const chunk = decoder.decode(value);
|
||
const lines = chunk.split('\n');
|
||
|
||
for (const line of lines) {
|
||
if (line.startsWith('data: ')) {
|
||
try {
|
||
const data = JSON.parse(line.slice(6));
|
||
|
||
if (data.error) {
|
||
// 显示错误信息和重新生成按钮
|
||
contentDiv.innerHTML = `
|
||
<div style="color: #e74c3c; text-align: center; padding: 20px;">
|
||
<i class="fas fa-exclamation-triangle" style="font-size: 24px; margin-bottom: 10px;"></i>
|
||
<div style="margin-bottom: 15px;">大纲生成失败</div>
|
||
<div style="font-size: 14px; margin-bottom: 20px; color: #7f8c8d;">${data.error}</div>
|
||
<button onclick="retryStreamingOutlineGeneration()" class="btn btn-warning btn-sm">
|
||
<i class="fas fa-sync"></i> 重新生成大纲
|
||
</button>
|
||
</div>
|
||
`;
|
||
cursorDiv.style.display = 'none';
|
||
if (stageElement) {
|
||
const statusIcon = stageElement.querySelector('.stage-status-icon');
|
||
if (statusIcon) statusIcon.textContent = '❌';
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (data.content) {
|
||
contentDiv.textContent += data.content;
|
||
contentDiv.scrollTop = contentDiv.scrollHeight;
|
||
}
|
||
|
||
if (data.done) {
|
||
cursorDiv.style.display = 'none';
|
||
if (stageElement) {
|
||
const statusIcon = stageElement.querySelector('.stage-status-icon');
|
||
if (statusIcon) statusIcon.textContent = '✓';
|
||
}
|
||
|
||
// 格式化JSON内容并用pre元素包装
|
||
try {
|
||
const jsonContent = contentDiv.textContent;
|
||
const parsed = JSON.parse(jsonContent);
|
||
const formattedContent = JSON.stringify(parsed, null, 2);
|
||
|
||
// 创建pre元素来正确显示JSON
|
||
const preElement = document.createElement('pre');
|
||
preElement.style.cssText = 'margin: 0; font-family: "Consolas", "Monaco", "Courier New", monospace; font-size: 13px; white-space: pre-wrap; word-wrap: break-word;';
|
||
preElement.textContent = formattedContent;
|
||
contentDiv.innerHTML = '';
|
||
contentDiv.appendChild(preElement);
|
||
|
||
console.log('Successfully formatted JSON outline with', parsed.slides ? parsed.slides.length : 0, 'slides');
|
||
} catch (e) {
|
||
console.warn('Failed to parse JSON content for formatting:', e);
|
||
// 保持原始内容,但仍然用pre包装
|
||
const preElement = document.createElement('pre');
|
||
preElement.style.cssText = 'margin: 0; font-family: "Consolas", "Monaco", "Courier New", monospace; font-size: 13px; white-space: pre-wrap; word-wrap: break-word;';
|
||
preElement.textContent = contentDiv.textContent;
|
||
contentDiv.innerHTML = '';
|
||
contentDiv.appendChild(preElement);
|
||
}
|
||
|
||
// Update the header to show completion
|
||
const outputDiv = document.getElementById(`outline-output-${stageId}`);
|
||
if (outputDiv) {
|
||
const header = outputDiv.querySelector('h6');
|
||
if (header && !header.querySelector('.completion-badge')) {
|
||
const badge = document.createElement('span');
|
||
badge.className = 'completion-badge';
|
||
badge.style.cssText = 'color: #27ae60; font-size: 0.8em; margin-left: 10px;';
|
||
badge.textContent = '✅ 生成完成';
|
||
header.appendChild(badge);
|
||
}
|
||
}
|
||
|
||
// 延迟刷新页面,让用户有时间看到格式化的内容
|
||
setTimeout(() => {
|
||
window.location.reload();
|
||
}, 3000);
|
||
return;
|
||
}
|
||
} catch (e) {
|
||
console.error('Error parsing outline stream data:', e);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error streaming outline generation:', error);
|
||
// 显示连接错误和重新生成按钮
|
||
contentDiv.innerHTML = `
|
||
<div style="color: #e74c3c; text-align: center; padding: 20px;">
|
||
<i class="fas fa-exclamation-triangle" style="font-size: 24px; margin-bottom: 10px;"></i>
|
||
<div style="margin-bottom: 15px;">连接失败</div>
|
||
<div style="font-size: 14px; margin-bottom: 20px; color: #7f8c8d;">连接错误: ${error.message}</div>
|
||
<button onclick="retryStreamingOutlineGeneration()" class="btn btn-warning btn-sm">
|
||
<i class="fas fa-sync"></i> 重新生成大纲
|
||
</button>
|
||
</div>
|
||
`;
|
||
cursorDiv.style.display = 'none';
|
||
if (stageElement) {
|
||
const statusIcon = stageElement.querySelector('.stage-status-icon');
|
||
if (statusIcon) statusIcon.textContent = '❌';
|
||
}
|
||
}
|
||
}
|
||
|
||
// Start execution for a complete stage
|
||
async function startStageExecution(stageId) {
|
||
console.log(`Starting stage execution for: ${stageId}`);
|
||
|
||
// Special handling for outline generation
|
||
if (stageId === 'outline_generation') {
|
||
// 检查是否已经开始了新的大纲生成
|
||
if (outlineGenerationStarted) {
|
||
console.log('Outline generation already started via new method, skipping stage execution...');
|
||
return;
|
||
}
|
||
return startOutlineGeneration();
|
||
}
|
||
|
||
const taskItem = document.querySelector(`[data-stage-id="${stageId}"]`);
|
||
if (!taskItem) {
|
||
console.error(`Task item not found for stage: ${stageId}`);
|
||
return;
|
||
}
|
||
|
||
// Check if stage is already running or completed
|
||
const stageStatusIcon = taskItem.querySelector('.stage-status-icon');
|
||
if (stageStatusIcon && (stageStatusIcon.textContent === '✓' || stageStatusIcon.innerHTML.includes('spin'))) {
|
||
console.log(`Stage ${stageId} is already running or completed`);
|
||
return;
|
||
}
|
||
|
||
const statusIcon = taskItem.querySelector('.task-status');
|
||
const outputDiv = taskItem.querySelector('.task-output');
|
||
const outputContent = taskItem.querySelector('.output-content');
|
||
const outputCursor = taskItem.querySelector('.output-cursor');
|
||
const streamBtn = taskItem.querySelector('.btn-stream');
|
||
|
||
// Update UI
|
||
taskItem.classList.add('active');
|
||
statusIcon.textContent = '🔄';
|
||
outputDiv.style.display = 'block';
|
||
outputContent.textContent = '';
|
||
outputCursor.classList.remove('hidden');
|
||
streamBtn.disabled = true;
|
||
streamBtn.textContent = '处理中...';
|
||
|
||
// If this is PPT creation stage, show editor button immediately
|
||
if (stageId === 'ppt_creation') {
|
||
// Show the existing editor button if it exists
|
||
const existingEditorBtn = document.getElementById(`editor-btn-${stageId}`);
|
||
if (existingEditorBtn) {
|
||
existingEditorBtn.style.display = 'inline-block';
|
||
console.log('Editor button shown for PPT creation stage');
|
||
} else {
|
||
// Create new editor button if not exists
|
||
const editorBtn = document.createElement('button');
|
||
editorBtn.onclick = openEditor;
|
||
editorBtn.className = 'editor-btn';
|
||
editorBtn.style.cssText = 'background: #27ae60; color: white; border: none; padding: 6px 12px; border-radius: 6px; font-size: 0.8em; cursor: pointer; margin-left: 8px;';
|
||
editorBtn.innerHTML = '<i class="fas fa-edit"></i> 打开编辑器';
|
||
|
||
// Insert after the stream button
|
||
if (streamBtn && streamBtn.parentNode) {
|
||
streamBtn.parentNode.insertBefore(editorBtn, streamBtn.nextSibling);
|
||
}
|
||
|
||
console.log('Editor button created for PPT creation stage');
|
||
}
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/projects/${currentProjectId}/stage-stream/${stageId}`);
|
||
const reader = response.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
|
||
const chunk = decoder.decode(value);
|
||
const lines = chunk.split('\n');
|
||
|
||
for (const line of lines) {
|
||
if (line.startsWith('data: ')) {
|
||
try {
|
||
const data = JSON.parse(line.slice(6));
|
||
|
||
if (data.error) {
|
||
outputContent.textContent = `错误: ${data.error}`;
|
||
statusIcon.textContent = '❌';
|
||
streamBtn.disabled = false;
|
||
streamBtn.textContent = '重试';
|
||
|
||
// If stage is already running or completed, don't show as error
|
||
if (data.error.includes('already running') || data.error.includes('already completed')) {
|
||
outputContent.textContent = `提示: ${data.error}`;
|
||
statusIcon.textContent = '⚠️';
|
||
streamBtn.style.display = 'none';
|
||
}
|
||
break;
|
||
}
|
||
|
||
if (data.content) {
|
||
outputContent.textContent += data.content;
|
||
outputDiv.scrollTop = outputDiv.scrollHeight;
|
||
}
|
||
|
||
if (data.done) {
|
||
outputCursor.classList.add('hidden');
|
||
statusIcon.textContent = '✅';
|
||
streamBtn.textContent = '完成';
|
||
streamBtn.style.display = 'none';
|
||
|
||
// 如果是大纲生成阶段,格式化JSON内容
|
||
if (stageId === 'outline_generation') {
|
||
try {
|
||
const jsonContent = outputContent.textContent;
|
||
const parsed = JSON.parse(jsonContent);
|
||
const formattedContent = JSON.stringify(parsed, null, 2);
|
||
|
||
// 创建pre元素来正确显示JSON
|
||
const preElement = document.createElement('pre');
|
||
preElement.style.cssText = 'margin: 0; font-family: "Consolas", "Monaco", "Courier New", monospace; font-size: 13px; white-space: pre-wrap; word-wrap: break-word;';
|
||
preElement.textContent = formattedContent;
|
||
outputContent.innerHTML = '';
|
||
outputContent.appendChild(preElement);
|
||
|
||
console.log('Successfully formatted JSON outline with', parsed.slides ? parsed.slides.length : 0, 'slides');
|
||
} catch (e) {
|
||
console.warn('Failed to parse JSON content for formatting:', e);
|
||
// 保持原始内容,但仍然用pre包装
|
||
const preElement = document.createElement('pre');
|
||
preElement.style.cssText = 'margin: 0; font-family: "Consolas", "Monaco", "Courier New", monospace; font-size: 13px; white-space: pre-wrap; word-wrap: break-word;';
|
||
preElement.textContent = outputContent.textContent;
|
||
outputContent.innerHTML = '';
|
||
outputContent.appendChild(preElement);
|
||
}
|
||
}
|
||
|
||
// If this is PPT creation stage, update editor button text
|
||
if (stageId === 'ppt_creation') {
|
||
const existingEditorBtn = taskItem.querySelector('.editor-btn');
|
||
if (existingEditorBtn) {
|
||
existingEditorBtn.innerHTML = '<i class="fas fa-edit"></i> 查看PPT';
|
||
console.log('Updated editor button text to "查看PPT"');
|
||
}
|
||
}
|
||
|
||
// Refresh the page to show updated progress
|
||
setTimeout(() => {
|
||
window.location.reload();
|
||
}, 2000);
|
||
break;
|
||
}
|
||
} catch (e) {
|
||
console.error('Error parsing stream data:', e);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error streaming stage:', error);
|
||
outputContent.textContent = `连接错误: ${error.message}`;
|
||
statusIcon.textContent = '❌';
|
||
} finally {
|
||
streamBtn.disabled = false;
|
||
if (streamBtn.textContent === '处理中...') {
|
||
streamBtn.textContent = '重试';
|
||
}
|
||
}
|
||
}
|
||
|
||
function startStage(stageId) {
|
||
updateStageStatus(stageId, 'running');
|
||
}
|
||
|
||
function completeStage(stageId) {
|
||
updateStageStatus(stageId, 'completed', 100);
|
||
}
|
||
|
||
function retryStage(stageId) {
|
||
updateStageStatus(stageId, 'running', 0);
|
||
}
|
||
|
||
// Continue from a specific stage - reset all subsequent stages and start from the selected stage
|
||
async function continueFromStage(stageId) {
|
||
console.log(`Continue from stage: ${stageId}`);
|
||
|
||
// Show confirmation dialog
|
||
const confirmMessage = `确定要从"${getStageDisplayName(stageId)}"步骤重新开始吗?\n\n这将重置该步骤及之后的所有步骤,并重新执行工作流程。`;
|
||
|
||
if (!confirm(confirmMessage)) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Call backend API to reset stages from the selected stage
|
||
const response = await fetch(`/api/projects/${currentProjectId}/continue-from-stage`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
stage_id: stageId
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
|
||
// Show success message
|
||
alert(`已从"${getStageDisplayName(stageId)}"步骤重新开始,正在重新执行工作流程...`);
|
||
|
||
// Reload page to show updated status
|
||
setTimeout(() => {
|
||
window.location.reload();
|
||
}, 1000);
|
||
} else {
|
||
const error = await response.json();
|
||
throw new Error(error.detail || '重新开始失败');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error continuing from stage:', error);
|
||
alert('重新开始失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// Get display name for stage ID
|
||
function getStageDisplayName(stageId) {
|
||
const stageNames = {
|
||
'requirements_confirmation': '需求确认',
|
||
'outline_generation': '大纲生成',
|
||
'theme_configuration': '主题配置',
|
||
'content_enhancement': '内容增强',
|
||
'ppt_creation': 'PPT生成',
|
||
'quality_review': '质量审核'
|
||
};
|
||
|
||
return stageNames[stageId] || stageId;
|
||
}
|
||
|
||
async function updateStageStatus(stageId, status, progress = null) {
|
||
try {
|
||
const response = await fetch(`/api/projects/${currentProjectId}/stages/${stageId}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
status: status,
|
||
progress: progress
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
// Reload page to show updated status
|
||
setTimeout(() => {
|
||
window.location.reload();
|
||
}, 500);
|
||
} else {
|
||
alert('更新失败,请重试');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating stage:', error);
|
||
alert('更新失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// Silent version that doesn't reload the page (for regeneration scenarios)
|
||
async function updateStageStatusSilent(stageId, status, progress = null) {
|
||
try {
|
||
const response = await fetch(`/api/projects/${currentProjectId}/stages/${stageId}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
status: status,
|
||
progress: progress
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
console.warn('Failed to update stage status silently');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating stage silently:', error);
|
||
}
|
||
}
|
||
|
||
// Subtask management functions removed - now using complete stage execution
|
||
|
||
// Real-time updates
|
||
setInterval(async function() {
|
||
try {
|
||
const response = await fetch(`/api/projects/${currentProjectId}/todo`);
|
||
const todoData = await response.json();
|
||
|
||
// Update overall progress
|
||
const progressBar = document.querySelector('.overall-progress-bar');
|
||
const progressText = document.querySelector('.overall-progress-text');
|
||
const progressDetails = document.querySelector('.progress-details');
|
||
|
||
if (progressBar && progressText && todoData.overall_progress !== undefined) {
|
||
progressBar.style.width = todoData.overall_progress + '%';
|
||
progressText.textContent = `总体进度: ${todoData.overall_progress.toFixed(1)}%`;
|
||
}
|
||
|
||
if (progressDetails && todoData.stages && Array.isArray(todoData.stages)) {
|
||
const completedStages = todoData.stages.filter(s => s.status === 'completed').length;
|
||
progressDetails.textContent = `已完成 ${completedStages} / ${todoData.stages.length} 个阶段`;
|
||
}
|
||
|
||
// Hide connection error if update is successful
|
||
const errorElement = document.getElementById('connection-error');
|
||
if (errorElement) {
|
||
errorElement.style.display = 'none';
|
||
}
|
||
|
||
// Update stage indicators
|
||
if (todoData.stages && Array.isArray(todoData.stages)) {
|
||
todoData.stages.forEach(stage => {
|
||
const stageElement = document.querySelector(`[data-stage-id="${stage.id}"]`);
|
||
if (stageElement) {
|
||
const icon = stageElement.querySelector('.stage-status-icon');
|
||
const progressBar = stageElement.querySelector('.stage-progress-bar');
|
||
const progressText = stageElement.querySelector('.stage-progress-text');
|
||
const taskStatus = stageElement.querySelector('.task-status');
|
||
|
||
// Update status icon
|
||
if (stage.status === 'completed') {
|
||
icon.textContent = '✓';
|
||
icon.parentElement.style.background = '#27ae60';
|
||
if (taskStatus) taskStatus.textContent = '✅';
|
||
|
||
// Auto-start outline generation when requirements confirmation is completed
|
||
if (stage.id === 'requirements_confirmation') {
|
||
const outlineStage = document.querySelector('[data-stage-id="outline_generation"]');
|
||
const outlineStatus = outlineStage?.querySelector('.stage-status-icon')?.textContent;
|
||
if (outlineStatus === '⏳') {
|
||
console.log('Requirements just completed, starting outline generation');
|
||
setTimeout(() => {
|
||
startStageExecution('outline_generation');
|
||
}, 2000); // Wait 2 seconds for UI to update
|
||
}
|
||
}
|
||
} else if (stage.status === 'running') {
|
||
icon.innerHTML = '<div style="width: 16px; height: 16px; border: 2px solid white; border-top: 2px solid transparent; border-radius: 50%; animation: spin 1s linear infinite;"></div>';
|
||
icon.parentElement.style.background = '#3498db';
|
||
if (taskStatus) taskStatus.textContent = '🔄';
|
||
|
||
// Show editor button for PPT creation stage when it starts running
|
||
if (stage.id === 'ppt_creation') {
|
||
const editorBtn = document.getElementById(`editor-btn-${stage.id}`);
|
||
if (editorBtn) {
|
||
editorBtn.style.display = 'inline-block';
|
||
}
|
||
}
|
||
} else if (stage.status === 'failed') {
|
||
icon.textContent = '✗';
|
||
icon.parentElement.style.background = '#e74c3c';
|
||
if (taskStatus) taskStatus.textContent = '❌';
|
||
}
|
||
|
||
// Update progress bar
|
||
if (progressBar && progressText && stage.status === 'running') {
|
||
progressBar.style.width = stage.progress + '%';
|
||
progressText.textContent = stage.progress.toFixed(1) + '%';
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error updating TODO board:', error);
|
||
// Show connection error message
|
||
const errorElement = document.getElementById('connection-error');
|
||
if (errorElement) {
|
||
errorElement.style.display = 'block';
|
||
errorElement.textContent = '连接错误,请刷新页面重试';
|
||
}
|
||
}
|
||
}, 3000); // Update every 3 seconds
|
||
|
||
// Modal functionality removed - using direct stage execution
|
||
|
||
// Open editor function
|
||
function openEditor() {
|
||
console.log(`Opening editor for project: ${currentProjectId}`);
|
||
const editorUrl = `/projects/${currentProjectId}/edit`;
|
||
console.log(`Editor URL: ${editorUrl}`);
|
||
window.open(editorUrl, '_blank');
|
||
}
|
||
|
||
// Auto-start workflow when page loads
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// Check if we just came from requirements confirmation (URL parameter or session storage)
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const fromRequirements = urlParams.get('from_requirements') === 'true' ||
|
||
sessionStorage.getItem('requirements_just_confirmed') === 'true';
|
||
|
||
// Clear the session storage flag
|
||
sessionStorage.removeItem('requirements_just_confirmed');
|
||
|
||
// Check requirements confirmation status and outline generation status
|
||
const requirementsStage = document.querySelector('[data-stage-id="requirements_confirmation"]');
|
||
const outlineStage = document.querySelector('[data-stage-id="outline_generation"]');
|
||
|
||
const requirementsCompleted = requirementsStage?.querySelector('.stage-status-icon')?.textContent === '✓';
|
||
const outlineStatus = outlineStage?.querySelector('.stage-status-icon')?.textContent;
|
||
|
||
// Auto-start outline generation if requirements just confirmed or if requirements completed and outline pending
|
||
if ((fromRequirements || requirementsCompleted) && outlineStatus === '⏳') {
|
||
// Requirements confirmed, automatically start outline generation (second step)
|
||
console.log('Requirements confirmed, starting outline generation automatically');
|
||
// 只有在不是来自需求确认页面时才调用,避免重复调用
|
||
if (!fromRequirements) {
|
||
setTimeout(() => {
|
||
startOutlineGenerationNew();
|
||
}, 1000);
|
||
}
|
||
} else if (!requirementsCompleted) {
|
||
// Check if this is a new project (all stages are pending)
|
||
const stages = document.querySelectorAll('.todo-stage');
|
||
let allPending = true;
|
||
|
||
stages.forEach(stage => {
|
||
const statusIcon = stage.querySelector('.stage-status-icon');
|
||
if (statusIcon && statusIcon.textContent !== '⏳') {
|
||
allPending = false;
|
||
}
|
||
});
|
||
|
||
if (allPending) {
|
||
// Start the workflow automatically for requirements confirmation
|
||
setTimeout(() => {
|
||
fetch(`/projects/${currentProjectId}/start-workflow`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
}
|
||
}).then(response => {
|
||
if (response.ok) {
|
||
console.log('Workflow started automatically');
|
||
} else {
|
||
console.error('Failed to start workflow');
|
||
}
|
||
}).catch(error => {
|
||
console.error('Error starting workflow:', error);
|
||
});
|
||
}, 2000); // Wait 2 seconds after page load
|
||
}
|
||
}
|
||
});
|
||
|
||
// Sequential stage execution - simplified to not interfere with backend workflow
|
||
async function startSequentialStageExecution() {
|
||
console.log('Workflow started automatically');
|
||
// The backend workflow will handle the actual execution
|
||
// Frontend just monitors progress through real-time updates
|
||
}
|
||
|
||
// Execute a single stage and wait for completion
|
||
async function executeStageAndWait(stageId) {
|
||
return new Promise((resolve, reject) => {
|
||
const taskItem = document.querySelector(`[data-stage-id="${stageId}"]`);
|
||
if (!taskItem) {
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
const statusIcon = taskItem.querySelector('.task-status');
|
||
const outputDiv = taskItem.querySelector('.task-output');
|
||
const outputContent = taskItem.querySelector('.output-content');
|
||
const outputCursor = taskItem.querySelector('.output-cursor');
|
||
const streamBtn = taskItem.querySelector('.btn-stream');
|
||
|
||
// Update UI
|
||
taskItem.classList.add('active');
|
||
statusIcon.textContent = '🔄';
|
||
outputDiv.style.display = 'block';
|
||
outputContent.textContent = '';
|
||
outputCursor.classList.remove('hidden');
|
||
if (streamBtn) {
|
||
streamBtn.disabled = true;
|
||
streamBtn.textContent = '处理中...';
|
||
}
|
||
|
||
// Start streaming
|
||
fetch(`/projects/${currentProjectId}/stage-stream/${stageId}`)
|
||
.then(response => response.body.getReader())
|
||
.then(reader => {
|
||
const decoder = new TextDecoder();
|
||
|
||
function readStream() {
|
||
return reader.read().then(({ done, value }) => {
|
||
if (done) {
|
||
// Stream completed
|
||
outputCursor.classList.add('hidden');
|
||
statusIcon.textContent = '✅';
|
||
if (streamBtn) {
|
||
streamBtn.textContent = '完成';
|
||
streamBtn.disabled = false;
|
||
streamBtn.style.display = 'none';
|
||
}
|
||
|
||
// Small delay before resolving to ensure UI updates
|
||
setTimeout(() => {
|
||
resolve();
|
||
}, 500);
|
||
return;
|
||
}
|
||
|
||
const chunk = decoder.decode(value);
|
||
const lines = chunk.split('\n');
|
||
|
||
for (const line of lines) {
|
||
if (line.startsWith('data: ')) {
|
||
try {
|
||
const data = JSON.parse(line.slice(6));
|
||
|
||
if (data.error) {
|
||
outputContent.textContent = `错误: ${data.error}`;
|
||
statusIcon.textContent = '❌';
|
||
reject(new Error(data.error));
|
||
return;
|
||
}
|
||
|
||
if (data.content) {
|
||
outputContent.textContent += data.content;
|
||
outputDiv.scrollTop = outputDiv.scrollHeight;
|
||
}
|
||
|
||
if (data.done) {
|
||
outputCursor.classList.add('hidden');
|
||
statusIcon.textContent = '✅';
|
||
if (streamBtn) {
|
||
streamBtn.textContent = '完成';
|
||
streamBtn.disabled = false;
|
||
}
|
||
|
||
// 如果是大纲生成相关的流式处理,格式化JSON内容
|
||
if (stageId === 'outline_generation' || outputContent.textContent.trim().startsWith('{')) {
|
||
try {
|
||
const jsonContent = outputContent.textContent;
|
||
const parsed = JSON.parse(jsonContent);
|
||
const formattedContent = JSON.stringify(parsed, null, 2);
|
||
|
||
// 创建pre元素来正确显示JSON
|
||
const preElement = document.createElement('pre');
|
||
preElement.style.cssText = 'margin: 0; font-family: "Consolas", "Monaco", "Courier New", monospace; font-size: 13px; white-space: pre-wrap; word-wrap: break-word;';
|
||
preElement.textContent = formattedContent;
|
||
outputContent.innerHTML = '';
|
||
outputContent.appendChild(preElement);
|
||
|
||
console.log('Successfully formatted JSON content with', parsed.slides ? parsed.slides.length : 0, 'slides');
|
||
} catch (e) {
|
||
console.warn('Failed to parse JSON content for formatting:', e);
|
||
// 保持原始内容,但仍然用pre包装
|
||
const preElement = document.createElement('pre');
|
||
preElement.style.cssText = 'margin: 0; font-family: "Consolas", "Monaco", "Courier New", monospace; font-size: 13px; white-space: pre-wrap; word-wrap: break-word;';
|
||
preElement.textContent = outputContent.textContent;
|
||
outputContent.innerHTML = '';
|
||
outputContent.appendChild(preElement);
|
||
}
|
||
}
|
||
|
||
resolve();
|
||
return;
|
||
}
|
||
} catch (e) {
|
||
console.error('Error parsing stream data:', e);
|
||
}
|
||
}
|
||
}
|
||
|
||
return readStream();
|
||
});
|
||
}
|
||
|
||
return readStream();
|
||
})
|
||
.catch(error => {
|
||
console.error('Error streaming subtask:', error);
|
||
outputContent.textContent = `连接错误: ${error.message}`;
|
||
statusIcon.textContent = '❌';
|
||
if (streamBtn) {
|
||
streamBtn.disabled = false;
|
||
streamBtn.textContent = '重试';
|
||
}
|
||
reject(error);
|
||
});
|
||
});
|
||
}
|
||
|
||
// Update stage visual status
|
||
function updateStageVisualStatus(stageElement, status) {
|
||
const statusIcon = stageElement.querySelector('.stage-status-icon');
|
||
const progressBar = stageElement.querySelector('.stage-progress-bar');
|
||
|
||
if (status === 'running') {
|
||
if (statusIcon) statusIcon.textContent = '🔄';
|
||
if (progressBar) progressBar.style.width = '50%';
|
||
stageElement.style.borderLeft = '4px solid #f39c12';
|
||
} else if (status === 'completed') {
|
||
if (statusIcon) statusIcon.textContent = '✅';
|
||
if (progressBar) progressBar.style.width = '100%';
|
||
stageElement.style.borderLeft = '4px solid #27ae60';
|
||
}
|
||
}
|
||
|
||
|
||
let currentView = 'outline';
|
||
let mindmapData = null;
|
||
let selectedNode = null;
|
||
let editingNode = null;
|
||
let mindmapSvg = null;
|
||
let mindmapZoom = null;
|
||
|
||
// 切换大纲视图(JSON编辑器 vs 大纲视图)
|
||
function switchOutlineView(viewType) {
|
||
const jsonView = document.getElementById('json-view');
|
||
const outlineView = document.getElementById('outline-view');
|
||
const jsonBtn = document.getElementById('json-view-btn');
|
||
const outlineBtn = document.getElementById('outline-view-btn');
|
||
|
||
if (viewType === 'json') {
|
||
// 切换到JSON视图
|
||
if (jsonView) jsonView.style.display = 'block';
|
||
if (outlineView) outlineView.style.display = 'none';
|
||
|
||
// 更新按钮样式
|
||
if (jsonBtn) {
|
||
jsonBtn.style.background = '#3498db';
|
||
jsonBtn.style.color = 'white';
|
||
}
|
||
if (outlineBtn) {
|
||
outlineBtn.style.background = 'transparent';
|
||
outlineBtn.style.color = '#6c757d';
|
||
}
|
||
|
||
currentView = 'json';
|
||
} else if (viewType === 'outline') {
|
||
// 切换到大纲视图
|
||
if (jsonView) jsonView.style.display = 'none';
|
||
if (outlineView) outlineView.style.display = 'block';
|
||
|
||
// 更新按钮样式
|
||
if (outlineBtn) {
|
||
outlineBtn.style.background = '#3498db';
|
||
outlineBtn.style.color = 'white';
|
||
}
|
||
if (jsonBtn) {
|
||
jsonBtn.style.background = 'transparent';
|
||
jsonBtn.style.color = '#6c757d';
|
||
}
|
||
|
||
currentView = 'outline';
|
||
|
||
// 渲染大纲视图
|
||
renderOutlineView();
|
||
}
|
||
}
|
||
|
||
// 渲染大纲视图
|
||
function renderOutlineView() {
|
||
const outlineContent = getOutlineContent();
|
||
if (!outlineContent) {
|
||
console.log('No outline content available');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const parsedOutline = JSON.parse(outlineContent);
|
||
if (parsedOutline && parsedOutline.slides) {
|
||
renderOutlinePreview(parsedOutline);
|
||
}
|
||
} catch (e) {
|
||
console.log('Failed to parse outline content for view rendering');
|
||
}
|
||
}
|
||
|
||
// 获取大纲内容
|
||
function getOutlineContent() {
|
||
const outlineDisplay = document.getElementById('outline-content-display');
|
||
if (!outlineDisplay) return null;
|
||
|
||
const preElement = outlineDisplay.querySelector('pre');
|
||
if (preElement) {
|
||
let content = preElement.textContent || preElement.innerText || '';
|
||
return content.trim();
|
||
}
|
||
|
||
// 如果没有pre元素,从整个div获取内容并处理HTML实体
|
||
let content = outlineDisplay.textContent || outlineDisplay.innerText || '';
|
||
content = content.replace(/ /g, ' ').replace(/\s+/g, ' ');
|
||
return content.trim();
|
||
}
|
||
|
||
|
||
// ========== 大纲预览功能 ==========
|
||
|
||
let isDetailView = false;
|
||
let currentOutlineData = null;
|
||
|
||
// 切换大纲视图(简洁视图 vs 详细视图)
|
||
function toggleOutlineView() {
|
||
const compactView = document.getElementById('compactView');
|
||
const detailView = document.getElementById('detailView');
|
||
const toggleText = document.getElementById('viewToggleText');
|
||
|
||
if (isDetailView) {
|
||
compactView.style.display = 'block';
|
||
detailView.style.display = 'none';
|
||
toggleText.textContent = '详细视图';
|
||
isDetailView = false;
|
||
} else {
|
||
compactView.style.display = 'none';
|
||
detailView.style.display = 'block';
|
||
toggleText.textContent = '简洁视图';
|
||
isDetailView = true;
|
||
}
|
||
}
|
||
|
||
// 渲染大纲预览
|
||
function renderOutlinePreview(outlineData) {
|
||
if (!outlineData) return;
|
||
|
||
currentOutlineData = outlineData;
|
||
|
||
// 隐藏空状态,显示内容
|
||
const emptyDiv = document.getElementById('outline-empty');
|
||
const loadingDiv = document.getElementById('outline-loading');
|
||
if (emptyDiv) emptyDiv.style.display = 'none';
|
||
if (loadingDiv) loadingDiv.style.display = 'none';
|
||
|
||
// 渲染简洁视图
|
||
renderCompactView(outlineData);
|
||
|
||
// 渲染详细视图
|
||
renderDetailView(outlineData);
|
||
}
|
||
|
||
// 渲染简洁视图
|
||
function renderCompactView(outlineData) {
|
||
const container = document.getElementById('outline-slides-compact');
|
||
if (!container || !outlineData.slides) return;
|
||
|
||
container.innerHTML = '';
|
||
|
||
outlineData.slides.forEach((slide, index) => {
|
||
const slideCard = document.createElement('div');
|
||
slideCard.style.cssText = `
|
||
padding: 15px; background: white; border-radius: 8px;
|
||
border-left: 4px solid #3498db; transition: all 0.3s ease;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1); position: relative;
|
||
cursor: pointer;
|
||
`;
|
||
|
||
slideCard.onmouseover = function() {
|
||
this.style.transform = 'translateY(-2px)';
|
||
this.style.boxShadow = '0 4px 8px rgba(0,0,0,0.15)';
|
||
};
|
||
slideCard.onmouseout = function() {
|
||
this.style.transform = 'translateY(0)';
|
||
this.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
|
||
};
|
||
|
||
const contentPoints = slide.content_points || slide.content || [];
|
||
const firstPoint = Array.isArray(contentPoints) ? contentPoints[0] : contentPoints;
|
||
const remainingCount = Array.isArray(contentPoints) ? Math.max(0, contentPoints.length - 1) : 0;
|
||
|
||
slideCard.innerHTML = `
|
||
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
||
<span style="background: #3498db; color: white; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; font-size: 0.8em; font-weight: bold; margin-right: 10px;">
|
||
${slide.page_number || index + 1}
|
||
</span>
|
||
<strong style="color: #2c3e50; font-size: 0.9em;">${slide.title || '未命名幻灯片'}</strong>
|
||
</div>
|
||
${slide.subtitle ? `<p style="color: #7f8c8d; font-size: 0.8em; margin: 5px 0; font-style: italic;">${slide.subtitle}</p>` : ''}
|
||
<div style="color: #666; font-size: 0.8em; line-height: 1.4;">
|
||
${firstPoint ? (typeof firstPoint === 'string' ? firstPoint.substring(0, 80) + (firstPoint.length > 80 ? '...' : '') : JSON.stringify(firstPoint).substring(0, 80) + '...') : '<span style="color: #95a5a6;">暂无内容</span>'}
|
||
${remainingCount > 0 ? `<br><span style="color: #95a5a6;">+${remainingCount} 个要点</span>` : ''}
|
||
</div>
|
||
`;
|
||
|
||
slideCard.onclick = () => viewSlideDetail(index);
|
||
container.appendChild(slideCard);
|
||
});
|
||
}
|
||
|
||
// 渲染详细视图
|
||
function renderDetailView(outlineData) {
|
||
const container = document.getElementById('outline-slides-detail');
|
||
if (!container || !outlineData.slides) return;
|
||
|
||
container.innerHTML = '';
|
||
|
||
outlineData.slides.forEach((slide, index) => {
|
||
const slideDiv = document.createElement('div');
|
||
slideDiv.style.cssText = `
|
||
padding: 20px; margin-bottom: 15px; background: #f8f9fa;
|
||
border-radius: 10px; border-left: 4px solid #3498db; position: relative;
|
||
`;
|
||
|
||
const contentPoints = slide.content_points || slide.content || [];
|
||
let contentHtml = '';
|
||
|
||
if (Array.isArray(contentPoints) && contentPoints.length > 0) {
|
||
contentHtml = `
|
||
<div style="margin-top: 10px;">
|
||
<h5 style="color: #555; margin-bottom: 8px; font-size: 0.9em;">内容要点:</h5>
|
||
<ul style="margin: 0; padding-left: 20px; color: #555; line-height: 1.6;">
|
||
${contentPoints.map(point => `<li style="margin-bottom: 5px;">${point}</li>`).join('')}
|
||
</ul>
|
||
</div>
|
||
`;
|
||
} else if (contentPoints) {
|
||
contentHtml = `
|
||
<div style="margin-top: 10px;">
|
||
<h5 style="color: #555; margin-bottom: 8px; font-size: 0.9em;">内容:</h5>
|
||
<div style="background: white; padding: 15px; border-radius: 6px; color: #555; line-height: 1.6; white-space: pre-wrap;">${contentPoints}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
slideDiv.innerHTML = `
|
||
<div style="display: flex; align-items: center; margin-bottom: 15px;">
|
||
<span style="background: #3498db; color: white; border-radius: 50%; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; font-weight: bold; margin-right: 15px;">
|
||
${slide.page_number || index + 1}
|
||
</span>
|
||
<div style="flex: 1;">
|
||
<strong style="color: #2c3e50; font-size: 1.1em;">${slide.title || '未命名幻灯片'}</strong>
|
||
${slide.subtitle ? `<br><em style="color: #7f8c8d; font-size: 0.9em;">${slide.subtitle}</em>` : ''}
|
||
</div>
|
||
</div>
|
||
${contentHtml}
|
||
${slide.slide_type ? `<div style="margin-top: 10px;"><span style="background: #e8f4fd; color: #3498db; padding: 4px 8px; border-radius: 4px; font-size: 0.8em;">类型: ${slide.slide_type}</span></div>` : ''}
|
||
`;
|
||
|
||
container.appendChild(slideDiv);
|
||
});
|
||
}
|
||
|
||
// 查看幻灯片详情
|
||
function viewSlideDetail(slideIndex) {
|
||
if (!currentOutlineData || !currentOutlineData.slides || !currentOutlineData.slides[slideIndex]) return;
|
||
|
||
const slide = currentOutlineData.slides[slideIndex];
|
||
|
||
// 创建模态框显示详细信息
|
||
const modal = document.createElement('div');
|
||
modal.style.cssText = `
|
||
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||
background: rgba(0,0,0,0.5); z-index: 1000; display: flex;
|
||
align-items: center; justify-content: center; padding: 20px;
|
||
`;
|
||
|
||
const content = document.createElement('div');
|
||
content.style.cssText = `
|
||
background: white; border-radius: 15px; padding: 30px;
|
||
max-width: 600px; max-height: 80vh; overflow-y: auto;
|
||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||
`;
|
||
|
||
const contentPoints = slide.content_points || slide.content || [];
|
||
let contentHtml = '';
|
||
|
||
if (Array.isArray(contentPoints) && contentPoints.length > 0) {
|
||
contentHtml = `
|
||
<h5 style="color: #555; margin: 15px 0 8px 0;">内容要点:</h5>
|
||
<ul style="margin: 0; padding-left: 20px; color: #555; line-height: 1.6;">
|
||
${contentPoints.map(point => `<li style="margin-bottom: 5px;">${point}</li>`).join('')}
|
||
</ul>
|
||
`;
|
||
} else if (contentPoints) {
|
||
contentHtml = `
|
||
<h5 style="color: #555; margin: 15px 0 8px 0;">内容:</h5>
|
||
<div style="background: #f8f9fa; padding: 15px; border-radius: 6px; color: #555; line-height: 1.6; white-space: pre-wrap;">${contentPoints}</div>
|
||
`;
|
||
}
|
||
|
||
content.innerHTML = `
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||
<h3 style="color: #2c3e50; margin: 0;">第${slide.page_number || slideIndex + 1}页详情</h3>
|
||
<button onclick="this.closest('.modal').remove()" style="background: #e74c3c; color: white; border: none; border-radius: 50%; width: 30px; height: 30px; cursor: pointer; font-size: 16px;">×</button>
|
||
</div>
|
||
<h4 style="color: #3498db; margin-bottom: 10px;">${slide.title || '未命名幻灯片'}</h4>
|
||
${slide.subtitle ? `<p style="color: #7f8c8d; font-style: italic; margin-bottom: 15px;">${slide.subtitle}</p>` : ''}
|
||
${contentHtml}
|
||
${slide.slide_type ? `<div style="margin-top: 15px;"><span style="background: #e8f4fd; color: #3498db; padding: 6px 12px; border-radius: 6px; font-size: 0.9em;">类型: ${slide.slide_type}</span></div>` : ''}
|
||
`;
|
||
|
||
modal.className = 'modal';
|
||
modal.appendChild(content);
|
||
document.body.appendChild(modal);
|
||
|
||
// 点击背景关闭
|
||
modal.addEventListener('click', (e) => {
|
||
if (e.target === modal) {
|
||
modal.remove();
|
||
}
|
||
});
|
||
}
|
||
|
||
// 编辑大纲内容
|
||
function editOutlineContent() {
|
||
// 获取当前大纲内容
|
||
let outlineContent = '';
|
||
if (currentOutlineData) {
|
||
outlineContent = JSON.stringify(currentOutlineData, null, 2);
|
||
} else {
|
||
// 尝试从显示区域获取内容
|
||
const outlineDisplay = document.getElementById('outline-content-display');
|
||
if (outlineDisplay) {
|
||
outlineContent = outlineDisplay.textContent || outlineDisplay.innerText || '';
|
||
}
|
||
}
|
||
|
||
// 创建编辑模态框
|
||
const modal = document.createElement('div');
|
||
modal.style.cssText = `
|
||
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||
background: rgba(0,0,0,0.5); z-index: 1000; display: flex;
|
||
align-items: center; justify-content: center; padding: 20px;
|
||
`;
|
||
|
||
const content = document.createElement('div');
|
||
content.style.cssText = `
|
||
background: white; border-radius: 15px; padding: 30px;
|
||
width: 90%; max-width: 800px; height: 80vh; display: flex;
|
||
flex-direction: column; box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||
`;
|
||
|
||
content.innerHTML = `
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||
<h3 style="color: #2c3e50; margin: 0;">编辑PPT大纲</h3>
|
||
<button onclick="this.closest('.modal').remove()" style="background: #e74c3c; color: white; border: none; border-radius: 50%; width: 30px; height: 30px; cursor: pointer; font-size: 16px;">×</button>
|
||
</div>
|
||
<div style="flex: 1; display: flex; flex-direction: column;">
|
||
<textarea id="outlineEditor" style="flex: 1; border: 2px solid #ecf0f1; border-radius: 8px; padding: 15px; font-family: 'Courier New', monospace; font-size: 14px; line-height: 1.5; resize: none;" placeholder="编辑大纲JSON...">${outlineContent}</textarea>
|
||
<div style="display: flex; gap: 8px; margin-top: 15px; justify-content: flex-end;">
|
||
<button onclick="this.closest('.modal').remove()" class="btn btn-sm" style="background: #95a5a6; color: white;">取消</button>
|
||
<button onclick="saveOutlineChanges()" class="btn btn-sm btn-primary">保存修改</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
modal.className = 'modal';
|
||
modal.appendChild(content);
|
||
document.body.appendChild(modal);
|
||
}
|
||
|
||
// 保存大纲修改
|
||
function saveOutlineChanges() {
|
||
const editor = document.getElementById('outlineEditor');
|
||
if (!editor) return;
|
||
|
||
try {
|
||
const newOutline = JSON.parse(editor.value);
|
||
|
||
// 更新当前大纲数据
|
||
currentOutlineData = newOutline;
|
||
|
||
// 重新渲染预览
|
||
renderOutlinePreview(newOutline);
|
||
|
||
// 更新JSON显示区域
|
||
const outlineDisplay = document.getElementById('outline-content-display');
|
||
if (outlineDisplay) {
|
||
const formattedContent = JSON.stringify(newOutline, null, 2);
|
||
const preElement = document.createElement('pre');
|
||
preElement.style.cssText = 'margin: 0; font-family: "Consolas", "Monaco", "Courier New", monospace; font-size: 13px; white-space: pre-wrap; word-wrap: break-word;';
|
||
preElement.textContent = formattedContent;
|
||
outlineDisplay.innerHTML = '';
|
||
outlineDisplay.appendChild(preElement);
|
||
}
|
||
|
||
// 保存到服务器
|
||
fetch(`/projects/${currentProjectId}/update-outline`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
outline_content: JSON.stringify(newOutline, null, 2)
|
||
})
|
||
}).then(response => {
|
||
if (response.ok) {
|
||
alert('大纲修改成功!');
|
||
// 关闭模态框
|
||
document.querySelector('.modal').remove();
|
||
|
||
// 立即重新渲染大纲视图,使用最新数据
|
||
setTimeout(() => {
|
||
// 如果当前是大纲视图,重新渲染
|
||
if (currentView === 'outline') {
|
||
renderOutlineView();
|
||
}
|
||
|
||
// 如果当前是新的大纲视图,使用最新数据重新渲染
|
||
if (currentViewNew === 'outline') {
|
||
renderOutlineViewNewWithData(newOutline);
|
||
}
|
||
}, 100); // 减少延迟
|
||
} else {
|
||
alert('保存失败,请重试');
|
||
}
|
||
}).catch(error => {
|
||
console.error('Error saving outline:', error);
|
||
alert('保存失败: ' + error.message);
|
||
});
|
||
|
||
} catch (error) {
|
||
if (error instanceof SyntaxError) {
|
||
alert('JSON格式错误,请检查语法!');
|
||
} else {
|
||
console.error('Error saving outline:', error);
|
||
alert('保存失败: ' + error.message);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 导出大纲为JSON文件
|
||
function exportOutlineJSON() {
|
||
if (!currentOutlineData) {
|
||
alert('大纲数据不存在,无法导出');
|
||
return;
|
||
}
|
||
|
||
const dataStr = JSON.stringify(currentOutlineData, null, 2);
|
||
const dataBlob = new Blob([dataStr], {type: 'application/json'});
|
||
|
||
const link = document.createElement('a');
|
||
link.href = URL.createObjectURL(dataBlob);
|
||
link.download = `${(currentOutlineData.title || 'ppt_outline')}_${new Date().toISOString().split('T')[0]}.json`;
|
||
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
|
||
console.log('大纲JSON文件已导出');
|
||
}
|
||
|
||
|
||
// 大纲视图模式切换(简洁视图 vs 详细视图)
|
||
function toggleOutlineViewMode() {
|
||
isDetailView = !isDetailView;
|
||
const toggleText = document.getElementById('viewToggleText');
|
||
|
||
if (toggleText) {
|
||
toggleText.textContent = isDetailView ? '简洁视图' : '详细视图';
|
||
}
|
||
|
||
// 重新渲染大纲视图
|
||
renderOutlineView();
|
||
}
|
||
|
||
|
||
|
||
// 更新JSON编辑器内容
|
||
function updateJsonEditor(jsonString) {
|
||
const contentDiv = document.getElementById('outline-content-display');
|
||
if (contentDiv) {
|
||
// 尝试格式化JSON
|
||
let formattedContent;
|
||
let parsedOutline = null;
|
||
try {
|
||
parsedOutline = JSON.parse(jsonString);
|
||
formattedContent = JSON.stringify(parsedOutline, null, 2);
|
||
} catch (e) {
|
||
formattedContent = jsonString;
|
||
}
|
||
|
||
// 创建pre元素来正确显示JSON
|
||
const preElement = document.createElement('pre');
|
||
preElement.style.cssText = 'margin: 0; font-family: "Consolas", "Monaco", "Courier New", monospace; font-size: 13px; white-space: pre-wrap; word-wrap: break-word;';
|
||
preElement.textContent = formattedContent;
|
||
contentDiv.innerHTML = '';
|
||
contentDiv.appendChild(preElement);
|
||
|
||
// 如果当前是大纲视图,立即更新大纲显示
|
||
if (parsedOutline && currentViewNew === 'outline') {
|
||
setTimeout(() => {
|
||
renderOutlineViewNewWithData(parsedOutline);
|
||
}, 100);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 保存大纲到服务器
|
||
async function saveOutlineToServer(jsonString) {
|
||
try {
|
||
const response = await fetch(`/projects/${currentProjectId}/update-outline`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
outline_content: jsonString
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('保存到服务器失败');
|
||
}
|
||
|
||
console.log('大纲已保存到服务器');
|
||
} catch (error) {
|
||
console.error('保存到服务器失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// ========== 辅助函数 ==========
|
||
|
||
// 隐藏右键菜单
|
||
function hideContextMenu() {
|
||
const contextMenu = document.getElementById('mindmap-context-menu');
|
||
if (contextMenu) {
|
||
contextMenu.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// ========== 事件监听器 ==========
|
||
|
||
// 页面加载完成后初始化
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// 隐藏右键菜单当点击其他地方时
|
||
document.addEventListener('click', function(event) {
|
||
if (!event.target.closest('#mindmap-context-menu')) {
|
||
hideContextMenu();
|
||
}
|
||
});
|
||
|
||
// 键盘事件处理
|
||
document.addEventListener('keydown', function(event) {
|
||
// ESC键取消编辑
|
||
if (event.key === 'Escape') {
|
||
if (document.getElementById('node-edit-modal').style.display === 'block') {
|
||
cancelNodeEdit();
|
||
}
|
||
hideContextMenu();
|
||
clearNodeSelection();
|
||
}
|
||
|
||
// Enter键保存编辑
|
||
if (event.key === 'Enter' && document.getElementById('node-edit-modal').style.display === 'block') {
|
||
saveNodeEdit();
|
||
}
|
||
});
|
||
|
||
// 页面加载时检查是否有现有的大纲数据
|
||
setTimeout(function() {
|
||
const outlineDisplay = document.getElementById('outline-content-display');
|
||
if (outlineDisplay) {
|
||
const content = outlineDisplay.textContent || outlineDisplay.innerText || '';
|
||
if (content.trim()) {
|
||
try {
|
||
const parsedOutline = JSON.parse(content);
|
||
if (parsedOutline && parsedOutline.slides) {
|
||
// 渲染大纲预览
|
||
renderOutlinePreview(parsedOutline);
|
||
|
||
// 如果当前是大纲视图,渲染大纲内容
|
||
if (currentView === 'outline') {
|
||
renderOutlineView();
|
||
}
|
||
|
||
// 如果当前是新的大纲视图,渲染新的大纲内容
|
||
if (currentViewNew === 'outline') {
|
||
renderOutlineViewNew();
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.log('现有内容不是有效的JSON格式');
|
||
}
|
||
}
|
||
}
|
||
}, 500);
|
||
});
|
||
|
||
// ========== 全屏功能 ==========
|
||
|
||
let isFullscreen = false;
|
||
let originalMindmapData = null;
|
||
|
||
|
||
|
||
|
||
// New view switching functions for the new outline section
|
||
let currentViewNew = 'json'; // Default to JSON view for better compatibility
|
||
let currentEditInput = null; // Track current editing input to prevent duplicates
|
||
|
||
function switchOutlineViewNew(view) {
|
||
console.log('Switching to view:', view);
|
||
|
||
const jsonViewBtn = document.getElementById('json-view-btn-new');
|
||
const outlineViewBtn = document.getElementById('outline-view-btn-new');
|
||
const jsonView = document.getElementById('outline-content-display');
|
||
const outlineView = document.getElementById('outline-view-new');
|
||
const actionsDiv = document.getElementById('outline-actions');
|
||
|
||
if (!jsonViewBtn || !outlineViewBtn || !jsonView || !outlineView) {
|
||
console.error('View elements not found');
|
||
return;
|
||
}
|
||
|
||
currentViewNew = view;
|
||
|
||
if (view === 'json') {
|
||
// Show JSON view
|
||
jsonView.style.display = 'block';
|
||
outlineView.style.display = 'none';
|
||
|
||
// Show action buttons in JSON view
|
||
if (actionsDiv) {
|
||
actionsDiv.style.display = 'block';
|
||
}
|
||
|
||
// Update button styles
|
||
jsonViewBtn.style.background = '#3498db';
|
||
jsonViewBtn.style.color = 'white';
|
||
outlineViewBtn.style.background = 'transparent';
|
||
outlineViewBtn.style.color = '#6c757d';
|
||
} else if (view === 'outline') {
|
||
// Show outline view
|
||
jsonView.style.display = 'none';
|
||
outlineView.style.display = 'block';
|
||
|
||
// Hide action buttons in outline view
|
||
if (actionsDiv) {
|
||
actionsDiv.style.display = 'none';
|
||
}
|
||
|
||
// Update button styles
|
||
outlineViewBtn.style.background = '#3498db';
|
||
outlineViewBtn.style.color = 'white';
|
||
jsonViewBtn.style.background = 'transparent';
|
||
jsonViewBtn.style.color = '#6c757d';
|
||
|
||
// Generate outline view if content exists
|
||
renderOutlineViewNew();
|
||
}
|
||
}
|
||
|
||
// 渲染新的大纲视图
|
||
function renderOutlineViewNew() {
|
||
let outlineContent = getOutlineContentNew();
|
||
const outlineContainer = document.getElementById('outline-content-new');
|
||
const loadingDiv = document.getElementById('outline-loading-new');
|
||
const errorDiv = document.getElementById('outline-error-new');
|
||
const emptyDiv = document.getElementById('outline-empty-new');
|
||
|
||
if (!outlineContainer) {
|
||
console.error('Outline container not found');
|
||
return;
|
||
}
|
||
|
||
// 显示加载状态
|
||
if (loadingDiv) loadingDiv.style.display = 'block';
|
||
if (errorDiv) errorDiv.style.display = 'none';
|
||
if (emptyDiv) emptyDiv.style.display = 'none';
|
||
|
||
try {
|
||
if (!outlineContent) {
|
||
// 没有内容,显示空状态
|
||
if (loadingDiv) loadingDiv.style.display = 'none';
|
||
if (emptyDiv) emptyDiv.style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
// 额外的内容清理,确保JSON格式正确
|
||
outlineContent = outlineContent.trim();
|
||
|
||
// 如果内容不是以{开头,尝试查找JSON开始位置
|
||
if (!outlineContent.startsWith('{')) {
|
||
const jsonStart = outlineContent.indexOf('{');
|
||
if (jsonStart > 0) {
|
||
outlineContent = outlineContent.substring(jsonStart);
|
||
console.log('从位置', jsonStart, '开始提取JSON内容');
|
||
}
|
||
}
|
||
|
||
// 如果内容不是以}结尾,尝试查找JSON结束位置
|
||
if (!outlineContent.endsWith('}')) {
|
||
const jsonEnd = outlineContent.lastIndexOf('}');
|
||
if (jsonEnd > 0) {
|
||
outlineContent = outlineContent.substring(0, jsonEnd + 1);
|
||
console.log('截取到位置', jsonEnd + 1, '的JSON内容');
|
||
}
|
||
}
|
||
|
||
let parsedOutline;
|
||
try {
|
||
parsedOutline = JSON.parse(outlineContent);
|
||
} catch (parseError) {
|
||
console.error('JSON解析失败:', parseError);
|
||
if (loadingDiv) loadingDiv.style.display = 'none';
|
||
if (errorDiv) errorDiv.style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
// 使用解析后的数据渲染
|
||
renderOutlineViewNewWithData(parsedOutline);
|
||
|
||
} catch (error) {
|
||
console.error('渲染大纲视图时出错:', error);
|
||
if (loadingDiv) loadingDiv.style.display = 'none';
|
||
if (errorDiv) errorDiv.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
// 使用指定数据渲染新的大纲视图
|
||
function renderOutlineViewNewWithData(parsedOutline) {
|
||
const outlineContainer = document.getElementById('outline-content-new');
|
||
const loadingDiv = document.getElementById('outline-loading-new');
|
||
const errorDiv = document.getElementById('outline-error-new');
|
||
const emptyDiv = document.getElementById('outline-empty-new');
|
||
|
||
if (!outlineContainer) {
|
||
console.error('Outline container not found');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (!parsedOutline || !parsedOutline.slides) {
|
||
// 没有有效数据,显示空状态
|
||
if (loadingDiv) loadingDiv.style.display = 'none';
|
||
if (errorDiv) errorDiv.style.display = 'none';
|
||
if (emptyDiv) emptyDiv.style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
if (!parsedOutline.slides || parsedOutline.slides.length === 0) {
|
||
// 没有slides数据,显示空状态
|
||
if (loadingDiv) loadingDiv.style.display = 'none';
|
||
if (emptyDiv) emptyDiv.style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
// 渲染大纲内容
|
||
renderOutlineContentNew(parsedOutline, outlineContainer);
|
||
|
||
// 隐藏加载状态
|
||
if (loadingDiv) loadingDiv.style.display = 'none';
|
||
|
||
} catch (error) {
|
||
console.error('Error rendering outline view with data:', error);
|
||
if (loadingDiv) loadingDiv.style.display = 'none';
|
||
if (errorDiv) {
|
||
errorDiv.style.display = 'block';
|
||
const errorP = errorDiv.querySelector('p');
|
||
if (errorP) {
|
||
errorP.textContent = '大纲渲染失败:' + error.message;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 大纲视图模式切换(简洁视图 vs 详细视图)
|
||
let isDetailViewNew = false;
|
||
|
||
function toggleOutlineViewModeNew() {
|
||
isDetailViewNew = !isDetailViewNew;
|
||
const toggleText = document.getElementById('viewToggleTextNew');
|
||
|
||
if (toggleText) {
|
||
toggleText.textContent = isDetailViewNew ? '简洁视图' : '详细视图';
|
||
}
|
||
|
||
// 重新渲染大纲视图
|
||
renderOutlineViewNew();
|
||
}
|
||
|
||
// 渲染大纲内容
|
||
function renderOutlineContentNew(outline, container) {
|
||
if (!outline || !outline.slides || !container) {
|
||
return;
|
||
}
|
||
|
||
// 清空容器
|
||
container.innerHTML = '';
|
||
|
||
// 创建标题区域
|
||
const titleSection = document.createElement('div');
|
||
titleSection.style.cssText = 'text-align: center; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 2px solid #ecf0f1;';
|
||
titleSection.innerHTML = `
|
||
<h4 style="color: #3498db; margin-bottom: 10px;">${outline.title || '未命名大纲'}</h4>
|
||
<p style="color: #7f8c8d; margin: 0;">
|
||
总共 ${outline.slides.length} 页幻灯片 |
|
||
场景: ${outline.metadata?.scenario || '未指定'} |
|
||
语言: ${outline.metadata?.language || 'zh'}
|
||
</p>
|
||
`;
|
||
container.appendChild(titleSection);
|
||
|
||
// 创建大纲内容区域
|
||
const contentSection = document.createElement('div');
|
||
contentSection.style.cssText = 'background: #f8f9fa; border-radius: 10px; padding: 20px;';
|
||
|
||
if (isDetailViewNew) {
|
||
// 详细视图
|
||
renderDetailedOutlineView(outline.slides, contentSection);
|
||
} else {
|
||
// 简洁视图
|
||
renderCompactOutlineView(outline.slides, contentSection);
|
||
}
|
||
|
||
container.appendChild(contentSection);
|
||
}
|
||
|
||
// 渲染简洁视图
|
||
function renderCompactOutlineView(slides, container) {
|
||
const gridContainer = document.createElement('div');
|
||
gridContainer.style.cssText = 'display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px;';
|
||
|
||
slides.forEach((slide, index) => {
|
||
const slideCard = document.createElement('div');
|
||
slideCard.style.cssText = `
|
||
padding: 15px; background: white; border-radius: 8px;
|
||
border-left: 4px solid #3498db; transition: all 0.3s ease;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1); position: relative; cursor: pointer;
|
||
`;
|
||
|
||
// 添加悬停效果
|
||
slideCard.addEventListener('mouseenter', function() {
|
||
this.style.transform = 'translateY(-2px)';
|
||
this.style.boxShadow = '0 4px 8px rgba(0,0,0,0.15)';
|
||
});
|
||
slideCard.addEventListener('mouseleave', function() {
|
||
this.style.transform = 'translateY(0)';
|
||
this.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
|
||
});
|
||
|
||
// 操作按钮
|
||
const actionButtons = document.createElement('div');
|
||
actionButtons.style.cssText = 'position: absolute; top: 10px; right: 10px; display: flex; gap: 5px;';
|
||
actionButtons.innerHTML = `
|
||
<button onclick="editSingleSlideNew(${index})" class="btn btn-primary" style="font-size: 0.7em; padding: 4px 8px; border-radius: 4px;" title="编辑此页">
|
||
<i class="fas fa-edit"></i>
|
||
</button>
|
||
<button onclick="viewSlideDetailNew(${index})" class="btn btn-info" style="font-size: 0.7em; padding: 4px 8px; border-radius: 4px;" title="查看详情">
|
||
<i class="fas fa-eye"></i>
|
||
</button>
|
||
`;
|
||
|
||
// 内容区域
|
||
const contentArea = document.createElement('div');
|
||
contentArea.style.cssText = 'margin-right: 60px;';
|
||
contentArea.addEventListener('click', () => viewSlideDetailNew(index));
|
||
|
||
const header = document.createElement('div');
|
||
header.style.cssText = 'display: flex; align-items: center; margin-bottom: 8px;';
|
||
header.innerHTML = `
|
||
<span style="background: #3498db; color: white; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; font-size: 0.8em; font-weight: bold; margin-right: 10px;">
|
||
${slide.page_number || index + 1}
|
||
</span>
|
||
<strong style="color: #2c3e50; font-size: 0.9em;">${slide.title || '未命名幻灯片'}</strong>
|
||
`;
|
||
|
||
const subtitle = document.createElement('div');
|
||
if (slide.subtitle) {
|
||
subtitle.innerHTML = `<p style="color: #7f8c8d; font-size: 0.8em; margin: 5px 0; font-style: italic;">${slide.subtitle}</p>`;
|
||
}
|
||
|
||
const content = document.createElement('div');
|
||
content.style.cssText = 'color: #666; font-size: 0.8em; line-height: 1.4;';
|
||
|
||
if (slide.content_points && slide.content_points.length > 0) {
|
||
const firstPoint = slide.content_points[0];
|
||
const truncatedPoint = firstPoint.length > 80 ? firstPoint.substring(0, 80) + '...' : firstPoint;
|
||
content.innerHTML = truncatedPoint;
|
||
|
||
if (slide.content_points.length > 1) {
|
||
content.innerHTML += `<br><span style="color: #95a5a6;">+${slide.content_points.length - 1} 个要点</span>`;
|
||
}
|
||
} else if (slide.content) {
|
||
const truncatedContent = slide.content.length > 80 ? slide.content.substring(0, 80) + '...' : slide.content;
|
||
content.innerHTML = truncatedContent;
|
||
} else {
|
||
content.innerHTML = '<span style="color: #95a5a6;">暂无内容</span>';
|
||
}
|
||
|
||
contentArea.appendChild(header);
|
||
contentArea.appendChild(subtitle);
|
||
contentArea.appendChild(content);
|
||
|
||
slideCard.appendChild(actionButtons);
|
||
slideCard.appendChild(contentArea);
|
||
gridContainer.appendChild(slideCard);
|
||
});
|
||
|
||
container.appendChild(gridContainer);
|
||
}
|
||
|
||
// 渲染详细视图
|
||
function renderDetailedOutlineView(slides, container) {
|
||
const detailContainer = document.createElement('div');
|
||
detailContainer.style.cssText = 'max-height: 400px; overflow-y: auto;';
|
||
|
||
slides.forEach((slide, index) => {
|
||
const slideDetail = document.createElement('div');
|
||
slideDetail.style.cssText = `
|
||
padding: 20px; margin-bottom: 15px; background: white;
|
||
border-radius: 10px; border-left: 4px solid #3498db; position: relative;
|
||
`;
|
||
|
||
const header = document.createElement('div');
|
||
header.style.cssText = 'display: flex; align-items: center; margin-bottom: 15px;';
|
||
header.innerHTML = `
|
||
<span style="background: #3498db; color: white; border-radius: 50%; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; font-weight: bold; margin-right: 15px;">
|
||
${slide.page_number || index + 1}
|
||
</span>
|
||
<div style="flex: 1;">
|
||
<strong style="color: #2c3e50; font-size: 1.1em;">${slide.title || '未命名幻灯片'}</strong>
|
||
${slide.subtitle ? `<br><em style="color: #7f8c8d; font-size: 0.9em;">${slide.subtitle}</em>` : ''}
|
||
</div>
|
||
<button onclick="editSingleSlideNew(${index})" class="btn btn-primary" style="font-size: 0.8em; padding: 6px 12px;" title="编辑此页">
|
||
<i class="fas fa-edit"></i> 编辑
|
||
</button>
|
||
`;
|
||
|
||
const contentSection = document.createElement('div');
|
||
|
||
if (slide.content_points && slide.content_points.length > 0) {
|
||
const pointsSection = document.createElement('div');
|
||
pointsSection.style.cssText = 'margin-top: 10px;';
|
||
pointsSection.innerHTML = '<h5 style="color: #555; margin-bottom: 8px; font-size: 0.9em;">内容要点:</h5>';
|
||
|
||
const pointsList = document.createElement('ul');
|
||
pointsList.style.cssText = 'margin: 0; padding-left: 20px; color: #555; line-height: 1.6;';
|
||
|
||
slide.content_points.forEach(point => {
|
||
const listItem = document.createElement('li');
|
||
listItem.style.cssText = 'margin-bottom: 5px;';
|
||
listItem.textContent = point;
|
||
pointsList.appendChild(listItem);
|
||
});
|
||
|
||
pointsSection.appendChild(pointsList);
|
||
contentSection.appendChild(pointsSection);
|
||
} else if (slide.content) {
|
||
const contentDiv = document.createElement('div');
|
||
contentDiv.style.cssText = 'margin-top: 10px;';
|
||
contentDiv.innerHTML = `
|
||
<h5 style="color: #555; margin-bottom: 8px; font-size: 0.9em;">内容:</h5>
|
||
<div style="background: #f8f9fa; padding: 15px; border-radius: 6px; color: #555; line-height: 1.6; white-space: pre-wrap;">${slide.content}</div>
|
||
`;
|
||
contentSection.appendChild(contentDiv);
|
||
}
|
||
|
||
if (slide.slide_type) {
|
||
const typeTag = document.createElement('div');
|
||
typeTag.style.cssText = 'margin-top: 10px;';
|
||
typeTag.innerHTML = `
|
||
<span style="background: #e8f4fd; color: #3498db; padding: 4px 8px; border-radius: 4px; font-size: 0.8em;">
|
||
类型: ${slide.slide_type}
|
||
</span>
|
||
`;
|
||
contentSection.appendChild(typeTag);
|
||
}
|
||
|
||
slideDetail.appendChild(header);
|
||
slideDetail.appendChild(contentSection);
|
||
detailContainer.appendChild(slideDetail);
|
||
});
|
||
|
||
container.appendChild(detailContainer);
|
||
}
|
||
|
||
// 查看幻灯片详情
|
||
function viewSlideDetailNew(slideIndex) {
|
||
const outlineContent = getOutlineContent();
|
||
if (!outlineContent) return;
|
||
|
||
try {
|
||
const parsedOutline = JSON.parse(outlineContent);
|
||
const slide = parsedOutline.slides[slideIndex];
|
||
|
||
if (!slide) return;
|
||
|
||
// 创建模态框显示详细信息
|
||
const modal = document.createElement('div');
|
||
modal.style.cssText = `
|
||
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||
background: rgba(0,0,0,0.5); z-index: 1000; display: flex;
|
||
align-items: center; justify-content: center; padding: 20px;
|
||
`;
|
||
|
||
const content = document.createElement('div');
|
||
content.style.cssText = `
|
||
background: white; border-radius: 15px; padding: 30px;
|
||
max-width: 600px; max-height: 80vh; overflow-y: auto;
|
||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||
`;
|
||
|
||
let contentPoints = '';
|
||
if (slide.content_points && slide.content_points.length > 0) {
|
||
contentPoints = '<h5 style="color: #555; margin: 15px 0 8px 0;">内容要点:</h5><ul style="margin: 0; padding-left: 20px; color: #555; line-height: 1.6;">';
|
||
slide.content_points.forEach(point => {
|
||
contentPoints += `<li style="margin-bottom: 5px;">${point}</li>`;
|
||
});
|
||
contentPoints += '</ul>';
|
||
} else if (slide.content) {
|
||
contentPoints = `<h5 style="color: #555; margin: 15px 0 8px 0;">内容:</h5><div style="background: #f8f9fa; padding: 15px; border-radius: 6px; color: #555; line-height: 1.6; white-space: pre-wrap;">${slide.content}</div>`;
|
||
}
|
||
|
||
content.innerHTML = `
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||
<h3 style="color: #2c3e50; margin: 0;">第${slide.page_number || slideIndex + 1}页详情</h3>
|
||
<button onclick="this.closest('.modal').remove()" style="background: #e74c3c; color: white; border: none; border-radius: 50%; width: 30px; height: 30px; cursor: pointer; font-size: 16px;">×</button>
|
||
</div>
|
||
<h4 style="color: #3498db; margin-bottom: 10px;">${slide.title || '未命名幻灯片'}</h4>
|
||
${slide.subtitle ? `<p style="color: #7f8c8d; font-style: italic; margin-bottom: 15px;">${slide.subtitle}</p>` : ''}
|
||
${contentPoints}
|
||
${slide.slide_type ? `<div style="margin-top: 15px;"><span style="background: #e8f4fd; color: #3498db; padding: 6px 12px; border-radius: 6px; font-size: 0.9em;">类型: ${slide.slide_type}</span></div>` : ''}
|
||
`;
|
||
|
||
modal.className = 'modal';
|
||
modal.appendChild(content);
|
||
document.body.appendChild(modal);
|
||
|
||
// 点击背景关闭
|
||
modal.addEventListener('click', (e) => {
|
||
if (e.target === modal) {
|
||
modal.remove();
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('Error viewing slide detail:', error);
|
||
alert('查看详情失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 编辑大纲
|
||
function editOutlineNew() {
|
||
const outlineContent = getOutlineContent();
|
||
if (!outlineContent) {
|
||
alert('大纲数据不存在,无法编辑');
|
||
return;
|
||
}
|
||
|
||
// 创建编辑模态框
|
||
const modal = document.createElement('div');
|
||
modal.style.cssText = `
|
||
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||
background: rgba(0,0,0,0.5); z-index: 1000; display: flex;
|
||
align-items: center; justify-content: center; padding: 20px;
|
||
`;
|
||
|
||
const content = document.createElement('div');
|
||
content.style.cssText = `
|
||
background: white; border-radius: 15px; padding: 30px;
|
||
width: 90%; max-width: 800px; height: 80vh; display: flex;
|
||
flex-direction: column; box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||
`;
|
||
|
||
content.innerHTML = `
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||
<h3 style="color: #2c3e50; margin: 0;">编辑PPT大纲</h3>
|
||
<button onclick="this.closest('.modal').remove()" style="background: #e74c3c; color: white; border: none; border-radius: 50%; width: 30px; height: 30px; cursor: pointer; font-size: 16px;">×</button>
|
||
</div>
|
||
<div style="flex: 1; display: flex; flex-direction: column;">
|
||
<textarea id="outlineEditorNew" style="flex: 1; border: 2px solid #ecf0f1; border-radius: 8px; padding: 15px; font-family: 'Courier New', monospace; font-size: 14px; line-height: 1.5; resize: none;" placeholder="编辑大纲JSON...">${outlineContent}</textarea>
|
||
<div style="display: flex; gap: 8px; margin-top: 15px; justify-content: flex-end;">
|
||
<button onclick="this.closest('.modal').remove()" class="btn btn-sm" style="background: #95a5a6; color: white;">取消</button>
|
||
<button onclick="saveOutlineChangesNew()" class="btn btn-sm btn-primary">保存修改</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
modal.className = 'modal';
|
||
modal.appendChild(content);
|
||
document.body.appendChild(modal);
|
||
}
|
||
|
||
// 保存大纲修改
|
||
async function saveOutlineChangesNew() {
|
||
const editor = document.getElementById('outlineEditorNew');
|
||
if (!editor) return;
|
||
|
||
try {
|
||
const newOutline = JSON.parse(editor.value);
|
||
|
||
const response = await fetch(`/projects/${currentProjectId}/update-outline`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ outline_content: editor.value })
|
||
});
|
||
|
||
if (response.ok) {
|
||
alert('大纲修改成功!');
|
||
// 关闭模态框
|
||
const modal = editor.closest('.modal');
|
||
if (modal) modal.remove();
|
||
|
||
// 更新JSON显示区域
|
||
const outlineDisplay = document.getElementById('outline-content-display');
|
||
if (outlineDisplay) {
|
||
const preElement = document.createElement('pre');
|
||
preElement.style.cssText = 'margin: 0; font-family: "Consolas", "Monaco", "Courier New", monospace; font-size: 13px; white-space: pre-wrap; word-wrap: break-word;';
|
||
preElement.textContent = editor.value;
|
||
outlineDisplay.innerHTML = '';
|
||
outlineDisplay.appendChild(preElement);
|
||
}
|
||
|
||
// 立即重新渲染大纲视图,使用最新数据
|
||
setTimeout(() => {
|
||
renderOutlineViewNewWithData(newOutline);
|
||
}, 100); // 减少延迟
|
||
} else {
|
||
const error = await response.json();
|
||
alert('保存失败: ' + (error.detail || '未知错误'));
|
||
}
|
||
} catch (error) {
|
||
if (error instanceof SyntaxError) {
|
||
alert('JSON格式错误,请检查语法!');
|
||
} else {
|
||
console.error('Error saving outline:', error);
|
||
alert('保存失败: ' + error.message);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 导出大纲为JSON文件
|
||
function exportOutlineJSONNew() {
|
||
const outlineContent = getOutlineContent();
|
||
if (!outlineContent) {
|
||
alert('大纲数据不存在,无法导出');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const parsedOutline = JSON.parse(outlineContent);
|
||
const dataStr = JSON.stringify(parsedOutline, null, 2);
|
||
const dataBlob = new Blob([dataStr], {type: 'application/json'});
|
||
|
||
const link = document.createElement('a');
|
||
link.href = URL.createObjectURL(dataBlob);
|
||
link.download = `${(parsedOutline.title || 'ppt_outline')}_${new Date().toISOString().split('T')[0]}.json`;
|
||
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
|
||
// 显示成功提示
|
||
const toast = document.createElement('div');
|
||
toast.style.cssText = `
|
||
position: fixed; top: 20px; right: 20px; background: #27ae60;
|
||
color: white; padding: 15px 20px; border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 1001;
|
||
font-size: 14px; font-weight: bold;
|
||
`;
|
||
toast.textContent = '✅ 大纲JSON文件已导出';
|
||
document.body.appendChild(toast);
|
||
|
||
setTimeout(() => {
|
||
toast.remove();
|
||
}, 3000);
|
||
} catch (error) {
|
||
console.error('Error exporting outline:', error);
|
||
alert('导出失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 编辑单页幻灯片
|
||
function editSingleSlideNew(slideIndex) {
|
||
const outlineContent = getOutlineContentNew();
|
||
if (!outlineContent) {
|
||
alert('大纲数据不存在,无法编辑');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const parsedOutline = JSON.parse(outlineContent);
|
||
const slide = parsedOutline.slides[slideIndex];
|
||
|
||
if (!slide) {
|
||
alert('幻灯片不存在');
|
||
return;
|
||
}
|
||
|
||
// 创建单页编辑模态框
|
||
const modal = document.createElement('div');
|
||
modal.style.cssText = `
|
||
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||
background: rgba(0,0,0,0.5); z-index: 1000; display: flex;
|
||
align-items: center; justify-content: center; padding: 20px;
|
||
`;
|
||
|
||
const content = document.createElement('div');
|
||
content.style.cssText = `
|
||
background: white; border-radius: 15px; padding: 30px;
|
||
width: 90%; max-width: 700px; max-height: 85vh; overflow-y: auto;
|
||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||
`;
|
||
|
||
// 构建内容要点的编辑界面 - 使用占位符,稍后用JavaScript创建
|
||
let contentPointsHtml = '';
|
||
if (slide.content_points && slide.content_points.length > 0) {
|
||
slide.content_points.forEach((point, index) => {
|
||
contentPointsHtml += `
|
||
<div class="content-point-item" data-point-index="${index}" style="display: flex; align-items: center; margin-bottom: 10px;">
|
||
<input type="text" class="content-point-input" data-index="${index}"
|
||
value="${point.replace(/"/g, '"')}"
|
||
style="flex: 1; padding: 8px 12px; border: 2px solid #ecf0f1; border-radius: 6px; font-size: 14px;">
|
||
<button class="remove-point-btn" data-remove-index="${index}"
|
||
style="background: #e74c3c; color: white; border: none; border-radius: 4px; padding: 6px 10px; margin-left: 8px; cursor: pointer;">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
`;
|
||
});
|
||
}
|
||
|
||
content.innerHTML = `
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px;">
|
||
<h3 style="color: #2c3e50; margin: 0;">编辑第${slide.page_number || slideIndex + 1}页</h3>
|
||
<button onclick="this.closest('.modal').remove()"
|
||
style="background: #e74c3c; color: white; border: none; border-radius: 50%; width: 30px; height: 30px; cursor: pointer; font-size: 16px;">×</button>
|
||
</div>
|
||
|
||
<form id="singleSlideEditForm" style="display: flex; flex-direction: column; gap: 20px;">
|
||
<!-- 页面标题 -->
|
||
<div>
|
||
<label style="display: block; color: #2c3e50; font-weight: bold; margin-bottom: 8px;">页面标题</label>
|
||
<input type="text" id="slideTitle" value="${(slide.title || '').replace(/"/g, '"')}"
|
||
style="width: 100%; padding: 12px; border: 2px solid #ecf0f1; border-radius: 8px; font-size: 16px;">
|
||
</div>
|
||
|
||
<!-- 页面副标题 -->
|
||
<div>
|
||
<label style="display: block; color: #2c3e50; font-weight: bold; margin-bottom: 8px;">副标题(可选)</label>
|
||
<input type="text" id="slideSubtitle" value="${(slide.subtitle || '').replace(/"/g, '"')}"
|
||
style="width: 100%; padding: 12px; border: 2px solid #ecf0f1; border-radius: 8px; font-size: 14px;">
|
||
</div>
|
||
|
||
<!-- 幻灯片类型 -->
|
||
<div>
|
||
<label style="display: block; color: #2c3e50; font-weight: bold; margin-bottom: 8px;">幻灯片类型</label>
|
||
<select id="slideType" style="width: 100%; padding: 12px; border: 2px solid #ecf0f1; border-radius: 8px; font-size: 14px;">
|
||
<option value="title" ${slide.slide_type === 'title' ? 'selected' : ''}>标题页</option>
|
||
<option value="content" ${slide.slide_type === 'content' ? 'selected' : ''}>内容页</option>
|
||
<option value="conclusion" ${slide.slide_type === 'conclusion' ? 'selected' : ''}>结论页</option>
|
||
<option value="thankyou" ${slide.slide_type === 'thankyou' ? 'selected' : ''}>感谢页</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 内容要点 -->
|
||
<div>
|
||
<label style="display: block; color: #2c3e50; font-weight: bold; margin-bottom: 8px;">内容要点</label>
|
||
<div id="contentPointsContainer" style="margin-bottom: 10px;">
|
||
${contentPointsHtml}
|
||
</div>
|
||
<button type="button" onclick="addSingleContentPoint()"
|
||
style="background: #3498db; color: white; border: none; border-radius: 6px; padding: 8px 16px; cursor: pointer;">
|
||
<i class="fas fa-plus"></i> 添加要点
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 页面描述 -->
|
||
<div>
|
||
<label style="display: block; color: #2c3e50; font-weight: bold; margin-bottom: 8px;">页面描述(可选)</label>
|
||
<textarea id="slideDescription" rows="3"
|
||
style="width: 100%; padding: 12px; border: 2px solid #ecf0f1; border-radius: 8px; font-size: 14px; resize: vertical;">${slide.description || ''}</textarea>
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
|
||
<button type="button" onclick="this.closest('.modal').remove()"
|
||
class="btn" style="background: #95a5a6; color: white; padding: 12px 24px;">取消</button>
|
||
<button type="button" onclick="saveSingleSlideEdit(${slideIndex})"
|
||
class="btn btn-primary" style="padding: 12px 24px;">保存修改</button>
|
||
</div>
|
||
</form>
|
||
`;
|
||
|
||
modal.className = 'modal';
|
||
modal.appendChild(content);
|
||
document.body.appendChild(modal);
|
||
|
||
// 为删除按钮添加事件监听器
|
||
const removeButtons = modal.querySelectorAll('.remove-point-btn');
|
||
removeButtons.forEach(button => {
|
||
button.addEventListener('click', function() {
|
||
const index = parseInt(this.getAttribute('data-remove-index'));
|
||
removeSingleContentPointByElement(this.parentElement);
|
||
});
|
||
});
|
||
|
||
// 点击背景关闭
|
||
modal.addEventListener('click', (e) => {
|
||
if (e.target === modal) {
|
||
modal.remove();
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Error editing single slide:', error);
|
||
alert('编辑失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 添加内容要点
|
||
function addSingleContentPoint() {
|
||
const container = document.getElementById('contentPointsContainer');
|
||
if (!container) return;
|
||
|
||
const currentPoints = container.querySelectorAll('.content-point-item');
|
||
const newIndex = currentPoints.length;
|
||
|
||
const pointDiv = document.createElement('div');
|
||
pointDiv.className = 'content-point-item';
|
||
pointDiv.style.cssText = 'display: flex; align-items: center; margin-bottom: 10px;';
|
||
|
||
// 创建输入框
|
||
const input = document.createElement('input');
|
||
input.type = 'text';
|
||
input.className = 'content-point-input';
|
||
input.setAttribute('data-index', newIndex);
|
||
input.placeholder = '输入新的内容要点...';
|
||
input.style.cssText = 'flex: 1; padding: 8px 12px; border: 2px solid #ecf0f1; border-radius: 6px; font-size: 14px;';
|
||
|
||
// 创建删除按钮
|
||
const button = document.createElement('button');
|
||
button.className = 'remove-point-btn';
|
||
button.setAttribute('data-remove-index', newIndex);
|
||
button.addEventListener('click', function() {
|
||
removeSingleContentPointByElement(this.parentElement);
|
||
});
|
||
button.style.cssText = 'background: #e74c3c; color: white; border: none; border-radius: 4px; padding: 6px 10px; margin-left: 8px; cursor: pointer;';
|
||
button.innerHTML = '<i class="fas fa-trash"></i>';
|
||
|
||
pointDiv.appendChild(input);
|
||
pointDiv.appendChild(button);
|
||
|
||
container.appendChild(pointDiv);
|
||
|
||
// 聚焦到新添加的输入框
|
||
const newInput = pointDiv.querySelector('.content-point-input');
|
||
if (newInput) {
|
||
newInput.focus();
|
||
}
|
||
}
|
||
|
||
// 删除内容要点(通过索引)
|
||
function removeSingleContentPoint(index) {
|
||
const container = document.getElementById('contentPointsContainer');
|
||
if (!container) return;
|
||
|
||
const pointItems = container.querySelectorAll('.content-point-item');
|
||
if (pointItems[index]) {
|
||
removeSingleContentPointByElement(pointItems[index]);
|
||
}
|
||
}
|
||
|
||
// 删除内容要点(通过元素)
|
||
function removeSingleContentPointByElement(element) {
|
||
if (!element) return;
|
||
|
||
element.remove();
|
||
|
||
// 重新编号剩余的要点
|
||
const container = document.getElementById('contentPointsContainer');
|
||
if (!container) return;
|
||
|
||
const remainingItems = container.querySelectorAll('.content-point-item');
|
||
remainingItems.forEach((item, newIndex) => {
|
||
const input = item.querySelector('.content-point-input');
|
||
const button = item.querySelector('.remove-point-btn, button');
|
||
if (input) input.setAttribute('data-index', newIndex);
|
||
if (button) {
|
||
if (button.classList.contains('remove-point-btn')) {
|
||
button.setAttribute('data-remove-index', newIndex);
|
||
} else {
|
||
// 重新创建按钮以更新事件处理
|
||
const newButton = document.createElement('button');
|
||
newButton.onclick = () => removeSingleContentPoint(newIndex);
|
||
newButton.style.cssText = 'background: #e74c3c; color: white; border: none; border-radius: 4px; padding: 6px 10px; margin-left: 8px; cursor: pointer;';
|
||
newButton.innerHTML = '<i class="fas fa-trash"></i>';
|
||
button.parentNode.replaceChild(newButton, button);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 保存单页编辑
|
||
async function saveSingleSlideEdit(slideIndex) {
|
||
try {
|
||
// 获取表单数据
|
||
const title = document.getElementById('slideTitle').value.trim();
|
||
const subtitle = document.getElementById('slideSubtitle').value.trim();
|
||
const slideType = document.getElementById('slideType').value;
|
||
const description = document.getElementById('slideDescription').value.trim();
|
||
|
||
// 获取所有内容要点
|
||
const contentPointInputs = document.querySelectorAll('.content-point-input');
|
||
const contentPoints = Array.from(contentPointInputs)
|
||
.map(input => input.value.trim())
|
||
.filter(point => point.length > 0);
|
||
|
||
if (!title) {
|
||
alert('页面标题不能为空');
|
||
return;
|
||
}
|
||
|
||
if (contentPoints.length === 0) {
|
||
alert('至少需要一个内容要点');
|
||
return;
|
||
}
|
||
|
||
// 获取当前大纲
|
||
const outlineContent = getOutlineContentNew();
|
||
const parsedOutline = JSON.parse(outlineContent);
|
||
|
||
// 更新指定的幻灯片
|
||
const updatedSlide = {
|
||
page_number: parsedOutline.slides[slideIndex].page_number || slideIndex + 1,
|
||
title: title,
|
||
content_points: contentPoints,
|
||
slide_type: slideType,
|
||
type: slideType // 保持兼容性
|
||
};
|
||
|
||
// 添加可选字段
|
||
if (subtitle) {
|
||
updatedSlide.subtitle = subtitle;
|
||
}
|
||
if (description) {
|
||
updatedSlide.description = description;
|
||
}
|
||
|
||
// 保留原有的图表配置(如果存在)
|
||
if (parsedOutline.slides[slideIndex].chart_config) {
|
||
updatedSlide.chart_config = parsedOutline.slides[slideIndex].chart_config;
|
||
}
|
||
|
||
// 更新大纲
|
||
parsedOutline.slides[slideIndex] = updatedSlide;
|
||
|
||
// 保存到服务器
|
||
const response = await fetch(`/projects/${currentProjectId}/update-outline`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
outline_content: JSON.stringify(parsedOutline, null, 2)
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
// 关闭模态框
|
||
const modal = document.querySelector('.modal');
|
||
if (modal) modal.remove();
|
||
|
||
// 更新JSON显示区域
|
||
const outlineDisplay = document.getElementById('outline-content-display');
|
||
if (outlineDisplay) {
|
||
const formattedContent = JSON.stringify(parsedOutline, null, 2);
|
||
const preElement = document.createElement('pre');
|
||
preElement.style.cssText = 'margin: 0; font-family: "Consolas", "Monaco", "Courier New", monospace; font-size: 13px; white-space: pre-wrap; word-wrap: break-word;';
|
||
preElement.textContent = formattedContent;
|
||
outlineDisplay.innerHTML = '';
|
||
outlineDisplay.appendChild(preElement);
|
||
}
|
||
|
||
// 显示成功消息
|
||
const toast = document.createElement('div');
|
||
toast.style.cssText = `
|
||
position: fixed; top: 20px; right: 20px; background: #27ae60;
|
||
color: white; padding: 15px 20px; border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 1001;
|
||
font-size: 14px; font-weight: bold;
|
||
`;
|
||
toast.textContent = `✅ 第${slideIndex + 1}页已更新`;
|
||
document.body.appendChild(toast);
|
||
|
||
setTimeout(() => {
|
||
toast.remove();
|
||
}, 3000);
|
||
|
||
// 立即重新渲染大纲视图,使用最新数据
|
||
setTimeout(() => {
|
||
renderOutlineViewNewWithData(parsedOutline);
|
||
}, 100);
|
||
|
||
} else {
|
||
const error = await response.json();
|
||
alert('保存失败: ' + (error.detail || '未知错误'));
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Error saving single slide:', error);
|
||
alert('保存失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
|
||
|
||
// Get outline content for new section
|
||
function getOutlineContentNew() {
|
||
const contentDiv = document.getElementById('outline-content-display');
|
||
if (!contentDiv) return null;
|
||
|
||
// 首先尝试从pre元素中获取原始文本内容
|
||
const preElement = contentDiv.querySelector('pre');
|
||
if (preElement) {
|
||
let content = preElement.textContent || preElement.innerText || '';
|
||
|
||
// Remove placeholder text
|
||
if (content.includes('正在生成大纲') || content.includes('等待大纲生成')) {
|
||
return null;
|
||
}
|
||
|
||
return content.trim();
|
||
}
|
||
|
||
// 如果没有pre元素,尝试从整个div获取内容
|
||
let content = contentDiv.textContent || contentDiv.innerText || '';
|
||
|
||
// 处理HTML实体和格式问题
|
||
content = content.replace(/ /g, ' ')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/&/g, '&')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, "'")
|
||
.replace(/<br\s*\/?>/gi, '\n')
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
|
||
// Remove placeholder text
|
||
if (content.includes('正在生成大纲') || content.includes('等待大纲生成')) {
|
||
return null;
|
||
}
|
||
|
||
return content;
|
||
}
|
||
|
||
// 调试大纲内容的函数
|
||
function debugOutlineContent() {
|
||
const outlineContent = getOutlineContentNew();
|
||
|
||
console.log('=== 大纲内容调试信息 ===');
|
||
console.log('原始内容长度:', outlineContent ? outlineContent.length : 0);
|
||
console.log('原始内容(前500字符):', outlineContent ? outlineContent.substring(0, 500) : 'null');
|
||
console.log('原始内容(后100字符):', outlineContent ? outlineContent.substring(Math.max(0, outlineContent.length - 100)) : 'null');
|
||
|
||
// 检查常见的JSON格式问题
|
||
if (outlineContent) {
|
||
console.log('首字符:', outlineContent.charAt(0), '(ASCII:', outlineContent.charCodeAt(0), ')');
|
||
console.log('末字符:', outlineContent.charAt(outlineContent.length - 1), '(ASCII:', outlineContent.charCodeAt(outlineContent.length - 1), ')');
|
||
|
||
// 检查是否包含HTML实体
|
||
if (outlineContent.includes(' ')) {
|
||
console.log('⚠️ 发现HTML实体 ');
|
||
}
|
||
if (outlineContent.includes('<br>')) {
|
||
console.log('⚠️ 发现HTML标签 <br>');
|
||
}
|
||
|
||
// 尝试清理内容
|
||
let cleanedContent = outlineContent.replace(/ /g, ' ').replace(/<br>/g, '\n').trim();
|
||
console.log('清理后内容(前200字符):', cleanedContent.substring(0, 200));
|
||
|
||
try {
|
||
JSON.parse(cleanedContent);
|
||
console.log('✅ 清理后的内容可以正确解析为JSON');
|
||
} catch (e) {
|
||
console.log('❌ 清理后仍然无法解析:', e.message);
|
||
}
|
||
}
|
||
|
||
// 显示调试信息弹窗
|
||
alert('调试信息已输出到控制台,请按F12查看');
|
||
}
|
||
|
||
// Convert JSON to mindmap data for new section
|
||
function convertJsonToMindmapNew(jsonData) {
|
||
const mindmapData = {
|
||
name: jsonData.title || 'PPT大纲',
|
||
children: []
|
||
};
|
||
|
||
if (jsonData.slides && Array.isArray(jsonData.slides)) {
|
||
jsonData.slides.forEach(slide => {
|
||
const slideNode = {
|
||
name: `${slide.page_number}. ${slide.title}`,
|
||
children: []
|
||
};
|
||
|
||
if (slide.content_points && Array.isArray(slide.content_points)) {
|
||
slide.content_points.forEach(point => {
|
||
slideNode.children.push({
|
||
name: point,
|
||
children: []
|
||
});
|
||
});
|
||
}
|
||
|
||
mindmapData.children.push(slideNode);
|
||
});
|
||
}
|
||
|
||
return mindmapData;
|
||
}
|
||
|
||
|
||
// 文件上传相关函数
|
||
function toggleContentSourceTodo() {
|
||
const fileSection = document.getElementById('file-upload-section-todo');
|
||
const processingOptions = document.getElementById('file-processing-options-todo');
|
||
const pdfProcessingMode = document.getElementById('pdf-processing-mode-todo');
|
||
const manualRadio = document.querySelector('input[name="content_source"][value="manual"]');
|
||
const fileRadio = document.querySelector('input[name="content_source"][value="file"]');
|
||
const topicInput = document.getElementById('topic');
|
||
|
||
if (fileRadio && fileRadio.checked) {
|
||
if (fileSection) fileSection.style.display = 'block';
|
||
if (topicInput) {
|
||
topicInput.required = false;
|
||
topicInput.placeholder = '可选:自定义标题(留空则从文件自动提取)';
|
||
}
|
||
// 检查当前选择的文件,决定是否显示处理选项
|
||
const fileInput = document.getElementById('file_upload_todo');
|
||
if (fileInput && fileInput.files[0]) {
|
||
const fileExt = '.' + fileInput.files[0].name.split('.').pop().toLowerCase();
|
||
// 对所有文件类型显示处理选项容器
|
||
if (processingOptions) {
|
||
processingOptions.style.display = 'block';
|
||
}
|
||
// 只对PDF文件显示处理方式选项
|
||
if (pdfProcessingMode) {
|
||
pdfProcessingMode.style.display = fileExt === '.pdf' ? 'block' : 'none';
|
||
}
|
||
}
|
||
} else {
|
||
if (fileSection) fileSection.style.display = 'none';
|
||
if (processingOptions) processingOptions.style.display = 'none';
|
||
if (pdfProcessingMode) pdfProcessingMode.style.display = 'none';
|
||
if (topicInput) {
|
||
topicInput.required = true;
|
||
topicInput.placeholder = '请输入PPT主题';
|
||
}
|
||
}
|
||
}
|
||
|
||
// 受众选择相关函数
|
||
function toggleCustomAudience() {
|
||
const audienceSelect = document.getElementById('audience_type');
|
||
const customSection = document.getElementById('custom-audience-section');
|
||
const customInput = document.getElementById('custom_audience');
|
||
|
||
if (audienceSelect && customSection && customInput) {
|
||
if (audienceSelect.value === '自定义') {
|
||
customSection.style.display = 'block';
|
||
customInput.required = true;
|
||
} else {
|
||
customSection.style.display = 'none';
|
||
customInput.required = false;
|
||
customInput.value = ''; // 清空自定义输入
|
||
}
|
||
}
|
||
}
|
||
|
||
// 文件上传验证和预览
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const fileInput = document.getElementById('file_upload_todo');
|
||
if (fileInput) {
|
||
fileInput.addEventListener('change', function(e) {
|
||
const file = e.target.files[0];
|
||
const processingOptions = document.getElementById('file-processing-options-todo');
|
||
const pdfProcessingMode = document.getElementById('pdf-processing-mode-todo');
|
||
|
||
if (file) {
|
||
// 验证文件大小 (100MB 限制)
|
||
if (file.size > 100 * 1024 * 1024) {
|
||
alert('文件大小不能超过 100MB');
|
||
e.target.value = '';
|
||
if (processingOptions) processingOptions.style.display = 'none';
|
||
if (pdfProcessingMode) pdfProcessingMode.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
// 验证文件类型
|
||
const allowedTypes = ['.pdf', '.docx', '.txt', '.md'];
|
||
const fileExt = '.' + file.name.split('.').pop().toLowerCase();
|
||
if (!allowedTypes.includes(fileExt)) {
|
||
alert('只支持 PDF、DOCX、TXT、MD 格式的文件');
|
||
e.target.value = '';
|
||
if (processingOptions) processingOptions.style.display = 'none';
|
||
if (pdfProcessingMode) pdfProcessingMode.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
// 显示文件处理选项(对所有文件类型)
|
||
if (processingOptions) {
|
||
processingOptions.style.display = 'block';
|
||
}
|
||
|
||
// 根据文件类型显示或隐藏PDF专用处理方式选项
|
||
if (pdfProcessingMode) {
|
||
if (fileExt === '.pdf') {
|
||
pdfProcessingMode.style.display = 'block';
|
||
} else {
|
||
pdfProcessingMode.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// 显示文件信息
|
||
const fileInfo = document.createElement('div');
|
||
fileInfo.style.marginTop = '10px';
|
||
fileInfo.style.padding = '10px';
|
||
fileInfo.style.background = '#e8f5e8';
|
||
fileInfo.style.borderRadius = '5px';
|
||
fileInfo.style.color = '#2d5a2d';
|
||
fileInfo.innerHTML = `
|
||
<strong>已选择文件:</strong>${file.name}<br>
|
||
<strong>文件大小:</strong>${(file.size / 1024 / 1024).toFixed(2)} MB<br>
|
||
<strong>文件类型:</strong>${fileExt.toUpperCase()}
|
||
`;
|
||
|
||
// 移除现有文件信息
|
||
const existingInfo = e.target.parentNode.querySelector('.file-info-todo');
|
||
if (existingInfo) {
|
||
existingInfo.remove();
|
||
}
|
||
|
||
fileInfo.className = 'file-info-todo';
|
||
e.target.parentNode.appendChild(fileInfo);
|
||
|
||
// 自动填充主题(如果为空)
|
||
const topicInput = document.getElementById('topic');
|
||
if (topicInput && !topicInput.value.trim()) {
|
||
const baseName = file.name.replace(/\.[^/.]+$/, "");
|
||
topicInput.value = baseName;
|
||
}
|
||
} else {
|
||
// 如果没有选择文件,隐藏处理选项
|
||
if (processingOptions) processingOptions.style.display = 'none';
|
||
if (pdfProcessingMode) pdfProcessingMode.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// 清除所有已选择的文件
|
||
function clearAllFiles() {
|
||
const fileInput = document.getElementById('file_upload_todo');
|
||
if (fileInput) {
|
||
fileInput.value = '';
|
||
updateFilesList();
|
||
}
|
||
}
|
||
|
||
// 移除单个文件
|
||
function removeFile(index) {
|
||
const fileInput = document.getElementById('file_upload_todo');
|
||
if (!fileInput || !fileInput.files) return;
|
||
|
||
const dt = new DataTransfer();
|
||
const files = Array.from(fileInput.files);
|
||
|
||
files.forEach((file, i) => {
|
||
if (i !== index) {
|
||
dt.items.add(file);
|
||
}
|
||
});
|
||
|
||
fileInput.files = dt.files;
|
||
updateFilesList();
|
||
}
|
||
|
||
// 更新文件列表显示
|
||
function updateFilesList() {
|
||
const fileInput = document.getElementById('file_upload_todo');
|
||
const filesList = document.getElementById('selected-files-list-todo');
|
||
const filesContainer = document.getElementById('files-list-container-todo');
|
||
const processingOptions = document.getElementById('file-processing-options-todo');
|
||
const pdfProcessingMode = document.getElementById('pdf-processing-mode-todo');
|
||
|
||
if (!fileInput || !filesList || !filesContainer) return;
|
||
|
||
const files = fileInput.files;
|
||
|
||
if (files.length === 0) {
|
||
filesList.style.display = 'none';
|
||
if (processingOptions) processingOptions.style.display = 'none';
|
||
if (pdfProcessingMode) pdfProcessingMode.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
// 显示文件列表
|
||
filesList.style.display = 'block';
|
||
|
||
// 显示文件处理选项
|
||
if (processingOptions) {
|
||
processingOptions.style.display = 'block';
|
||
}
|
||
|
||
// 检查是否有PDF文件
|
||
let hasPdf = false;
|
||
for (let i = 0; i < files.length; i++) {
|
||
const fileExt = '.' + files[i].name.split('.').pop().toLowerCase();
|
||
if (fileExt === '.pdf') {
|
||
hasPdf = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 根据是否有PDF显示/隐藏PDF处理选项
|
||
if (pdfProcessingMode) {
|
||
pdfProcessingMode.style.display = hasPdf ? 'block' : 'none';
|
||
}
|
||
|
||
// 生成文件列表HTML
|
||
let html = '';
|
||
let totalSize = 0;
|
||
|
||
for (let i = 0; i < files.length; i++) {
|
||
const file = files[i];
|
||
const fileExt = '.' + file.name.split('.').pop().toLowerCase();
|
||
const sizeInMB = (file.size / 1024 / 1024).toFixed(2);
|
||
totalSize += file.size;
|
||
|
||
html += `
|
||
<div style="display: flex; justify-content: space-between; align-items: center; padding: 8px; background: #f8f9fa; border-radius: 4px; margin-bottom: 5px; border-left: 3px solid #3498db;">
|
||
<div style="flex: 1;">
|
||
<div style="font-weight: 500; color: #2c3e50;">${i + 1}. ${file.name}</div>
|
||
<div style="font-size: 12px; color: #7f8c8d;">
|
||
<span>${fileExt.toUpperCase()}</span> |
|
||
<span>${sizeInMB} MB</span>
|
||
</div>
|
||
</div>
|
||
<button type="button" onclick="removeFile(${i})"
|
||
style="padding: 4px 8px; background: #e74c3c; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px;">
|
||
删除
|
||
</button>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 添加总计信息
|
||
html += `
|
||
<div style="margin-top: 10px; padding: 8px; background: #e8f5e9; border-radius: 4px; text-align: center;">
|
||
<strong style="color: #2c3e50;">共 ${files.length} 个文件 | 总大小: ${(totalSize / 1024 / 1024).toFixed(2)} MB</strong>
|
||
</div>
|
||
`;
|
||
|
||
filesContainer.innerHTML = html;
|
||
|
||
// 自动填充主题(如果为空且只有一个文件)
|
||
const topicInput = document.getElementById('topic');
|
||
if (topicInput && !topicInput.value.trim() && files.length === 1) {
|
||
const baseName = files[0].name.replace(/\.[^/.]+$/, "");
|
||
topicInput.value = baseName;
|
||
} else if (topicInput && !topicInput.value.trim() && files.length > 1) {
|
||
topicInput.value = `合并文档 (${files.length}个文件)`;
|
||
}
|
||
}
|
||
|
||
// 监听文件选择变化 - 支持多文件
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const fileInput = document.getElementById('file_upload_todo');
|
||
if (fileInput) {
|
||
fileInput.addEventListener('change', function(e) {
|
||
const files = e.target.files;
|
||
const processingOptions = document.getElementById('file-processing-options-todo');
|
||
const pdfProcessingMode = document.getElementById('pdf-processing-mode-todo');
|
||
|
||
if (files && files.length > 0) {
|
||
// 验证所有文件
|
||
const allowedTypes = ['.pdf', '.docx', '.txt', '.md', '.jpg', '.jpeg', '.png', '.xlsx', '.csv'];
|
||
let hasInvalidFile = false;
|
||
let hasOversizedFile = false;
|
||
|
||
for (let i = 0; i < files.length; i++) {
|
||
const file = files[i];
|
||
const fileExt = '.' + file.name.split('.').pop().toLowerCase();
|
||
|
||
// 验证文件大小
|
||
if (file.size > 100 * 1024 * 1024) {
|
||
alert(`文件 "${file.name}" 大小超过100MB限制`);
|
||
hasOversizedFile = true;
|
||
break;
|
||
}
|
||
|
||
// 验证文件类型
|
||
if (!allowedTypes.includes(fileExt)) {
|
||
alert(`文件 "${file.name}" 格式不支持\n支持的格式: ${allowedTypes.join(', ')}`);
|
||
hasInvalidFile = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (hasInvalidFile || hasOversizedFile) {
|
||
e.target.value = '';
|
||
if (processingOptions) processingOptions.style.display = 'none';
|
||
if (pdfProcessingMode) pdfProcessingMode.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
// 更新文件列表显示
|
||
updateFilesList();
|
||
} else {
|
||
// 如果没有选择文件,隐藏处理选项
|
||
const filesList = document.getElementById('selected-files-list-todo');
|
||
if (filesList) filesList.style.display = 'none';
|
||
if (processingOptions) processingOptions.style.display = 'none';
|
||
if (pdfProcessingMode) pdfProcessingMode.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// 表单提交处理
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const form = document.getElementById('requirements-form');
|
||
if (form) {
|
||
form.addEventListener('submit', function(e) {
|
||
const fileRadio = document.querySelector('input[name="content_source"][value="file"]');
|
||
const fileInput = document.getElementById('file_upload_todo');
|
||
|
||
if (fileRadio && fileRadio.checked && fileInput && fileInput.files.length === 0) {
|
||
e.preventDefault();
|
||
alert('请选择要上传的文件');
|
||
return false;
|
||
}
|
||
|
||
// 显示加载状态
|
||
const submitBtn = document.getElementById('confirm-requirements-btn');
|
||
if (submitBtn) {
|
||
submitBtn.disabled = true;
|
||
submitBtn.innerHTML = '⏳ 处理中...';
|
||
}
|
||
});
|
||
}
|
||
});
|
||
</script>
|
||
|
||
{% endblock %}
|