# -*- 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 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( 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_conversation_box() -> rx.Component: """ 渲染对话框 :return: Component """ def item(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.vstack( rx.hstack( rx.spacer(), rx.vstack( # 用户问题 rx.markdown( run.user_prompt, max_width="600px", padding="12px 16px", background_color="var(--devui-global-bg)", border_radius="12px", word_wrap="break-word", word_break="break-all", style={ "& p.rt-Text": { "padding": "0", "margin": "0 !important", }, }, ), # 再次发送按钮 rx.hstack( rx.box( rx.icon( "rotate-ccw", width="14px", height="14px", color="var(--devui-text-weak)", ), padding="4px", border_radius="4px", cursor="pointer", ), width="100%", margin_top="8px", ), ), # 用户身份标识 rx.avatar( src="/user_avatar.svg", align_self="flex-start", width="32px", height="32px", border_radius="100%", border="none", ), width="100%", margin_top="8px", align_items="flex-start", gap="4px", font_size="var(--devui-font-size)", ), # 智能体回答 rx.hstack( # 智能体身份标识 rx.avatar( src="/logo.svg", align_self="flex-start", width="32px", height="32px", border_radius="100%", border="none", ), rx.box( rx.vstack( # 标题栏 rx.cond( is_reasoning, rx.hstack( rx.icon("loader"), rx.text("推理中..."), rx.icon("chevron-down"), ), rx.hstack( rx.icon("check"), rx.text("已推理"), rx.icon("chevron-up"), ), ), ) ), # 标题栏 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="100%", key=run_id, ) # 运行字典 runs = ConversationState.get_runs # 若运行字典为空则渲染品牌图标、名称和介绍、预设用户提示词等,否则遍历渲染运行项 return rx.cond( runs.length() == 0, rx.vstack( rx.vstack( # 品牌图标、名称和介绍 rx.vstack( rx.hstack( rx.image( src="/logo.png", width="64px", height="64px", object_fit="contain", ), rx.box( "棱镜球", font_size="32px", font_weight="700", letter_spacing="1px", ), display="flex", align_items="center", gap="8px", ), rx.vstack( rx.text( "产品汪的棱镜球,精准捕捉各类业务需求", width="100%", text_align="center", ), rx.text( "可帮助检索业务资料,协助生成产品方案、输出 PRD 、流程图和原型图等", width="100%", text_align="center", ), width="100%", line_height="1.5", font_size="var(--devui-font-size)", ), display="flex", flex_direction="column", width="100%", align_items="center", gap="12px", color="var(--devui-text)", ), # 猜你想问 rx.vstack( rx.hstack( rx.text( "猜你想问", line_height="24px", font_size="16px", font_weight="700", ), display="flex", width="100%", justify_content="space-between", align_items="center", margin_bottom="16px", ), rx.hstack( rx.box( "需求梳理", padding="10px 16px", background_color="var(--devui-dividing-line)", border_radius="var(--devui-border-radius-full)", font_size="var(--devui-font-size)", color="var(--devui-aide-text)", cursor="pointer", ), display="flex", flex_wrap="wrap", align_items="center", gap="12px", ), width="100%", padding="24px", background_color="var(--devui-base-bg)", border_radius="24px", ), display="flex", flex_direction="column", width="100%", min_height="0", margin="auto 0", align_items="center", gap="24px", color="var(--devui-text)", ), display="flex", flex="1", flex_direction="column", width="100%", max_width="1200px", padding="0 12px", justify_content="flex-start", gap="24px", overflow="auto", style={ "&::-webkit-scrollbar": { "display": "none", }, }, ), # 遍历渲染对话项 rx.vstack( rx.auto_scroll( rx.foreach( runs, lambda i, _: item(i[0], i[1]), ), width="100%", padding="0 12px", ), flex="1", width="100%", max_width="1200px", margin_x="auto", padding_top="20px", justify_content="flex-start", overflow_x="hidden", overflow_y="auto", ), ) def render_create_conversation_button() -> rx.Component: """ 渲染新建对话按钮 :return: Component """ return rx.hstack( rx.el.style( """ .rt-TooltipArrow polygon { fill: #ffffff !important; } .rt-TooltipText { color: var(--devui-text) !important; opacity: 1 !important; } """ ), 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="#ffffff", box_shadow="0 2px 12px 0 rgba(37, 43, 58, .24)", color="#252b3a", 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换行", 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", )