Files
LandPPT/src/landppt/web/templates/todo_board.html
2025-11-07 09:05:32 +08:00

5598 lines
217 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% 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 %}