Python/agent/application/pages/conversation.py

816 lines
28 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.models import Conversation, Dialog, ThoughtNode
from application.states import ConversationState
def render_conversation_history_item(
conversation_id: str, conversation: Conversation
) -> rx.Component:
"""
渲染会话历史条目
:param conversation_id: 会话唯一标识
:param conversation: 会话实例
:return: Component
"""
# 会话激活状态
is_actived = conversation_id == ConversationState.conversation_id
return rx.box(
rx.hstack(
# 渲染对话描述
rx.text(
conversation.description,
flex=1,
height="22px",
line_height="22px",
font_size="var(--devui-font-size)",
padding_right="4px",
overflow="hidden",
text_overflow="ellipsis",
white_space="nowrap",
),
# 渲染更多按钮
rx.box(
rx.popover.root(
# 触发事件:点击更多按钮
rx.popover.trigger(
rx.box(
rx.icon(
"ellipsis", size=14, color="var(--devui-text-weak)"
),
cursor="pointer",
)
),
# 渲染气泡卡片
rx.popover.content(
rx.vstack(
rx.box(
position="absolute",
top="-12px",
left="50%",
transform="translateX(-50%) rotate(45deg)",
width="8px",
height="8px",
background_color="#ffffff",
box_shadow="-2px -2px 4px rgba(0,0,0,0.05)",
),
rx.popover.close(
rx.box(
"删除",
width="100%",
height="24px",
padding="4px",
border_radius="4px",
line_height="16px",
font_family="HuaweiFont,Helvetica,Arial,PingFangSC-Regular,Hiragino Sans GB,Microsoft YaHei,微软雅黑,Microsoft JhengHei",
font_size="12px",
color="#252b3a",
style={
"_hover": {
"background_color": "#f2f2f3",
}
},
# 点击事件:删除对话
on_click=lambda: ConversationState.delete_conversation(
conversation_id
),
)
),
position="relative",
width="100%",
),
position="relative",
align="center",
padding="8px",
border_radius="4px",
background_color="#ffffff",
box_shadow="0 2px 12px rgba(0,0,0,0.1)",
overflow="visible",
side="bottom",
side_offset=9,
),
open_delay=0,
),
min_width="14px",
cursor="pointer",
style={
"opacity": rx.cond(
is_actived, "1", "0"
), # 若当前会话已激活则不透明,否则透明
"transition": "opacity 0.18s ease",
"pointer_events": rx.cond(is_actived, "auto", "none"),
},
),
display="flex",
align_items="center",
width="100%",
margin_bottom="8px",
),
width="100%",
padding="16px",
margin_bottom="8px",
border_radius="8px",
line_height="1.5",
color="var(--devui-text-weak)",
cursor="pointer",
style={
"background": rx.cond(
is_actived,
"linear-gradient(to right, #f3efff, #f3efff33, #e2f1fd33, #e2f1fd)",
"var(--devui-base-bg)",
),
"box_shadow": rx.cond(is_actived, "2px 2px 8px #e9e9e9", "none"),
"&:hover": {
"background": "linear-gradient(to right, #f3efff, #f3efff33, #e2f1fd33, #e2f1fd)",
"box_shadow": "2px 2px 8px #e9e9e9",
}, # 鼠标悬停渲染背景颜色和阴影
"&:hover > div > div:last-child": {
"opacity": "1 !important",
"pointer_events": "auto !important",
}, # 鼠标悬停强制显示右侧三点按钮
},
# 点击事件:将指定对话唯一标识设置为当前对话唯一标识
on_click=ConversationState.switch_conversation(conversation_id),
)
def render_conversation_history_collapse(is_collapse_expanded: bool) -> rx.Component:
"""
渲染对话历史折叠面板
:param is_collapse_expanded: 对话历史折叠面板展开状态
:return: Component
"""
return rx.box(
rx.vstack(
# 渲染标题
rx.hstack(
rx.text(
"会话历史",
margin_bottom="8px",
font_size="var(--devui-font-size-lg)",
font_weight="700",
white_space="nowrap",
),
display="flex",
align_items="center",
justify_content="space-between",
),
# 渲染列表
rx.auto_scroll(
rx.foreach(
ConversationState.conversation_history,
lambda item, _: render_conversation_history_item(item[0], item[1]),
),
flex="1",
margin_top="8px",
width="100%",
align_items="stretch",
overflow="auto",
style={
"&::-webkit-scrollbar": {
"display": "none",
},
},
),
display="flex",
flex_direction="column",
height="100%",
padding="12px",
background_color="#f9f9f9cc",
backdrop_filter="blur(50px)",
gap="12px",
color="var(--prismui-color-text)",
),
width=rx.cond(is_collapse_expanded, "25%", "0px"),
min_width=rx.cond(is_collapse_expanded, "240px", "0px"),
max_width=rx.cond(is_collapse_expanded, "380px", "0px"),
height="100%",
overflow="hidden",
transition="all 0.3s ease-in-out",
)
def render_greeting() -> rx.Component:
"""
渲染问候
:return: Component
"""
return rx.vstack(
rx.vstack(
# 品牌图标、名称和介绍
rx.vstack(
rx.hstack(
rx.image(
src="/logo.png",
width="64px",
height="64px",
object_fit="contain",
),
rx.box(
"棱镜球",
font_size="32px",
font_weight="700",
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",
font_size="var(--devui-font-size)",
),
display="flex",
flex_direction="column",
width="100%",
align_items="center",
gap="12px",
color="var(--prismui-color-text)",
),
# 猜你想问
rx.vstack(
rx.hstack(
rx.text(
"猜你想问",
line_height="24px",
font_size="16px",
font_weight="700",
),
display="flex",
width="100%",
justify_content="space-between",
align_items="center",
margin_bottom="16px",
),
rx.hstack(
rx.box(
"需求梳理",
padding="10px 16px",
background_color="var(--devui-dividing-line)",
border_radius="var(--devui-border-radius-full)",
font_size="var(--devui-font-size)",
color="var(--devui-aide-text)",
cursor="pointer",
),
display="flex",
flex_wrap="wrap",
align_items="center",
gap="12px",
),
width="100%",
padding="24px",
background_color="var(--devui-base-bg)",
border_radius="24px",
),
display="flex",
flex_direction="column",
width="100%",
min_height="0",
margin="auto 0",
align_items="center",
gap="24px",
color="var(--prismui-color-text)",
),
display="flex",
flex="1",
flex_direction="column",
width="100%",
max_width="1200px",
padding="0 12px",
justify_content="flex-start",
gap="24px",
overflow="auto",
style={
"&::-webkit-scrollbar": {
"display": "none",
},
},
)
def render_thought_node(thought_node_id: int, thought_node: ThoughtNode):
"""
渲染思考节点
:param thought_node_id: 思考节点唯一标识
:param thought_node: 思考节点实例
:return: Component
"""
return rx.vstack(
rx.match(
thought_node.kind,
(
"thinking",
rx.text(
thought_node.content,
line_height="22px",
font_size="var(--prismui-font-size-2)",
color="var(--prismui-color-3)",
),
),
rx.fragment(),
),
align_items="flex-start",
width="100%",
key=thought_node_id,
)
def render_dialog_history_item(dialog_id: str, dialog: Dialog) -> rx.Component:
"""
渲染对话历史条目
:param dialog_id: 对话唯一标识
:param dialog: 对话实例
:return: Component
"""
# 推理状态
is_thinking = dialog.is_thinking
# 思考折叠面板展开状态
is_collapse_expanded = dialog.is_collapse_expanded
# 思考节点
thought_nodes = dialog.thought_nodes
return rx.vstack(
# 渲染问题和操作栏
rx.hstack(
rx.spacer(),
rx.vstack(
# 渲染问题
rx.text(
dialog.question,
max_width="600px",
padding="12px 16px",
background_color="var(--prismui-background-color-3)",
border_radius="12px",
word_wrap="break-word",
word_break="break-all",
white_space="pre-line",
color="var(--prismui-color-text)",
),
# 操作栏
rx.hstack(
rx.box(
rx.icon(
"rotate-ccw",
width="14px",
height="14px",
color="var(--prismui-color-2)",
),
padding="4px",
border_radius="4px",
cursor="pointer",
style={
"_hover": {
"background_color": "var(--prismui-background-color-2)",
}
},
),
width="100%",
margin_top="8px",
),
),
width="100%",
margin_top="8px",
align_items="flex-start",
gap="4px",
font_size="var(--devui-font-size)",
),
# 渲染思考折叠面板
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_collapse_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(
thought_nodes,
lambda i, _: render_thought_node(i[0], i[1]),
),
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_collapse_expanded, "1fr", "0fr"),
"opacity": rx.cond(is_collapse_expanded, "1", "0"),
# 双属性同步过渡,和千问一致
"transition": "grid-template-rows 0.3s ease-out, opacity 0.3s ease-out",
},
),
),
# 渲染回答
rx.markdown(
dialog.answer,
component_map={
"p": lambda text: rx.text(
text,
margin_bottom="8px",
word_wrap="break-word",
word_break="break-all",
white_space="pre-line",
line_height="24px",
font_size="var(--prismui-font-size-1)",
color="var(--prismui-color-1)",
),
"strong": lambda text: rx.text(
text,
display="inline",
font_weight="var(--prismui-font-weight-1)",
),
"ul": lambda children: rx.vstack(
children, margin_bottom="8px", gap="4px"
),
"li": lambda text: rx.text(
text,
word_wrap="break-word",
word_break="break-all",
white_space="pre-line",
line_height="24px",
font_size="var(--prismui-font-size-1)",
color="var(--prismui-color-1)",
style={
"display": "list-item",
"list_style_type": "disc",
"list_style_position": "inside",
},
),
},
width="100%",
margin_top="8px",
),
padding="0 16px",
width="100%",
key=dialog_id,
)
def render_dialog_history() -> rx.Component:
"""
渲染对话历史
:return: Component
"""
# 获取当前会话的对话历史
dialog_history = ConversationState.dialog_history
# 若运行字典为空则渲染品牌图标、名称和介绍、预设用户提示词等,否则遍历渲染运行项
return rx.cond(
dialog_history.length() == 0,
# 渲染问候
render_greeting(),
# 遍历渲染对话项
rx.vstack(
rx.auto_scroll(
rx.foreach(
dialog_history,
lambda i, _: render_dialog_history_item(i[0], i[1]),
),
width="100%",
padding="0 12px",
),
flex="1",
width="100%",
max_width="1200px",
margin_x="auto",
padding_top="20px",
justify_content="flex-start",
overflow_x="hidden",
overflow_y="auto",
),
)
def render_create_conversation_button() -> rx.Component:
"""
渲染新建会话按钮
:return: Component
"""
return rx.hstack(
rx.el.style(
"""
.rt-TooltipArrow polygon {
fill: #ffffff !important;
}
.rt-TooltipText {
color: var(--prismui-color-text) !important;
opacity: 1 !important;
}
"""
),
rx.spacer(),
rx.dialog.root(
# 触发事件:点击图标
rx.dialog.trigger(
rx.box(
rx.tooltip(
rx.icon(
"plus",
size=14,
color="var(--prismui-color-text)",
style={
"_hover": {
"color": "var(--devui-brand)",
}
},
),
content="新建会话",
background_color="#ffffff",
box_shadow="0 2px 12px 0 rgba(37, 43, 58, .24)",
color="#252b3a",
side="top",
side_offset=9,
),
display="flex",
width="24px",
height="24px",
background_color="var(--devui-base-bg)",
box_shadow="0 1px 8px #1919190f",
border_radius="var(--devui-border-radius-full)",
justify_content="center",
align_items="center",
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_creating_conversation,
on_open_change=ConversationState.toggle_creating_conversation,
),
display="flex",
width="100%",
max_width="1200px",
height="39px",
padding="0 12px",
justify_content="flex-end",
align_items="center",
gap="4px",
)
def render_input() -> rx.Component:
"""
渲染输入框
:return: Component
"""
return rx.vstack(
# 渲染自定义输入组件
rx.form(
rx.box(
rx.vstack(
rx.hstack(
rx.text_area(
name="question",
placeholder="请输入您的问题按Enter换行",
vertical_align="middle",
width="100%",
height="64px",
padding="4px 0",
background_color="var(--devui-base-bg)",
font_size="var(--devui-font-size)",
color="var(--prismui-color-text)",
style={
"border": "none !important",
"outline": "none !important",
"boxShadow": "none !important",
},
),
width="100%",
padding="0 16px",
),
rx.hstack(
rx.spacer(),
rx.button(
rx.icon(
"send",
margin_right="4px",
width="12px",
height="12px",
),
rx.text("发送"),
position="relative",
display="inline-flex",
padding="0 12px",
background_color="var(--devui-primary)",
border="none",
border_radius="20px",
align_items="center",
justify_content="center",
white_space="nowrap",
inline_height="1.5",
font_size="var(--devui-font-size)",
color="var(--devui-light-text)",
overflow="hidden",
cursor="pointer",
type="submit",
loading=ConversationState.running_status,
disabled=ConversationState.running_status,
),
width="100%",
justify_content="flex-end",
align_items="center",
height="32px",
padding="0 16px",
),
),
style={
"* textarea::placeholder": {
"fontFamily": "var(--font-family)",
"fontSize": "var(--devui-font-size)",
"color": "var(--placeholder)",
"opacity": 1,
},
},
),
display="flex",
flex_direction="column",
width="100%",
padding="12px 0",
background_color="var(--devui-base-bg)",
border="none",
border_radius="16px",
box_shadow="0 1px 8px 0 var(--mc-box-shadow)",
on_submit=ConversationState.run,
reset_on_submit=True,
),
# 渲染底部文案
rx.text(
"内容由大模型生成,无法确保准确性和完整性,仅供参考",
margin_top="8px",
text_align="center",
font_size="12px",
color="var(--devui-aide-text)",
),
width="100%",
max_width="1200px",
padding="0 12px 12px",
align_items="center",
)
def render_conversation_workplace() -> rx.Component:
"""
渲染会话工作区
:return: Component
"""
return rx.box(
rx.vstack(
# 渲染对话历史
render_dialog_history(),
# 渲染新建对话按钮
render_create_conversation_button(),
# 渲染输入框
render_input(),
display="flex",
flex_flow="column",
width="100%",
height="100%",
gap="8px",
align_items="center",
),
position="relative",
display="flex",
flex="1",
width="0",
height="100%",
background="linear-gradient(180deg, #fffffff2, #f8fafff2 99%)",
)
def render_conversation_history_collapse_button(
is_expanded: bool,
) -> rx.Component:
"""
渲染会话历史折叠面板的打开/关闭按钮
:param is_expanded: 会话历史折叠面板展开状态
:return: Component
"""
return rx.button(
rx.cond(
is_expanded,
rx.icon(
"chevron-left",
width="16px",
height="16px",
color="var(--devui-icon-fill-weak)",
),
rx.icon(
"chevron-right",
width="16px",
height="16px",
color="var(--devui-icon-fill-weak)",
),
),
position="absolute",
top="50%",
left=rx.cond(is_expanded, "calc(clamp(240px, 25%, 380px) - 8px)", "0"),
z_index=99,
width="16px",
height="40px",
background="var(--prismui-background-color-1)",
box_shadow="2px 0 4px 0 var(--mc-float-block-shadow)",
border_radius=rx.cond(is_expanded, "6px", "0 6px 6px 0"),
transition="all 0.3s ease-in-out",
cursor="pointer",
# 点击事件:切换会话历史折叠面板展开状态
on_click=ConversationState.toggle_conversation_history_shown,
)
def render_conversation() -> rx.Component:
"""
渲染会话页面:布局参考 MetaChat左侧为会话历史右侧为对话历史和输入框
"""
# 会话历史折叠面板的展开状态
is_expanded = ConversationState.is_conversation_history_shown
return rx.box(
# 渲染折叠面板
rx.hstack(
# 渲染折叠面板
render_conversation_history_collapse(is_expanded),
# 渲染会话工作区
render_conversation_workplace(),
position="relative",
display="flex",
flex="1",
height="100%",
min_height="0",
overflow="hidden",
transition="all 0.3s ease-in-out",
),
# 渲染对话历史折叠面板打开/关闭按钮
render_conversation_history_collapse_button(is_expanded),
position="relative",
width="100%",
height="100%",
border_radius="12px",
overflow="hidden",
# 挂载事件:初始化会话历史
on_mount=ConversationState.on_mount,
)