# -*- coding: utf-8 -*- """ 聊天页面相关组件 """ import reflex as rx from application.models import PartType, Part, Dialog from application.state import ChatState def render_user_prompt(user_prompt: str) -> rx.Component: """ 渲染用户提示词 :param user_prompt: 用户提示词 :return: Component """ return rx.markdown( user_prompt, 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", # 圆角 ) def render_part(dialog_id: str, part_id: str, part: Part): """ 渲染片段 :param dialog_id: 对话唯一标识 :param part_id: 片段唯一标识 :param part: 片段实例 :return: Component """ return rx.match( part.part_type, ( PartType.TEXT, rx.markdown( part.content, font_size="0.90rem", color=rx.color("gray", 12), # 字体颜色 background_color="transparent", # 背景颜色:设置为透明以继承父元素背景颜色 display="block", # 布局模式:铺满 max_width="85%", # 最大宽度 padding="0", # 内边距 margin_right="auto", # 右侧外边距自动调整 margin_bottom="12px", # 底部外边距 key=part_id, ), ), # 文本片段 (PartType.FINISHED, rx.fragment(key=part_id)), # 结束片段 rx.box( rx.cond( part.is_open, rx.hstack( rx.text( part.content, font_size="0.90rem", bold=True, color=rx.color("gray", 10), ), rx.spacer(), rx.icon("chevron_up", size=16, color=rx.color("gray", 6)), width="100%", cursor="pointer", padding_x="1em", padding_y="0.6em", background_color=rx.color("gray", 2), border_radius="8px", on_click=ChatState.toggle_collapse_panel(dialog_id, part_id), ), # 折叠面板打开时标题栏 rx.hstack( rx.hstack( rx.text( loading_prefix, font_size="0.90rem", bold=True, color=rx.color("gray", 10), ), loading_dot_group(part.is_streaming), spacing="8", # 间距 align_items="center", ), rx.spacer(), rx.icon("chevron_down", size=16, color=rx.color("gray", 6)), width="100%", cursor="pointer", on_click=ChatState.toggle_collapse_panel(dialog_id, part_id), padding_x="1em", padding_y="0.6em", background_color=rx.color("gray", 2), border_radius="8px", ), # 折叠面板关闭时标题栏 ), rx.cond(part.is_open, detail_box, rx.fragment()), width="100%", max_width="85%", margin_bottom="10px", cursor="pointer", key=part_id, ), ) def render_dialog_item(dialog_id: str, dialog: Dialog) -> rx.Component: """ 渲染对话项 :param dialog: 对话实例 :return: Component """ return rx.box( rx.box( render_user_prompt(user_prompt=dialog.user_prompt), text_align="right", width="100%", margin_bottom="8px", ), rx.box( rx.foreach( dialog.output, lambda part_id, part: render_part( dialog_id=dialog_id, part_id=part_id, part=part ), ), text_align="left", width="100%", margin_bottom="8px", ), width="min(100%, 50em)", # 最大宽度:父级元素最大宽度和50em中较小值 margin_x="auto", # 水平外边距:自动调整 key=dialog_id, ) def render_dialog_list() -> rx.Component: """ 渲染对话列表 :return: Component """ return rx.auto_scroll( rx.foreach( ChatState.get_dialogs, lambda dialog_id, dialog: render_dialog_item( dialog_id=dialog_id, dialog=dialog ), ), flex="1", padding="8px", overflow_y="auto", ) def render_input_bar() -> rx.Component: """ 渲染输入栏 """ return rx.center( rx.vstack( rx.form( rx.hstack( rx.input( name="user_prompt", placeholder="请输入...", flex="auto", ), rx.button( "发送", type="submit", loading=ChatState.get_current_chat_status, # 正在流式输出时按钮显示为 loading disabled=ChatState.get_current_chat_status, # 正在流式输出时按钮禁用 ), max_width="50em", margin="0 auto", align_items="center", ), on_submit=ChatState.process_input, # 处理输入,返回流式输出 reset_on_submit=True, # 提交后清空输入框 ), rx.text( "抹茶兔兔工作室", text_align="center", font_size=".75em", color=rx.color("mauve", 10), ), width="100%", padding_x="16px", align="stretch", ), position="sticky", bottom="0", left="0", padding_y="16px", backdrop_filter="auto", backdrop_blur="lg", border_top=f"1px solid {rx.color('mauve', 3)}", background_color=rx.color("mauve", 2), align="stretch", width="100%", ) # rx.center 等价 rx.box(display="flex", align_items="center", justify_content="center")