# -*- coding: utf-8 -*- """ 会话页面 """ import reflex as rx from application.models import Conversation, Dialog, ThoughtNode from application.states import ( ConversationHistoryState, ConversationState, CreateConversationState, ) def render_conversation_history_item( conversation_id: str, conversation: Conversation ) -> rx.Component: """ 渲染会话历史条目 :param conversation_id: 会话唯一标识 :param conversation: 会话实例 :return: Component """ # 会话激活状态 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( 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", top="-12px", left="50%", transform="translateX(-50%) rotate(45deg)", width="8px", height="8px", 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, ), min_width="14px", cursor="pointer", style={ "opacity": rx.cond( is_actived, "1", "0" ), # 若当前会话已激活则不透明,否则透明 "transition": "opacity 0.18s ease", "pointer_events": rx.cond(is_actived, "auto", "none"), }, ), display="flex", align_items="center", width="100%", margin_bottom="8px", ), width="100%", padding="16px", margin_bottom="8px", border_radius="8px", line_height="1.5", color="var(--devui-text-weak)", cursor="pointer", 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=ConversationState.switch_conversation(conversation_id), ) def render_conversation_history_collapse(is_collapse_expanded: bool) -> rx.Component: """ 渲染对话历史折叠面板 :param is_collapse_expanded: 对话历史折叠面板展开状态 :return: Component """ return rx.box( rx.vstack( # 渲染标题 rx.hstack( rx.text( "会话历史", margin_bottom="8px", font_size="var(--devui-font-size-lg)", font_weight="700", white_space="nowrap", ), display="flex", align_items="center", justify_content="space-between", ), # 渲染列表 rx.auto_scroll( rx.foreach( ConversationState.conversation_history, lambda item, _: render_conversation_history_item(item[0], item[1]), ), flex="1", margin_top="8px", width="100%", align_items="stretch", overflow="auto", style={ "&::-webkit-scrollbar": { "display": "none", }, }, ), display="flex", flex_direction="column", height="100%", padding="12px", background_color="#f9f9f9cc", backdrop_filter="blur(50px)", gap="12px", color="var(--prismui-color-text)", ), width=rx.cond(is_collapse_expanded, "25%", "0px"), min_width=rx.cond(is_collapse_expanded, "240px", "0px"), max_width=rx.cond(is_collapse_expanded, "380px", "0px"), height="100%", overflow="hidden", transition="all 0.3s ease-in-out", ) def render_greeting() -> rx.Component: """ 渲染问候 :return: Component """ return 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(--prismui-color-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(--prismui-color-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", }, }, ) def render_thought_node(thought_node_id: int, thought_node: ThoughtNode): """ 渲染思考节点 :param thought_node_id: 思考节点唯一标识 :param thought_node: 思考节点实例 :return: Component """ return rx.vstack( rx.match( thought_node.kind, ( "thinking", rx.text( thought_node.content, font_size="var(--prismui-font-size-2)", color="var(--prismui-color-3)", ), ), rx.fragment(), ), align_items="flex-start", width="100%", key=thought_node_id, ) def render_dialog_history_item(dialog_id: str, dialog: Dialog) -> rx.Component: """ 渲染对话历史条目 :param dialog_id: 对话唯一标识 :param dialog: 对话实例 :return: Component """ # 推理状态 is_thinking = dialog.is_thinking # 思考折叠面板展开状态 is_collapse_expanded = dialog.is_collapse_expanded # 思考节点 thought_nodes = dialog.thought_nodes return rx.vstack( # 渲染问题和操作栏 rx.hstack( rx.spacer(), rx.vstack( # 渲染问题 rx.text( dialog.question, max_width="600px", padding="12px 16px", background_color="var(--prismui-background-color-3)", border_radius="12px", word_wrap="break-word", word_break="break-all", white_space="pre-line", color="var(--prismui-color-text)", ), # 操作栏 rx.hstack( rx.box( rx.icon( "rotate-ccw", width="14px", height="14px", color="var(--prismui-color-2)", ), padding="4px", border_radius="4px", cursor="pointer", style={ "_hover": { "background_color": "var(--prismui-background-color-2)", } }, ), width="100%", margin_top="8px", ), ), width="100%", margin_top="8px", align_items="flex-start", gap="4px", font_size="var(--devui-font-size)", ), # 渲染思考折叠面板 rx.vstack( # 标题栏 rx.box( rx.hstack( rx.text( rx.cond(is_thinking, "思考中", "思考完成"), color="var(--prismui-color-3)", font_size="var(--prismui-font-size-2)", ), rx.icon( "chevron-right", width="14px", height="14px", color="var(--prismui-color-3)", style={ "transform": rx.cond( is_collapse_expanded, "rotate(90deg)", "rotate(0deg)" ), "transition": "transform 0.2s ease-out", }, ), align_items="center", gap="4px", margin_bottom="8px", line_height="22px", ), cursor="pointer", # 点击事件,展开/折叠思考折叠面板 on_click=lambda: ConversationState.toggle_collapse(dialog_id), ), rx.box( rx.auto_scroll( rx.foreach( thought_nodes, lambda i, _: render_thought_node(i[0], i[1]), ), width="100%", margin_top="8px", align_self="start", ), width="100%", overflow="hidden", style={ "max-height": rx.cond(is_collapse_expanded, "1200px", "20px"), "opacity": rx.cond(is_collapse_expanded, "1", "0"), "transition": "max-height 0.2s ease-out, opacity 0.2s ease-out", }, ), width="100%", margin_top="8px", text_align="left", overflow="visible", ), # 渲染回答 rx.markdown( dialog.answer, component_map={ "p": lambda text: rx.text( text, margin_bottom="8px", word_wrap="break-word", word_break="break-all", white_space="pre-line", line_height="24px", font_size="var(--prismui-font-size-1)", color="var(--prismui-color-1)", ), "strong": lambda text: rx.text( text, display="inline", font_weight="var(--prismui-font-weight-1)", ), "ul": lambda children: rx.vstack( children, margin_bottom="8px", gap="4px" ), "li": lambda text: rx.text( text, word_wrap="break-word", word_break="break-all", white_space="pre-line", line_height="24px", font_size="var(--prismui-font-size-1)", color="var(--prismui-color-1)", style={ "display": "list-item", "list_style_type": "disc", "list_style_position": "inside", }, ), }, width="100%", margin_top="8px", ), padding="0 16px", width="100%", key=dialog_id, ) def render_dialog_history() -> rx.Component: """ 渲染对话历史 :return: Component """ # 获取当前会话的对话历史 dialog_history = ConversationState.dialog_history # 若运行字典为空则渲染品牌图标、名称和介绍、预设用户提示词等,否则遍历渲染运行项 return rx.cond( dialog_history.length() == 0, # 渲染问候 render_greeting(), # 遍历渲染对话项 rx.vstack( rx.auto_scroll( rx.foreach( dialog_history, lambda i, _: render_dialog_history_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(--prismui-color-text) !important; opacity: 1 !important; } """ ), rx.spacer(), rx.dialog.root( # 触发事件:点击图标 rx.dialog.trigger( rx.box( rx.tooltip( rx.icon( "plus", size=14, color="var(--prismui-color-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=CreateConversationState.is_modal_open, on_open_change=CreateConversationState.toggle_modal, ), display="flex", width="100%", max_width="1200px", height="39px", padding="0 12px", justify_content="flex-end", align_items="center", gap="4px", ) def render_input() -> rx.Component: """ 渲染输入框 :return: Component """ return rx.vstack( # 渲染自定义输入组件 rx.form( rx.box( rx.vstack( rx.hstack( rx.text_area( name="question", 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(--prismui-color-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.running_status, disabled=ConversationState.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", ) def render_conversation_workplace() -> rx.Component: """ 渲染会话工作区 :return: Component """ return rx.box( rx.vstack( # 渲染对话历史 render_dialog_history(), # 渲染新建对话按钮 render_create_conversation_button(), # 渲染输入框 render_input(), display="flex", flex_flow="column", width="100%", height="100%", gap="8px", align_items="center", ), position="relative", display="flex", flex="1", width="0", height="100%", background="linear-gradient(180deg, #fffffff2, #f8fafff2 99%)", ) def render_conversation_history_collapse_button( is_collapse_expanded: bool, ) -> rx.Component: """ 渲染会话历史折叠面板的打开/关闭按钮 :param is_collapse_expanded: 会话历史折叠面板展开状态 :return: Component """ return rx.button( rx.cond( is_collapse_expanded, rx.icon( "chevron-left", width="16px", height="16px", color="var(--devui-icon-fill-weak)", ), rx.icon( "chevron-right", width="16px", height="16px", color="var(--devui-icon-fill-weak)", ), ), position="absolute", top="50%", left=rx.cond(is_collapse_expanded, "calc(clamp(240px, 25%, 380px) - 8px)", "0"), z_index=99, width="16px", height="40px", background="var(--prismui-background-color-1)", box_shadow="2px 0 4px 0 var(--mc-float-block-shadow)", border_radius=rx.cond(is_collapse_expanded, "6px", "0 6px 6px 0"), transition="all 0.3s ease-in-out", cursor="pointer", # 点击事件:打开/关闭对话历史折叠面板 on_click=ConversationHistoryState.toggle_collapse, ) def render_conversation() -> rx.Component: """ 渲染会话页面:布局参考 MetaChat,左侧为会话历史,右侧为对话历史和输入框 """ # 会话历史折叠面板展开状态 is_collapse_expanded = ConversationHistoryState.is_collapse_expanded return rx.box( # 渲染对话历史折叠面板 rx.hstack( # 渲染对话历史折叠面板 render_conversation_history_collapse(is_collapse_expanded), # 渲染会话工作区 render_conversation_workplace(), position="relative", display="flex", flex="1", height="100%", min_height="0", overflow="hidden", transition="all 0.3s ease-in-out", ), # 渲染对话历史折叠面板打开/关闭按钮 render_conversation_history_collapse_button(is_collapse_expanded), position="relative", width="100%", height="100%", border_radius="12px", overflow="hidden", # 挂载事件:初始化会话历史 on_mount=ConversationState.on_mount, )