# -*- coding: utf-8 -*- """ 会话页面 """ import reflex as rx from application.states import AuthState, TaskState from application.states.models import ( TaskHistoryItem, MessageHistoryItem, MessageType, WorkflowType, ) def workflow_button(workflow_type: WorkflowType): """ 工作流按钮 :param workflow_type: 工作类型 :return: rx.Component """ return rx.box( workflow_type.value, # 点击事件:设置工作流 on_click=lambda: TaskState.set_workflow(workflow_type), padding="10px 16px", background_color="var(--prismui-background-color-6)", border_radius="var(--prismui-border-radius-9)", color="var(--prismui-color-10)", cursor="pointer", ) 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( # 工作流按钮:预定航班 workflow_button(WorkflowType.BOOK_FLIGHT), # 工作流按钮:银行客户服务 workflow_button(WorkflowType.BANK_CUSTOMER_SERVICE), # 工作流按钮:联网检索 workflow_button(WorkflowType.WEB_RETRIEVE), # 工作流按钮:知识库检索 workflow_button(WorkflowType.KNOWLEDGE_BASE_RETRIEVE), 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)", class_name=rx.cond(item.is_running, "is_running", None), ), rx.icon( "chevron-right", width="14px", height="14px", 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: TaskState.toggle_message_history_item_shown(item.id), cursor="pointer", ), # 思考的内容 rx.box( rx.box( rx.cond( item.is_shown, rx.markdown( item.content, width="100%", 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 tool_call(item: MessageHistoryItem) -> rx.Component: """ 工具调用 :return: Component """ return rx.vstack( # 标题栏 rx.box( rx.hstack( rx.icon( "square-function", width="14px", height="14px", ), rx.text( item.title, font_size="var(--prismui-font-size-1)", class_name=rx.cond(item.is_running, "is_running", None), ), rx.icon( "chevron-right", width="14px", height="14px", 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: TaskState.toggle_message_history_item_shown(item.id), cursor="pointer", ), rx.box( rx.box( rx.cond( item.is_shown, rx.hstack( # 垂线 rx.box( width="1px", margin="12px 7px", background_color="var(--prismui-background-color-2)", ), rx.vstack( # 内容 rx.markdown( item.content, allow_html=True, width="100%", margin="4px 0", line_height="1.6", font_size="var(--prismui-font-size-1)", ), # 若有审批 rx.cond( item.has_approval, rx.vstack( rx.text_area( # 值绑定审批内容 value=item.approval_content, # 值变化事件:设置审批内容 on_change=lambda content: TaskState.set_approval_content( item.id, content ), placeholder="请输入", vertical_align="middle", width="100%", height="auto", padding="4px 0", outline="none !important", box_shadow="none !important", style={ ".rt-TextAreaInput": { "font-size": "var(--prismui-font-size-1) !important", "border": "none !important", "overflow-y": "hidden !important", }, ".rt-TextAreaInput:disabled": { "background-color": "var(--prismui-background-color-1) !important", "font-size": "var(--prismui-font-size-1) !important", "cursor": "not-allowed !important", }, }, ), rx.hstack( rx.button( "提交", # 点击事件:审批 on_click=lambda: TaskState.run( True, item.id ), padding="0 12px", background_color="var(--prismui-background-color-7)", border_radius="var(--prismui-border-radius-3)", line_height="18px", font_size="var(--prismui-font-size-1)", color="var(--prismui-color-9)", cursor="pointer", ), justify_content="flex-end", width="100%", ), width="100%", padding="12px 16px", background_color="var(--prismui-background-color-1)", border="var(--prismui-border-1)", border_radius="var(--prismui-border-radius-4)", ), rx.fragment(), ), width="100%", gap="4px", ), align_items="stretch", width="100%", margin_bottom="8px", gap="4px", ), 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 text(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", ), "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", ), "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", ), "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", ), "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", ), 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%", margin_bottom="8px", ) 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.TOOL_CALL, tool_call(item)), (MessageType.TEXT, text(item)), ), padding="0 16px", width="100%", ) def message_history() -> rx.Component: """ 消息历史 :return: Component """ return rx.box( # 若消息历史为空则显示使用指引,否则渲染消息历史项 rx.cond( TaskState.message_history.length() == 0, use_guidance(), rx.vstack( rx.auto_scroll( rx.vstack( rx.foreach(TaskState.message_history, message_history_item), # 若等待流式输出则显示加载动效 rx.cond( TaskState.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="8px 0", 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=TaskState.create_task, 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_box() -> rx.Component: """ 用户提示词框 :return: rx.Component """ return rx.vstack( # 创建对话 conversation_creating(), rx.vstack( rx.text_area( # 值绑定:用户提示词 value=TaskState.user_prompt, # 输入事件:设置用户提示词 on_change=TaskState.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=TaskState.is_running, # 不可点击状态:绑定用户提示词发送按钮不可点击状态 disabled=TaskState.is_user_prompt_sending_button_disabled, # 点击事件:运行 on_click=lambda: TaskState.run(), display="inline-flex", position="relative", justify_content="center", align_items="center", padding="0 12px", background_color=rx.cond( TaskState.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", cursor=rx.cond( TaskState.is_user_prompt_sending_button_disabled, "not-allowed", "pointer", ), ), justify_content="flex-end", align_items="center", height="32px", width="100%", ), 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)", ), z_index="99", display="flex", flex_direction="column", width="100%", max_width="1200px", padding="0 12px 12px", margin_x="auto", border="none", ) def task_page() -> rx.Component: """ 任务页面 :return: rx.Component """ return rx.hstack( # 渲染消息历史和用户提示词框 rx.vstack( rx.vstack( # 渲染消息历史 message_history(), align_items="center", gap="8px", flex="1", width="100%", overflow_y="auto", ), user_prompt_box(), width="100%", height="100%", background="var(--prismui-background-2)", overflow="hidden", ), # 渲染... width="100%", height="100%", # 挂载事件:恢复任务状态 on_mount=AuthState.resume_task_state, )