# -*- coding: utf-8 -*- """ 渲染对话相关组件 """ import reflex as rx from typing import Tuple from application.models import Run, Reasoning, Conversation from application.state.conversation import ConversationState from application.state.create_conversation_modal import CreateConversationModalState def render_conversation_history_item( conversation_id: str, conversation: Conversation ) -> rx.Component: """ 渲染对话历史的对话卡片 :param conversation_id: 对话唯一标识 :param conversation: 对话实例 :return: Component """ def render_more_button( conversation_id: str, ) -> rx.Component: """ 渲染更多按钮 :param conversation_id: 对话唯一标识 :return: Component """ return rx.popover.root( # 触发事件:点击更多按钮 rx.popover.trigger( rx.button( rx.icon("ellipsis", size=14, color="var(--devui-text-weak)"), cursor="pointer", # 鼠标光标显示为手指 ) ), # 渲染气泡卡片 rx.popover.content( rx.box( width="14px", height="14px", position="absolute", top="-7px", left="50%", transform="translateX(-50%) rotate(45deg)", background_color="var(--devui-base-bg, #ffffff)", box_shadow="-2px -2px 4px rgba(0,0,0,0.05)", ), rx.popover.close( rx.box( "删除", width="100%", # 宽度 height="24px", # 高度 padding="4px", # 内边距 line_height="16px", # 行高 font_size="12px", # 字体大小 color="#252b3a", display="flex", # 弹性布局 flex_wrap="wrap", # 换行 align_items="center", # 子元素垂直居中 background_color="var(--devui-base-bg)", # 背景颜色 white_space="nowrap", # 不换行 border_radius="var(--devui-border-radius-feedback)", # 圆角 # 点击事件:删除对话 on_click=lambda: ConversationState.delete_conversation( conversation_id ), ), ), padding="8px", # 内边距 arrow_size=10, # 三角大小 side="bottom", # 气泡在按钮下方,三角朝上 side_offset=8, # 气泡和按钮之间间距 align="center", # 水平居中对齐触发按钮中线 style={ "position": "relative", # 关键:给三角提供定位基准 "background_color": "var(--devui-base-bg, #ffffff) !important", "border_radius": "var(--devui-border-radius-feedback, 8px) !important", "box_shadow": "0 2px 12px rgba(0,0,0,0.1)", }, ), ) # 指定对话的激活状态 is_actived = conversation_id == ConversationState.conversation_id return rx.box( rx.hstack( # 渲染对话描述 rx.text( conversation.description, flex=1, height="22px", line_height="22px", font_size="var(--devui-font-size)", padding_right="4px", overflow="hidden", text_overflow="ellipsis", white_space="nowrap", ), # 渲染更多按钮 rx.box( render_more_button(conversation_id=conversation_id), min_width="14px", cursor="pointer", # 鼠标光标显示为手指 style={ "opacity": rx.cond( is_actived, "1", "0" ), # 若已激活则不透明,否则透明 "pointer_events": rx.cond( is_actived, "auto", "none" ), # 若已激活则可点击,否则不可点击 "transition": "opacity 0.18s ease", }, ), display="flex", # 弹性布局 align_items="center", # 子元素垂直居中 width="100%", # 宽度 margin_bottom="8px", # 底部外边距 ), line_height="1.5", # 行高 color="var(--devui-text-weak)", # 字体颜色 cursor="pointer", # 鼠标悬停显示手指 margin_bottom="8px", # 底部外边距 width="100%", # 宽度 padding="16px", # 内边距 border_radius="8px", # 圆角 style={ "background": rx.cond( is_actived, "linear-gradient(to right, #f3efff, #f3efff33, #e2f1fd33, #e2f1fd)", "var(--devui-base-bg)", ), "box_shadow": rx.cond(is_actived, "2px 2px 8px #e9e9e9", "none"), # 鼠标悬停时渲染背景颜色和阴影 "&:hover": { "background": "linear-gradient(to right, #f3efff, #f3efff33, #e2f1fd33, #e2f1fd)", "box_shadow": "2px 2px 8px #e9e9e9", }, # 鼠标悬停时强制显示右侧三点按钮,覆盖默认隐藏 "&:hover > div > div:last-child": { "opacity": "1 !important", "pointer_events": "auto !important", }, }, # 点击事件:将指定对话唯一标识设置为当前对话唯一标识 on_click=lambda: ConversationState.switch_conversation(conversation_id), ) 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_create_conversation_component() -> rx.Component: """ 渲染创建对话组件 """ return 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%", ) def render_input_component() -> rx.Component: """ 渲染输入组件 """ return rx.vstack( # 渲染自定义输入组件 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", )