Files
LandPPT/src/landppt/web/templates/todo_board.html

5598 lines
217 KiB
HTML
Raw Normal View History

2025-11-07 09:05:32 +08:00
{% 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(/&nbsp;/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, '&quot;')}"
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, '&quot;')}"
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, '&quot;')}"
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(/&nbsp;/g, ' ')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.replace(/&quot;/g, '"')
.replace(/&#39;/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('&nbsp;')) {
console.log('⚠️ 发现HTML实体 &nbsp;');
}
if (outlineContent.includes('<br>')) {
console.log('⚠️ 发现HTML标签 <br>');
}
// 尝试清理内容
let cleanedContent = outlineContent.replace(/&nbsp;/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 %}