Python/agent/application/pages/conversation.py

912 lines
31 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 typing import Tuple
from application.domain_models import (
Conversation,
Dialog,
Thought,
)
from application.states import ConversationState, AuthState
def conversation_history_item_showing(
item: Tuple[str, Conversation],
) -> rx.Component:
"""
会话历史项展示
:param item: 会话历史项
:return: Component
"""
conversation = item[1]
# 高亮:若为当前会话或显示更多则高亮
highlight: bool = (
conversation.id == ConversationState.shown_more_conversation_id
) | (conversation.id == ConversationState.conversation_id)
return rx.list.item(
rx.vstack(
rx.hstack(
# 对话描述
rx.text(
conversation.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(
conversation.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.switch_conversation(conversation.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(
conversation.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_shown_more_conversation_id(
conversation.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(highlight, "1", "0"),
transition="opacity 0.18s ease-in-out",
pointer_events=rx.cond(highlight, "auto", "none"),
),
display="flex",
position="relative",
align_items="flex-start",
width="100%",
padding="16px",
margin_bottom="8px",
background=rx.cond(
highlight,
"var(--prismui-background-3)",
"var(--prismui-background-color-1)",
),
border_radius="var(--prismui-border-radius-3)",
box_shadow=rx.cond(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_items_showing(
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.conversations,
conversation_history_item_showing,
),
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 greeting_showing() -> rx.Component:
"""
欢迎展示
:return: Component
"""
return rx.vstack(
rx.vstack(
# 品牌图标、名称和介绍
rx.vstack(
rx.hstack(
rx.image(
src="/logo.png",
width="64px",
height="64px",
),
rx.box(
"棱镜球",
font_size="calc(var(--prismui-font-size-3) * 2)",
font_weight="var(--prismui-font-weight-3)",
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",
),
display="flex",
flex_direction="column",
width="100%",
align_items="center",
gap="12px",
),
# 演示案例
rx.vstack(
rx.hstack(
rx.text(
"演示案例",
line_height="24px",
font_size="var(--prismui-font-size-3)",
font_weight="var(--prismui-font-weight-3)",
),
display="flex",
justify_content="space-between",
align_items="center",
width="100%",
margin_bottom="16px",
),
rx.hstack(
rx.box(
"智能客服",
padding="10px 16px",
background_color="var(--prismui-background-color-6)",
border_radius="var(--prismui-border-radius-9)",
color="var(--prismui-color-10)",
cursor="pointer",
),
display="flex",
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)",
),
display="flex",
flex_direction="column",
align_items="center",
gap="24px",
width="100%",
min_height="0",
margin="auto 0",
color="var(--prismui-color-text)",
),
display="flex",
flex="1",
flex_direction="column",
justify_content="flex-start",
gap="24px",
width="100%",
max_width="1200px",
padding="0 12px",
overflow="auto",
style={
"&::-webkit-scrollbar": {
"display": "none",
},
},
)
def thought_showing(item: Tuple[int, Thought]):
"""
思考展示
:param item: 思考实例
:return: Component
"""
# 思考实例
thought = item[1]
return rx.vstack(
rx.match(
thought.type,
(
"thinking",
rx.text(
thought.content,
line_height="1.6",
font_size="var(--prismui-font-size-1)",
color="var(--prismui-color-3)",
),
),
rx.fragment(),
),
align_items="flex-start",
width="100%",
)
def dialog_showing(item: Tuple[str, Dialog]) -> rx.Component:
"""
对话展示
:param item: 对话实例
:return: Component
"""
# 对话实例
dialog = item[1]
# 推理状态
is_thinking = dialog.is_thinking
# 思考折叠面板展开状态
is_expanded = dialog.is_expanded
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)",
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",
),
# 思考折叠面板
rx.vstack(
# 标题
rx.box(
rx.hstack(
rx.text(
rx.cond(is_thinking, "思考中", "思考完成"),
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(is_expanded, "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_collapse(dialog.id),
cursor="pointer",
),
# 思考
rx.box(
rx.box(
rx.auto_scroll(
rx.foreach(
dialog.thoughts,
thought_showing,
),
width="100%",
margin_top="8px",
align_self="start",
),
style={"overflow": "hidden", "min-height": "0"},
),
display="grid",
grid_template_rows=rx.cond(is_expanded, "1fr", "0fr"),
width="100%",
margin_bottom="8px",
opacity=rx.cond(is_expanded, "1", "0"),
overflow="hidden",
transition="grid-template-rows 0.18s ease-in-out, opacity 0.18s ease-in-out",
),
),
# 结果输出
rx.markdown(
dialog.result_output,
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-6)",
),
"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%",
),
padding="0 16px",
width="100%",
key=dialog.id,
)
def dialog_items_showing() -> rx.Component:
"""
对话项列表展示
:return: Component
"""
# 当前会话的对话列表
dialogs = ConversationState.dialogs
# 若对话列表为空则显示欢迎,否则显示对话列表
return rx.cond(
dialogs.length() == 0,
# 欢迎展示
greeting_showing(),
rx.vstack(
rx.auto_scroll(
rx.foreach(
dialogs,
dialog_showing,
),
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",
),
)
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.dialog.root(
rx.dialog.trigger(
rx.box(
rx.tooltip(
rx.icon(
"plus",
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",
)
),
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",
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.running_status,
# 是否禁用绑定用户提示词发送禁用状态
disabled=ConversationState.is_user_prompt_sending_disabled,
# 点击事件:发送用户提示词
on_click=ConversationState.handle_user_prompt,
display="inline-flex",
position="relative",
justify_content="center",
align_items="center",
padding="0 12px",
background_color=rx.cond(
ConversationState.is_user_prompt_sending_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_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_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)",
),
# 点击事件:切换会话历史折叠面板展开状态
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() -> rx.Component:
"""
会话页面:布局参考 MetaChat左侧为折叠面板右侧为工作区。其中工作区包含展示区和操作区。若对话历史为空则展示欢迎文案否则展示对话历史
"""
# 会话历史展示状态
is_conversation_history_shown = ConversationState.is_conversation_history_shown
return rx.box(
rx.hstack(
# 会话历史列表展示
conversation_history_items_showing(is_conversation_history_shown),
rx.box(
rx.vstack(
# 对话列表展示
dialog_items_showing(),
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_collapse_button(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",
)