Python/agent/application/pages/conversation.py

815 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.domain_models import (
Conversation,
Dialog,
Thought,
ConversationHistoryItem,
)
from application.states import ConversationState, AuthState
def conversation_history_item(
item: ConversationHistoryItem,
) -> rx.Component:
"""
会话历史项
:param item: 会话历史项
:return: Component
"""
# 会话激活状态
is_actived: bool = item.id == ConversationState.conversation_id
return rx.list.item(
rx.vstack(
rx.hstack(
# 对话描述
rx.text(
item.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(
item.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_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",
cursor="pointer",
),
rx.hstack(
rx.spacer(),
# 创建时间
rx.text(
"1",
margin_bottom="8px",
),
),
),
width="100%",
padding="16px",
margin_bottom="8px",
background=rx.cond(
is_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_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(item.id),
)
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_items,
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 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 thought(thought: Thought):
"""
思考节点
:param thought: 思考节点实例
:return: Component
"""
return rx.vstack(
rx.match(
thought.type,
(
"thinking",
rx.text(
thought.content,
line_height="22px",
font_size="var(--prismui-font-size-2)",
color="var(--prismui-color-3)",
),
),
rx.fragment(),
),
align_items="flex-start",
width="100%",
)
def dialog(dialog: Dialog) -> rx.Component:
"""
对话
:param dialog: 对话实例
:return: Component
"""
# 推理状态
is_thinking = dialog.is_thinking
# 思考折叠面板展开状态
is_expanded = dialog.is_expanded
# 思考节点
thoughts = dialog.thoughts
return rx.vstack(
# 问题和操作栏
rx.hstack(
rx.spacer(),
rx.vstack(
# 问题
rx.text(
dialog.user_prompt,
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_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(
thoughts,
thought,
),
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_expanded, "1fr", "0fr"),
"opacity": rx.cond(is_expanded, "1", "0"),
# 双属性同步过渡,和千问一致
"transition": "grid-template-rows 0.3s ease-out, opacity 0.3s ease-out",
},
),
),
# 结果输出
rx.markdown(
dialog.result_output,
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 dialogs() -> rx.Component:
"""
对话列表
:return: Component
"""
# 获取当前会话的对话历史
dialogs = ConversationState.dialogs
# 若运行字典为空则品牌图标、名称和介绍、预设用户提示词等,否则遍历运行项
return rx.cond(
dialogs.length() == 0,
# 欢迎
greeting(),
# 遍历对话项
rx.vstack(
rx.auto_scroll(
rx.foreach(
dialogs,
dialog,
),
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 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 operation() -> 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 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 conversation() -> rx.Component:
"""
会话页面:布局参考 MetaChat左侧为折叠面板右侧为工作区。其中工作区包含展示区和操作区。若对话历史为空则展示欢迎文案否则展示对话历史
"""
# 会话历史展示状态
is_conversation_history_shown = ConversationState.is_conversation_history_shown
return rx.box(
rx.hstack(
# 会话历史
conversation_history(is_conversation_history_shown),
rx.box(
rx.vstack(
# 对话历史
dialogs(),
# 新建对话按钮
create_conversation_button(),
# 操作区
operation(),
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%)",
),
position="relative",
display="flex",
flex="1",
height="100%",
min_height="0",
overflow="hidden",
transition="all 0.3s ease-in-out",
),
# 会话历史折叠面板按钮
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.resume_conversation_state,
)