File size: 24,330 Bytes
43e19b8
c36065e
f396014
dabaa03
ca4ea75
bd39596
ab21d03
c36065e
0e89edd
e06be5d
0e89edd
ab21d03
 
e39a5b8
 
e06be5d
e39a5b8
ab21d03
9cbc975
ab21d03
 
e66fa0a
 
 
ab21d03
 
 
 
9cbc975
ab21d03
 
 
9cbc975
 
 
 
ab21d03
9cbc975
 
 
 
 
 
 
 
ab21d03
e66fa0a
9cbc975
3b93023
 
 
 
 
 
 
ab21d03
3b93023
ab21d03
3b93023
9cbc975
 
4c0264a
1708a78
f396014
4c0264a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ab21d03
3b93023
 
 
9cbc975
4c0264a
 
 
3b93023
ab21d03
3b93023
f396014
9cbc975
0a32a7b
f396014
9cbc975
 
 
f396014
 
3b93023
9cbc975
f396014
9cbc975
 
f396014
 
 
3b93023
f396014
3b93023
f396014
3b93023
f396014
3b93023
f396014
3b93023
 
 
f396014
3b93023
f396014
3b93023
 
f396014
3b93023
f396014
e66fa0a
f396014
 
 
 
e66fa0a
 
3b93023
f396014
3b93023
 
f396014
 
 
 
 
 
3b93023
f396014
3b93023
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0e89edd
ab21d03
1ccf718
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ca4ea75
1ccf718
 
 
 
 
 
 
 
ab21d03
 
3b93023
f396014
3b93023
ab21d03
 
e39a5b8
6ec656b
3b93023
6ec656b
586ae2d
ab21d03
1e4ff71
7560532
ab21d03
f396014
3b93023
f396014
 
1ccf718
3b93023
 
f396014
3b93023
 
f396014
 
 
1ccf718
f396014
 
 
1ccf718
1e4ff71
1ccf718
 
f396014
 
1ccf718
 
 
1e4ff71
f396014
 
3b93023
f396014
 
3b93023
f396014
1ccf718
3b93023
f396014
 
 
1e4ff71
f396014
 
 
1ccf718
f396014
 
 
 
 
 
 
3b93023
 
 
f396014
1ccf718
f396014
1e4ff71
f396014
ab21d03
f396014
 
 
 
 
 
ab21d03
 
 
1ccf718
1e4ff71
f396014
ab21d03
f396014
1ccf718
f396014
 
 
 
1ccf718
f396014
 
1ccf718
f396014
 
 
 
ab21d03
f396014
ab21d03
f396014
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ab21d03
 
f396014
ab21d03
e39a5b8
 
 
 
1ccf718
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1e4ff71
1ccf718
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e39a5b8
7fc598d
e39a5b8
ab21d03
 
1ccf718
 
132536a
c36065e
ab21d03
 
1ccf718
 
 
 
e39a5b8
 
ab21d03
 
 
1ccf718
ab21d03
1ccf718
ab21d03
 
 
 
1ccf718
 
1e4ff71
1ccf718
 
1e4ff71
1ccf718
 
 
1e4ff71
1ccf718
 
 
f396014
7560532
 
1ccf718
132536a
 
ab21d03
1ccf718
 
ab21d03
e39a5b8
7fc598d
e39a5b8
1e4ff71
9cbc975
3b93023
 
e66fa0a
e39a5b8
1e4ff71
ab21d03
 
 
9cbc975
 
e66fa0a
9cbc975
7560532
ab21d03
 
 
 
 
9cbc975
7fc598d
ab21d03
9cbc975
 
 
 
 
 
1ccf718
 
 
 
 
 
 
ab21d03
e66fa0a
e06be5d
 
 
1e4ff71
712d21c
1e4ff71
712d21c
1e4ff71
712d21c
e06be5d
 
1708a78
 
e06be5d
1708a78
 
 
ab21d03
3b93023
 
