# -*- coding: utf-8 -*- """ 会话页面 """ import reflex as rx from typing import Tuple from application.domain_models import ( Conversation, Dialog, Thought, ) from application.states import ConversationState, AuthState def conversation_history_item_showing( item: Tuple[str, Conversation], ) -> rx.Component: """ 会话历史项展示 :param item: 会话历史项 :return: Component """ conversation = item[1] # 高亮:若为当前会话或显示更多则高亮 highlight: bool = ( conversation.id == ConversationState.shown_more_conversation_id ) | (conversation.id == ConversationState.conversation_id) return rx.list.item( rx.vstack( rx.hstack( # 对话描述 rx.text( conversation.description, flex=1, height="22px", line_height="22px", padding_right="18px", color="var(--prismui-color-2)", text_overflow="ellipsis", white_space="nowrap", overflow="hidden", ), display="flex", align_items="center", width="100%", margin_bottom="8px", ), rx.hstack( rx.spacer(), # 创建时间 rx.text( conversation.created_at, line_height="20px", font_size="var(--prismui-font-size-1)", color="var(--prismui-color-8)", ), justify_content="space-between", align_items="center", width="100%", height="20px", margin_bottom="8px", ), # 点击事件:切换会话 on_click=lambda: ConversationState.switch_conversation(conversation.id), width="100%", ), # 更多按钮:点击更多按钮显示气泡卡片,可删除会话 rx.box( rx.popover.root( rx.popover.trigger( rx.box( rx.icon( "ellipsis", width="14px", height="14px", color="var(--prismui-color-2)", ), cursor="pointer", ) ), rx.popover.content( rx.vstack( rx.box( position="absolute", top="-6px", left="50%", transform="translateX(-50%) rotate(45deg)", width="8px", height="8px", background_color="var(--prismui-background-color-1)", ), rx.popover.close( rx.box( "删除", # 点击事件:删除对话 on_click=lambda: ConversationState.delete_conversation( conversation.id ), display="flex", align_items="center", justify_content="center", width="100%", height="24px", border_radius="var(--prismui-border-radius-1)", line_height="16px", font_family="var(--prismui-font-family)", font_size="var(--prismui-font-size-1)", color="var(--prismui-color-1)", style={ "_hover": { "background_color": "var(--prismui-background-color-6)", }, # 鼠标悬停显示背景颜色 }, cursor="pointer", ), ), position="relative", width="100%", ), side="bottom", side_offset=8, position="relative", align="center", padding="4px 8px", border_radius="var(--prismui-border-radius-1)", box_shadow="var(--prismui-box-shadow-6)", overflow="visible", ), on_open_change=lambda is_shown: ConversationState.set_shown_more_conversation_id( conversation.id, is_shown ), open_delay=0, ), position="absolute", top="27px", right="16px", transform="translateY(-50%)", z_index="99", min_width="14px", cursor="pointer", opacity=rx.cond(highlight, "1", "0"), transition="opacity 0.18s ease", pointer_events=rx.cond(highlight, "auto", "none"), ), display="flex", position="relative", align_items="flex-start", width="100%", padding="16px", margin_bottom="8px", background=rx.cond( highlight, "var(--prismui-background-3)", "var(--prismui-background-color-1)", ), border_radius="var(--prismui-border-radius-3)", box_shadow=rx.cond(highlight, "var(--prismui-box-shadow-7)", "none"), cursor="pointer", style={ "&:hover": { "background": "var(--prismui-background-3)", "box_shadow": "var(--prismui-box-shadow-7)", }, # 鼠标悬停背景颜色和阴影 "&:hover > div:last-child": { "opacity": "1", "pointer_events": "auto", }, # 鼠标悬停显示更多按钮 }, ) def conversation_history_items_showing( is_conversation_history_shown: bool, ) -> rx.Component: """ 会话历史列表展示 :param is_conversation_history_shown: 会话历史展示状态 :return: Component """ return rx.box( rx.vstack( # 标题 rx.hstack( rx.text( "会话历史", margin_bottom="8px", font_weight="var(--prismui-font-weight-3)", white_space="nowrap", ), align_items="center", justify_content="space-between", ), rx.auto_scroll( rx.foreach( ConversationState.conversations, conversation_history_item_showing, ), flex="1", align_items="stretch", width="100%", margin_top="8px", style={ "&::-webkit-scrollbar": { "display": "none", }, }, overflow="auto", ), display="flex", flex_direction="column", height="100%", padding="12px", background_color="#f9f9f9cc", backdrop_filter="blur(50px)", gap="12px", ), width=rx.cond(is_conversation_history_shown, "25%", "0px"), min_width=rx.cond(is_conversation_history_shown, "240px", "0px"), max_width=rx.cond(is_conversation_history_shown, "380px", "0px"), height="100%", transition="all 0.3s ease-in-out", overflow="hidden", ) def greeting_showing() -> rx.Component: """ 欢迎展示 :return: Component """ return rx.vstack( rx.vstack( # 品牌图标、名称和介绍 rx.vstack( rx.hstack( rx.image( src="/logo.png", width="64px", height="64px", ), rx.box( "棱镜球", font_size="calc(var(--prismui-font-size-3) * 2)", font_weight="var(--prismui-font-weight-3)", 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", ), display="flex", flex_direction="column", width="100%", align_items="center", gap="12px", ), # 演示案例 rx.vstack( rx.hstack( rx.text( "演示案例", line_height="24px", font_size="var(--prismui-font-size-3)", font_weight="var(--prismui-font-weight-3)", ), display="flex", justify_content="space-between", align_items="center", width="100%", margin_bottom="16px", ), rx.hstack( rx.box( "智能客服", padding="10px 16px", background_color="var(--prismui-background-color-6)", border_radius="var(--prismui-border-radius-9)", color="var(--prismui-color-10)", cursor="pointer", ), display="flex", flex_wrap="wrap", align_items="center", gap="12px", ), width="100%", padding="24px", background_color="var(--prismui-background-color-1)", border_radius="var(--prismui-border-radius-7)", ), display="flex", flex_direction="column", align_items="center", gap="24px", width="100%", min_height="0", margin="auto 0", color="var(--prismui-color-text)", ), display="flex", flex="1", flex_direction="column", justify_content="flex-start", gap="24px", width="100%", max_width="1200px", padding="0 12px", overflow="auto", style={ "&::-webkit-scrollbar": { "display": "none", }, }, ) def thought_showing(item: Tuple[int, Thought]): """ 思考展示 :param item: 思考实例 :return: Component """ # 思考实例 thought = item[1] return rx.vstack( rx.match( thought.type, ( "thinking", rx.text( thought.content, line_height="22px", font_size="var(--prismui-font-size-2)", color="var(--prismui-color-3)", ), ), rx.fragment(), ), align_items="flex-start", width="100%", ) def dialog_showing(item: Tuple[str, Dialog]) -> rx.Component: """ 对话展示 :param item: 对话实例 :return: Component """ # 对话实例 dialog = item[1] # 推理状态 is_thinking = dialog.is_thinking # 思考折叠面板展开状态 is_expanded = dialog.is_expanded return rx.vstack( rx.hstack( rx.spacer(), rx.vstack( # 用户提示词 rx.text( dialog.user_prompt, max_width="600px", padding="12px 16px", background_color="var(--prismui-background-color-3)", border_radius="var(--prismui-border-radius-3)", line_height="1.5", word_wrap="break-word", word_break="break-all", white_space="pre-line", ), rx.hstack( # 再次发送 rx.box( rx.icon( "rotate-ccw", width="14px", height="14px", color="var(--prismui-color-2)", ), padding="4px", border_radius="var(--prismui-border-radius-1)", cursor="pointer", style={ "_hover": { "background_color": "var(--prismui-background-color-2)", } }, ), width="100%", margin_top="8px", ), ), align_items="flex-start", gap="4px", width="100%", margin_top="8px", ), # 思考折叠面板 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_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.box( rx.auto_scroll( rx.foreach( dialog.thoughts, thought_showing, ), width="100%", margin_top="8px", align_self="start", ), style={"overflow": "hidden", "min-height": "0"}, ), width="100%", style={ "display": "grid", "overflow": "hidden", "grid_template_rows": rx.cond(is_expanded, "1fr", "0fr"), "opacity": rx.cond(is_expanded, "1", "0"), # 双属性同步过渡,和千问一致 "transition": "grid-template-rows 0.3s ease-out, opacity 0.3s ease-out", }, ), ), # 结果输出 rx.markdown( dialog.result_output, component_map={ "p": lambda text: rx.text( text, margin="4px 0", line_height="1.75", word_wrap="break-word", word_break="break-all", white_space="pre-line", ), "h1": lambda text: rx.text( text, margin="8px 0 4px", line_height="1.75", font_size="18px", font_weight="var(--prismui-font-weight-3)", word_wrap="break-word", word_break="break-all", white_space="pre-line", ), "h2": lambda text: rx.text( text, margin="6px 0 4px", line_height="1.75", font_size="16px", font_weight="var(--prismui-font-weight-3)", word_wrap="break-word", word_break="break-all", white_space="pre-line", ), "h3": lambda text: rx.text( text, margin="4px 0", line_height="1.75", font_weight="var(--prismui-font-weight-3)", word_wrap="break-word", word_break="break-all", white_space="pre-line", ), "strong": lambda text: rx.text( text, display="inline", font_weight="var(--prismui-font-weight-3)", ), "ul": lambda children: rx.vstack( children, align_items="flex-start", margin="4px 0", style={"padding": "0 !important"}, ), "ol": lambda children: rx.vstack( children, align_items="flex-start", margin="4px 0", style={"padding": "0 !important"}, ), "li": lambda text: rx.hstack( rx.box( width="4.5px", height="4.5px", margin_right="8px", background_color="var(--prismui-color-5)", border_radius="50%", ), rx.text( text, line_height="1.75", word_wrap="break-word", word_break="break-all", white_space="pre-line", ), align_items="center", justify_content="flex-start", margin_bottom="4px", ), "hr": lambda _: rx.divider( height="1px", margin="12px 0", background_color="var(--prismui-background-color-6)", ), "table": lambda children: rx.el.table( children, style={ "margin": "12px 0", "width": "100%", "border-collapse": "collapse", }, ), "thead": lambda children: rx.el.thead(children), "tbody": lambda children: rx.el.tbody(children), "tr": lambda children: rx.el.tr(children), "th": lambda text: rx.el.th( text, style={ "padding": "10px 12px", "background": "var(--prismui-background-color-3)", "border": "1px solid var(--prismui-background-color-6)", "text-align": "left", }, ), "td": lambda text: rx.el.td( text, style={ "padding": "10px 12px", "border": "1px solid var(--prismui-background-color-6)", "vertical-align": "top", }, ), }, width="100%", ), padding="0 16px", width="100%", key=dialog.id, ) def dialog_items_showing() -> rx.Component: """ 对话项列表展示 :return: Component """ # 当前会话的对话列表 dialogs = ConversationState.dialogs # 若对话列表为空则显示欢迎,否则显示对话列表 return rx.cond( dialogs.length() == 0, # 欢迎展示 greeting_showing(), rx.vstack( rx.auto_scroll( rx.foreach( dialogs, dialog_showing, ), width="100%", padding="0 12px", padding_bottom="180px", style={ "&::-webkit-scrollbar": { "display": "none", }, }, ), flex="1", width="100%", max_width="1200px", margin_x="auto", padding_top="20px", justify_content="flex-start", overflow_x="hidden", overflow_y="auto", ), ) def conversation_creating() -> rx.Component: """ 创建会话 :return: Component """ return rx.hstack( rx.el.style( """ .rt-TooltipArrow polygon { fill: var(--prismui-background-color-1) !important; } .rt-TooltipText { font-family: var(--prismui-font-family) !important; color: var(--prismui-color-1) !important; opacity: 1 !important; } """ ), rx.spacer(), rx.dialog.root( rx.dialog.trigger( rx.box( rx.tooltip( rx.icon( "plus", width="14px", height="14px", style={ "_hover": { "color": "var(--prismui-color-5)", } }, ), content="新建会话", side="top", side_offset=9, background_color="var(--prismui-background-color-1)", box_shadow="var(--prismui-box-shadow-3)", ), display="flex", justify_content="center", align_items="center", width="24px", height="24px", background_color="var(--prismui-background-color-1)", box_shadow="var(--prismui-box-shadow-8)", border_radius="var(--prismui-border-radius-9)", 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=ConversationState.is_conversation_creating, on_open_change=ConversationState.toggle_conversation_creating, ), display="flex", justify_content="flex-end", align_items="center", gap="4px", padding="0 12px", margin_bottom="8px", width="100%", max_width="1200px", height="39px", background="transparent", ) def user_prompt_sending() -> rx.Component: """ 用户提示词发送 :return: Component """ return rx.vstack( # 创建对话 conversation_creating(), rx.vstack( rx.text_area( # 值绑定用户提示词 value=ConversationState.user_prompt, # 输入事件:设置用户提示词 on_change=ConversationState.set_user_prompt, placeholder="请输入您的问题,按Enter换行", vertical_align="middle", width="100%", min_height="64px", max_height="224px", padding="4px 0", background_color="var(--prismui-background-color-1)", border="none !important", outline="none !important", box_shadow="none !important", style={ ".rt-TextAreaInput": { "border": "none !important", "outline": "none !important", "box-shadow": "none !important", "field-sizing": "content !important", "min-height": "64px", "max-height": "224px", "overflow-y": "auto", }, }, ), rx.hstack( rx.spacer(), rx.button( rx.icon( "send", margin_right="4px", width="15px", height="15px", color="var(--prismui-color-9)", ), rx.text( "发送", line_height="21px", color="var(--prismui-color-9)", ), loading=ConversationState.running_status, # 是否禁用绑定用户提示词发送禁用状态 disabled=ConversationState.is_user_prompt_sending_disabled, # 点击事件:发送用户提示词 on_click=ConversationState.handle_user_prompt, display="inline-flex", position="relative", justify_content="center", align_items="center", padding="0 12px", background_color=rx.cond( ConversationState.is_user_prompt_sending_disabled, "var(--prismui-color-11)", "var(--prismui-color-5)", ), border="none", border_radius="var(--prismui-border-radius-5)", inline_height="1.5", font_size="var(--prismui-font-size-2)", white_space="nowrap", overflow="hidden", cursor=rx.cond( ConversationState.is_user_prompt_sending_disabled, "not-allowed", "pointer", ), ), width="100%", justify_content="flex-end", align_items="center", height="32px", ), width="100%", padding="12px 16px", background_color="var(--prismui-background-color-1)", border_radius="var(--prismui-border-radius-6)", box_shadow="var(--prismui-box-shadow-9)", ), # 免责声明 rx.text( "内容由大模型生成,无法确保准确性和完整性,仅供参考", width="100%", text_align="center", margin_top="8px", line_height="18px", font_size="var(--prismui-font-size-1)", color="var(--prismui-color-10)", ), position="absolute", bottom="0", left="0", right="0", z_index="99", display="flex", flex_direction="column", width="100%", max_width="1200px", padding="0 12px 12px", margin_x="auto", border="none", ) def conversation_history_collapse_button( is_conversation_history_shown: bool, ) -> rx.Component: """ 会话历史折叠面板按钮 :param is_conversation_history_shown: 会话历史展示状态 :return: Component """ return rx.button( rx.icon( rx.cond(is_conversation_history_shown, "chevron-left", "chevron-right"), stroke_width=1, width="16px", height="16px", color="var(--prismui-color-3)", ), # 点击事件:切换会话历史折叠面板展开状态 on_click=ConversationState.toggle_conversation_history_shown, position="absolute", top="calc(50% - 20px)", left=rx.cond( is_conversation_history_shown, "calc(clamp(240px, 25%, 380px) - 8px)", "0" ), z_index="99", width="16px", height="40px", background_color="var(--prismui-background-color-1)", border_radius=rx.cond( is_conversation_history_shown, "var(--prismui-border-radius-2)", "0 var(--prismui-border-radius-2)", ), box_shadow="var(--prismui-box-shadow-4)", transition="all 0.3s ease-in-out", cursor="pointer", ) def conversation() -> rx.Component: """ 会话页面:布局参考 MetaChat,左侧为折叠面板,右侧为工作区。其中,工作区包含展示区和操作区。若对话历史为空则展示欢迎文案,否则展示对话历史 """ # 会话历史展示状态 is_conversation_history_shown = ConversationState.is_conversation_history_shown return rx.box( rx.hstack( # 会话历史列表展示 conversation_history_items_showing(is_conversation_history_shown), rx.box( rx.vstack( # 对话列表展示 dialog_items_showing(), width="100%", height="100%", gap="8px", align_items="center", ), # 用户提示词发送 user_prompt_sending(), position="relative", display="flex", flex="1", width="0", height="100%", padding_bottom="120px", background="var(--prismui-background-2)", ), position="relative", display="flex", flex="1", height="100%", min_height="0", overflow="hidden", transition="all 0.3s ease-in-out", ), # 会话历史折叠面板按钮 conversation_history_collapse_button(is_conversation_history_shown), # 挂载事件:恢复会话状态 on_mount=AuthState.resume_conversation_state, position="relative", width="100%", height="100%", border_radius="var(--prismui-border-radius-4)", overflow="hidden", )