# -*- 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_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.box( rx.icon("ellipsis", size=14, color="var(--devui-text-weak)"), cursor="pointer", # 鼠标光标显示为手指 ) ), # 渲染气泡卡片 rx.popover.content( rx.vstack( rx.box( position="absolute", # 绝对定位 width="8px", # 宽度 height="8px", # 高度 top="-12px", # 向上移动 left="50%", # 向左移动 transform="translateX(-50%) rotate(45deg)", # 旋转45度 background_color="#ffffff", # 背景颜色 box_shadow="-2px -2px 4px rgba(0,0,0,0.05)", # 阴影 ), rx.popover.close( rx.box( "删除", width="100%", # 宽度 height="24px", # 高度 padding="4px", # 内边距 border_radius="4px", # 圆角 line_height="16px", # 行高 font_family="HuaweiFont,Helvetica,Arial,PingFangSC-Regular,Hiragino Sans GB,Microsoft YaHei,微软雅黑,Microsoft JhengHei", # 字体 font_size="12px", # 字体大小 color="#252b3a", # 字体颜色 style={ "_hover": { "background_color": "#f2f2f3", # 鼠标悬停显示背景颜色 } }, # 点击事件:删除对话 on_click=lambda: ConversationState.delete_conversation( conversation_id ), ) ), position="relative", # 相对定位 width="100%", # 宽度 ), position="relative", # 相对定位 align="center", # 水平居中对齐 padding="8px", # 内边距 border_radius="4px", # 圆角 background_color="#ffffff", # 背景颜色 box_shadow="0 2px 12px rgba(0,0,0,0.1)", # 阴影 overflow="visible", # 溢出可见 side="bottom", # 气泡卡片位于底部 side_offset=9, # 偏移量 ), open_delay=0, ) # 指定对话的激活状态 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_box() -> 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", style={ "&::-webkit-scrollbar": { "display": "none", }, }, ) def render_create_conversation_button() -> rx.Component: """ 渲染新建对话按钮 :return: Component """ return rx.hstack( rx.spacer(), # 占位符 rx.dialog.root( # 触发事件:点击图标 rx.dialog.trigger( rx.box( rx.tooltip( rx.icon( "plus", size=14, color="var(--devui-text)", style={ "_hover": { "color": "var(--devui-brand)", } }, ), content="新建对话", background_color="var(--devui-base-bg)", box_shadow="0 2px 12px 0 rgba(37, 43, 58, .24)", side="top", side_offset=9, ), display="flex", width="24px", height="24px", background_color="var(--devui-base-bg)", box_shadow="0 1px 8px #1919190f", border_radius="var(--devui-border-radius-full)", justify_content="center", align_items="center", cursor="pointer", ) ), 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, ), display="flex", width="100%", max_width="1200px", height="39px", padding="0 12px", justify_content="flex-end", align_items="center", gap="4px", ) def render_input_box() -> rx.Component: """ 渲染输入框 """ return rx.vstack( # 渲染自定义输入组件 rx.form( rx.box( rx.vstack( rx.hstack( rx.text_area( name="user_prompt", placeholder="请输入您的问题,并按Enter发送,按Shift+Enter换行", vertical_align="middle", width="100%", height="64px", padding="4px 0", background_color="var(--devui-base-bg)", font_size="var(--devui-font-size)", color="var(--devui-text)", style={ "border": "none !important", "outline": "none !important", "boxShadow": "none !important", }, ), width="100%", padding="0 16px", ), rx.hstack( rx.spacer(), # 占位符 rx.button( rx.icon( "send", margin_right="4px", width="12px", height="12px", ), rx.text("发送"), position="relative", display="inline-flex", padding="0 12px", background_color="var(--devui-primary)", border="none", border_radius="20px", align_items="center", justify_content="center", white_space="nowrap", inline_height="1.5", font_size="var(--devui-font-size)", color="var(--devui-light-text)", overflow="hidden", cursor="pointer", type="submit", loading=ConversationState.get_running_status, disabled=ConversationState.get_running_status, ), width="100%", justify_content="flex-end", align_items="center", height="32px", padding="0 16px", ), ), style={ "* textarea::placeholder": { "fontFamily": "var(--font-family)", "fontSize": "var(--devui-font-size)", "color": "var(--placeholder)", "opacity": 1, }, }, ), display="flex", flex_direction="column", width="100%", padding="12px 0", background_color="var(--devui-base-bg)", border="none", border_radius="16px", box_shadow="0 1px 8px 0 var(--mc-box-shadow)", on_submit=ConversationState.run, reset_on_submit=True, ), # 渲染底部文案 rx.text( "内容由大模型生成,无法确保准确性和完整性,仅供参考", margin_top="8px", text_align="center", font_size="12px", color="var(--devui-aide-text)", ), width="100%", max_width="1200px", padding="0 12px 12px", align_items="center", )