# -*- coding: utf-8 -*- """ 渲染对话相关组件 """ import reflex as rx from application.models import Run, Reasoning from application.state.conversation import ConversationState def render_user_prompt(run: Run) -> rx.Component: """ 渲染用户提示词 :param run: 运行实例 :return: Component """ return rx.box( rx.markdown( run.user_prompt, color=rx.color("gray", 12), # 文字颜色 background_color=rx.color("gray", 2), # 背景颜色 display="inline-block", # 布局模式:自适应文本宽度 max_width="85%", # 最大宽度 padding_x="1.25em", # 水平内边距 padding_y="0.5em", # 垂直内边距 margin_left="auto", # 左侧外边距自动调整 margin_bottom="8px", # 底部外边距 border_radius="12px", # 圆角 ), text_align="right", width="100%", margin_bottom="8px", ) def render_reasoning(reasoning_id: int, reasoning: Reasoning): """ 渲染推理 :param reasoning_id: 对话唯一标识 :param reasoning: 推理实例 :return: Component """ return rx.hstack( rx.vstack( rx.box( width="8px", height="8px", background_color=rx.color("blue", 7), ), rx.box(width="2px", flex=1, background_color=rx.color("blue", 3)), align_items="center", spacing="0", height="100%", ), rx.markdown(reasoning.content, color=rx.color("gray", 11), padding_y="4px"), align_items="flex-start", spacing="2", width="100%", key=reasoning_id, ) def render_reasoning_panel(run_id: str, run: Run) -> rx.Component: """ 渲染推理面板 :param run_id: 运行唯一标识 :param run: 运行实例 :return: Component """ # 推理字典 reasonings = run.reasonings # 推理面板展开状态 is_reasoning_panel_open = run.is_reasoning_panel_open return rx.cond( not reasonings, rx.fragment(), rx.box( # 标题栏 rx.hstack( rx.cond( run.is_reasoning, rx.badge("正在推理"), rx.badge("推理完成"), ), rx.spacer(), # 若推理面板展开则渲染上箭头,否则渲染下箭头 rx.icon( rx.cond(is_reasoning_panel_open, "chevron_up", "chevron_down"), size=16, color=rx.color("mauve", 6), ), width="100%", margin_bottom="6px", on_click=lambda: ConversationState.toggle_reasoning_panel( run_id ), # 点击事件:展开/折叠指定运行唯一标识的推理面板 ), # 若推理面板展开则遍历渲染推理,否则不渲染 rx.cond( is_reasoning_panel_open, rx.foreach( reasonings, lambda reasoning_id, reasoning: render_reasoning( reasoning_id=reasoning_id, reasoning=reasoning ), ), rx.fragment(), ), text_align="left", width="100%", margin_bottom="8px", ), ) def render_assistant_content(run: Run) -> rx.Component: """ 渲染回复正文 :param run: 运行实例,包含用户提示词、推理字典和回复正文 :return: Component """ # 若回复正文为空则不渲染,否则渲染回复正文 return rx.cond( not run.assistant_content, rx.fragment(), rx.box( rx.markdown( run.assistant_content, color=rx.color("gray", 12), # 文字颜色 background_color=rx.color("gray", 2), # 背景颜色 display="inline-block", # 布局模式:自适应文本宽度 max_width="85%", # 最大宽度 padding_x="1.25em", # 水平内边距 padding_y="0.5em", # 垂直内边距 margin_left="auto", # 左侧外边距自动调整 margin_bottom="8px", # 底部外边距 border_radius="12px", # 圆角 ), ), ) def render_run(run_id: str, run: Run) -> rx.Component: """ 渲染运行 :param run: 运行,包含用户提示词、推理字典和回复正文 :return: Component """ return rx.box( # 渲染用户提示词 render_user_prompt(run=run), # 渲染推理面板 render_reasoning_panel(run_id=run_id, run=run), # 渲染回复正文 render_assistant_content(run=run), width="min(100%, 50em)", # 最大宽度:父级元素最大宽度和50em中较小值 margin_x="auto", # 水平外边距:自动调整 key=run_id, ) def render_welcome() -> rx.Component: """ 渲染欢迎信息 :return: Component """ return rx.center( rx.vstack( rx.spacer(), # 智能体图标和名称 rx.hstack( rx.icon("info", size=18), rx.text("智能体"), ), # 预设用户提示词 rx.box( rx.vstack( # 标题 rx.text("猜你想问"), rx.hstack( rx.button("你可以帮我做什么"), ), ) ), spacing="4", width="min(100%, 40em)", padding_y="40px", ), border="none", ) def render_conversation() -> rx.Component: """ 渲染对话,包括若干次运行 :return: Component """ # 运行字典 runs = ConversationState.get_runs # 若运行字典为空则渲染欢迎信息,否则遍历渲染运行 return rx.auto_scroll( rx.cond( not runs, # 渲染欢迎信息 render_welcome(), rx.foreach( runs, lambda run_id, run: render_run(run_id=run_id, run=run), ), ), flex="1", padding="8px", overflow_y="auto", ) def render_custom_input_box() -> rx.Component: """ 渲染自定义输入框 """ return rx.form( rx.box( rx.vstack( # 输入区域 rx.input( name="user_prompt", placeholder="发消息...", flex="auto", border="none", # 外框边线 outline="none", # 高亮轮廓线 padding_bottom="8px", ), # 操作区域,暂仅包含发送按钮 rx.hstack( rx.spacer(), # 占位符 # 发送按钮 rx.button( rx.icon("arrow-up", size=18), color_scheme="blue", radius="full", width="36px", height="36px", padding="0", type="submit", loading=ConversationState.get_running_status, disabled=ConversationState.get_running_status, ), width="100%", ), spacing="8", padding_x="12px", padding_y="12px", ), border=f"1px solid {rx.color('mauve', 4)}", radius="large", background_color="white", ), max_width="50em", # 最大宽度 margin="0 auto", # 水平居中 align_items="center", # 子元素垂直居中 spacing="0", # 子元素间距 ) def render_input_box() -> rx.Component: """ 渲染输入框 """ return rx.center( rx.vstack( render_custom_input_box(), # 渲染自定义输入框 # 底部文案 rx.text( "内容由大模型生成,无法确保准确性和完整性,仅供参考", text_align="center", font_size=".75em", color=rx.color("mauve", 10), ), width="100%", padding_x="16px", align="stretch", ), position="sticky", bottom="0", left="0", padding_y="16px", backdrop_filter="auto", backdrop_blur="lg", border_top=f"1px solid {rx.color('mauve', 3)}", background_color=rx.color("mauve", 2), align="stretch", width="100%", ) # rx.center 等价 rx.box(display="flex", align_items="center", justify_content="center")