Python/agent/application/pages/conversation.py

885 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
会话页面
"""
import reflex as rx
from application.states import AuthState, ConversationState
from application.states.models import (
ConversationHistoryItem,
MessageHistoryItem,
MessageType,
WorkFlowType,
)
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_flow(
WorkFlowType.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="flexstart",
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",
)