460 lines
17 KiB
Python
460 lines
17 KiB
Python
# -*- 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.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=-2, # 偏移量
|
||
),
|
||
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_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_bar() -> 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",
|
||
)
|