1ccf718
 
 
 
 
 
 
 
 
 
 
 
 
 
3b93023
e39a5b8
7fc598d
 
ab21d03
 
e66fa0a
9cbc975
f396014
 
e66fa0a
3b93023
 
f396014
e66fa0a
3b93023
1ccf718
 
 
 
f396014
3b93023
 
 
 
1ccf718
3b93023
 
 
 
e66fa0a
 
 
 
 
 
 
 
 
 
 
ab21d03
1ccf718
 
 
 
ab21d03
3b93023
ab21d03
0e89edd
3b93023
ab21d03
 
 
1ccf718
 
 
 
 
 
 
ca4ea75
1ccf718
 
ab21d03
e39a5b8
43e19b8
3b93023
 
1ccf718
 
f396014
 
 
3b93023
1ccf718
 
f396014
 
 
1ccf718
 
 
 
 
 
 
 
 
 
3b93023
 
 
1ccf718
 
 
3b93023
 
 
 
 
1ccf718
3b93023
 
 
1ccf718
 
 
3b93023
 
 
 
 
1ccf718
3b93023
 
1ccf718
 
 
 
 
 
 
 
 
 
e39a5b8
132536a
1ccf718
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
import os
import time
import math
import gradio as gr
import html as py_html

from gradio_client import Client

# ==========================================
# 0. 配置
# ==========================================
PRIVATE_SPACE_ID = "Yoyo2004/Longstory-backend"
HF_TOKEN = os.environ.get("HF_TOKEN")

