Python/agent/application/components/conversation.py

499 lines
18 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 render_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(
render_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_run_component(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.box(
# 渲染用户提示词组件
rx.box(
rx.markdown(
run.user_prompt,
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", # 圆角
),
text_align="right",
width="100%",
margin_bottom="8px",
),
# 渲染推理面板组件
rx.box(
# 标题栏
rx.hstack(
# 若推理状态为正在推理则渲染正在推理,否则渲染推理完成
rx.cond(
is_reasoning,
rx.badge("正在推理"),
rx.badge("推理完成"),
),
rx.spacer(),
# 若推理面板展开则渲染上箭头,否则渲染下箭头
rx.icon(
rx.cond(is_reasoning_panel_open, "chevron_up", "chevron_down"),
size=16,
color=rx.color("mauve", 6),
),
width="100%",
margin_bottom="6px",
on_click=lambda: ConversationState.toggle_reasoning_panel(
run_id
), # 点击事件:展开/折叠指定运行唯一标识的推理面板
),
# 若推理面板展开则遍历渲染推理项组件,否则不渲染
rx.cond(
is_reasoning_panel_open,
rx.foreach(
reasonings,
lambda i, _: render_reasoning_item_component(i[0], i[1]),
),
rx.fragment(),
),
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="min(100%, 50em)", # 最大宽度父级元素最大宽度和50em中较小值
margin_x="auto", # 水平外边距:自动调整
key=run_id,
)
def render_conversation_box() -> rx.Component:
"""
渲染对话框
:return: Component
"""
# 运行字典
runs = ConversationState.get_runs
# 若运行字典为空则渲染品牌图标和名称、预设用户提示词,否则遍历渲染运行项
return rx.auto_scroll(
rx.cond(
ConversationState.is_runs_empty,
# 渲染空运行字典组件
rx.center(
rx.vstack(
rx.spacer(),
# 智能体图标和名称
rx.hstack(
rx.icon("info", size=18),
rx.text("智能体"),
),
# 预设用户提示词
rx.box(
rx.vstack(
# 标题
rx.text("猜你想问"),
rx.hstack(
rx.button("你可以帮我做什么"),
),
)
),
spacing="4",
width="min(100%, 40em)",
padding_y="40px",
),
border="none",
),
# 遍历渲染运行组件
rx.foreach(
runs,
lambda i, _: render_run_component(i[0], i[1]),
),
),
flex="1",
padding="8px",
overflow_y="auto",
style={
"&::-webkit-scrollbar": {
"display": "none",
},
},
)
def render_create_conversation_button() -> rx.Component:
"""
渲染新建对话按钮
:return: Component
"""
return rx.hstack(
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="var(--devui-base-bg)",
box_shadow="0 2px 12px 0 rgba(37, 43, 58, .24)",
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发送按Shift+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",
)