# -*- coding: utf-8 -*- """ 会话页面 """ import reflex as rx from application.states import AuthState, ConversationState from application.states.models import ( ConversationHistoryItem, MessageHistoryItem, MessageType, WorkType, ) def conversation_history_item( item: ConversationHistoryItem, ) -> rx.Component: """ 会话历史项 :param item: 会话历史项 :return: Component """ # 高亮 is_highlight = ( item.id == ConversationState.is_conversation_history_item_popover_shown ) | (item.id == ConversationState.actived_conversation_id) return rx.list.item( rx.vstack( rx.hstack( # 会话历史项的会话描述 rx.text( item.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( item.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.set_actived_conversation(item.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( item.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_conversation_history_item_popover_shown( item.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(is_highlight, "1", "0"), transition="opacity 0.18s ease-in-out", pointer_events=rx.cond(is_highlight, "auto", "none"), ), display="flex", position="relative", align_items="flex-start", width="100%", padding="16px", margin_bottom="8px", background=rx.cond( is_highlight, "var(--prismui-background-3)", "var(--prismui-background-color-1)", ), border_radius="var(--prismui-border-radius-3)", box_shadow=rx.cond(is_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( 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.conversation_history, conversation_history_item, ), 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.18s ease-in-out", overflow="hidden", ) def use_guidance() -> rx.Component: """ 使用指引 :return: Component """ return rx.vstack( # 品牌图标、名称和介绍 rx.vstack( rx.hstack( rx.image( src="/logo.png", width="64px", height="64px", ), rx.text( "棱镜球", font_size="calc(var(--prismui-font-size-3) * 2)", font_weight="var(--prismui-font-weight-3)", letter_spacing="1px", ), align_items="center", gap="8px", ), rx.vstack( rx.text( "产品汪的棱镜球,精准捕捉各类业务需求", width="100%", text_align="center", ), rx.text( "可帮助检索业务资料,协助生成产品方案、输出 PRD 、流程图和原型图等", width="100%", text_align="center", ), line_height="1.5", gap="4px", ), align_items="center", gap="12px", ), # 演示案例 rx.vstack( rx.text( "演示案例", margin_bottom="16px", line_height="24px", font_size="var(--prismui-font-size-3)", font_weight="var(--prismui-font-weight-3)", ), rx.hstack( rx.box( "预定航班", on_click=lambda: ConversationState.init_work( WorkType.BOOK_FLIGHT ), padding="10px 16px", background_color="var(--prismui-background-color-6)", border_radius="var(--prismui-border-radius-9)", color="var(--prismui-color-10)", cursor="pointer", ), 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)", ), flex="1", justify_content="center", align_items="center", gap="24px", width="100%", max_width="1200px", height="100%", padding="0 12px", margin="auto", color="var(--prismui-color-text)", ) def user_prompt(item: MessageHistoryItem) -> rx.Component: """ 用户提示词 :return: Component """ return rx.hstack( rx.spacer(), rx.vstack( rx.text( item.content, 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", ) def thinking(item: MessageHistoryItem) -> rx.Component: """ 思考 :return: Component """ return rx.vstack( # 思考的标题栏 rx.box( rx.hstack( rx.text( item.title, font_size="var(--prismui-font-size-1)", color="var(--prismui-color-3)", ), rx.icon( "chevron-right", width="14px", height="14px", color="var(--prismui-color-3)", transform=rx.cond(item.is_shown, "rotate(90deg)", "rotate(0deg)"), transition="transform 0.18s ease-in-out", ), align_items="center", gap="4px", margin_bottom="8px", line_height="22px", ), # 点击事件,展示 / 隐藏消息历史项 on_click=lambda: ConversationState.toggle_message_history_item_shown( item.id ), cursor="pointer", ), # 思考的内容 rx.box( rx.box( rx.cond( item.is_shown, rx.text( item.content, width="100%", margin="4px 0", line_height="1.6", font_size="var(--prismui-font-size-1)", color="var(--prismui-color-3)", ), rx.fragment(), ), min_height="0", ), display="grid", grid_template_rows=rx.cond(item.is_shown, "1fr", "0fr"), width="100%", opacity=rx.cond(item.is_shown, "1", "0"), transition="grid-template-rows 0.18s ease-in-out, opacity 0.18s ease-in-out", overflow="hidden", ), ) def result_output(item: MessageHistoryItem) -> rx.Component: """ 结果输出 :return: Component """ return rx.markdown( item.content, 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", ), "strong": lambda text: rx.text( text, display="inline", font_weight="var(--prismui-font-weight-3)", ), "code": lambda text: rx.text( text, display="inline", background_color="var(--prismui-background-color-6)", border_radius="var(--prismui-border-radius-1)", color="var(--prismui-color-5)", ), "h1": lambda text: rx.text( text, margin="8px 0 4px", line_height="1.75", font_size="var(--prismui-font-size-4)", 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="var(--prismui-font-size-3)", 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", ), "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-background-color-7)", border_radius="100%", ), 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-2)", ), "table": lambda children: rx.el.table( children, width="100%", margin="8px 0", 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 children: rx.el.th( children, padding="8px 12px", background_color="var(--prismui-background-color-6)", border="1px solid var(--prismui-background-color-2)", text_align="left", ), "td": lambda children: rx.el.td( children, padding="8px 12px", border="1px solid var(--prismui-background-color-2)", text_align="left", ), }, width="100%", ) def message_history_item(item: MessageHistoryItem) -> rx.Component: """ 消息历史项 :param item: 对话实例 :return: Component """ return rx.vstack( rx.match( item.type, (MessageType.USER_PROMPT, user_prompt(item)), (MessageType.THINKING, thinking(item)), (MessageType.RESULT_OUTPUT, result_output(item)), ), padding="0 16px", width="100%", ) def message_history() -> rx.Component: """ 消息历史 :return: Component """ return rx.box( # 若消息历史为空则显示使用指引,否则渲染消息历史项 rx.cond( ConversationState.message_history.length() == 0, use_guidance(), rx.vstack( rx.auto_scroll( rx.vstack( rx.foreach( ConversationState.message_history, message_history_item ), # 若等待流式输出则显示加载动效 rx.cond( ConversationState.awaiting_stream, rx.box( rx.box( class_name="awaiting_stream", flex_shrink="0", ), display="flex", justify_content="flex‑start", align_items="center", width="100%", padding="0 16px", margin_bottom="8px", overflow="hidden", ), rx.fragment(), ), width="100%", ), 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", ), ), display="flex", flex_direction="column", width="100%", height="100%", min_height="0", ) 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.box( rx.tooltip( rx.icon( "plus", on_click=ConversationState.create_conversation, 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", ), 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.is_running, # 不可点击状态:绑定用户提示词发送按钮不可点击状态 disabled=ConversationState.is_user_prompt_sending_button_disabled, # 点击事件:运行 on_click=ConversationState.run, display="inline-flex", position="relative", justify_content="center", align_items="center", padding="0 12px", background_color=rx.cond( ConversationState.is_user_prompt_sending_button_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_button_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_toggle_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.18s ease-in-out", cursor="pointer", ) def conversation_page() -> rx.Component: """ 会话页面:布局参考 MetaChat,从左到右分别为会话历史和工作区。其中,工作区从上到下分别为消息历史和用户提示词输入框。 """ return rx.box( rx.hstack( # 会话历史 conversation_history(ConversationState.is_conversation_history_shown), # 工作区 rx.box( # 对话历史 rx.vstack( message_history(), 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.18s ease-in-out", ), # 会话历史显示 / 隐藏按钮 conversation_history_toggle_button( ConversationState.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", )