ling-series-spaces / tab_smart_writer.py
GitHub Action
Sync ling-space changes (filtered) from commit 127300e
b931367
raw
history blame
11.1 kB
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]
)