File size: 11,058 Bytes
b931367
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import gradio as gr
import time
import random

# --- Mock Data ---

MOCK_STYLE = """风格:赛博朋克 / 黑色电影
视角:第三人称限制视角(主角:凯)
基调:阴郁、压抑、霓虹闪烁的高科技低生活
核心规则:
1. 强调感官描写,特别是光影和声音。
2. 避免过多的心理独白,通过行动展现心理。
"""

MOCK_KNOWLEDGE_BASE = [
    ["凯 (Kai)", "主角,前黑客,现在是义体医生。左臂是老式的军用义体。"],
    ["夜之城 (Night City)", "故事发生的舞台,一座永夜的巨型都市,被企业掌控。"],
    ["荒坂塔 (Arasaka Tower)", "市中心的最高建筑,象征着绝对的权力。"],
    ["赛博精神病 (Cyberpsychosis)", "过度改装义体导致的解离性精神障碍。"],
    ["网络监察 (NetWatch)", "负责维护网络安全的组织,被黑客们视为走狗。"]
]

MOCK_SHORT_TERM_OUTLINE = [
    [True, "凯接到一个神秘电话,对方声称知道他失踪妹妹的下落。"],
    [False, "凯前往'来生'酒吧与接头人见面。"],
    [False, "在酒吧遇到旧识,引发一场关于过去的争执。"],
    [False, "接头人出现,但似乎被跟踪了。"]
]

MOCK_LONG_TERM_OUTLINE = [
    [False, "揭露夜之城背后的惊天阴谋。"],
    [False, "凯找回妹妹,或者接受她已经改变的事实。"],
    [False, "与荒坂公司的最终决战。"]
]

MOCK_INSPIRATIONS = [
    "霓虹灯光在雨后的路面上破碎成无数光斑,凯拉紧了风衣的领口,义体手臂在寒风中隐隐作痛。来生酒吧的招牌在雾气中若隐若现,像是一只在黑暗中窥视的电子眼。",
    "\"你来晚了。\"接头人的声音经过变声器处理,听起来像是指甲划过玻璃。他坐在阴影里,只有指尖的一点红光在闪烁——那是他正在抽的廉价合成烟。",
    "突如其来的爆炸声震碎了酒吧的玻璃,人群尖叫着四散奔逃。凯本能地拔出了腰间的动能手枪,他的视觉系统瞬间切换到了战斗模式,周围的一切都变成了数据流。"
]

MOCK_FLOW_SUGGESTIONS = [
    "他感觉到了...",
    "空气中弥漫着...",
    "那是他从未见过的...",
    "就在这一瞬间..."
]

# --- Logic Functions ---

