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] )