# ==========================================
# 1. CSS
# ==========================================
custom_css = """
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&family=Noto+Serif+SC:wght@400;700&family=JetBrains+Mono:wght@400&family=Inter:wght@400;600&display=swap');

:root {
    --primary-color: #4f46e5; /* indigo 靛蓝色 */
    --paper-bg: #fdf6e3; /* 米黄色 */
    --glass-bg: rgba(255, 255, 255, 0.95); /* 半透明白 */
}

body, .gradio-container {
    font-family: 'Inter', 'Noto Sans SC', system-ui, sans-serif !important;
    background: linear-gradient(135deg, #eef2f3 0%, #8e9eab 100%) !important;
    background-attachment: fixed !important;
}

/* --- 通用容器 --- */
.header-box { text-align: center; padding: 25px; background: var(--glass-bg); border-radius: 16px; margin-bottom: 20px; box-shadow: 0 4px 20px rgba(0,0,0,0.08); backdrop-filter: blur(10px); }
.title-text { font-size: 2.2rem; font-weight: 800; font-family: 'Noto Serif SC', serif; color: #1a202c; letter-spacing: -0.5px; }
.subtitle-text { color: #718096; letter-spacing: 1.5px; text-transform: uppercase; font-size: 0.85rem; font-weight: 600; margin-top: 5px; }

/* --- 左侧控制面板 --- */
.control-panel { 
    background: var(--glass-bg); 
    padding: 24px !important; 
    border-radius: 16px; 
    border: 1px solid rgba(255,255,255,0.6);
    box-shadow: 0 8px 32px rgba(0,0,0,0.05); 
}

/* 输入框与按钮 */
.input-label { font-weight: 700; color: #4a5568; margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }
.input-box textarea { background: #f8fafc !important; border: 2px solid #e2e8f0 !important; border-radius: 12px !important; padding: 12px !important; }
.input-box textarea:focus { border-color: var(--primary-color) !important; background: #fff !important; }

.generate-btn { 
    background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%) !important; 
    color: white !important; font-weight: 600; border-radius: 12px !important; padding: 12px !important; font-size: 1.1rem !important;
    box-shadow: 0 4px 15px rgba(79, 70, 229, 0.4); transition: all 0.3s ease;
}
.generate-btn:hover { transform: translateY(-2px); filter: brightness(110%); }

/* Examples */
.examples-container { margin-top: 15px; }
.examples-label { font-size: 0.85rem; color: #a0aec0; font-weight: 600; margin-bottom: 10px; text-transform: uppercase; }

.examples-table table { border-collapse: separate !important; border-spacing: 0 8px !important; background: transparent !important; }
.examples-table thead { display: none; }
.examples-table tbody tr { 
    background: #fff; 
    box-shadow: 0 2px 5px rgba(0,0,0,0.05); 
    border-radius: 8px; 
    cursor: pointer;
    transition: transform 0.2s, box-shadow 0.2s;
    border: 1px solid #edf2f7;
    display: block; 
    margin-bottom: 8px;
}
.examples-table tbody tr:hover { 
    transform: translateY(-2px); 
    box-shadow: 0 5px 12px rgba(0,0,0,0.1); 
    border-color: var(--primary-color);
}
.examples-table td { padding: 12px 15px !important; font-size: 13px !important; color: #4a5568 !important; line-height: 1.5 !important; border: none !important; display: block; }

/* 终端日志 */
.terminal-wrapper { margin-top: 25px; background: #1e1e1e; border-radius: 10px; overflow: hidden; }
.terminal-header { background: #2d2d2d; padding: 8px 12px; display: flex; gap: 6px; border-bottom: 1px solid #333; }
.dot { width: 10px; height: 10px; border-radius: 50%; }
.dot-red { background: #ff5f56; } 
.dot-yellow { background: #ffbd2e; } 
.dot-green { background: #27c93f; }
.terminal-log textarea { font-family: 'JetBrains Mono', monospace !important; background: #1e1e1e !important; color: #4ade80 !important; border: none !important; padding: 10px !important; }

/* 1. 书页容器 */
.book-page-container {
    background-color: var(--paper-bg);
    height: 900px; 
    width: 100%;
    border-radius: 8px 16px 16px 8px;
    box-shadow: inset 20px 0 50px rgba(0,0,0,0.05), 10px 10px 30px rgba(0,0,0,0.1);
    position: relative;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}
.book-page-container::before {
    content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 6px;
    background: linear-gradient(to right, rgba(0,0,0,0.1), transparent);
    z-index: 1;
}

/* 2. 内容包装器 */
.book-content-wrapper {
    flex: 1; padding: 50px 70px 20px 70px; overflow-y: hidden; position: relative; z-index: 2;
}
.book-page-container.empty-state .book-content-wrapper { display: flex; justify-content: center; align-items: center; }

/* 3. 排版细节 */
.page-top-spacer { height: 20px; }
.chapter-header-block { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 2px solid rgba(139, 69, 19, 0.2); }
.chapter-subtitle { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 2px; color: #8b4513; opacity: 0.6; }
.chapter-title { font-family: 'Noto Serif SC', serif; font-size: 2.2rem; font-weight: 700; color: #2c1810; line-height: 1.2; }

.page-running-header { height: 40px; border-bottom: 1px solid rgba(0,0,0,0.05); margin-bottom: 30px; color: #a0aec0; font-size: 0.75rem; display: flex; justify-content: space-between; align-items: center; text-transform: uppercase; }

.chapter-text-content { font-family: 'Noto Serif SC', serif; font-size: 1.15rem; color: #333; line-height: 1.9; text-align: justify; }
.chapter-text-content p { margin-bottom: 1.2em; text-indent: 2em; }

.page-footer { height: 50px; display: flex; align-items: center; justify-content: center; font-size: 0.8rem; color: #999; border-top: 1px dashed rgba(0,0,0,0.05); z-index: 2; }

/* 1. 侧边箭头*/
.arrow-btn {
    background: transparent !important;
    border: none !important;
    box-shadow: none !important;
    color: #cbd5e0 !important;
    font-size: 3rem !important;
    font-weight: 200 !important;
    padding: 0 !important;
    height: 100% !important;
    width: 100% !important;
    display: flex !important;
    align-items: center !important;
    justify-content: center !important;
    transition: all 0.2s ease !important;
}
.arrow-btn:hover {
    color: var(--primary-color) !important; 
    transform: scale(1.1);
    background: transparent !important; 
}

/* 2. 底部章节导航按钮 */
.chapter-nav-btn {
    background: white !important;
    border: 1px solid #e2e8f0 !important;
    color: #718096 !important;
    border-radius: 8px !important;
    font-size: 0.9rem !important;
    padding: 8px 16px !important;
    box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.chapter-nav-btn:hover {
    background: #f7fafc !important;
    color: var(--primary-color) !important;
    border-color: #cbd5e0 !important;
}

/* 全文导出区域 */
.fulltext-wrapper { padding: 10px 0; }
.fulltext-copy-btn {
    background: #4f46e5;
    color: #fff;
    border: none;
    border-radius: 8px;
    padding: 6px 12px;
    font-size: 0.9rem;
    cursor: pointer;
    margin-bottom: 10px;
}
.fulltext-copy-btn:hover {
    filter: brightness(1.08);
}
.fulltext-area {
    width: 100%;
    min-height: 500px;
    height: 900px;
    border-radius: 8px;
    border: 1px solid #e2e8f0;
    padding: 10px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.9rem;
    resize: vertical;
}

/* 移动端适配 */
@media (max-width: 768px) {
    .book-page-container { padding: 0; height: auto; min-height: 500px; }
    .book-content-wrapper { padding: 30px 20px; }
    .arrow-btn { font-size: 2rem !important; }
}
"""

