Python/agent/application/pages/conversation.py

833 lines
29 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
from reflex_base.vars.object import ObjectVar
def render_conversation_history_item(
item: rx.Var[tuple],
) -> rx.Component:
"""
渲染会话历史条目
:param item: 会话历史条目
:return: Component
"""
conversation_id: rx.Var[str] = item[0]
conversation: ObjectVar[Conversation] = item[1].to(Conversation)
# 会话激活状态
is_conversation_actived = conversation_id == ConversationState.conversation_id
return rx.box(
rx.vstack(
rx.hstack(
# 对话描述
rx.text(
conversation.description,
flex=1,
height="22px",
line_height="22px",
padding_right="4px",
color="var(--prismui-color-2)",
overflow="hidden",
text_overflow="ellipsis",
white_space="nowrap",
),
# 更多按钮
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="-12px",
left="50%",
transform="translateX(-50%) rotate(45deg)",
width="8px",
height="8px",
background_color="var(--prismui-background-color-1)",
),
rx.popover.close(
rx.box(
"删除",
width="100%",
height="24px",
padding="4px",
border_radius="var(--prismui-border-radius-1)",
line_height="16px",
font_size="var(--prismui-font-size-1)",
color="var(--prismui-color-1)",
style={
"_hover": {
"background_color": "var(--prismui-background-color-6)",
}
},
# 点击事件:删除对话
on_click=lambda: ConversationState.delete_conversation(
conversation_id
),
),
),
position="relative",
width="100%",
),
side="bottom",
side_offset=8,
position="relative",
align="center",
padding="8px",
border_radius="var(--prismui-border-radius-1)",
box_shadow="0 2px 12px rgba(0,0,0,0.1)",
font_family="var(--prismui-font-family)",
overflow="visible",
),
open_delay=0,
),
min_width="14px",
cursor="pointer",
style={
"opacity": rx.cond(
is_conversation_actived, "1", "0"
), # 若当前会话已激活则不透明,否则透明
"transition": "opacity 0.18s ease",
"pointer_events": rx.cond(
is_conversation_actived, "auto", "none"
),
},
),
display="flex",
align_items="center",
width="100%",
margin_bottom="8px",
cursor="pointer",
),
rx.hstack(
rx.spacer(),
# 创建时间
rx.text(
"1",
margin_bottom="8px",
),
),
),
width="100%",
padding="16px",
margin_bottom="8px",
background=rx.cond(
is_conversation_actived,
"linear-gradient(to right, #f3efff, #f3efff33, #e2f1fd33, #e2f1fd)",
"var(--prismui-background-color-1)",
),
border_radius="var(--prismui-border-radius-3)",
box_shadow=rx.cond(is_conversation_actived, "2px 2px 8px #e9e9e9", "none"),
style={
"&: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",
}, # 鼠标悬停强制显示右侧三点按钮
},
cursor="pointer",
# 点击事件:将指定对话唯一标识设置为当前对话唯一标识
on_click=ConversationState.switch_conversation(conversation_id),
)
def conversation_history_collapse(
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,
render_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.3s ease-in-out",
overflow="hidden",
)
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="var(--prismui-border-radius-3)",
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="var(--prismui-border-radius-1)",
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_conversation_creating,
on_open_change=ConversationState.toggle_conversation_creating,
),
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="20pxvar(--devui-border-radius-4)",
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 workbench() -> 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_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)",
),
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.3s ease-in-out",
cursor="pointer",
# 点击事件:切换会话历史折叠面板展开状态
on_click=ConversationState.toggle_conversation_history_shown,
)
def render_conversation() -> rx.Component:
"""
渲染会话页面:布局参考 MetaChat左侧为折叠面板右侧为工作区。其中工作区包含展示区和操作区。若对话历史为空则展示欢迎文案否则展示对话历史
"""
# 会话历史展示状态
is_conversation_history_shown = ConversationState.is_conversation_history_shown
return rx.box(
rx.hstack(
<<<<<<< HEAD
# 会话历史折叠面板
conversation_history_collapse(is_conversation_history_shown),
# 工作台
workbench(),
=======
# 渲染折叠面板
render_collapse(is_conversation_history_shown),
# 渲染工作区
render_workplace(),
>>>>>>> 7f6a992830604a43946173a1d6573aa63f835d55
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_conversation_history_shown),
position="relative",
width="100%",
height="100%",
border_radius="var(--prismui-border-radius-4)",
overflow="hidden",
# 挂载事件:同步用户唯一标识
on_mount=AuthState.sync_user_id,
)