Python/agent/application/components/conversation.py

420 lines
15 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.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.button(
rx.icon("ellipsis", size=14, color="var(--devui-text-weak)"),
cursor="pointer", # 鼠标光标显示为手指
)
),
# 渲染气泡卡片
rx.popover.content(
rx.box(
width="14px",
height="14px",
position="absolute",
top="-7px",
left="50%",
transform="translateX(-50%) rotate(45deg)",
background_color="var(--devui-base-bg, #ffffff)",
box_shadow="-2px -2px 4px rgba(0,0,0,0.05)",
),
rx.popover.close(
rx.box(
"删除",
width="100%", # 宽度
height="24px", # 高度
padding="4px", # 内边距
line_height="16px", # 行高
font_size="12px", # 字体大小
color="#252b3a",
display="flex", # 弹性布局
flex_wrap="wrap", # 换行
align_items="center", # 子元素垂直居中
background_color="var(--devui-base-bg)", # 背景颜色
white_space="nowrap", # 不换行
border_radius="var(--devui-border-radius-feedback)", # 圆角
# 点击事件:删除对话
on_click=lambda: ConversationState.delete_conversation(
conversation_id
),
),
),
padding="8px", # 内边距
arrow_size=10, # 三角大小
side="bottom", # 气泡在按钮下方,三角朝上
side_offset=8, # 气泡和按钮之间间距
align="center", # 水平居中对齐触发按钮中线
style={
"position": "relative", # 关键:给三角提供定位基准
"background_color": "var(--devui-base-bg, #ffffff) !important",
"border_radius": "var(--devui-border-radius-feedback, 8px) !important",
"box_shadow": "0 2px 12px rgba(0,0,0,0.1)",
},
),
)
# 指定对话的激活状态
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_component() -> 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",
)
def render_create_conversation_component() -> rx.Component:
"""
渲染创建对话组件
"""
return rx.hstack(
rx.spacer(), # 占位符
rx.dialog.root(
# 触发事件:点击创建对话图标,支持鼠标悬停提示
rx.dialog.trigger(
rx.box(rx.tooltip(rx.icon("circle-plus"), content="创建对话"))
),
# 内容层
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,
),
width="100%",
)
def render_input_component() -> rx.Component:
"""
渲染输入组件
"""
return rx.vstack(
# 渲染自定义输入组件
rx.form(
rx.box(
rx.vstack(
# 输入区域
rx.input(
name="user_prompt",
placeholder="发消息...",
flex="auto",
border="none", # 外框边线
outline="none", # 高亮轮廓线
padding_bottom="8px",
),
# 操作区域,暂仅包含发送按钮
rx.hstack(
rx.spacer(), # 占位符
# 发送按钮
rx.button(
rx.icon("arrow-up", size=18),
color_scheme="blue",
radius="full",
width="36px",
height="36px",
padding="0",
type="submit",
loading=ConversationState.get_running_status,
disabled=ConversationState.get_running_status,
),
width="100%",
),
spacing="8",
padding_x="12px",
padding_y="12px",
),
border=f"1px solid {rx.color('mauve', 4)}",
radius="large",
background_color="white",
),
max_width="50em", # 最大宽度
margin="0 auto", # 水平居中
align_items="center", # 子元素垂直居中
spacing="0", # 子元素间距
on_submit=ConversationState.run,
reset_on_submit=True,
),
# 渲染底部文案
rx.text(
"内容由大模型生成,无法确保准确性和完整性,仅供参考",
text_align="center",
font_size=".75em",
color=rx.color("mauve", 10),
),
width="100%",
padding_x="16px",
align="stretch",
)