# ==========================================
# 2. 逻辑处理
# ==========================================
def paginate_story(story_data, chars_per_page=450, first_page_ratio=0.8):
    """
    分章 -> 分页
    story_data: List[{"title": ..., "content": ...}, ...]
    """
    if not story_data:
        return [], []

    flat_pages = []
    chapter_start_indices = []
    current_global_page_index = 0

    for chapter in story_data:
        chapter_start_indices.append(current_global_page_index)

        title = chapter.get("title", "")
        content = chapter.get("content", "")
        paragraphs = content.split('\n')

        current_page_content = []
        current_char_count = 0
        is_start = True

        # 首页为标题预留空间
        limit = int(chars_per_page * first_page_ratio)

        for p in paragraphs:
            p = p.strip()
            if not p:
                continue

            # 换页
            if current_char_count + len(p) > limit and current_page_content:
                flat_pages.append({
                    "title": title if is_start else "",
                    "content": "\n".join(current_page_content),
                    "is_chapter_start": is_start,
                    "chapter_title": title
                })
                current_global_page_index += 1

                current_page_content = [p]
                current_char_count = len(p)
                is_start = False
                limit = chars_per_page
            else:
                current_page_content.append(p)
                current_char_count += len(p)

        if current_page_content:
            flat_pages.append({
                "title": title if is_start else "",
                "content": "\n".join(current_page_content),
                "is_chapter_start": is_start,
                "chapter_title": title
            })
            current_global_page_index += 1

    return flat_pages, chapter_start_indices


def render_book_page(flat_pages, page_index):
    # 空状态
    if not flat_pages or len(flat_pages) == 0:
        return """
        <div class='book-page-container empty-state'>
            <div class='book-content-wrapper'>
                <div style='text-align:center; opacity:0.5'>
                    <div style='font-size:3rem; margin-bottom:20px;'>☕</div>
                    <h3>Waiting for Story...</h3>
                </div>
            </div>
        </div>
        """

    # 正常渲染页面
    total_pages = len(flat_pages)
    page_index = max(0, min(page_index, total_pages - 1))
    page_data = flat_pages[page_index]

    is_start = page_data.get("is_chapter_start", False)
    display_title = page_data.get("title", "")
    chapter_ref = page_data.get("chapter_title", "")
    content = page_data.get("content", "")

    paragraphs = [f"<p>{p}</p>" for p in content.split('\n') if p.strip()]
    content_html = "".join(paragraphs)

    if is_start:
        header_html = f"""
        <div class="page-top-spacer"></div>
        <div class="chapter-header-block">
            <div class="chapter-subtitle">Generated Novel</div>
            <div class="chapter-title">{display_title}</div>
        </div>
        """
    else:
        header_html = f"""
        <div class="page-running-header">
            <span>{chapter_ref}</span>
            <span style="float:right">... continuing</span>
        </div>
        """

    html = f"""
    <div class="book-page-container">
        <div class="book-content-wrapper">
            {header_html}
            <div class="chapter-text-content">
                {content_html}
            </div>
        </div>
        <div class="page-footer">
            Page {page_index + 1} of {total_pages}
        </div>
    </div>
    """
    return html


