Python/agent/application/components/conversation.py

623 lines
23 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 Run, Reasoning, Conversation
from application.state.conversation import ConversationState
from application.state.create_conversation_modal import CreateConversationModalState
def render_conversation_history_item(
conversation_id: str, conversation: Conversation
) -> rx.Component:
"""
渲染对话历史的对话卡片
:param conversation_id: 对话唯一标识
:param conversation: 对话实例
:return: Component
"""
def more_button(
conversation_id: str,
) -> rx.Component:
"""
渲染更多按钮
:param conversation_id: 对话唯一标识
:return: Component
"""
return 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", # 绝对定位
width="8px", # 宽度
height="8px", # 高度
top="-12px", # 向上移动
left="50%", # 向左移动
transform="translateX(-50%) rotate(45deg)", # 旋转45度
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,
)
# 指定对话的激活状态
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(
more_button(conversation_id=conversation_id),
min_width="14px",
cursor="pointer", # 鼠标光标显示为手指
style={
"opacity": rx.cond(
is_actived, "1", "0"
), # 若已激活则不透明,否则透明
"pointer_events": rx.cond(
is_actived, "auto", "none"
), # 若已激活则可点击,否则不可点击
"transition": "opacity 0.18s ease",
},
),
display="flex", # 弹性布局
align_items="center", # 子元素垂直居中
width="100%", # 宽度
margin_bottom="8px", # 底部外边距
),
line_height="1.5", # 行高
color="var(--devui-text-weak)", # 字体颜色
cursor="pointer", # 鼠标悬停显示手指
margin_bottom="8px", # 底部外边距
width="100%", # 宽度
padding="16px", # 内边距
border_radius="8px", # 圆角
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=lambda: ConversationState.switch_conversation(conversation_id),
)
def render_reasoning_item_component(reasoning_id: int, reasoning: Reasoning):
"""
渲染推理项组件
:param reasoning_id: 推理唯一标识
:param reasoning: 推理实例
:return: Component
"""
return rx.hstack(
rx.vstack(
rx.box(
width="8px",
height="8px",
background_color=rx.color("blue", 7),
),
rx.box(width="2px", flex=1, background_color=rx.color("blue", 3)),
align_items="center",
spacing="0",
height="100%",
),
rx.markdown(reasoning.content, color=rx.color("gray", 11), padding_y="4px"),
align_items="flex-start",
spacing="2",
width="100%",
key=reasoning_id,
)
def render_conversation_box() -> rx.Component:
"""
渲染对话框
:return: Component
"""
def item(run_id: str, run: Run) -> rx.Component:
"""
对话项
:param run: 对话实例
:return: Component
"""
# 推理字典
reasonings = run.reasonings
# 推理状态
is_reasoning = run.is_reasoning
# 推理面板展开状态
is_reasoning_panel_open = run.is_reasoning_panel_open
return rx.vstack(
rx.hstack(
rx.spacer(),
rx.vstack(
# 用户问题
rx.markdown(
run.user_prompt,
max_width="600px",
padding="12px 16px",
background_color="var(--devui-global-bg)",
border_radius="12px",
word_wrap="break-word",
word_break="break-all",
style={
"& p.rt-Text": {
"padding": "0",
"margin": "0 !important",
},
},
),
# 再次发送按钮
rx.hstack(
rx.box(
rx.icon(
"rotate-ccw",
width="14px",
height="14px",
color="var(--devui-text-weak)",
),
padding="4px",
border_radius="4px",
cursor="pointer",
),
width="100%",
margin_top="8px",
),
),
# 用户身份标识
rx.avatar(
src="/user_avatar.svg",
align_self="flex-start",
width="32px",
height="32px",
border_radius="100%",
border="none",
),
width="100%",
margin_top="8px",
align_items="flex-start",
gap="4px",
font_size="var(--devui-font-size)",
),
# 智能体回答
rx.hstack(
# 智能体身份标识
rx.avatar(
src="/logo.svg",
align_self="flex-start",
width="32px",
height="32px",
border_radius="100%",
border="none",
),
rx.box(
rx.vstack(
# 标题栏
rx.cond(
is_reasoning,
rx.hstack(
rx.icon("loader"),
rx.text("推理中..."),
rx.icon("chevron-down"),
),
rx.hstack(
rx.icon("check"),
rx.text("已推理"),
rx.icon("chevron-up"),
),
),
)
),
# 标题栏
text_align="left",
width="100%",
margin_bottom="8px",
),
# 渲染回复正文组件,若正在推理则不渲染,否则渲染回复正文组件
rx.cond(
is_reasoning,
rx.fragment(),
rx.box(
rx.markdown(
run.assistant_content,
color=rx.color("gray", 12), # 文字颜色
background_color=rx.color("gray", 2), # 背景颜色
display="inline-block", # 布局模式:自适应文本宽度
max_width="85%", # 最大宽度
padding_x="1.25em", # 水平内边距
padding_y="0.5em", # 垂直内边距
margin_left="auto", # 左侧外边距自动调整
margin_bottom="8px", # 底部外边距
border_radius="12px", # 圆角
),
),
),
width="100%",
key=run_id,
)
# 运行字典
runs = ConversationState.get_runs
# 若运行字典为空则渲染品牌图标、名称和介绍、预设用户提示词等,否则遍历渲染运行项
return rx.cond(
runs.length() == 0,
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(--devui-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(--devui-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",
},
},
),
# 遍历渲染对话项
rx.vstack(
rx.auto_scroll(
rx.foreach(
runs,
lambda i, _: 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(--devui-text) !important;
opacity: 1 !important;
}
"""
),
rx.spacer(), # 占位符
rx.dialog.root(
# 触发事件:点击图标
rx.dialog.trigger(
rx.box(
rx.tooltip(
rx.icon(
"plus",
size=14,
color="var(--devui-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=CreateConversationModalState.is_open,
on_open_change=CreateConversationModalState.toggle,
),
display="flex",
width="100%",
max_width="1200px",
height="39px",
padding="0 12px",
justify_content="flex-end",
align_items="center",
gap="4px",
)
def render_input_box() -> rx.Component:
"""
渲染输入框
"""
return rx.vstack(
# 渲染自定义输入组件
rx.form(
rx.box(
rx.vstack(
rx.hstack(
rx.text_area(
name="user_prompt",
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(--devui-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.get_running_status,
disabled=ConversationState.get_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",
)