Youngger9765 Claude commited on
Commit
698390e
·
1 Parent(s): 0ab885b

Fix Jinja2 template variable rendering issue

Browse files

- Add {% raw %} tags to prevent template variables from being processed during main page render
- Add extensive logging to both frontend and backend for debugging
- Implement preview, copy, and download functionality for generated HTML
- Store generated HTML globally for access by utility functions

The issue was that template variables like {{ title }} were being processed when rendering the main index.html page, resulting in empty values. Now the template is properly protected and variables are rendered correctly when generating HTML.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>

Files changed (2) hide show
  1. app.py +64 -1
  2. templates/index.html +1315 -192
app.py CHANGED
@@ -95,4 +95,67 @@ async def generate_html(
95
  @app.get("/api/awards")
96
  async def get_awards():
97
  """API endpoint to fetch awards data"""
98
- return {"awards": get_sheets_data()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  @app.get("/api/awards")
96
  async def get_awards():
97
  """API endpoint to fetch awards data"""
98
+ return {"awards": get_sheets_data()}
99
+
100
+ @app.post("/api/preview")
101
+ async def preview_html(
102
+ template_content: str = Form(...),
103
+ variables: str = Form("{}")
104
+ ):
105
+ """API endpoint to preview HTML with variables"""
106
+ try:
107
+ # Log 1: 接收到的原始資料
108
+ print("=== API Preview Called ===")
109
+ print(f"Template length: {len(template_content)} characters")
110
+ print(f"Template preview: {template_content[:200]}...")
111
+ print(f"Variables received: {variables}")
112
+
113
+ # Log 2: 解析 JSON
114
+ variables_dict = json.loads(variables)
115
+ print(f"\nParsed variables: {json.dumps(variables_dict, indent=2, ensure_ascii=False)}")
116
+
117
+ # Log 3: 獲取額外資料
118
+ awards_data = get_sheets_data()
119
+ print(f"\nAwards data count: {len(awards_data)}")
120
+
121
+ # Log 4: 添加額外變數
122
+ variables_dict['awards_data'] = awards_data
123
+ variables_dict['current_year'] = datetime.now().year
124
+ print(f"\nAdded current_year: {variables_dict['current_year']}")
125
+
126
+ # Log 5: 所有變數的 keys
127
+ print(f"\nAll variable keys: {list(variables_dict.keys())}")
128
+
129
+ # Log 6: 渲染模板
130
+ from jinja2 import Template
131
+ template = Template(template_content)
132
+ generated_html = template.render(**variables_dict)
133
+ print(f"\nGenerated HTML length: {len(generated_html)} characters")
134
+ print(f"Generated HTML preview: {generated_html[:300]}...")
135
+
136
+ # Log 7: 返回資料
137
+ return_data = {
138
+ "html": generated_html,
139
+ "debug": {
140
+ "variables_count": len(variables_dict),
141
+ "html_length": len(generated_html),
142
+ "template_length": len(template_content)
143
+ }
144
+ }
145
+ print(f"\nReturning success with HTML length: {len(generated_html)}")
146
+
147
+ return return_data
148
+ except Exception as e:
149
+ print(f"\n!!! ERROR in preview_html: {str(e)}")
150
+ print(f"Error type: {type(e).__name__}")
151
+ import traceback
152
+ print(f"Traceback: {traceback.format_exc()}")
153
+
154
+ return {
155
+ "error": str(e),
156
+ "html": "",
157
+ "debug": {
158
+ "error_type": type(e).__name__,
159
+ "traceback": traceback.format_exc()
160
+ }
161
+ }
templates/index.html CHANGED
@@ -3,257 +3,1380 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>龍顏文學獎 HTML 建立器</title>
 
 
7
  <style>
8
- * {
9
- margin: 0;
10
- padding: 0;
11
- box-sizing: border-box;
12
- }
13
  body {
14
- font-family: 'Microsoft JhengHei', Arial, sans-serif;
15
- background-color: #f5f5f5;
16
- padding: 20px;
17
- }
18
- .container {
19
- max-width: 1400px;
20
- margin: 0 auto;
21
- background-color: white;
22
- padding: 30px;
23
- border-radius: 10px;
24
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
25
- }
26
- h1 {
27
- color: #333;
28
- margin-bottom: 30px;
29
- text-align: center;
30
- font-size: 2.5em;
31
  }
32
- .section {
33
- margin-bottom: 30px;
34
- padding: 20px;
35
- background-color: #f9f9f9;
36
- border-radius: 8px;
37
- border: 1px solid #e0e0e0;
38
  }
39
- .section h2 {
40
- color: #555;
41
- margin-bottom: 15px;
42
- font-size: 1.5em;
43
  }
44
- .template-area {
45
- width: 100%;
46
- min-height: 400px;
47
- padding: 15px;
48
- border: 1px solid #ddd;
49
- border-radius: 5px;
50
- font-family: 'Courier New', monospace;
51
- font-size: 14px;
52
- background-color: #fafafa;
53
- resize: vertical;
54
  }
55
- .variables-area {
56
- width: 100%;
57
- min-height: 200px;
58
- padding: 15px;
59
- border: 1px solid #ddd;
60
- border-radius: 5px;
61
- font-family: 'Courier New', monospace;
62
  font-size: 14px;
63
- background-color: #fafafa;
64
- resize: vertical;
65
  }
66
- .awards-table {
67
- width: 100%;
68
- border-collapse: collapse;
69
- margin-top: 10px;
70
  }
71
- .awards-table th,
72
- .awards-table td {
73
- padding: 10px;
74
- border: 1px solid #ddd;
75
- text-align: left;
76
  }
77
- .awards-table th {
78
- background-color: #f0f0f0;
79
- font-weight: bold;
 
80
  }
81
- .generate-btn {
82
- background-color: #4CAF50;
83
- color: white;
84
- padding: 15px 40px;
85
- font-size: 18px;
86
- border: none;
87
- border-radius: 5px;
88
- cursor: pointer;
89
- display: block;
90
- margin: 30px auto;
91
- transition: background-color 0.3s;
92
- }
93
- .generate-btn:hover {
94
- background-color: #45a049;
95
- }
96
- .info-box {
97
- background-color: #e8f4f8;
98
- border-left: 4px solid #2196F3;
99
- padding: 15px;
100
- margin-bottom: 20px;
101
  }
102
- .template-help {
103
- margin-top: 10px;
104
- padding: 10px;
105
- background-color: #f0f8ff;
106
- border-radius: 5px;
107
- font-size: 0.9em;
108
- color: #666;
 
109
  }
110
- .loading {
 
 
 
 
 
 
 
 
111
  display: none;
112
- text-align: center;
113
- color: #666;
114
- margin: 20px 0;
 
 
 
 
 
 
 
 
 
115
  }
116
  </style>
117
  </head>
118
  <body>
119
- <div class="container">
120
- <h1>龍顏文學獎 HTML 建立器</h1>
121
-
122
- <div class="info-box">
123
- <p><strong>使用說明:</strong></p>
124
- <ul style="margin-left: 20px; margin-top: 10px;">
125
- <li>在模板區域輸入 HTML 模板,使用 {{ 變數名 }} 來標記變數</li>
126
- <li>在變數區域輸入 JSON 格式的變數值</li>
127
- <li>歷年得獎紀錄會自動從 Google Sheets 同步</li>
128
- <li>點擊生成按鈕即可下載完整的 HTML 檔案</li>
129
- </ul>
130
  </div>
 
 
 
 
131
 
132
- <form id="htmlGeneratorForm" action="/generate-html" method="post">
133
- <div class="section">
134
- <h2>HTML 模板</h2>
135
- <div class="template-help">
136
- <strong>可用變數:</strong>
137
- <code>{{ title }}</code>, <code>{{ subtitle }}</code>, <code>{{ content }}</code>,
138
- <code>{{ current_year }}</code>, <code>{{ awards_data }}</code>
 
 
 
139
  </div>
140
- <textarea name="template_content" class="template-area" placeholder="輸入 HTML 模板..."><!DOCTYPE html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  <html lang="zh-TW">
142
  <head>
143
  <meta charset="UTF-8">
144
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
145
  <title>{{ title }}</title>
 
146
  <style>
147
- body {
148
- font-family: 'Microsoft JhengHei', Arial, sans-serif;
149
  margin: 0;
150
  padding: 0;
151
- background-color: #f5f5f5;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  }
 
 
153
  .hero {
154
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  color: white;
156
- padding: 60px 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  text-align: center;
 
 
158
  }
159
- .hero h1 {
160
- font-size: 3em;
161
- margin-bottom: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  }
163
- .content {
164
- max-width: 1200px;
165
- margin: 40px auto;
166
- padding: 0 20px;
167
  }
168
- .awards-section {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  background: white;
170
  padding: 40px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  border-radius: 10px;
172
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
173
  }
174
- .award-item {
 
 
175
  padding: 20px;
176
- border-bottom: 1px solid #eee;
 
177
  }
178
- .award-item:last-child {
179
- border-bottom: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  }
181
  </style>
182
  </head>
183
  <body>
184
- <div class="hero">
185
- <h1>{{ title }}</h1>
186
- <p>{{ subtitle }}</p>
187
- </div>
188
-
189
- <div class="content">
190
- <div class="awards-section">
191
- <h2>歷年得獎紀錄</h2>
192
- {% for award in awards_data %}
193
- <div class="award-item">
194
- <h3>{{ award.年份 }} - {{ award.獎項 }}</h3>
195
- <p><strong>得獎者:</strong>{{ award.得獎者 }}</p>
196
- <p><strong>作品:</strong>{{ award.作品名稱 }}</p>
 
 
 
 
 
 
 
197
  </div>
198
- {% endfor %}
199
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  </div>
201
  </body>
202
- </html></textarea>
203
- </div>
204
-
205
- <div class="section">
206
- <h2>變數設定</h2>
207
- <textarea name="variables" class="variables-area" placeholder='輸入 JSON 格式的變數,例如:{"title": "龍顏文學獎", "subtitle": "2024年度頒獎典禮"}'>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  {
209
- "title": "龍顏文學獎",
210
- "subtitle": "致力於推廣優秀文學創作",
211
- "content": "歡迎來到龍顏文學獎官方網站"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  }</textarea>
213
- </div>
 
 
 
 
214
 
215
- <div class="section">
216
- <h2>歷年得獎紀錄(自動同步)</h2>
217
- <div class="loading" id="awardsLoading">載入中...</div>
218
- {% if awards_data %}
219
- <table class="awards-table">
220
- <thead>
221
- <tr>
222
- {% for key in awards_data[0].keys() %}
223
- <th>{{ key }}</th>
224
- {% endfor %}
225
- </tr>
226
- </thead>
227
- <tbody>
228
- {% for award in awards_data %}
229
- <tr>
230
- {% for value in award.values() %}
231
- <td>{{ value }}</td>
232
- {% endfor %}
233
- </tr>
234
- {% endfor %}
235
- </tbody>
236
- </table>
237
- {% else %}
238
- <p style="color: #666;">尚未設定 Google Sheets 或無資料</p>
239
- {% endif %}
240
  </div>
241
-
242
- <button type="submit" class="generate-btn">生成 HTML 檔案</button>
243
- </form>
244
  </div>
245
 
246
  <script>
247
- document.getElementById('htmlGeneratorForm').addEventListener('submit', function(e) {
248
- const variables = document.querySelector('[name="variables"]').value;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  try {
250
- JSON.parse(variables);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  } catch (error) {
252
- e.preventDefault();
253
- alert('變數格式錯誤!請輸入有效的 JSON 格式');
254
- return false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  }
256
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  </script>
258
  </body>
259
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>樂寫網徵文比賽 HTML 建立器</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
8
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
9
  <style>
10
+ @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@300;400;500;700&display=swap');
11
+
 
 
 
12
  body {
13
+ font-family: 'Noto Sans TC', sans-serif;
14
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
15
+ min-height: 100vh;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  }
17
+
18
+ .glass-effect {
19
+ background: rgba(255, 255, 255, 0.95);
20
+ backdrop-filter: blur(10px);
21
+ box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
22
+ border: 1px solid rgba(255, 255, 255, 0.18);
23
  }
24
+
25
+ .editor-container {
26
+ height: calc(100vh - 200px);
27
+ overflow: hidden;
28
  }
29
+
30
+ .split-panel {
31
+ height: 100%;
32
+ overflow-y: auto;
 
 
 
 
 
 
33
  }
34
+
35
+ textarea, .preview-frame {
36
+ font-family: 'Consolas', 'Monaco', monospace;
 
 
 
 
37
  font-size: 14px;
38
+ line-height: 1.6;
 
39
  }
40
+
41
+ .btn-primary {
42
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
43
+ transition: all 0.3s ease;
44
  }
45
+
46
+ .btn-primary:hover {
47
+ transform: translateY(-2px);
48
+ box-shadow: 0 10px 20px rgba(0,0,0,0.2);
 
49
  }
50
+
51
+ .tab-active {
52
+ border-bottom: 3px solid #667eea;
53
+ color: #667eea;
54
  }
55
+
56
+ .preview-frame {
57
+ background: white;
58
+ border: 1px solid #e5e7eb;
59
+ border-radius: 8px;
60
+ padding: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  }
62
+
63
+ .loading-spinner {
64
+ display: none;
65
+ position: fixed;
66
+ top: 50%;
67
+ left: 50%;
68
+ transform: translate(-50%, -50%);
69
+ z-index: 9999;
70
  }
71
+
72
+ .toast {
73
+ position: fixed;
74
+ bottom: 20px;
75
+ right: 20px;
76
+ padding: 16px 24px;
77
+ background: #10b981;
78
+ color: white;
79
+ border-radius: 8px;
80
  display: none;
81
+ animation: slideIn 0.3s ease;
82
+ }
83
+
84
+ @keyframes slideIn {
85
+ from {
86
+ transform: translateX(100%);
87
+ opacity: 0;
88
+ }
89
+ to {
90
+ transform: translateX(0);
91
+ opacity: 1;
92
+ }
93
  }
94
  </style>
95
  </head>
96
  <body>
97
+ <!-- Loading Spinner -->
98
+ <div class="loading-spinner">
99
+ <div class="bg-white rounded-lg p-8 shadow-xl">
100
+ <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-purple-600 mx-auto"></div>
101
+ <p class="mt-4 text-gray-600">處理中...</p>
 
 
 
 
 
 
102
  </div>
103
+ </div>
104
+
105
+ <!-- Toast Notification -->
106
+ <div class="toast" id="toast"></div>
107
 
108
+ <div class="container mx-auto p-6">
109
+ <!-- Header -->
110
+ <div class="glass-effect rounded-xl p-6 mb-6">
111
+ <div class="flex items-center justify-between">
112
+ <div>
113
+ <h1 class="text-3xl font-bold text-gray-800 flex items-center">
114
+ <i class="fas fa-file-code mr-3 text-purple-600"></i>
115
+ 樂寫網徵文比賽 HTML 建立器
116
+ </h1>
117
+ <p class="text-gray-600 mt-2">輕鬆建立精美的徵文比賽頁面</p>
118
  </div>
119
+ <div class="text-gray-600">
120
+ <i class="fas fa-info-circle mr-2"></i>
121
+ 填寫變數後點擊「套用變數並生成 HTML」
122
+ </div>
123
+ </div>
124
+ </div>
125
+
126
+ <!-- Main Content -->
127
+ <div class="glass-effect rounded-xl p-6 editor-container">
128
+ <div class="grid grid-cols-2 gap-6 h-full">
129
+ <!-- Left Panel - Editor -->
130
+ <div class="space-y-4">
131
+ <!-- Only Variables Tab -->
132
+ <h3 class="text-lg font-medium text-gray-700 mb-4">
133
+ <i class="fas fa-cog mr-2"></i>變數設定
134
+ </h3>
135
+
136
+ <!-- Variables Content -->
137
+ <div class="split-panel" style="overflow-y: auto; max-height: calc(100vh - 250px);">
138
+ <!-- Hidden template -->
139
+ <textarea id="template" class="hidden">{% raw %}<!DOCTYPE html>
140
  <html lang="zh-TW">
141
  <head>
142
  <meta charset="UTF-8">
143
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
144
  <title>{{ title }}</title>
145
+ <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@300;400;500;700&display=swap" rel="stylesheet">
146
  <style>
147
+ * {
 
148
  margin: 0;
149
  padding: 0;
150
+ box-sizing: border-box;
151
+ }
152
+
153
+ body {
154
+ font-family: 'Noto Sans TC', sans-serif;
155
+ background: #f0f4f3;
156
+ color: #333;
157
+ line-height: 1.6;
158
+ }
159
+
160
+ .container {
161
+ max-width: 1400px;
162
+ margin: 0 auto;
163
+ background: white;
164
+ box-shadow: 0 0 20px rgba(0,0,0,0.1);
165
  }
166
+
167
+ /* Hero Section */
168
  .hero {
169
+ background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
170
+ padding: 60px 40px;
171
+ position: relative;
172
+ overflow: hidden;
173
+ }
174
+
175
+ .hero::after {
176
+ content: '';
177
+ position: absolute;
178
+ right: -100px;
179
+ top: -100px;
180
+ width: 300px;
181
+ height: 300px;
182
+ background: rgba(255,255,255,0.3);
183
+ border-radius: 50%;
184
+ }
185
+
186
+ .hero-content {
187
+ position: relative;
188
+ z-index: 1;
189
+ display: flex;
190
+ justify-content: space-between;
191
+ align-items: center;
192
+ gap: 40px;
193
+ }
194
+
195
+ .hero-text h1 {
196
+ font-size: 48px;
197
+ color: #2e7d32;
198
+ margin-bottom: 20px;
199
+ font-weight: 700;
200
+ }
201
+
202
+ .hero-text h2 {
203
+ font-size: 36px;
204
+ color: #388e3c;
205
+ margin-bottom: 30px;
206
+ font-weight: 500;
207
+ }
208
+
209
+ .hero-description {
210
+ font-size: 18px;
211
+ color: #555;
212
+ line-height: 1.8;
213
+ margin-bottom: 30px;
214
+ }
215
+
216
+ .hero-buttons {
217
+ display: flex;
218
+ gap: 20px;
219
+ }
220
+
221
+ .btn {
222
+ padding: 15px 30px;
223
+ border-radius: 30px;
224
+ text-decoration: none;
225
+ font-weight: 500;
226
+ transition: all 0.3s ease;
227
+ display: inline-block;
228
+ }
229
+
230
+ .btn-primary {
231
+ background: #4caf50;
232
  color: white;
233
+ }
234
+
235
+ .btn-primary:hover {
236
+ background: #45a049;
237
+ transform: translateY(-2px);
238
+ box-shadow: 0 5px 15px rgba(76,175,80,0.3);
239
+ }
240
+
241
+ .btn-secondary {
242
+ background: white;
243
+ color: #4caf50;
244
+ border: 2px solid #4caf50;
245
+ }
246
+
247
+ .btn-secondary:hover {
248
+ background: #4caf50;
249
+ color: white;
250
+ }
251
+
252
+ .hero-image {
253
+ flex-shrink: 0;
254
+ }
255
+
256
+ .hero-image img {
257
+ max-width: 500px;
258
+ width: 100%;
259
+ height: auto;
260
+ border-radius: 20px;
261
+ }
262
+
263
+ /* Info Cards Section */
264
+ .info-section {
265
+ padding: 60px 40px;
266
+ }
267
+
268
+ .section-title {
269
+ font-size: 32px;
270
+ color: #2e7d32;
271
  text-align: center;
272
+ margin-bottom: 50px;
273
+ font-weight: 600;
274
  }
275
+
276
+ .info-cards {
277
+ display: grid;
278
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
279
+ gap: 30px;
280
+ margin-bottom: 60px;
281
+ }
282
+
283
+ .info-card {
284
+ background: #f8f9fa;
285
+ padding: 30px;
286
+ border-radius: 15px;
287
+ box-shadow: 0 5px 20px rgba(0,0,0,0.08);
288
+ transition: transform 0.3s ease;
289
+ }
290
+
291
+ .info-card:hover {
292
+ transform: translateY(-5px);
293
+ }
294
+
295
+ .info-card h3 {
296
+ color: #4caf50;
297
+ font-size: 24px;
298
+ margin-bottom: 15px;
299
+ display: flex;
300
+ align-items: center;
301
+ gap: 10px;
302
+ }
303
+
304
+ .info-card .icon {
305
+ width: 30px;
306
+ height: 30px;
307
+ background: #4caf50;
308
+ border-radius: 50%;
309
+ display: inline-flex;
310
+ align-items: center;
311
+ justify-content: center;
312
+ color: white;
313
+ font-size: 16px;
314
+ }
315
+
316
+ .highlight-box {
317
+ background: #fff3cd;
318
+ border-left: 4px solid #ffc107;
319
+ padding: 20px;
320
+ margin: 20px 0;
321
+ border-radius: 5px;
322
  }
323
+
324
+ .highlight-box strong {
325
+ color: #ff9800;
 
326
  }
327
+
328
+ /* Categories Section */
329
+ .categories-section {
330
+ background: #f5f5f5;
331
+ padding: 60px 40px;
332
+ }
333
+
334
+ .category-grid {
335
+ display: grid;
336
+ grid-template-columns: repeat(2, 1fr);
337
+ gap: 40px;
338
+ max-width: 1000px;
339
+ margin: 0 auto;
340
+ }
341
+
342
+ .category-card {
343
  background: white;
344
  padding: 40px;
345
+ border-radius: 15px;
346
+ text-align: center;
347
+ box-shadow: 0 5px 20px rgba(0,0,0,0.08);
348
+ transition: all 0.3s ease;
349
+ }
350
+
351
+ .category-card:hover {
352
+ transform: translateY(-5px);
353
+ box-shadow: 0 10px 30px rgba(0,0,0,0.15);
354
+ }
355
+
356
+ .category-icon {
357
+ width: 80px;
358
+ height: 80px;
359
+ margin: 0 auto 20px;
360
+ background: #e8f5e9;
361
+ border-radius: 50%;
362
+ display: flex;
363
+ align-items: center;
364
+ justify-content: center;
365
+ font-size: 40px;
366
+ }
367
+
368
+ .category-card h3 {
369
+ color: #2e7d32;
370
+ font-size: 28px;
371
+ margin-bottom: 20px;
372
+ }
373
+
374
+ /* Scoring Table */
375
+ .scoring-section {
376
+ padding: 60px 40px;
377
+ }
378
+
379
+ .scoring-table {
380
+ width: 100%;
381
+ max-width: 800px;
382
+ margin: 0 auto;
383
+ border-collapse: collapse;
384
+ background: white;
385
+ box-shadow: 0 5px 20px rgba(0,0,0,0.08);
386
  border-radius: 10px;
387
+ overflow: hidden;
388
  }
389
+
390
+ .scoring-table th,
391
+ .scoring-table td {
392
  padding: 20px;
393
+ text-align: left;
394
+ border-bottom: 1px solid #e0e0e0;
395
  }
396
+
397
+ .scoring-table th {
398
+ background: #4caf50;
399
+ color: white;
400
+ font-weight: 500;
401
+ }
402
+
403
+ .scoring-table tr:hover {
404
+ background: #f5f5f5;
405
+ }
406
+
407
+ .scoring-table td:first-child {
408
+ font-weight: 500;
409
+ color: #2e7d32;
410
+ }
411
+
412
+ /* Timeline Section */
413
+ .timeline-section {
414
+ background: #e8f5e9;
415
+ padding: 60px 40px;
416
+ }
417
+
418
+ .timeline {
419
+ max-width: 800px;
420
+ margin: 0 auto;
421
+ }
422
+
423
+ .timeline-item {
424
+ background: white;
425
+ padding: 30px;
426
+ border-radius: 10px;
427
+ margin-bottom: 20px;
428
+ box-shadow: 0 3px 10px rgba(0,0,0,0.08);
429
+ border-left: 4px solid #4caf50;
430
+ }
431
+
432
+ .timeline-date {
433
+ color: #4caf50;
434
+ font-weight: 600;
435
+ font-size: 18px;
436
+ margin-bottom: 10px;
437
+ }
438
+
439
+ /* Footer */
440
+ .footer {
441
+ background: #2e7d32;
442
+ color: white;
443
+ padding: 40px;
444
+ text-align: center;
445
+ }
446
+
447
+ .footer a {
448
+ color: #81c784;
449
+ text-decoration: none;
450
+ }
451
+
452
+ .footer a:hover {
453
+ color: #a5d6a7;
454
+ text-decoration: underline;
455
+ }
456
+
457
+ /* Responsive */
458
+ @media (max-width: 768px) {
459
+ .hero-content {
460
+ flex-direction: column;
461
+ }
462
+
463
+ .hero-text h1 {
464
+ font-size: 36px;
465
+ }
466
+
467
+ .hero-text h2 {
468
+ font-size: 28px;
469
+ }
470
+
471
+ .category-grid {
472
+ grid-template-columns: 1fr;
473
+ }
474
+
475
+ .info-cards {
476
+ grid-template-columns: 1fr;
477
+ }
478
  }
479
  </style>
480
  </head>
481
  <body>
482
+ <div class="container">
483
+ <!-- Hero Section -->
484
+ <section class="hero">
485
+ <div class="hero-content">
486
+ <div class="hero-text">
487
+ <h1>{{ year }}{{ eventType }}</h1>
488
+ <h2>{{ theme }}</h2>
489
+ <p class="hero-description">
490
+ {{ description }}
491
+ </p>
492
+ <div class="hero-buttons">
493
+ <a href="#register" class="btn btn-primary">個人報名</a>
494
+ <a href="#register" class="btn btn-secondary">學校報名</a>
495
+ </div>
496
+ </div>
497
+ {% if heroImage %}
498
+ <div class="hero-image">
499
+ <img src="{{ heroImage }}" alt="活動主視覺">
500
+ </div>
501
+ {% endif %}
502
  </div>
503
+ </section>
504
+
505
+ <!-- Activity Info Section -->
506
+ <section class="info-section">
507
+ <h2 class="section-title">活動辦法</h2>
508
+
509
+ <div class="info-cards">
510
+ <div class="info-card">
511
+ <h3><span class="icon">👥</span> 參加對象</h3>
512
+ <p>{{ participants }}</p>
513
+ </div>
514
+
515
+ <div class="info-card">
516
+ <h3><span class="icon">📝</span> 進行方式</h3>
517
+ <p>{{ method }}</p>
518
+ {% if methodHighlight %}
519
+ <div class="highlight-box">
520
+ <strong>第一關:</strong>{{ methodStep1 }}<br>
521
+ <strong>活動期間:</strong>{{ activityPeriod }}
522
+ </div>
523
+ {% endif %}
524
+ </div>
525
+
526
+ <div class="info-card">
527
+ <h3><span class="icon">🏆</span> 積分獎</h3>
528
+ <p>{{ scoringInfo }}</p>
529
+ {% if scoringHighlight %}
530
+ <div class="highlight-box">
531
+ {{ scoringHighlight }}
532
+ </div>
533
+ {% endif %}
534
+ </div>
535
+ </div>
536
+
537
+ {% if stage2Info %}
538
+ <div class="info-cards">
539
+ <div class="info-card">
540
+ <h3><span class="icon">✍️</span> 第二關:{{ stage2Title }}</h3>
541
+ <p><strong>活動期間:</strong>{{ stage2Period }}</p>
542
+ <p>{{ stage2Description }}</p>
543
+ </div>
544
+
545
+ <div class="info-card">
546
+ <h3><span class="icon">🎁</span> {{ stage2AwardTitle }}</h3>
547
+ <p>{{ stage2AwardInfo }}</p>
548
+ </div>
549
+ </div>
550
+ {% endif %}
551
+ </section>
552
+
553
+ <!-- Categories Section -->
554
+ {% if hasCategories %}
555
+ <section class="categories-section">
556
+ <h2 class="section-title">報名說明</h2>
557
+
558
+ <div class="category-grid">
559
+ <div class="category-card">
560
+ <div class="category-icon">👤</div>
561
+ <h3>個人報名</h3>
562
+ <p>{{ individualSignup }}</p>
563
+ </div>
564
+
565
+ <div class="category-card">
566
+ <div class="category-icon">🏫</div>
567
+ <h3>學校報名</h3>
568
+ <p>{{ schoolSignup }}</p>
569
+ </div>
570
+ </div>
571
+ </section>
572
+ {% endif %}
573
+
574
+ <!-- Scoring Table -->
575
+ {% if scoringCriteria %}
576
+ <section class="scoring-section">
577
+ <h2 class="section-title">評分標準</h2>
578
+
579
+ <table class="scoring-table">
580
+ <thead>
581
+ <tr>
582
+ <th>評分項目</th>
583
+ <th>評分占比</th>
584
+ <th>內容說明</th>
585
+ </tr>
586
+ </thead>
587
+ <tbody>
588
+ {% for criteria in scoringCriteria %}
589
+ <tr>
590
+ <td>{{ criteria.item }}</td>
591
+ <td>{{ criteria.percentage }}</td>
592
+ <td>{{ criteria.description }}</td>
593
+ </tr>
594
+ {% endfor %}
595
+ </tbody>
596
+ </table>
597
+ </section>
598
+ {% endif %}
599
+
600
+ <!-- Timeline Section -->
601
+ {% if timeline %}
602
+ <section class="timeline-section">
603
+ <h2 class="section-title">重要日期與聯絡方式</h2>
604
+
605
+ <div class="timeline">
606
+ {% for event in timeline %}
607
+ <div class="timeline-item">
608
+ <div class="timeline-date">{{ event.date }}</div>
609
+ <div class="timeline-content">{{ event.description }}</div>
610
+ </div>
611
+ {% endfor %}
612
+ </div>
613
+ </section>
614
+ {% endif %}
615
+
616
+ <!-- Footer -->
617
+ <footer class="footer">
618
+ <h3>{{ footerTitle }}</h3>
619
+ {% if websiteUrl %}
620
+ <p>活動網頁:<a href="{{ websiteUrl }}" target="_blank">{{ websiteUrl }}</a></p>
621
+ {% endif %}
622
+ {% if facebookUrl %}
623
+ <p>Facebook:<a href="{{ facebookUrl }}" target="_blank">{{ facebookUrl }}</a></p>
624
+ {% endif %}
625
+ {% if contactInfo %}
626
+ <p class="contact-info">{{ contactInfo }}</p>
627
+ {% endif %}
628
+ </footer>
629
  </div>
630
  </body>
631
+ </html>{% endraw %}</textarea>
632
+
633
+ <!-- Variables Section -->
634
+ <div id="variables-content">
635
+ <div class="mb-4">
636
+ <label class="block text-sm font-medium text-gray-700 mb-2">
637
+ 變數設定
638
+ </label>
639
+ <div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
640
+ <p class="text-sm text-blue-800">
641
+ <i class="fas fa-info-circle mr-2"></i>
642
+ 填寫下方表單來設定模板變數,或切換到 JSON 模式進行進階編輯
643
+ </p>
644
+ </div>
645
+
646
+ <!-- Toggle between Form and JSON mode -->
647
+ <div class="flex justify-between mb-4">
648
+ <button onclick="applyVariables()" class="bg-green-500 hover:bg-green-600 text-white px-6 py-2 rounded-lg font-medium flex items-center">
649
+ <i class="fas fa-sync-alt mr-2"></i>套用變數並生成 HTML
650
+ </button>
651
+ <div class="bg-gray-100 rounded-lg p-1 inline-flex">
652
+ <button id="form-mode-btn" onclick="switchVariableMode('form')" class="px-4 py-2 bg-white rounded text-sm font-medium text-gray-700 shadow-sm">
653
+ <i class="fas fa-edit mr-2"></i>表單模式
654
+ </button>
655
+ <button id="json-mode-btn" onclick="switchVariableMode('json')" class="px-4 py-2 rounded text-sm font-medium text-gray-700">
656
+ <i class="fas fa-code mr-2"></i>JSON 模式
657
+ </button>
658
+ </div>
659
+ </div>
660
+
661
+ <!-- Form Mode -->
662
+ <div id="variables-form" class="space-y-4">
663
+ <!-- 基本資訊 -->
664
+ <div class="bg-gray-50 p-4 rounded-lg mb-4">
665
+ <h4 class="text-sm font-semibold text-gray-700 mb-3">基本資訊</h4>
666
+ <div class="space-y-3">
667
+ <div>
668
+ <label class="block text-sm font-medium text-gray-700 mb-1">網頁標題</label>
669
+ <input type="text" id="var-title" value="2025聯發科技公益活動" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
670
+ </div>
671
+ <div>
672
+ <label class="block text-sm font-medium text-gray-700 mb-1">年份</label>
673
+ <input type="text" id="var-year" value="2025" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
674
+ </div>
675
+ <div>
676
+ <label class="block text-sm font-medium text-gray-700 mb-1">活動類型</label>
677
+ <input type="text" id="var-eventType" value="聯發科技公益活動" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
678
+ </div>
679
+ <div>
680
+ <label class="block text-sm font-medium text-gray-700 mb-1">活動主題</label>
681
+ <input type="text" id="var-theme" value="世界觀察員計畫" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
682
+ </div>
683
+ <div>
684
+ <label class="block text-sm font-medium text-gray-700 mb-1">活動描述</label>
685
+ <textarea id="var-description" rows="3" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">讓孩子透過文字探索自然,成為真正的世界觀察員!樂寫公益學習網誠摯邀請全國國小學生,參加『自然書寫徵文比賽』,展現你獨特的觀察力與創意思維。</textarea>
686
+ </div>
687
+ </div>
688
+ </div>
689
+
690
+ <!-- 活動資訊 -->
691
+ <div class="bg-gray-50 p-4 rounded-lg mb-4">
692
+ <h4 class="text-sm font-semibold text-gray-700 mb-3">活動資訊</h4>
693
+ <div class="space-y-3">
694
+ <div>
695
+ <label class="block text-sm font-medium text-gray-700 mb-1">參加對象</label>
696
+ <input type="text" id="var-participants" value="全國公私立國小學生(本屆小六畢業生亦可參加)。" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
697
+ </div>
698
+ <div>
699
+ <label class="block text-sm font-medium text-gray-700 mb-1">進行方式</label>
700
+ <textarea id="var-method" rows="2" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">本活動分為兩關兩獎進行,讓學生循序漸進培養寫作能力</textarea>
701
+ </div>
702
+ <div>
703
+ <label class="block text-sm font-medium text-gray-700 mb-1">第一關說明</label>
704
+ <input type="text" id="var-methodStep1" value="平台陪練寫作積分" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
705
+ </div>
706
+ <div>
707
+ <label class="block text-sm font-medium text-gray-700 mb-1">活動期間</label>
708
+ <textarea id="var-activityPeriod" rows="2" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">報名至2025年8月30日截止,於樂寫公益平台發表至少三篇文章,累積寫作經驗。</textarea>
709
+ </div>
710
+ <div>
711
+ <label class="block text-sm font-medium text-gray-700 mb-1">積分獎說明</label>
712
+ <input type="text" id="var-scoringInfo" value="企業參訪體驗" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
713
+ </div>
714
+ <div>
715
+ <label class="block text-sm font-medium text-gray-700 mb-1">積分獎重點</label>
716
+ <textarea id="var-scoringHighlight" rows="2" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">從報名活動到2025年8月31日前,於樂寫公益學習平台積分最高的前三十名得主,可獲得聯發科技企業參訪活動體驗資格</textarea>
717
+ </div>
718
+ </div>
719
+ </div>
720
+
721
+ <!-- 第二關資訊 -->
722
+ <div class="bg-gray-50 p-4 rounded-lg mb-4">
723
+ <h4 class="text-sm font-semibold text-gray-700 mb-3">第二關:徵文比賽</h4>
724
+ <div class="space-y-3">
725
+ <div>
726
+ <label class="block text-sm font-medium text-gray-700 mb-1">第二關標題</label>
727
+ <input type="text" id="var-stage2Title" value="徵文比賽" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
728
+ </div>
729
+ <div>
730
+ <label class="block text-sm font-medium text-gray-700 mb-1">第二關期間</label>
731
+ <input type="text" id="var-stage2Period" value="2025年9月1日至9月30日" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
732
+ </div>
733
+ <div>
734
+ <label class="block text-sm font-medium text-gray-700 mb-1">第二關說明</label>
735
+ <textarea id="var-stage2Description" rows="2" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">通過第一關的學員將於8月31日收到徵文比賽邀請通知及徵文題目。</textarea>
736
+ </div>
737
+ <div>
738
+ <label class="block text-sm font-medium text-gray-700 mb-1">徵文獎標題</label>
739
+ <input type="text" id="var-stage2AwardTitle" value="徵文獎:企業受獎殊榮" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
740
+ </div>
741
+ <div>
742
+ <label class="block text-sm font-medium text-gray-700 mb-1">徵文獎說明</label>
743
+ <textarea id="var-stage2AwardInfo" rows="3" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">初審入圍前一百名者可獲得電子獎狀一幅,複審前三十名者:榮獲流臨聯發科技頒獎,現場公布得獎獎項。</textarea>
744
+ </div>
745
+ </div>
746
+ </div>
747
+
748
+ <!-- 報名資訊 -->
749
+ <div class="bg-gray-50 p-4 rounded-lg mb-4">
750
+ <h4 class="text-sm font-semibold text-gray-700 mb-3">報名資訊</h4>
751
+ <div class="space-y-3">
752
+ <div>
753
+ <label class="block text-sm font-medium text-gray-700 mb-1">個人報名說明</label>
754
+ <textarea id="var-individualSignup" rows="3" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">請先填寫線上表單進行報名,平台會寄送確認信件開通您的帳號。完成報名後,即可開始在樂寫公益平台發表文章,累積寫作積分。</textarea>
755
+ </div>
756
+ <div>
757
+ <label class="block text-sm font-medium text-gray-700 mb-1">學校報名說明</label>
758
+ <textarea id="var-schoolSignup" rows="3" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">以學校或班級為單位填寫表單報名,樂寫專員將主動與校方聯繫,協助學生集體開通帳號,讓老師能夠更便利帶領全班參與這次有意義的寫作活動。</textarea>
759
+ </div>
760
+ </div>
761
+ </div>
762
+
763
+ <!-- 評分與時程 -->
764
+ <div class="bg-gray-50 p-4 rounded-lg mb-4">
765
+ <h4 class="text-sm font-semibold text-gray-700 mb-3">評分與時程</h4>
766
+ <div class="space-y-3">
767
+ <div>
768
+ <label class="block text-sm font-medium text-gray-700 mb-1">評分標準 (JSON 格式)</label>
769
+ <textarea id="var-scoringCriteria" rows="6" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono text-xs">[
770
+ {"item": "內容與結構", "percentage": "25%", "description": "切合題旨,思想積極健康,論點合理,文字生動;結構嚴謹,行文流暢,兼具廣度與深度。"},
771
+ {"item": "邏輯與修辭", "percentage": "25%", "description": "邏輯分明,條理清晰,敘事明白;用字遣詞合宜,修辭靈活優美。"},
772
+ {"item": "創意與觀察", "percentage": "20%", "description": "富含想像力,觀察微小細節,洞悉人性幽微。"},
773
+ {"item": "平台練習積分", "percentage": "20%", "description": "2025.5.1~2025.8.30期間,練習篇數與教練評分會影響最終積分。"},
774
+ {"item": "標點符號與字詞正確", "percentage": "10%", "description": "標點符號運用得宜,詞能達義,無錯別字。"}
775
+ ]</textarea>
776
+ </div>
777
+ <div>
778
+ <label class="block text-sm font-medium text-gray-700 mb-1">時間線 (JSON 格式)</label>
779
+ <textarea id="var-timeline" rows="3" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono text-xs">[
780
+ {"date": "2025年11月17日(星期一)", "description": "於聯發科技總部舉行盛大頒獎典禮,並於當日公告最終得獎名單。得獎者將獲邀參加,一同分享寫作的喜悅與成果!"}
781
+ ]</textarea>
782
+ </div>
783
+ </div>
784
+ </div>
785
+
786
+ <!-- 聯絡資訊 -->
787
+ <div class="bg-gray-50 p-4 rounded-lg mb-4">
788
+ <h4 class="text-sm font-semibold text-gray-700 mb-3">聯絡資訊</h4>
789
+ <div class="space-y-3">
790
+ <div>
791
+ <label class="block text-sm font-medium text-gray-700 mb-1">頁尾標題</label>
792
+ <input type="text" id="var-footerTitle" value="活動資訊與聯絡方式" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
793
+ </div>
794
+ <div>
795
+ <label class="block text-sm font-medium text-gray-700 mb-1">活動網頁</label>
796
+ <input type="url" id="var-websiteUrl" value="https://www.colearn30.com/" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
797
+ </div>
798
+ <div>
799
+ <label class="block text-sm font-medium text-gray-700 mb-1">Facebook 頁面</label>
800
+ <input type="url" id="var-facebookUrl" value="https://www.facebook.com/CoWrite30" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
801
+ </div>
802
+ <div>
803
+ <label class="block text-sm font-medium text-gray-700 mb-1">聯絡說明</label>
804
+ <textarea id="var-contactInfo" rows="2" class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">如有任何問題,歡迎透過樂寫公益學習臉書聯繫我們</textarea>
805
+ </div>
806
+ </div>
807
+ </div>
808
+ <div>
809
+ <button onclick="addCustomVariable()" class="text-purple-600 hover:text-purple-800 text-sm font-medium">
810
+ <i class="fas fa-plus-circle mr-1"></i>新增自訂變數
811
+ </button>
812
+ </div>
813
+ <div id="custom-variables" class="space-y-3">
814
+ <!-- Custom variables will be added here -->
815
+ </div>
816
+ </div>
817
+
818
+ <!-- JSON Mode -->
819
+ <div id="variables-json" class="hidden">
820
+ <textarea id="variables" class="w-full h-96 p-4 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent font-mono text-sm" placeholder='{"title": "標題", "subtitle": "副標題"}'>
821
  {
822
+ "title": "2025聯發科技公益活動",
823
+ "year": "2025",
824
+ "eventType": "聯發科技公益活動",
825
+ "theme": "世界觀察員計畫",
826
+ "description": "讓孩子透過文字探索自然,成為真正的世界觀察員!樂寫公益學習網誠摯邀請全國國小學生,參加『自然書寫徵文比賽』,展現你獨特的觀察力與創意思維。",
827
+ "participants": "全國公私立國小學生(本屆小六畢業生亦可參加)。",
828
+ "method": "本活動分為兩關兩獎進行,讓學生循序漸進培養寫作能力",
829
+ "methodStep1": "平台陪練寫作積分",
830
+ "activityPeriod": "報名至2025年8月30日截止,於樂寫公益平台發表至少三篇文章,累積寫作經驗。",
831
+ "scoringInfo": "企業參訪體驗",
832
+ "scoringHighlight": "從報名活動到2025年8月31日前,於樂寫公益學習平台積分最高的前三十名得主,可獲得聯發科技企業參訪活動體驗資格",
833
+ "methodHighlight": true,
834
+ "stage2Info": true,
835
+ "stage2Title": "徵文比賽",
836
+ "stage2Period": "2025年9月1日至9月30日",
837
+ "stage2Description": "通過第一關的學員將於8月31日收到徵文比賽邀請通知及徵文題目。",
838
+ "stage2AwardTitle": "徵文獎:企業受獎殊榮",
839
+ "stage2AwardInfo": "初審入圍前一百名者可獲得電子獎狀一幅,複審前三十名者:榮獲流臨聯發科技頒獎,現場公布得獎獎項。",
840
+ "hasCategories": true,
841
+ "individualSignup": "請先填寫線上表單進行報名,平台會寄送確認信件開通您的帳號。完成報名後,即可開始在樂寫公益平台發表文章,累積寫作積分。",
842
+ "schoolSignup": "以學校或班級為單位填寫表單報名,樂寫專員將主動與校��聯繫,協助學生集體開通帳號,讓老師能夠更便利帶領全班參與這次有意義的寫作活動。",
843
+ "scoringCriteria": [
844
+ {"item": "內容與結構", "percentage": "25%", "description": "切合題旨,思想積極健康,論點合理,文字生動;結構嚴謹,行文流暢,兼具廣度與深度。"},
845
+ {"item": "邏輯與修辭", "percentage": "25%", "description": "邏輯分明,條理清晰,敘事明白;用字遣詞合宜,修辭靈活優美。"},
846
+ {"item": "創意與觀察", "percentage": "20%", "description": "富含想像力,觀察微小細節,洞悉人性幽微。"},
847
+ {"item": "平台練習積分", "percentage": "20%", "description": "2025.5.1~2025.8.30期間,練習篇數與教練評分會影響最終積分。"},
848
+ {"item": "標點符號與字詞正確", "percentage": "10%", "description": "標點符號運用得宜,詞能達義,無錯別字。"}
849
+ ],
850
+ "timeline": [
851
+ {"date": "2025年11月17日(星期一)", "description": "於聯發科技總部舉行盛大頒獎典禮,並於當日公告最終得獎名單。得獎者將獲邀參加,一同分享寫作的喜悅與成果!"}
852
+ ],
853
+ "footerTitle": "活動資訊與聯絡方式",
854
+ "websiteUrl": "https://www.colearn30.com/",
855
+ "facebookUrl": "https://www.facebook.com/CoWrite30",
856
+ "contactInfo": "如有任何問題,歡迎透過樂寫公益學習臉書聯繫我們"
857
  }</textarea>
858
+ </div>
859
+ </div>
860
+ </div>
861
+ </div>
862
+ </div>
863
 
864
+ <!-- Right Panel - HTML Source Code -->
865
+ <div class="space-y-4">
866
+ <div class="flex items-center justify-between">
867
+ <h3 class="text-lg font-medium text-gray-700">
868
+ <i class="fas fa-code mr-2"></i>HTML 原始碼
869
+ </h3>
870
+ <div class="flex gap-2">
871
+ <button onclick="previewHTML()" class="text-sm text-purple-600 hover:text-purple-800">
872
+ <i class="fas fa-eye mr-1"></i>預覽
873
+ </button>
874
+ <button onclick="copyToClipboard()" class="text-sm text-purple-600 hover:text-purple-800">
875
+ <i class="fas fa-copy mr-1"></i>複製
876
+ </button>
877
+ <button onclick="downloadHTML()" class="text-sm text-purple-600 hover:text-purple-800">
878
+ <i class="fas fa-download mr-1"></i>下載
879
+ </button>
880
+ </div>
881
+ </div>
882
+ <div class="bg-gray-900 rounded-lg p-4 split-panel" style="overflow-y: auto; max-height: calc(100vh - 250px);">
883
+ <pre class="text-green-400 text-sm font-mono" id="source-code"><code><!-- 點擊「套用變數並生成 HTML」按鈕後,這裡會顯示渲染後的 HTML 原始碼 --></code></pre>
884
+ </div>
885
+ </div>
 
 
 
886
  </div>
887
+ </div>
 
 
888
  </div>
889
 
890
  <script>
891
+ let currentTab = 'template';
892
+ const awardsData = {{ awards_data | tojson | safe }};
893
+
894
+ // No more tab switching needed
895
+
896
+ function showToast(message, type = 'success') {
897
+ const toast = document.getElementById('toast');
898
+ toast.textContent = message;
899
+ if (type === 'success') {
900
+ toast.style.background = '#10b981';
901
+ } else if (type === 'error') {
902
+ toast.style.background = '#ef4444';
903
+ } else if (type === 'info') {
904
+ toast.style.background = '#3b82f6';
905
+ }
906
+ toast.style.display = 'block';
907
+
908
+ setTimeout(() => {
909
+ toast.style.display = 'none';
910
+ }, 3000);
911
+ }
912
+
913
+ function showLoading(show = true) {
914
+ document.querySelector('.loading-spinner').style.display = show ? 'block' : 'none';
915
+ }
916
+
917
+ function updatePreview() {
918
+ try {
919
+ const template = document.getElementById('template').value;
920
+ const variables = JSON.parse(document.getElementById('variables').value);
921
+
922
+ // Add awards data to variables
923
+ variables.awards_data = awardsData;
924
+
925
+ // Enhanced template rendering
926
+ let preview = template;
927
+
928
+ // Replace simple variables first
929
+ Object.keys(variables).forEach(key => {
930
+ if (typeof variables[key] === 'string' || typeof variables[key] === 'number') {
931
+ const regex = new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'g');
932
+ preview = preview.replace(regex, variables[key]);
933
+ }
934
+ });
935
+
936
+ // Handle if statements
937
+ preview = preview.replace(/\{\%\s*if\s+(\w+)\s*\%\}([\s\S]*?)\{\%\s*endif\s*\%\}/g, (match, condition, content) => {
938
+ return variables[condition] ? content : '';
939
+ });
940
+
941
+ // Handle nested if statements (like if organizer)
942
+ preview = preview.replace(/\{\%\s*if\s+(\w+)\s*\%\}([\s\S]*?)(?:\{\%\s*endif\s*\%\})/g, (match, condition, content) => {
943
+ return variables[condition] ? content : '';
944
+ });
945
+
946
+ // Handle for loops with arrays like scoringCriteria
947
+ if (variables.scoringCriteria && Array.isArray(variables.scoringCriteria)) {
948
+ const scoringMatch = preview.match(/\{\%\s*for\s+criteria\s+in\s+scoringCriteria\s*\%\}([\s\S]*?)\{\%\s*endfor\s*\%\}/);
949
+ if (scoringMatch) {
950
+ const loopContent = scoringMatch[1];
951
+ let renderedLoop = '';
952
+
953
+ variables.scoringCriteria.forEach(criteria => {
954
+ let itemContent = loopContent;
955
+ Object.keys(criteria).forEach(key => {
956
+ const regex = new RegExp(`\\{\\{\\s*criteria\\.${key}\\s*\\}\\}`, 'g');
957
+ itemContent = itemContent.replace(regex, criteria[key] || '');
958
+ });
959
+ renderedLoop += itemContent;
960
+ });
961
+
962
+ preview = preview.replace(scoringMatch[0], renderedLoop);
963
+ }
964
+ }
965
+
966
+ // Handle timeline loop
967
+ if (variables.timeline && Array.isArray(variables.timeline)) {
968
+ const timelineMatch = preview.match(/\{\%\s*for\s+event\s+in\s+timeline\s*\%\}([\s\S]*?)\{\%\s*endfor\s*\%\}/);
969
+ if (timelineMatch) {
970
+ const loopContent = timelineMatch[1];
971
+ let renderedLoop = '';
972
+
973
+ variables.timeline.forEach(event => {
974
+ let itemContent = loopContent;
975
+ Object.keys(event).forEach(key => {
976
+ const regex = new RegExp(`\\{\\{\\s*event\\.${key}\\s*\\}\\}`, 'g');
977
+ itemContent = itemContent.replace(regex, event[key] || '');
978
+ });
979
+ renderedLoop += itemContent;
980
+ });
981
+
982
+ preview = preview.replace(timelineMatch[0], renderedLoop);
983
+ }
984
+ }
985
+
986
+ // Handle awards loop
987
+ if (awardsData && awardsData.length > 0) {
988
+ const loopMatch = preview.match(/\{\%\s*for\s+award\s+in\s+awards_data\s*\%\}([\s\S]*?)\{\%\s*endfor\s*\%\}/);
989
+ if (loopMatch) {
990
+ const loopContent = loopMatch[1];
991
+ let renderedLoop = '';
992
+
993
+ awardsData.forEach(award => {
994
+ let itemContent = loopContent;
995
+ Object.keys(award).forEach(key => {
996
+ const regex = new RegExp(`\\{\\{\\s*award\\.${key}\\s*\\}\\}`, 'g');
997
+ itemContent = itemContent.replace(regex, award[key] || '');
998
+ });
999
+ renderedLoop += itemContent;
1000
+ });
1001
+
1002
+ preview = preview.replace(loopMatch[0], renderedLoop);
1003
+ }
1004
+ }
1005
+
1006
+ // Clean up any remaining template syntax
1007
+ preview = preview.replace(/\{\%[^%]*\%\}/g, '');
1008
+ preview = preview.replace(/\{\{[^}]*\}\}/g, '');
1009
+
1010
+ // This function is no longer needed for preview
1011
+ // The actual rendering is done by the API
1012
+ } catch (error) {
1013
+ console.error('Preview error:', error);
1014
+ }
1015
+ }
1016
+
1017
+ function previewHTML() {
1018
+ const sourceCode = document.getElementById('source-code').textContent;
1019
+
1020
+ // Check if HTML has been generated
1021
+ if (sourceCode.includes('<!-- 點擊')) {
1022
+ showToast('請先生成 HTML!', 'error');
1023
+ return;
1024
+ }
1025
+
1026
+ // Open in new window
1027
+ const previewWindow = window.open('', '_blank', 'width=1200,height=800');
1028
+ previewWindow.document.write(sourceCode);
1029
+ previewWindow.document.close();
1030
+ }
1031
+
1032
+ function copyToClipboard() {
1033
+ const sourceCode = document.getElementById('source-code').textContent;
1034
+
1035
+ // Check if HTML has been generated
1036
+ if (sourceCode.includes('<!-- 點擊')) {
1037
+ showToast('請先生成 HTML!', 'error');
1038
+ return;
1039
+ }
1040
+
1041
+ navigator.clipboard.writeText(sourceCode).then(() => {
1042
+ showToast('已複製到剪貼簿!', 'success');
1043
+ }).catch(err => {
1044
+ showToast('複製失敗', 'error');
1045
+ });
1046
+ }
1047
+
1048
+ function downloadHTML() {
1049
+ const sourceCode = document.getElementById('source-code').textContent;
1050
+
1051
+ // Check if HTML has been generated
1052
+ if (sourceCode.includes('<!-- 點擊')) {
1053
+ showToast('請先生成 HTML!', 'error');
1054
+ return;
1055
+ }
1056
+
1057
+ const blob = new Blob([sourceCode], { type: 'text/html;charset=utf-8' });
1058
+ const url = window.URL.createObjectURL(blob);
1059
+ const a = document.createElement('a');
1060
+ a.href = url;
1061
+ a.download = `generated_${new Date().toISOString().slice(0, 10)}.html`;
1062
+ document.body.appendChild(a);
1063
+ a.click();
1064
+ window.URL.revokeObjectURL(url);
1065
+ document.body.removeChild(a);
1066
+ showToast('HTML 檔案已下載!', 'success');
1067
+ }
1068
+
1069
+ async function generateHTML() {
1070
+ // This function is now replaced by the preview functionality
1071
+ applyVariables();
1072
+ }
1073
+
1074
+ // Remove auto-update on input
1075
+ // Only update when user clicks the apply button
1076
+
1077
+ function debounce(func, wait) {
1078
+ let timeout;
1079
+ return function executedFunction(...args) {
1080
+ const later = () => {
1081
+ clearTimeout(timeout);
1082
+ func(...args);
1083
+ };
1084
+ clearTimeout(timeout);
1085
+ timeout = setTimeout(later, wait);
1086
+ };
1087
+ }
1088
+
1089
+ // Variable mode switching
1090
+ let variableMode = 'form';
1091
+ let customVarCount = 0;
1092
+
1093
+ function switchVariableMode(mode) {
1094
+ variableMode = mode;
1095
+
1096
+ if (mode === 'form') {
1097
+ document.getElementById('form-mode-btn').classList.add('bg-white', 'shadow-sm');
1098
+ document.getElementById('json-mode-btn').classList.remove('bg-white', 'shadow-sm');
1099
+ document.getElementById('variables-form').classList.remove('hidden');
1100
+ document.getElementById('variables-json').classList.add('hidden');
1101
+
1102
+ // Load JSON data into form
1103
+ try {
1104
+ const jsonData = JSON.parse(document.getElementById('variables').value);
1105
+ Object.keys(jsonData).forEach(key => {
1106
+ const element = document.getElementById(`var-${key}`);
1107
+ if (element) {
1108
+ element.value = jsonData[key] || '';
1109
+ }
1110
+ });
1111
+ } catch (e) {
1112
+ console.error('Error parsing JSON:', e);
1113
+ }
1114
+ } else {
1115
+ document.getElementById('json-mode-btn').classList.add('bg-white', 'shadow-sm');
1116
+ document.getElementById('form-mode-btn').classList.remove('bg-white', 'shadow-sm');
1117
+ document.getElementById('variables-form').classList.add('hidden');
1118
+ document.getElementById('variables-json').classList.remove('hidden');
1119
+
1120
+ // Update JSON from form
1121
+ updateJsonFromForm();
1122
+ }
1123
+ }
1124
+
1125
+ function updateJsonFromForm() {
1126
+ const variables = {};
1127
+
1128
+ // Get all form field values
1129
+ formFields.forEach(field => {
1130
+ const element = document.getElementById(`var-${field}`);
1131
+ if (element) {
1132
+ variables[field] = element.value;
1133
+ }
1134
+ });
1135
+
1136
+ // Add boolean flags
1137
+ variables.methodHighlight = true;
1138
+ variables.stage2Info = true;
1139
+ variables.hasCategories = true;
1140
+
1141
+ // Parse scoring criteria from JSON if exists
1142
+ try {
1143
+ const scoringElement = document.getElementById('var-scoringCriteria');
1144
+ if (scoringElement && scoringElement.value) {
1145
+ variables.scoringCriteria = JSON.parse(scoringElement.value);
1146
+ }
1147
+ } catch (e) {
1148
+ console.error('Error parsing scoring criteria:', e);
1149
+ }
1150
+
1151
+ // Parse timeline from JSON if exists
1152
+ try {
1153
+ const timelineElement = document.getElementById('var-timeline');
1154
+ if (timelineElement && timelineElement.value) {
1155
+ variables.timeline = JSON.parse(timelineElement.value);
1156
+ }
1157
+ } catch (e) {
1158
+ console.error('Error parsing timeline:', e);
1159
+ }
1160
+
1161
+ // Add custom variables
1162
+ const customVars = document.querySelectorAll('[id^="custom-var-"]');
1163
+ customVars.forEach(input => {
1164
+ const key = input.getAttribute('data-key');
1165
+ if (key && input.value) {
1166
+ variables[key] = input.value;
1167
+ }
1168
+ });
1169
+
1170
+ document.getElementById('variables').value = JSON.stringify(variables, null, 4);
1171
+ }
1172
+
1173
+ function addCustomVariable() {
1174
+ customVarCount++;
1175
+ const container = document.getElementById('custom-variables');
1176
+ const div = document.createElement('div');
1177
+ div.className = 'flex gap-2';
1178
+ div.innerHTML = `
1179
+ <input type="text" id="custom-key-${customVarCount}" placeholder="變數名稱" class="flex-1 px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
1180
+ <input type="text" id="custom-var-${customVarCount}" placeholder="變數值" class="flex-1 px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent">
1181
+ <button onclick="removeCustomVariable(this)" class="px-3 py-2 text-red-600 hover:text-red-800">
1182
+ <i class="fas fa-trash"></i>
1183
+ </button>
1184
+ `;
1185
+ container.appendChild(div);
1186
+
1187
+ // Update data-key when name changes
1188
+ document.getElementById(`custom-key-${customVarCount}`).addEventListener('input', function(e) {
1189
+ const valueInput = document.getElementById(`custom-var-${e.target.id.replace('custom-key-', 'custom-var-')}`);
1190
+ valueInput.setAttribute('data-key', e.target.value);
1191
+ if (variableMode === 'form') {
1192
+ updateJsonFromForm();
1193
+ }
1194
+ });
1195
+
1196
+ // Update JSON when value changes
1197
+ document.getElementById(`custom-var-${customVarCount}`).addEventListener('input', function() {
1198
+ if (variableMode === 'form') {
1199
+ updateJsonFromForm();
1200
+ }
1201
+ });
1202
+ }
1203
+
1204
+ function removeCustomVariable(button) {
1205
+ button.parentElement.remove();
1206
+ if (variableMode === 'form') {
1207
+ updateJsonFromForm();
1208
+ }
1209
+ }
1210
+
1211
+ // Add event listeners for form inputs
1212
+ const formFields = [
1213
+ 'title', 'year', 'eventType', 'theme', 'description',
1214
+ 'participants', 'method', 'methodStep1', 'activityPeriod',
1215
+ 'scoringInfo', 'scoringHighlight', 'stage2Title', 'stage2Period',
1216
+ 'stage2Description', 'stage2AwardTitle', 'stage2AwardInfo',
1217
+ 'individualSignup', 'schoolSignup', 'footerTitle',
1218
+ 'websiteUrl', 'facebookUrl', 'contactInfo'
1219
+ ];
1220
+
1221
+ // Remove auto-update listeners - only sync form to JSON when switching modes
1222
+
1223
+ // Add apply variables function
1224
+ async function applyVariables() {
1225
+ console.log('========== 開始生成 HTML ==========');
1226
+ showToast('正在生成 HTML...', 'info');
1227
+
1228
+ // Step 1: 檢查當前模式並更新 JSON
1229
+ console.log('Step 1: 當前模式:', variableMode);
1230
+ if (variableMode === 'form') {
1231
+ console.log('Step 1.1: 執行 updateJsonFromForm()');
1232
+ updateJsonFromForm();
1233
+ }
1234
+
1235
+ // Step 2: 取得模板和變數內容
1236
+ const templateContent = document.getElementById('template').value;
1237
+ const variablesContent = document.getElementById('variables').value;
1238
+
1239
+ console.log('Step 2: 準備發送的資料');
1240
+ console.log('Template 長度:', templateContent.length, '字元');
1241
+ console.log('Template 預覽:', templateContent.substring(0, 200) + '...');
1242
+ console.log('Variables 內容:', variablesContent);
1243
+
1244
+ // Step 3: 解析並驗證 JSON
1245
  try {
1246
+ const parsedVariables = JSON.parse(variablesContent);
1247
+ console.log('Step 3: JSON 解析成功');
1248
+ console.log('變數物件:', parsedVariables);
1249
+ console.log('變數 keys:', Object.keys(parsedVariables));
1250
+ } catch (e) {
1251
+ console.error('Step 3: JSON 解析失敗:', e);
1252
+ }
1253
+
1254
+ // Step 4: 準備 FormData
1255
+ const formData = new FormData();
1256
+ formData.append('template_content', templateContent);
1257
+ formData.append('variables', variablesContent);
1258
+
1259
+ console.log('Step 4: FormData 準備完成');
1260
+ console.log('FormData entries:');
1261
+ for (let [key, value] of formData.entries()) {
1262
+ console.log(` ${key}:`, value.substring(0, 100) + '...');
1263
+ }
1264
+
1265
+ try {
1266
+ // Step 5: 發送請求
1267
+ console.log('Step 5: 發送 POST 請求到 /api/preview');
1268
+ const response = await fetch('/api/preview', {
1269
+ method: 'POST',
1270
+ body: formData
1271
+ });
1272
+
1273
+ console.log('Step 6: 收到回應');
1274
+ console.log('Response status:', response.status);
1275
+ console.log('Response headers:', response.headers);
1276
+
1277
+ // Step 7: 解析回應
1278
+ const result = await response.json();
1279
+ console.log('Step 7: 解析回應 JSON');
1280
+ console.log('完整回應:', result);
1281
+
1282
+ if (result.debug) {
1283
+ console.log('Debug 資訊:', result.debug);
1284
+ }
1285
+
1286
+ if (result.error) {
1287
+ console.error('Step 8: 後端回傳錯誤');
1288
+ console.error('錯誤訊息:', result.error);
1289
+ if (result.debug && result.debug.traceback) {
1290
+ console.error('錯誤堆疊:', result.debug.traceback);
1291
+ }
1292
+
1293
+ showToast(`錯誤:${result.error}`, 'error');
1294
+ document.getElementById('source-code').innerHTML = `<code style="color: #ef4444;">錯誤:${result.error}</code>`;
1295
+ } else {
1296
+ console.log('Step 8: 成功生成 HTML');
1297
+ console.log('HTML 長度:', result.html.length, '字元');
1298
+ console.log('HTML 預覽:', result.html.substring(0, 300) + '...');
1299
+
1300
+ // Store the generated HTML
1301
+ generatedHTML = result.html;
1302
+
1303
+ // Display HTML source code with syntax highlighting
1304
+ const htmlCode = result.html;
1305
+ const escapedHtml = htmlCode
1306
+ .replace(/&/g, '&amp;')
1307
+ .replace(/</g, '&lt;')
1308
+ .replace(/>/g, '&gt;')
1309
+ .replace(/"/g, '&quot;')
1310
+ .replace(/'/g, '&#039;');
1311
+
1312
+ document.getElementById('source-code').innerHTML = `<code>${escapedHtml}</code>`;
1313
+ showToast('HTML 已成功生成!', 'success');
1314
+
1315
+ console.log('Step 9: HTML 已顯示在畫面上');
1316
+ }
1317
  } catch (error) {
1318
+ console.error('Step X: 發生錯誤');
1319
+ console.error('錯誤類型:', error.name);
1320
+ console.error('錯誤訊息:', error.message);
1321
+ console.error('錯誤堆疊:', error.stack);
1322
+
1323
+ showToast(`錯誤:${error.message}`, 'error');
1324
+ }
1325
+
1326
+ console.log('========== 結束生成 HTML ==========');
1327
+ }
1328
+
1329
+ // Store generated HTML globally
1330
+ let generatedHTML = '';
1331
+
1332
+ // Preview HTML in new window
1333
+ function previewHTML() {
1334
+ if (!generatedHTML) {
1335
+ showToast('請先生成 HTML', 'error');
1336
+ return;
1337
+ }
1338
+
1339
+ const previewWindow = window.open('', '_blank');
1340
+ previewWindow.document.write(generatedHTML);
1341
+ previewWindow.document.close();
1342
+ }
1343
+
1344
+ // Copy HTML to clipboard
1345
+ async function copyToClipboard() {
1346
+ if (!generatedHTML) {
1347
+ showToast('請先生成 HTML', 'error');
1348
+ return;
1349
  }
1350
+
1351
+ try {
1352
+ await navigator.clipboard.writeText(generatedHTML);
1353
+ showToast('已複製到剪貼簿', 'success');
1354
+ } catch (err) {
1355
+ showToast('複製失敗', 'error');
1356
+ }
1357
+ }
1358
+
1359
+ // Download HTML file
1360
+ function downloadHTML() {
1361
+ if (!generatedHTML) {
1362
+ showToast('請先生成 HTML', 'error');
1363
+ return;
1364
+ }
1365
+
1366
+ const blob = new Blob([generatedHTML], { type: 'text/html' });
1367
+ const url = URL.createObjectURL(blob);
1368
+ const a = document.createElement('a');
1369
+ a.href = url;
1370
+ a.download = `generated_${new Date().toISOString().slice(0,10)}.html`;
1371
+ document.body.appendChild(a);
1372
+ a.click();
1373
+ document.body.removeChild(a);
1374
+ URL.revokeObjectURL(url);
1375
+ showToast('檔案下載中...', 'success');
1376
+ }
1377
+
1378
+ // Don't auto-generate on page load
1379
+ // Wait for user to click the button
1380
  </script>
1381
  </body>
1382
  </html>