def build_full_text(story_data):
    """
    把章节列表拼成一整本小说文本,供导出/复制。
    story_data: List[{"title": ..., "content": ...}, ...]
    """
    if not story_data:
        return ""

    blocks = []
    for ch in story_data:
        title = ch.get("title", "").strip()
        content = ch.get("content", "").strip()
        if title:
            blocks.append(title)
        if content:
            blocks.append(content)
    return "\n\n".join(blocks).strip()


def build_full_text_html(full_text: str) -> str:
    """
    生成「一键复制」按钮
    """
    escaped = py_html.escape(full_text or "")
    return f"""
    <div class="fulltext-wrapper">
        <button class="fulltext-copy-btn" onclick="
            const ta = document.getElementById('full-story-area');
            if (ta) {{
                ta.select();
                ta.setSelectionRange(0, 999999);
                navigator.clipboard && navigator.clipboard.writeText(ta.value);
            }}
        ">
            📋 一键复制全文
        </button>
        <textarea id="full-story-area" class="fulltext-area">{escaped}</textarea>
    </div>
    """

# ==========================================
# 3. 后端连接
# ==========================================
def bridge_to_backend(premise):
    if not premise.strip():
        empty_full_html = build_full_text_html("")
        yield "⚠️ 请输入故事梗概...", None, None, None, [], [], empty_full_html, render_book_page([], 0)
        return

    log_buffer = "🚀 初始化前端连接...\n"
    initial_html = render_book_page([], 0)
    empty_full_html = build_full_text_html("")

    # 初始状态 [log, outline, plan, personas, pages, chap_indices, full_text_html, book_html]
    yield log_buffer, None, None, None, [], [], empty_full_html, initial_html

    try:
        log_buffer += f"🔗 连接后端 Space: {PRIVATE_SPACE_ID}...\n"
        client = Client(PRIVATE_SPACE_ID, hf_token=HF_TOKEN)
        job = client.submit(premise, api_name="/generate_novel")

        for result in job:
            # result: [0]Log, [1]Outline, [2]Plan, [3]Personas, [4]StoryList(章节列表)
            backend_log = result[0]
            outline = result[1]
            plan = result[2]
            personas_html = result[3]
            raw_story_list = result[4]

            # 分页 + 计算章节索引
            flat_pages, chap_indices = paginate_story(
                raw_story_list,
                chars_per_page=500,
                first_page_ratio=0.8,
            )

            # 一键复制全文文本
            full_text = build_full_text(raw_story_list)
            full_text_html = build_full_text_html(full_text)

            book_html = render_book_page(flat_pages, 0)
            
            # flat_pages: List[Dict]
            yield backend_log, outline, plan, personas_html, flat_pages, chap_indices, full_text_html, book_html

    except Exception as e:
        error_msg = f"❌ 前端连接错误: {str(e)}"
        error_full_html = build_full_text_html(str(e))
        yield error_msg, None, None, None, [], [], error_full_html, render_book_page([], 0)

