ling-series-spaces / tab_smart_writer.py
GitHub Action
Sync ling-space changes from GitHub commit b5051bd
bb2bd12
raw
history blame
10.7 kB
import gradio as gr
import time
from smart_writer_kit.agent_for_streaming_completion import fetch_flow_suggestion_agent, accept_flow_suggestion_agent
from smart_writer_kit.agent_for_inspiration_expansion import fetch_inspiration_agent, apply_inspiration_agent
from smart_writer_kit.agent_for_outline_update import update_outline_status_agent
from smart_writer_kit.agent_for_kb_update import suggest_new_kb_terms_agent
# --- Mock Data (for UI population only) ---
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, "与荒坂公司的最终决战。"]
]
# --- UI Helper Functions ---
def get_stats(text):
"""Calculate word count and read time."""
if not text:
return "0 Words | 0 mins"
words = len(text.split())
read_time = max(1, words // 200) # Average reading speed
return f"{words} Words | ~{read_time} mins"
def dismiss_inspiration():
return gr.update(visible=False)
# --- UI Construction ---
def create_smart_writer_tab():
debounce_state = gr.State({"last_change": 0, "active": False, "style": "", "kb": [], "short_outline": [], "long_outline": []})
debounce_timer = gr.Timer(0.5, active=False)
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.Row():
btn_suggest_kb = gr.Button("🔍 提取新词条", size="sm")
md_suggested_terms_header = gr.Markdown("#### 推荐词条", visible=False) # Placeholder for suggested terms
suggested_kb_dataframe = gr.Dataframe(
headers=["Term", "Description"],
datatype=["str", "str"],
visible=False, # Initially hidden
interactive=False,
label="推荐词条"
)
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.Row():
btn_sync_outline = gr.Button("🔄 同步状态", size="sm")
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/Ctrl+Enter)", size="sm", variant="primary", elem_id="btn-action-create-paragraph")
# 主要编辑器区域
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("采纳(Tab)", scale=1, elem_id='btn-action-accept-flow')
refresh_flow_btn = gr.Button("换一个(Shift+Tab)", scale=1, elem_id='btn-action-change-flow')
# Debounce Progress
debounce_progress = gr.HTML(value="", visible=False)
# 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("生成选项(Shift+Enter)")
with gr.Row():
opt1_btn = gr.Button("...", elem_classes=["inspiration-card"])
opt2_btn = gr.Button("...", elem_classes=["inspiration-card"])
opt3_btn = gr.Button("...", 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 (triggered by visible button or hidden trigger button for Cmd+Enter)
open_inspiration_modal_fn = lambda: (gr.update(visible=True), "")
inspiration_btn.click(fn=open_inspiration_modal_fn, outputs=[inspiration_modal, inspiration_prompt_input])
# Generate Options based on Prompt
refresh_inspiration_btn.click(
fn=fetch_inspiration_agent,
inputs=[inspiration_prompt_input, editor, style_input, kb_input, short_outline_input, long_outline_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_agent,
inputs=[editor, btn],
outputs=[editor, inspiration_modal, inspiration_prompt_input]
)
cancel_insp_btn.click(fn=dismiss_inspiration, outputs=inspiration_modal, show_progress="hidden")
# 3. Flow Suggestion with Debounce
def start_debounce(editor_content, style, kb, short_outline, long_outline):
return {"last_change": time.time(), "active": True, "style": style, "kb": kb, "short_outline": short_outline, "long_outline": long_outline}, gr.update(active=True), gr.update(visible=True, value="<progress value='0' max='100'></progress> 补全中... 3.0s")
def update_debounce(debounce_state, editor_content):
if not debounce_state["active"]:
return gr.update(), gr.update(), debounce_state, gr.update()
elapsed = time.time() - debounce_state["last_change"]
if elapsed >= 3:
suggestion = fetch_flow_suggestion_agent(editor_content, debounce_state["style"], debounce_state["kb"], debounce_state["short_outline"], debounce_state["long_outline"])
return gr.update(visible=False), suggestion, {"last_change": 0, "active": False, "style": "", "kb": [], "short_outline": [], "long_outline": []}, gr.update(active=False)
else:
progress = int((elapsed / 3) * 100)
remaining = 3 - elapsed
progress_html = f"<progress value='{progress}' max='100'></progress> 补全中... {remaining:.1f}s"
return gr.update(value=progress_html), gr.update(), debounce_state, gr.update()
editor.change(fn=start_debounce, inputs=[editor, style_input, kb_input, short_outline_input, long_outline_input], outputs=[debounce_state, debounce_timer, debounce_progress])
debounce_timer.tick(fn=update_debounce, inputs=[debounce_state, editor], outputs=[debounce_progress, flow_suggestion_display, debounce_state, debounce_timer])
refresh_flow_btn.click(fn=fetch_flow_suggestion_agent, inputs=[editor, style_input, kb_input, short_outline_input, long_outline_input], outputs=flow_suggestion_display)
# Accept Flow (Triggered by visible Button or hidden Tab Key trigger)
accept_flow_fn_inputs = [editor, flow_suggestion_display]
accept_flow_fn_outputs = [editor]
accept_flow_btn.click(fn=accept_flow_suggestion_agent, inputs=accept_flow_fn_inputs, outputs=accept_flow_fn_outputs, show_progress="hidden")
# 4. Agent-based Context Updates
btn_sync_outline.click(
fn=update_outline_status_agent,
inputs=[short_outline_input, editor],
outputs=[short_outline_input]
)
btn_suggest_kb.click(
fn=suggest_new_kb_terms_agent,
inputs=[kb_input, editor],
outputs=[suggested_kb_dataframe, md_suggested_terms_header]
)