# -*- coding: utf-8 -*- """ 渲染对话相关组件 """ import reflex as rx from application.models import Run, Reasoning, Conversation from application.state.conversation import ConversationState from application.state.create_conversation_modal import CreateConversationModalState def render_delete_conversation_component( conversation_id: str, ): """ 渲染删除对话组件 :param conversation_id: 对话唯一标识 :return: 删除对话组件 """ return rx.popover.root( rx.popover.trigger( rx.button( rx.icon("ellipsis", size=16), variant="ghost", padding="4px", height="auto", ) ), rx.popover.content( rx.popover.close( rx.button( "删除", color_scheme="red", variant="ghost", width="100%", justify_content="center", on_click=lambda: ConversationState.delete_conversation( conversation_id ), ) ), padding="4px", ), open_delay=0, ) def render_conversation_item_component( conversation_id: str, conversation: Conversation ): """ 渲染对话项组件 """ # 对话描述 description = conversation.description # 若当前对话唯一标识非指定对话唯一标识则正常渲染,否则高亮渲染 return rx.cond( ConversationState.conversation_id != conversation_id, # 正常渲染 rx.button( rx.hstack( rx.text( description, flex=1, overflow="hidden", text_overflow="ellipsis", white_space="nowrap", text_align="left", ), render_delete_conversation_component(conversation_id), align_items="center", width="100%", spacing="2", ), width="100%", justify_content="flex-start", padding_x="12px", padding_y="10px", variant="surface", color_scheme="blue", on_click=lambda: ConversationState.switch_conversation(conversation_id), ), # 高亮渲染 rx.button( rx.hstack( rx.text( description, flex=1, overflow="hidden", text_overflow="ellipsis", white_space="nowrap", text_align="left", ), render_delete_conversation_component(conversation_id), align_items="center", width="100%", spacing="2", ), width="100%", justify_content="flex-start", padding_x="12px", padding_y="10px", variant="soft", color_scheme="blue", ), ) def render_reasoning_item_component(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_run_component(run_id: str, run: Run) -> rx.Component: """ 渲染运行组件 :param run: 运行,包含用户提示词、推理字典和回复正文 :return: Component """ # 推理字典 reasonings = run.reasonings # 推理状态 is_reasoning = run.is_reasoning # 推理面板展开状态 is_reasoning_panel_open = run.is_reasoning_panel_open return rx.box( # 渲染用户提示词组件 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", ), # 渲染推理面板组件 rx.box( # 标题栏 rx.hstack( # 若推理状态为正在推理则渲染正在推理,否则渲染推理完成 rx.cond( 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 i, _: render_reasoning_item_component(i[0], i[1]), ), rx.fragment(), ), text_align="left", width="100%", margin_bottom="8px", ), # 渲染回复正文组件,若正在推理则不渲染,否则渲染回复正文组件 rx.cond( is_reasoning, 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", # 圆角 ), ), ), width="min(100%, 50em)", # 最大宽度:父级元素最大宽度和50em中较小值 margin_x="auto", # 水平外边距:自动调整 key=run_id, ) def render_conversation_component() -> rx.Component: """ 渲染对话组件,包括若干次运行 :return: Component """ # 运行字典 runs = ConversationState.get_runs # 若运行字典为空则渲染空智能体图标、名称和预设用户提示词,否则遍历渲染运行组件 return rx.auto_scroll( rx.cond( ConversationState.is_runs_empty, # 渲染空运行字典组件 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", ), # 遍历渲染运行组件 rx.foreach( runs, lambda i, _: render_run_component(i[0], i[1]), ), ), flex="1", padding="8px", overflow_y="auto", ) def render_input_component() -> rx.Component: """ 渲染输入和其它组件 """ return rx.center( rx.vstack( # 渲染创建对话组件 rx.hstack( rx.spacer(), # 占位符 rx.dialog.root( # 触发事件:点击创建对话图标,支持鼠标悬停提示 rx.dialog.trigger( rx.box(rx.tooltip(rx.icon("circle-plus"), content="创建对话")) ), # 内容层 rx.dialog.content( rx.form( rx.hstack( rx.input( name="description", placeholder="请输入对话描述", flex="auto", min_width="20ch", ), rx.button("创建"), spacing="2", wrap="wrap", width="100%", ), on_submit=ConversationState.create_conversation, ), background_color=rx.color("mauve", 1), ), open=CreateConversationModalState.is_open, on_open_change=CreateConversationModalState.toggle, ), width="100%", ), # 渲染自定义输入组件 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", # 子元素间距 on_submit=ConversationState.run, reset_on_submit=True, ), # 渲染底部文案 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%", )