# ==========================================
# 4. 前端 UI 布局
# ==========================================
with gr.Blocks(theme=gr.themes.Soft(primary_hue="indigo", neutral_hue="slate"), css=custom_css, title="LongStory Agent") as demo:
    # --- 状态管理 ---
    story_pages_state = gr.State([])      # 所有的页面 List[Dict]
    chapter_indices_state = gr.State([])  # 每一章起始页的 index List[int]
    current_page_state = gr.State(0)      # 当前页面

    
    # --- 顶部标题 ---
    with gr.Row(elem_classes=["header-box"]):
        gr.HTML("""
            <div style="display:flex; flex-direction:column; align-items:center;">
                <div class="title-wrapper">
                    <span class="title-text">LongStory Agent</span>
                </div>
                <div class="subtitle-text"></div>
            </div>
        """)

    with gr.Row(elem_classes=["main-container"]):
        
        # === 左侧:控制中心 ===
        with gr.Column(scale=4, elem_classes=["control-panel-col"]):
            with gr.Column(elem_classes=["control-panel"]):
                gr.HTML("""
                <div class="input-label">
                    <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path></svg>
                    Story Premise
                </div>
                """)
                premise_input = gr.Textbox(
                    label="Premise",
                    show_label=False,
                    lines=5,
                    elem_classes=["input-box"],
                    placeholder="输入故事创意..."
                )
                
                # Examples
                gr.HTML('<div class="examples-container"><div class="examples-label">⚡ 快速开始 (Quick Inspirations)</div></div>')
                
                example_prompts = [
                    # 青春校园
                    ["为了帮好友打探男神的情报,她混入了他的线上读书会,却意外与他成了无话不谈的“书友”;而现实中,他正是那个总在课堂上与她针锋相对的“死对头”。当线上与线下的世界逐渐重叠,她开始分不清,自己究竟该继续这场虚拟的知己游戏,还是鼓起勇气,在现实里看清他眼睛里的答案。"],
                    # 玄幻修仙
                    ["他生于一个以“灵根”品级决定一切的修真世界,却身负亘古未闻的“空灵根”——无法自主吸收灵气,被视作彻底的凡人。在受尽屈辱后,他于家族禁地中发现先祖手札,得知“空灵根”并非无根,而是天地未开时的“太初之根”。它无法利用现世的秩序灵气,却能直接感知并驾驭天地间最本初、最狂暴的“混沌元气”。当世人还在为一道天级灵脉争得头破血流时,他已悄然将一缕混沌元气化为己用,走上了一条重塑天地规则、只为在体内开辟“最初宇宙”的孤独之路。"],   
                    # 都市爱情
                    ["为应付催婚,我和认识仅两小时的陌生人闪婚了。婚后第一天,我惊恐地发现,这位协议丈夫竟成了我的新上司——那位传闻中冷面无情的CEO。从此,我们白天是严苛上司与忐忑下属,夜晚却是同居一室的合约夫妻,直到我发现,我们那场“偶然”的相遇,原来是他蓄谋已久的开局。"] 
                ]
                
                with gr.Column(elem_classes=["examples-table"]):
                    gr.Examples(
                        examples=example_prompts,
                        inputs=premise_input,
                        label=None
                    )
                
                submit_btn = gr.Button("✨ 开始生成 (GENERATE)", elem_classes=["generate-btn"])
                
                gr.HTML(
                    '<div class="terminal-wrapper"><div class="terminal-header">'
                    '<div class="dot dot-red"></div><div class="dot dot-yellow"></div>'
                    '<div class="dot dot-green"></div><div class="terminal-title">system.log</div>'
                    '</div>'
                )
                log_output = gr.Textbox(
                    label="Log",
                    lines=10,
                    interactive=False,
                    elem_classes=["terminal-log"],
                    show_label=False,
                    value="> System initialized..."
                )
                gr.HTML("</div>")

        # === 右侧:内容展示 ===
        with gr.Column(scale=8):
            with gr.Tabs(elem_classes=["tabs-container"]):
                
                # Tab 1: 正文
                with gr.TabItem("📖 正文阅读", id="tab-story"):
                    with gr.Row(elem_classes=["book-reader-row"], equal_height=True):
                        
                        # 左侧箭头
                        with gr.Column(scale=1, min_width=40, elem_classes=["arrow-col"]):
                            btn_prev_page = gr.Button("‹", elem_classes=["arrow-btn"])
                        
                        # 核心内容
                        with gr.Column(scale=15):
                            story_display = gr.HTML(
                                label="Book View",
                                value=render_book_page([], 0)
                            )
                        
                        # 右侧箭头
                        with gr.Column(scale=1, min_width=40, elem_classes=["arrow-col"]):
                            btn_next_page = gr.Button("›", elem_classes=["arrow-btn"])
                    
                    # 章节导航
                    with gr.Row(elem_classes=["chapter-nav-row"]):
                        btn_prev_chap = gr.Button("⏮️ 上一章", elem_classes=["chapter-nav-btn"])
                        btn_next_chap = gr.Button("⏭️ 下一章", elem_classes=["chapter-nav-btn"])

                # Tab 2: 大纲
                with gr.TabItem("🗺️ 故事大纲", id="tab-outline"):
                    outline_output = gr.JSON(label="Structure Data")
                
                # Tab 3: 规划
                with gr.TabItem("📅 剧情规划", id="tab-planning"):
                    plan_output = gr.JSON(label="Event Planning")
                
                # Tab 4: 人设
                with gr.TabItem("👥 人物档案", id="tab-persona"):
                    persona_output = gr.HTML(label="Character Cards")

                # Tab 5: 全文导出
                with gr.TabItem("📄 全文导出", id="tab-fulltext"):
                    full_text_html = gr.HTML(label="Full Story Export")

    # ==========================================
    # 5. 事件交互
    # ==========================================
    
    # A. 生成
    submit_btn.click(
        fn=bridge_to_backend,
        inputs=[premise_input],
        outputs=[
            log_output,
            outline_output,
            plan_output,
            persona_output,
            story_pages_state,
            chapter_indices_state,
            full_text_html,
            story_display
        ],
        concurrency_limit=1
    )

    # B. 翻页 (上一页/下一页)
    def on_prev_page(pages, current_idx):
        if not pages:
            return 0, render_book_page([], 0)
        new_idx = max(0, current_idx - 1)
        return new_idx, render_book_page(pages, new_idx)

    def on_next_page(pages, current_idx):
        if not pages:
            return 0, render_book_page([], 0)
        new_idx = min(len(pages) - 1, current_idx + 1)
        return new_idx, render_book_page(pages, new_idx)

    btn_prev_page.click(
        fn=on_prev_page,
        inputs=[story_pages_state, current_page_state],
        outputs=[current_page_state, story_display]
    )
    btn_next_page.click(
        fn=on_next_page,
        inputs=[story_pages_state, current_page_state],
        outputs=[current_page_state, story_display]
    )

    # C. 章节跳转 (上一章/下一章)
    def on_prev_chap(pages, current_idx, chap_indices):
        if not pages or not chap_indices:
            return current_idx, render_book_page(pages, current_idx)

        target_idx = 0
        for start_idx in reversed(chap_indices):
            if start_idx < current_idx:
                target_idx = start_idx
                break

        return target_idx, render_book_page(pages, target_idx)

    def on_next_chap(pages, current_idx, chap_indices):
        if not pages or not chap_indices:
            return current_idx, render_book_page(pages, current_idx)

        target_idx = current_idx
        for start_idx in chap_indices:
            if start_idx > current_idx:
                target_idx = start_idx
                break

        return target_idx, render_book_page(pages, target_idx)

    btn_prev_chap.click(
        fn=on_prev_chap,
        inputs=[story_pages_state, current_page_state, chapter_indices_state],
        outputs=[current_page_state, story_display]
    )
    btn_next_chap.click(
        fn=on_next_chap,
        inputs=[story_pages_state, current_page_state, chapter_indices_state],
        outputs=[current_page_state, story_display]
    )

if __name__ == "__main__":
    demo.queue().launch()