def get_stats(text):
    """Mock word count and read time."""
    if not text:
        return "0 Words | 0 mins"
    words = len(text)
    read_time = max(1, words // 500)
    return f"{words} Words | ~{read_time} mins"

def fetch_inspiration(prompt):
    """Simulate fetching inspiration options based on user prompt."""
    time.sleep(1)
    
    # Simple Mock Logic based on prompt keywords
    if prompt and "打斗" in prompt:
        opts = [
            "凯侧身闪过那一记重拳,义体关节发出尖锐的摩擦声。他顺势抓住对方的手腕,电流顺着接触点瞬间爆发。",
            "激光刃切开空气,留下一道灼热的残影。凯没有退缩,他的视觉系统已经计算出了对方唯一的破绽。",
            "周围的空气仿佛凝固了,只剩下心跳声和能量枪充能的嗡嗡声。谁先动,谁就会死。"
        ]
    elif prompt and "风景" in prompt:
        opts = [
            "酸雨冲刷着生锈的金属外墙,流下一道道黑色的泪痕。远处的全息广告牌在雨雾中显得格外刺眼。",
            "清晨的阳光穿透厚重的雾霾,无力地洒在贫民窟的屋顶上。这里没有希望,只有生存。",
            "夜之城的地下就像是一个巨大的迷宫,管道交错,蒸汽弥漫,老鼠和瘾君子在阴影中通过眼神交流。"
        ]
    else:
        opts = MOCK_INSPIRATIONS
        
    return gr.update(visible=True), opts[0], opts[1], opts[2]

def apply_inspiration(current_text, inspiration_text):
    """Append selected inspiration to the editor."""
    if not current_text:
        new_text = inspiration_text
    else:
        new_text = current_text + "\n\n" + inspiration_text
    return new_text, gr.update(visible=False), "" # Clear prompt

def dismiss_inspiration():
    return gr.update(visible=False)

def fetch_flow_suggestion(current_text):
    """Simulate fetching a short continuation."""
    # If text ends with newline, maybe don't suggest? Or suggest new paragraph start.
    time.sleep(0.5)
    return random.choice(MOCK_FLOW_SUGGESTIONS)

def accept_flow_suggestion(current_text, suggestion):
    if not suggestion or "等待输入" in suggestion:
        return current_text
    return current_text + suggestion

def refresh_context(current_outline):
    """Mock refreshing the outline context (auto-complete task or add new one)."""
    new_outline = [row[:] for row in current_outline] 
    
    # Try to complete the first pending task
    task_completed = False
    for row in new_outline:
        if not row[0]:
            row[0] = True 
            task_completed = True
            break
            
    # If all done, or randomly, add a new event
    if not task_completed or random.random() > 0.7:
         new_outline.append([False, f"新的动态事件: 突发情况 #{random.randint(100, 999)}"])

    return new_outline

# --- UI Construction ---

def create_smart_writer_tab():
    # Hidden Buttons for JS triggers
    btn_accept_flow_trigger = gr.Button(visible=False, elem_id="btn_accept_flow_trigger")
    btn_refresh_context_trigger = gr.Button(visible=False, elem_id="btn_refresh_context_trigger")

    with gr.Row(equal_height=False, elem_id="indicator-writing-tab"):
        # --- Left Column: Entity Console ---
        with gr.Column(scale=0, min_width=384) as left_panel:
            gr.Markdown("### 🧠 核心实体控制台")
            
            with gr.Accordion("整体章程 (Style)", open=True):
                style_input = gr.Textbox(
                    label="整体章程", 
                    lines=8, 
                    value=MOCK_STYLE,
                    interactive=True
                )
            
            with gr.Accordion("知识库 (Knowledge Base)", open=True):
                kb_input = gr.Dataframe(
                    headers=["Term", "Description"],
                    datatype=["str", "str"],
                    value=MOCK_KNOWLEDGE_BASE,
                    interactive=True,
                    label="知识库",
                    wrap=True
                )
                
            with gr.Accordion("当前章节大纲 (Short-Term)", open=True):
                short_outline_input = gr.Dataframe(
                    headers=["Done", "Task"],
                    datatype=["bool", "str"],
                    value=MOCK_SHORT_TERM_OUTLINE,
                    interactive=True,
                    label="当前章节大纲",
                    col_count=(2, "fixed"),
                )

            with gr.Accordion("故事总纲 (Long-Term)", open=False):
                long_outline_input = gr.Dataframe(
                    headers=["Done", "Task"],
                    datatype=["bool", "str"],
                    value=MOCK_LONG_TERM_OUTLINE,
                    interactive=True,
                    label="故事总纲",
                    col_count=(2, "fixed"),
                )

        # --- Right Column: Writing Canvas ---
        with gr.Column(scale=1) as right_panel:
            # Toolbar
            with gr.Row(elem_classes=["toolbar"]):
                stats_display = gr.Markdown("0 Words | 0 mins")
                inspiration_btn = gr.Button("✨ 灵感扩写 (Cmd+Enter)", size="sm", variant="primary")
            
            # 主要编辑器区域
            editor = gr.Textbox(
                label="沉浸写作画布", 
                placeholder="开始你的创作...", 
                lines=30, 
                elem_classes=["writing-editor"],
                elem_id="writing-editor",
                show_label=False,
            )

            # Flow Suggestion
            with gr.Row(variant="panel"):
                flow_suggestion_display = gr.Textbox(
                    label="AI 实时续写建议 (按 Tab 采纳)", 
                    value="(等待输入...)", 
                    interactive=False,
                    scale=4,
                    elem_classes=["flow-suggestion-box"]
                )
                accept_flow_btn = gr.Button("采纳", scale=1, elem_id='btn-action-accept-flow')
                refresh_flow_btn = gr.Button("换一个", scale=1)

            # Inspiration Modal
            with gr.Group(visible=False) as inspiration_modal:
                gr.Markdown("### 💡 灵感选项 (由 Ling 模型生成)")
                
                inspiration_prompt_input = gr.Textbox(
                    label="设定脉络 (可选)",
                    placeholder="例如:写一段激烈的打斗 / 描写赛博朋克夜景...",
                    lines=1
                )
                refresh_inspiration_btn = gr.Button("生成选项")

                with gr.Row():
                    opt1_btn = gr.Button(MOCK_INSPIRATIONS[0], elem_classes=["inspiration-card"])
                    opt2_btn = gr.Button(MOCK_INSPIRATIONS[1], elem_classes=["inspiration-card"])
                    opt3_btn = gr.Button(MOCK_INSPIRATIONS[2], elem_classes=["inspiration-card"])
                cancel_insp_btn = gr.Button("取消")

    # --- Interactions ---

    # 1. Stats
    editor.change(fn=get_stats, inputs=editor, outputs=stats_display)

    # 2. Inspiration Workflow
    # Open Modal (reset prompt)
    inspiration_btn.click(
        fn=lambda: (gr.update(visible=True), ""),
        outputs=[inspiration_modal, inspiration_prompt_input]
    )
    
    # Generate Options based on Prompt
    refresh_inspiration_btn.click(
        fn=fetch_inspiration,
        inputs=[inspiration_prompt_input],
        outputs=[inspiration_modal, opt1_btn, opt2_btn, opt3_btn]
    )

    # Apply Option
    for btn in [opt1_btn, opt2_btn, opt3_btn]:
        btn.click(
            fn=apply_inspiration,
            inputs=[editor, btn],
            outputs=[editor, inspiration_modal, inspiration_prompt_input]
        )
    
    cancel_insp_btn.click(fn=dismiss_inspiration, outputs=inspiration_modal)

    # 3. Flow Suggestion
    editor.change(fn=fetch_flow_suggestion, inputs=editor, outputs=flow_suggestion_display)
    refresh_flow_btn.click(fn=fetch_flow_suggestion, inputs=editor, outputs=flow_suggestion_display)
    
    # Accept Flow (Triggered by Button or Tab Key via JS)
    accept_flow_fn_inputs = [editor, flow_suggestion_display]
    accept_flow_fn_outputs = [editor]
    
    accept_flow_btn.click(fn=accept_flow_suggestion, inputs=accept_flow_fn_inputs, outputs=accept_flow_fn_outputs)
    btn_accept_flow_trigger.click(fn=accept_flow_suggestion, inputs=accept_flow_fn_inputs, outputs=accept_flow_fn_outputs)

    # 4. Context Refresh (Triggered by Enter Key via JS)
    btn_refresh_context_trigger.click(
        fn=refresh_context,
        inputs=[short_outline_input],
        outputs=[short_outline_input]
    )