This commit is contained in:
parent
1789f8a0a3
commit
a6b2bcf81c
|
|
@ -1,221 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
聊天页面相关组件
|
||||
"""
|
||||
import reflex as rx
|
||||
|
||||
from application.models import PartType, Part, Dialog
|
||||
from application.state import ChatState
|
||||
|
||||
|
||||
def render_user_prompt(user_prompt: str) -> rx.Component:
|
||||
"""
|
||||
渲染用户提示词
|
||||
:param user_prompt: 用户提示词
|
||||
:return: Component
|
||||
"""
|
||||
return rx.markdown(
|
||||
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", # 圆角
|
||||
)
|
||||
|
||||
|
||||
def render_part(dialog_id: str, part_id: str, part: Part):
|
||||
"""
|
||||
渲染片段
|
||||
:param dialog_id: 对话唯一标识
|
||||
:param part_id: 片段唯一标识
|
||||
:param part: 片段实例
|
||||
:return: Component
|
||||
"""
|
||||
return rx.match(
|
||||
part.part_type,
|
||||
(
|
||||
PartType.TEXT,
|
||||
rx.markdown(
|
||||
part.content,
|
||||
color=rx.color("gray", 12), # 字体颜色
|
||||
background_color="transparent", # 背景颜色:设置为透明以继承父元素背景颜色
|
||||
display="block", # 布局模式:铺满
|
||||
max_width="85%", # 最大宽度
|
||||
padding="0", # 内边距
|
||||
margin_right="auto", # 右侧外边距自动调整
|
||||
margin_bottom="12px", # 底部外边距
|
||||
key=part_id,
|
||||
),
|
||||
), # 片段类型为文本
|
||||
(PartType.FINISHED, rx.fragment(key=part_id)), # 片段类型为结束
|
||||
rx.box(
|
||||
rx.cond(
|
||||
part.is_open,
|
||||
rx.hstack(
|
||||
rx.match(
|
||||
part.part_type,
|
||||
(
|
||||
PartType.THINKING,
|
||||
rx.text(
|
||||
"正在思考",
|
||||
font_size="0.90rem",
|
||||
bold=True,
|
||||
color=rx.color("gray", 10), # 字体颜色
|
||||
),
|
||||
),
|
||||
(
|
||||
PartType.TOOL_NAME,
|
||||
rx.text(
|
||||
"正在调用",
|
||||
" ",
|
||||
part.content,
|
||||
" ",
|
||||
font_size="0.90rem",
|
||||
bold=True,
|
||||
color=rx.color("gray", 10),
|
||||
),
|
||||
),
|
||||
),
|
||||
rx.spacer(),
|
||||
rx.icon("chevron_up", size=16, color=rx.color("gray", 6)),
|
||||
width="100%",
|
||||
cursor="pointer",
|
||||
padding_x="1em",
|
||||
padding_y="0.6em",
|
||||
background_color=rx.color("gray", 2),
|
||||
border_radius="8px",
|
||||
on_click=lambda: ChatState.toggle_part_collapse(dialog_id, part_id),
|
||||
), # 折叠面板打开时标题栏
|
||||
rx.hstack(
|
||||
rx.hstack(
|
||||
rx.text(
|
||||
loading_prefix,
|
||||
font_size="0.90rem",
|
||||
bold=True,
|
||||
color=rx.color("gray", 10),
|
||||
),
|
||||
loading_dot_group(part.is_streaming),
|
||||
spacing="8", # 间距
|
||||
align_items="center",
|
||||
),
|
||||
rx.spacer(),
|
||||
rx.icon("chevron_down", size=16, color=rx.color("gray", 6)),
|
||||
width="100%",
|
||||
cursor="pointer",
|
||||
on_click=lambda: ChatState.toggle_part_collapse(dialog_id, part_id),
|
||||
padding_x="1em",
|
||||
padding_y="0.6em",
|
||||
background_color=rx.color("gray", 2),
|
||||
border_radius="8px",
|
||||
), # 折叠面板关闭时标题栏
|
||||
),
|
||||
rx.cond(part.is_open, detail_box, rx.fragment()),
|
||||
width="100%",
|
||||
max_width="85%",
|
||||
margin_bottom="10px",
|
||||
cursor="pointer",
|
||||
key=part_id,
|
||||
), # 片段类型为工具相关
|
||||
)
|
||||
|
||||
|
||||
def render_dialog_item(dialog_id: str, dialog: Dialog) -> rx.Component:
|
||||
"""
|
||||
渲染对话项
|
||||
:param dialog: 对话实例
|
||||
:return: Component
|
||||
"""
|
||||
return rx.box(
|
||||
rx.box(
|
||||
render_user_prompt(user_prompt=dialog.user_prompt),
|
||||
text_align="right",
|
||||
width="100%",
|
||||
margin_bottom="8px",
|
||||
),
|
||||
rx.box(
|
||||
rx.foreach(
|
||||
dialog.output,
|
||||
lambda part_id, part: render_part(
|
||||
dialog_id=dialog_id, part_id=part_id, part=part
|
||||
),
|
||||
),
|
||||
text_align="left",
|
||||
width="100%",
|
||||
margin_bottom="8px",
|
||||
),
|
||||
width="min(100%, 50em)", # 最大宽度:父级元素最大宽度和50em中较小值
|
||||
margin_x="auto", # 水平外边距:自动调整
|
||||
key=dialog_id,
|
||||
)
|
||||
|
||||
|
||||
def render_dialog_list() -> rx.Component:
|
||||
"""
|
||||
渲染对话列表
|
||||
:return: Component
|
||||
"""
|
||||
return rx.auto_scroll(
|
||||
rx.foreach(
|
||||
ChatState.get_dialogs,
|
||||
lambda dialog_id, dialog: render_dialog_item(
|
||||
dialog_id=dialog_id, dialog=dialog
|
||||
),
|
||||
),
|
||||
flex="1",
|
||||
padding="8px",
|
||||
overflow_y="auto",
|
||||
)
|
||||
|
||||
|
||||
def render_input_bar() -> rx.Component:
|
||||
"""
|
||||
渲染输入栏
|
||||
"""
|
||||
return rx.center(
|
||||
rx.vstack(
|
||||
rx.form(
|
||||
rx.hstack(
|
||||
rx.input(
|
||||
name="user_prompt",
|
||||
placeholder="请输入...",
|
||||
flex="auto",
|
||||
),
|
||||
rx.button(
|
||||
"发送",
|
||||
type="submit",
|
||||
loading=ChatState.get_current_chat_status, # 正在流式输出时按钮显示为 loading
|
||||
disabled=ChatState.get_current_chat_status, # 正在流式输出时按钮禁用
|
||||
),
|
||||
max_width="50em",
|
||||
margin="0 auto",
|
||||
align_items="center",
|
||||
),
|
||||
on_submit=ChatState.process_input, # 处理输入,返回流式输出
|
||||
reset_on_submit=True, # 提交后清空输入框
|
||||
),
|
||||
rx.text(
|
||||
"抹茶兔兔工作室",
|
||||
text_align="center",
|
||||
font_size=".75em",
|
||||
color=rx.color("mauve", 10),
|
||||
),
|
||||
width="100%",
|
||||
padding_x="16px",
|
||||
align="stretch",
|
||||
),
|
||||
position="sticky",
|
||||
bottom="0",
|
||||
left="0",
|
||||
padding_y="16px",
|
||||
backdrop_filter="auto",
|
||||
backdrop_blur="lg",
|
||||
border_top=f"1px solid {rx.color('mauve', 3)}",
|
||||
background_color=rx.color("mauve", 2),
|
||||
align="stretch",
|
||||
width="100%",
|
||||
) # rx.center 等价 rx.box(display="flex", align_items="center", justify_content="center")
|
||||
|
|
@ -0,0 +1,295 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
渲染对话相关组件
|
||||
"""
|
||||
import reflex as rx
|
||||
|
||||
from application.models import Run, Reasoning
|
||||
from application.state.conversation import ConversationState
|
||||
|
||||
|
||||
def render_user_prompt(run: Run) -> rx.Component:
|
||||
"""
|
||||
渲染用户提示词
|
||||
:param run: 运行实例
|
||||
:return: Component
|
||||
"""
|
||||
return 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",
|
||||
)
|
||||
|
||||
|
||||
def render_reasoning(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_reasoning_panel(run_id: str, run: Run) -> rx.Component:
|
||||
"""
|
||||
渲染推理面板
|
||||
:param run_id: 运行唯一标识
|
||||
:param run: 运行实例
|
||||
:return: Component
|
||||
"""
|
||||
# 推理字典
|
||||
reasonings = run.reasonings
|
||||
# 推理面板展开状态
|
||||
is_reasoning_panel_open = run.is_reasoning_panel_open
|
||||
|
||||
return rx.cond(
|
||||
not reasonings,
|
||||
rx.fragment(),
|
||||
rx.box(
|
||||
# 标题栏
|
||||
rx.hstack(
|
||||
rx.cond(
|
||||
run.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 reasoning_id, reasoning: render_reasoning(
|
||||
reasoning_id=reasoning_id, reasoning=reasoning
|
||||
),
|
||||
),
|
||||
rx.fragment(),
|
||||
),
|
||||
text_align="left",
|
||||
width="100%",
|
||||
margin_bottom="8px",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def render_assistant_content(run: Run) -> rx.Component:
|
||||
"""
|
||||
渲染回复正文
|
||||
:param run: 运行实例,包含用户提示词、推理字典和回复正文
|
||||
:return: Component
|
||||
"""
|
||||
# 若回复正文为空则不渲染,否则渲染回复正文
|
||||
return rx.cond(
|
||||
not run.assistant_content,
|
||||
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", # 圆角
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def render_run(run_id: str, run: Run) -> rx.Component:
|
||||
"""
|
||||
渲染运行
|
||||
:param run: 运行,包含用户提示词、推理字典和回复正文
|
||||
:return: Component
|
||||
"""
|
||||
return rx.box(
|
||||
# 渲染用户提示词
|
||||
render_user_prompt(run=run),
|
||||
# 渲染推理面板
|
||||
render_reasoning_panel(run_id=run_id, run=run),
|
||||
# 渲染回复正文
|
||||
render_assistant_content(run=run),
|
||||
width="min(100%, 50em)", # 最大宽度:父级元素最大宽度和50em中较小值
|
||||
margin_x="auto", # 水平外边距:自动调整
|
||||
key=run_id,
|
||||
)
|
||||
|
||||
|
||||
def render_welcome() -> rx.Component:
|
||||
"""
|
||||
渲染欢迎信息
|
||||
:return: Component
|
||||
"""
|
||||
return 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",
|
||||
)
|
||||
|
||||
|
||||
def render_conversation() -> rx.Component:
|
||||
"""
|
||||
渲染对话,包括若干次运行
|
||||
:return: Component
|
||||
"""
|
||||
# 运行字典
|
||||
runs = ConversationState.get_runs
|
||||
|
||||
# 若运行字典为空则渲染欢迎信息,否则遍历渲染运行
|
||||
return rx.auto_scroll(
|
||||
rx.cond(
|
||||
not runs,
|
||||
# 渲染欢迎信息
|
||||
render_welcome(),
|
||||
rx.foreach(
|
||||
runs,
|
||||
lambda run_id, run: render_run(run_id=run_id, run=run),
|
||||
),
|
||||
),
|
||||
flex="1",
|
||||
padding="8px",
|
||||
overflow_y="auto",
|
||||
)
|
||||
|
||||
|
||||
def render_custom_input_box() -> rx.Component:
|
||||
"""
|
||||
渲染自定义输入框
|
||||
"""
|
||||
return 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", # 子元素间距
|
||||
)
|
||||
|
||||
|
||||
def render_input_box() -> rx.Component:
|
||||
"""
|
||||
渲染输入框
|
||||
"""
|
||||
return rx.center(
|
||||
rx.vstack(
|
||||
render_custom_input_box(), # 渲染自定义输入框
|
||||
# 底部文案
|
||||
rx.text(
|
||||
"内容由大模型生成,无法确保准确性和完整性,仅供参考",
|
||||
text_align="center",
|
||||
font_size=".75em",
|
||||
color=rx.color("mauve", 10),
|
||||
),
|
||||
width="100%",
|
||||
padding_x="16px",
|
||||
align="stretch",
|
||||
),
|
||||
position="sticky",
|
||||
bottom="0",
|
||||
left="0",
|
||||
padding_y="16px",
|
||||
backdrop_filter="auto",
|
||||
backdrop_blur="lg",
|
||||
border_top=f"1px solid {rx.color('mauve', 3)}",
|
||||
background_color=rx.color("mauve", 2),
|
||||
align="stretch",
|
||||
width="100%",
|
||||
) # rx.center 等价 rx.box(display="flex", align_items="center", justify_content="center")
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
渲染框架相关组件
|
||||
"""
|
||||
|
||||
import reflex as rx
|
||||
|
||||
from application.state.conversation import ConversationState
|
||||
from application.state.create_conversation_modal import CreateConversationModalState
|
||||
from application.state.frame import FrameState
|
||||
from application.models import SidebarIconNavButtonKind
|
||||
|
||||
|
||||
def render_conversation(chat_id: str, chat_description: str) -> rx.Component:
|
||||
"""
|
||||
渲染对话
|
||||
:param chat_id: 聊天唯一标识
|
||||
:param description: 聊天描述
|
||||
:return: Component
|
||||
"""
|
||||
return rx.drawer.close(
|
||||
rx.hstack(
|
||||
rx.button(
|
||||
chat_description,
|
||||
on_click=lambda: ChatState.switch_chat(chat_id), # 点击按钮将切换会话
|
||||
width="80%",
|
||||
variant="surface",
|
||||
), # 点击按钮将切换聊天
|
||||
rx.button(
|
||||
rx.icon(
|
||||
tag="trash",
|
||||
on_click=lambda: ChatState.delete_chat(chat_id), # 点击按钮删除聊天
|
||||
stroke_width=1,
|
||||
),
|
||||
width="20%",
|
||||
variant="surface",
|
||||
color_scheme="red",
|
||||
),
|
||||
width="100%",
|
||||
),
|
||||
key=chat_id, # 使用聊天唯一标识作为键
|
||||
)
|
||||
|
||||
|
||||
def render_chat_list(trigger) -> rx.Component:
|
||||
"""
|
||||
渲染聊天列表
|
||||
"""
|
||||
return rx.drawer.root(
|
||||
rx.drawer.trigger(trigger),
|
||||
rx.drawer.overlay(),
|
||||
rx.drawer.portal(
|
||||
rx.drawer.content(
|
||||
rx.vstack(
|
||||
rx.heading("聊天列表", color=rx.color("mauve", 11)),
|
||||
rx.divider(),
|
||||
rx.foreach(
|
||||
ChatState.get_chats, # 获取聊天列表
|
||||
lambda chat_id, chat: render_chat_item(
|
||||
chat_id=chat_id,
|
||||
chat_description=chat.description,
|
||||
), # 创建聊天组件
|
||||
),
|
||||
align_items="stretch",
|
||||
width="100%",
|
||||
),
|
||||
top="auto",
|
||||
right="auto",
|
||||
height="100%",
|
||||
width="20em",
|
||||
padding="2em",
|
||||
background_color=rx.color("mauve", 2),
|
||||
outline="none",
|
||||
)
|
||||
),
|
||||
direction="left",
|
||||
)
|
||||
|
||||
|
||||
def render_create_chat_modal(trigger) -> rx.Component:
|
||||
"""
|
||||
渲染新建聊天模态窗
|
||||
"""
|
||||
return rx.dialog.root(
|
||||
rx.dialog.trigger(trigger),
|
||||
rx.dialog.content(
|
||||
rx.form(
|
||||
rx.hstack(
|
||||
rx.input(
|
||||
name="chat_description",
|
||||
placeholder="请输入聊天描述(可选)",
|
||||
flex="auto",
|
||||
min_width="20ch",
|
||||
),
|
||||
rx.button("新建"),
|
||||
spacing="2",
|
||||
wrap="wrap",
|
||||
width="100%",
|
||||
),
|
||||
on_submit=ChatState.create_chat,
|
||||
),
|
||||
background_color=rx.color("mauve", 1),
|
||||
), # 模态窗内容容器
|
||||
open=CreateChatState.is_open,
|
||||
on_open_change=CreateChatState.toggle,
|
||||
)
|
||||
|
||||
|
||||
def render_sidebar() -> rx.Component:
|
||||
"""
|
||||
渲染侧边栏
|
||||
"""
|
||||
return rx.box(
|
||||
rx.vstack(
|
||||
rx.vstack(
|
||||
# 智能体图标和名称
|
||||
rx.vstack(
|
||||
rx.icon("info"),
|
||||
rx.text("智能体"),
|
||||
spacing="2",
|
||||
margin_bottom="16px",
|
||||
),
|
||||
# 分隔线
|
||||
rx.divider(margin_bottom="12px"),
|
||||
# 对话
|
||||
rx.button(
|
||||
rx.vstack(
|
||||
rx.icon("message-square", size=18),
|
||||
rx.text("对话"),
|
||||
spacing="2",
|
||||
),
|
||||
width="100%",
|
||||
justify_content="flex-start",
|
||||
variant=rx.cond(
|
||||
ChatState.active_sidebar_tab == "chat", "soft", "surface"
|
||||
),
|
||||
color_scheme="blue",
|
||||
on_click=ChatState.set_active_tab("chat"),
|
||||
),
|
||||
spacing="10px",
|
||||
),
|
||||
# 底部语言、设置按钮
|
||||
rx.vstack(
|
||||
rx.button(rx.text("EN"), variant="surface", width="100%"),
|
||||
rx.button(rx.icon("settings"), variant="surface", width="100%"),
|
||||
spacing="6px",
|
||||
),
|
||||
height="100vh",
|
||||
width="64px",
|
||||
padding_y="20px",
|
||||
padding_x="12px",
|
||||
align_items="stretch",
|
||||
justify_content="space-between",
|
||||
),
|
||||
width="64px",
|
||||
min_width="64px",
|
||||
background=rx.color("mauve", 2),
|
||||
border_right=f"1px solid {rx.color('mauve', 3)}",
|
||||
position="sticky",
|
||||
top=0,
|
||||
)
|
||||
|
||||
|
||||
def render_sidebar_icon_nav_button(sidebar_icon_nav_button: SidebarIconNavButton) -> rx.Component:
|
||||
"""
|
||||
渲染侧边栏图标导航按钮
|
||||
:param sidebar_icon_nav_button: 侧边栏图标导航按钮
|
||||
"""
|
||||
# 获取侧边栏图标导航按钮名称
|
||||
sidebar_icon_nav_button_name = sidebar_icon_nav_buttons[sidebar_icon_nav_button]["name"]
|
||||
# 获取侧边栏图标导航按钮图标名称
|
||||
sidebar_icon_nav_button_icon = sidebar_icon_nav_buttons[sidebar_icon_nav_button]["icon"]
|
||||
|
||||
# 若当前侧边栏图标导航按钮非指定图标导航按钮则正常渲染,否则高亮渲染
|
||||
return rx.cond(
|
||||
FrameState.sidebar_icon_nav_button != sidebar_icon_nav_button,
|
||||
rx.button(
|
||||
rx.vstack(
|
||||
rx.icon(sidebar_icon_nav_button, size=18),
|
||||
rx.text(sidebar_icon_nav_button),
|
||||
spacing="2",
|
||||
align_items="center",
|
||||
),
|
||||
width="100%",
|
||||
justify_content="center",
|
||||
variant="surface",
|
||||
color_scheme="blue",
|
||||
on_click=FrameState.set_active_sidebar_tab(sidebar_icon_nav_button_name),
|
||||
padding_y="10px",
|
||||
),
|
||||
rx.button(
|
||||
rx.vstack(
|
||||
rx.icon(sidebar_icon_nav_button_name, size=18),
|
||||
rx.text(sidebar_icon_nav_button_name),
|
||||
spacing="2",
|
||||
align_items="center",
|
||||
),
|
||||
width="100%",
|
||||
justify_content="center",
|
||||
variant="soft",
|
||||
color_scheme="blue",
|
||||
on_click=FrameState.set_active_sidebar_tab(sidebar_icon_nav_button_name),
|
||||
padding_y="10px",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def render_frame() -> rx.Component:
|
||||
"""
|
||||
渲染框架,参考 MateChat
|
||||
"""
|
||||
return rx.hstack(
|
||||
rx.badge(
|
||||
ConversationState.get_current_chat_description,
|
||||
size="3",
|
||||
variant="soft",
|
||||
margin_inline_end="auto",
|
||||
),
|
||||
render_create_chat_modal(
|
||||
rx.box(rx.tooltip(rx.icon("message-square-plus"), content="新建聊天"))
|
||||
),
|
||||
render_chat_list(
|
||||
rx.box(
|
||||
rx.tooltip(
|
||||
rx.icon("messages-square"),
|
||||
content="聊天历史",
|
||||
)
|
||||
)
|
||||
),
|
||||
justify_content="space-between",
|
||||
align_items="center",
|
||||
padding="12px",
|
||||
border_bottom=f"1px solid {rx.color('mauve', 3)}",
|
||||
background_color=rx.color("mauve", 2),
|
||||
)
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
导航栏相关组件
|
||||
"""
|
||||
|
||||
import reflex as rx
|
||||
|
||||
from application.state.chat import ChatState
|
||||
from application.state.create_chat import CreateChatState
|
||||
|
||||
|
||||
def render_chat_item(chat_id: str, chat_description: str) -> rx.Component:
|
||||
"""
|
||||
渲染聊天项
|
||||
:param chat_id: 聊天唯一标识
|
||||
:param description: 聊天描述
|
||||
:return: Component
|
||||
"""
|
||||
return rx.drawer.close(
|
||||
rx.hstack(
|
||||
rx.button(
|
||||
chat_description,
|
||||
on_click=lambda: ChatState.switch_chat(chat_id), # 点击按钮将切换会话
|
||||
width="80%",
|
||||
variant="surface",
|
||||
), # 点击按钮将切换聊天
|
||||
rx.button(
|
||||
rx.icon(
|
||||
tag="trash",
|
||||
on_click=lambda: ChatState.delete_chat(chat_id), # 点击按钮删除聊天
|
||||
stroke_width=1,
|
||||
),
|
||||
width="20%",
|
||||
variant="surface",
|
||||
color_scheme="red",
|
||||
),
|
||||
width="100%",
|
||||
),
|
||||
key=chat_id, # 使用聊天唯一标识作为键
|
||||
)
|
||||
|
||||
|
||||
def render_chat_list(trigger) -> rx.Component:
|
||||
"""
|
||||
渲染聊天列表
|
||||
"""
|
||||
return rx.drawer.root(
|
||||
rx.drawer.trigger(trigger),
|
||||
rx.drawer.overlay(),
|
||||
rx.drawer.portal(
|
||||
rx.drawer.content(
|
||||
rx.vstack(
|
||||
rx.heading("聊天列表", color=rx.color("mauve", 11)),
|
||||
rx.divider(),
|
||||
rx.foreach(
|
||||
ChatState.get_chats, # 获取聊天列表
|
||||
lambda chat_id, chat: render_chat_item(
|
||||
chat_id=chat_id,
|
||||
chat_description=chat.description,
|
||||
), # 创建聊天组件
|
||||
),
|
||||
align_items="stretch",
|
||||
width="100%",
|
||||
),
|
||||
top="auto",
|
||||
right="auto",
|
||||
height="100%",
|
||||
width="20em",
|
||||
padding="2em",
|
||||
background_color=rx.color("mauve", 2),
|
||||
outline="none",
|
||||
)
|
||||
),
|
||||
direction="left",
|
||||
)
|
||||
|
||||
|
||||
def render_create_chat_modal(trigger) -> rx.Component:
|
||||
"""
|
||||
渲染新建聊天模态窗
|
||||
"""
|
||||
return rx.dialog.root(
|
||||
rx.dialog.trigger(trigger),
|
||||
rx.dialog.content(
|
||||
rx.form(
|
||||
rx.hstack(
|
||||
rx.input(
|
||||
name="chat_description",
|
||||
placeholder="请输入聊天描述(可选)",
|
||||
flex="auto",
|
||||
min_width="20ch",
|
||||
),
|
||||
rx.button("新建"),
|
||||
spacing="2",
|
||||
wrap="wrap",
|
||||
width="100%",
|
||||
),
|
||||
on_submit=ChatState.create_chat,
|
||||
),
|
||||
background_color=rx.color("mauve", 1),
|
||||
), # 模态窗内容容器
|
||||
open=CreateChatState.is_open,
|
||||
on_open_change=CreateChatState.toggle,
|
||||
)
|
||||
|
||||
|
||||
def render_navbar() -> rx.Component:
|
||||
"""
|
||||
渲染导航栏
|
||||
"""
|
||||
return rx.hstack(
|
||||
rx.badge(
|
||||
ChatState.get_current_chat_description,
|
||||
size="3",
|
||||
variant="soft",
|
||||
margin_inline_end="auto",
|
||||
),
|
||||
render_create_chat_modal(
|
||||
rx.box(rx.tooltip(rx.icon("message-square-plus"), content="新建聊天"))
|
||||
),
|
||||
render_chat_list(
|
||||
rx.box(
|
||||
rx.tooltip(
|
||||
rx.icon("messages-square"),
|
||||
content="聊天历史",
|
||||
)
|
||||
)
|
||||
),
|
||||
justify_content="space-between",
|
||||
align_items="center",
|
||||
padding="12px",
|
||||
border_bottom=f"1px solid {rx.color('mauve', 3)}",
|
||||
background_color=rx.color("mauve", 2),
|
||||
)
|
||||
|
|
@ -12,6 +12,20 @@ from pydantic_ai.messages import ModelMessage, ModelMessagesTypeAdapter
|
|||
from sqlmodel import Field as SqlField, SQLModel
|
||||
|
||||
|
||||
class SidebarIconNavButtonKind(StrEnum):
|
||||
"""侧边栏图标导航按钮种类"""
|
||||
|
||||
CONVERSATION = "conversation"
|
||||
KNOWLEDGE = "knowledge"
|
||||
|
||||
|
||||
class SidebarIconNavButton(BaseModel):
|
||||
"""侧边栏图标导航按钮类"""
|
||||
|
||||
icon: str = Field(..., description="侧边栏图标导航按钮图标")
|
||||
name: str = Field(..., description="侧边栏图标导航按钮名称")
|
||||
|
||||
|
||||
# 消息历史数据表模型
|
||||
# 重新初始化:先手动删除 alembic 相关配置和文件夹,再使用 reflex db init 初始化数据库表
|
||||
class MessageHistory(SQLModel, table=True):
|
||||
|
|
@ -44,37 +58,12 @@ class MessageHistory(SQLModel, table=True):
|
|||
)
|
||||
|
||||
|
||||
class EventKind(StrEnum):
|
||||
"""事件类型"""
|
||||
|
||||
PART_START = "part_start"
|
||||
PART_DELTA = "part_delta"
|
||||
PART_END = "part_end"
|
||||
FUNCTION_TOOL_CALL = "function_tool_call"
|
||||
FUNCTION_TOOL_RESULT = "function_tool_result"
|
||||
RUN_START = "run_start"
|
||||
RUN_END = "run_end"
|
||||
|
||||
|
||||
class PartKind(StrEnum):
|
||||
"""分片类型"""
|
||||
class ReasoningKind(StrEnum):
|
||||
"""推理种类"""
|
||||
|
||||
THINKING = "thinking"
|
||||
TOOL_SEARCH = "tool-search"
|
||||
CAPABILITY_LOAD = "capability-load"
|
||||
TOOL_CALL = "tool-call"
|
||||
TEXT = "text"
|
||||
TOOL_RETURN = "tool-return"
|
||||
RETRY_PROMPT = "retry-prompt"
|
||||
RUN_RETURN = "run-return"
|
||||
|
||||
|
||||
class Kind(StrEnum):
|
||||
"""分段种类"""
|
||||
|
||||
THINKING = "thinking"
|
||||
TOOL_SEARCH = "tool-search"
|
||||
LOAD_CAPABILITY = "load-capability"
|
||||
CAPABILITY_LOAD = "load-capability"
|
||||
TOOL_CALL = "tool-call"
|
||||
TEXT = "text"
|
||||
TOOL_RETURN = "tool-return"
|
||||
|
|
@ -85,7 +74,7 @@ class Kind(StrEnum):
|
|||
class Reasoning(BaseModel):
|
||||
"""推理类"""
|
||||
|
||||
kind: Kind = Field(..., description="推理类型")
|
||||
kind: ReasoningKind = Field(..., description="推理种类")
|
||||
content: str = Field(default="", description="推理内容")
|
||||
|
||||
|
||||
|
|
@ -98,12 +87,16 @@ class Run(BaseModel):
|
|||
)
|
||||
is_reasoning: bool = Field(
|
||||
default=False,
|
||||
description="推理状态,True 表示正在推理,False 表示非正在推理",
|
||||
description="推理状态,True 表示正在推理,False 表示推理完成",
|
||||
)
|
||||
is_reasoning_panel_open: bool = Field(
|
||||
default=False,
|
||||
description="推理面板展开状态,True 表示推理面板展开,False 表示推理面板折叠",
|
||||
)
|
||||
assistant_content: str = Field(default="", description="回复正文")
|
||||
is_running: bool = Field(
|
||||
default=False,
|
||||
description="运行状态,True 表示正在运行,False 表示非正在运行",
|
||||
description="运行状态,True 表示正在运行,False 表示运行完成",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,16 +2,14 @@
|
|||
"""
|
||||
对话状态
|
||||
"""
|
||||
from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, cast
|
||||
from typing import Any, AsyncGenerator, Dict
|
||||
|
||||
from pydantic_ai import Agent, ThinkingPartDelta
|
||||
from pydantic_ai._uuid import uuid7
|
||||
from pydantic_ai.capabilities import AgentCapability
|
||||
from pydantic_ai.messages import (
|
||||
FunctionToolCallEvent,
|
||||
FunctionToolResultEvent,
|
||||
LoadCapabilityCallPart,
|
||||
ModelMessage,
|
||||
PartDeltaEvent,
|
||||
PartEndEvent,
|
||||
PartStartEvent,
|
||||
|
|
@ -21,12 +19,12 @@ from pydantic_ai.messages import (
|
|||
ToolCallPart,
|
||||
ToolSearchCallPart,
|
||||
)
|
||||
from pydantic_ai.run import AgentRunResultEvent
|
||||
from pydantic_ai.models.openai import OpenAIChatModel
|
||||
from pydantic_ai.providers.openai import OpenAIProvider
|
||||
from pydantic_ai.run import AgentRunResultEvent
|
||||
import reflex as rx
|
||||
|
||||
from application.models import Conversation, EventKind, PartKind, Run, Reasoning, Kind
|
||||
from application.models import Conversation, Reasoning, ReasoningKind, Run
|
||||
from application.state.create_conversation_modal import CreateConversationModalState
|
||||
from application.state.database import DatabaseState
|
||||
|
||||
|
|
@ -156,10 +154,10 @@ class ConversationState(rx.State):
|
|||
return conversation.runs if conversation else {}
|
||||
|
||||
@rx.var
|
||||
def get_run_running_status(self) -> bool:
|
||||
def get_running_status(self) -> bool:
|
||||
"""
|
||||
获取当前运行运行状态
|
||||
:return: 当前运行状态(True 表示正在运行,False 表示非正在运行)
|
||||
获取当前运行状态
|
||||
:return: 当前运行状态(True 表示正在运行,False 表示运行完成)
|
||||
"""
|
||||
# 当前对话
|
||||
conversation = self.conversations.get(self.conversation_id)
|
||||
|
|
@ -221,12 +219,13 @@ class ConversationState(rx.State):
|
|||
match part:
|
||||
# 思考分片开始事件
|
||||
case ThinkingPart(content=content):
|
||||
# 若上一分片种类为空则将推理状态设置为正在推理
|
||||
# 若上一分片种类为空则将推理状态设置为正在推理、推理面板展开状态设置为展开
|
||||
if not previous_part_kind:
|
||||
run.is_reasoning = True
|
||||
run.is_reasoning_panel_open = True
|
||||
|
||||
run.reasonings[index] = Reasoning(
|
||||
kind=Kind.THINKING, content=content
|
||||
kind=ReasoningKind.THINKING, content=content
|
||||
)
|
||||
yield
|
||||
|
||||
|
|
@ -236,7 +235,7 @@ class ConversationState(rx.State):
|
|||
tool_call_ids[tool_call_id] = index
|
||||
|
||||
run.reasonings[index] = Reasoning(
|
||||
kind=Kind.TOOL_SEARCH, content="正在检索"
|
||||
kind=ReasoningKind.TOOL_SEARCH, content="正在生成检索关键词"
|
||||
)
|
||||
yield
|
||||
|
||||
|
|
@ -245,7 +244,8 @@ class ConversationState(rx.State):
|
|||
tool_call_ids[tool_call_id] = index
|
||||
|
||||
run.reasonings[index] = Reasoning(
|
||||
kind=Kind.LOAD_CAPABILITY, content="正在加载能力"
|
||||
kind=ReasoningKind.CAPABILITY_LOAD,
|
||||
content="正在生成加载参数",
|
||||
)
|
||||
yield
|
||||
|
||||
|
|
@ -254,7 +254,7 @@ class ConversationState(rx.State):
|
|||
tool_call_ids[tool_call_id] = index
|
||||
|
||||
run.reasonings[index] = Reasoning(
|
||||
kind=Kind.TOOL_CALL, content="正在调用工具"
|
||||
kind=ReasoningKind.TOOL_CALL, content="正在生成调用参数"
|
||||
)
|
||||
yield
|
||||
|
||||
|
|
@ -289,54 +289,58 @@ class ConversationState(rx.State):
|
|||
match part:
|
||||
# 思考分片结束事件
|
||||
case ThinkingPart(part_kind=part_kind, content=content):
|
||||
# 若下一分片种类为文本则将推理状态设置为非正在推理
|
||||
if next_part_kind == Kind.TEXT:
|
||||
# 若下一分片种类为文本则将推理状态设置为推理完成、推理面板展开状态设置为折叠
|
||||
if next_part_kind == ReasoningKind.TEXT:
|
||||
run.is_reasoning = False
|
||||
run.is_reasoning_panel_open = False
|
||||
yield
|
||||
|
||||
# 加载能力分片结束事件
|
||||
case LoadCapabilityCallPart(
|
||||
tool_kind=tool_kind, tool_name=tool_name
|
||||
):
|
||||
yield Event(
|
||||
event_kind=EventKind(event_kind),
|
||||
part_index=index,
|
||||
part_kind=PartKind(tool_kind),
|
||||
next_part_kind=PartKind(next_part_kind),
|
||||
tool_name=tool_name,
|
||||
run_id=current_run_id,
|
||||
)
|
||||
# 调用工具分片结束事件
|
||||
case ToolCallPart(part_kind=part_kind, tool_name=tool_name):
|
||||
yield Event(
|
||||
event_kind=EventKind(event_kind),
|
||||
part_index=index,
|
||||
part_kind=PartKind(part_kind),
|
||||
next_part_kind=PartKind(next_part_kind),
|
||||
tool_name=tool_name,
|
||||
run_id=current_run_id,
|
||||
)
|
||||
|
||||
# ========== 函数工具调用事件 ==========
|
||||
case FunctionToolCallEvent(
|
||||
part=part,
|
||||
):
|
||||
match part:
|
||||
# 工具检索分片函数工具调用事件
|
||||
case ToolSearchCallPart(tool_call_id=tool_call_id):
|
||||
index = tool_call_ids[tool_call_id]
|
||||
run.reasonings[index].content = "检索完成"
|
||||
case FunctionToolCallEvent(tool_call_id=tool_call_id, part=part):
|
||||
# 获取分片索引
|
||||
index = tool_call_ids[tool_call_id]
|
||||
match run.reasonings[index].kind:
|
||||
# 工具检索
|
||||
case ReasoningKind.TOOL_SEARCH:
|
||||
run.reasonings[index].content = "正在检索"
|
||||
yield
|
||||
|
||||
# 能力加载
|
||||
case ReasoningKind.CAPABILITY_LOAD:
|
||||
run.reasonings[index].content = (
|
||||
f"正在加载能力 {part.tool_name}"
|
||||
)
|
||||
yield
|
||||
|
||||
# 工具调用
|
||||
case ReasoningKind.TOOL_CALL:
|
||||
run.reasonings[index].content = (
|
||||
f"正在调用工具 {part.tool_name}"
|
||||
)
|
||||
yield
|
||||
|
||||
# ========== 函数工具结果事件 ==========
|
||||
case FunctionToolResultEvent(
|
||||
tool_call_id=tool_call_id,
|
||||
content=content,
|
||||
part=part,
|
||||
):
|
||||
match part:
|
||||
# 工具检索分片函数工具结果事件
|
||||
case ToolSearchCallPart(tool_call_id=tool_call_id):
|
||||
index = tool_call_ids[tool_call_id]
|
||||
run.reasonings[index].content += content or ""
|
||||
index = tool_call_ids[tool_call_id]
|
||||
match run.reasonings[index].kind:
|
||||
# 工具检索
|
||||
case ReasoningKind.TOOL_SEARCH:
|
||||
run.reasonings[index].content = (
|
||||
content if isinstance(content, str) else ""
|
||||
) # 暂仅考虑文本内容
|
||||
yield
|
||||
|
||||
# 能力加载
|
||||
case ReasoningKind.CAPABILITY_LOAD:
|
||||
run.reasonings[index].content = "已加载"
|
||||
yield
|
||||
|
||||
# 工具调用
|
||||
case ReasoningKind.TOOL_CALL:
|
||||
run.reasonings[index].content = f"已调用"
|
||||
yield
|
||||
|
||||
# ========== 智能体运行结果事件 ==========
|
||||
|
|
@ -347,6 +351,15 @@ class ConversationState(rx.State):
|
|||
run_id=run_id,
|
||||
new_messages=result.new_messages(),
|
||||
)
|
||||
# 将运行状态设置为非正在运行
|
||||
# 将运行状态设置为运行完成
|
||||
run.is_running = False
|
||||
yield
|
||||
|
||||
@rx.event
|
||||
def toggle_reasoning_panel(self, run_id: str) -> None:
|
||||
"""
|
||||
展开/折叠指定运行唯一标识的推理面板
|
||||
"""
|
||||
# 指定运行
|
||||
run = self.conversations[self.conversation_id].runs[run_id]
|
||||
run.is_reasoning_panel_open = not run.is_reasoning_panel_open
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
框架状态
|
||||
"""
|
||||
import reflex as rx
|
||||
from application.models import SidebarIconNavButtonKind, SidebarIconNavButton
|
||||
|
||||
# 初始化侧边栏图标导航按钮字典
|
||||
sidebar_icon_nav_buttons = {
|
||||
SidebarIconNavButtonKind.CONVERSATION: {"icon": "message-square", "name": "对话"},
|
||||
SidebarIconNavButtonKind.KNOWLEDGE: {"icon": "book", "name": "知识库"},
|
||||
}
|
||||
|
||||
|
||||
class FrameState(rx.State):
|
||||
"""
|
||||
框架状态
|
||||
"""
|
||||
|
||||
# 初始化侧边栏图标导航按钮字典
|
||||
SIDEBAR_ICON_NAV_BUTTONS = {
|
||||
SidebarIconNavButtonKind.CONVERSATION: SidebarIconNavButton(
|
||||
icon="message-circle-more", name="对话"
|
||||
),
|
||||
SidebarIconNavButtonKind.KNOWLEDGE: SidebarIconNavButton(
|
||||
icon="book", name="知识库"
|
||||
),
|
||||
} # 侧边栏图标导航按钮字典
|
||||
|
||||
# 当前侧边栏图标导航按钮
|
||||
sidebar_icon_nav_button: SidebarIconNavButton =
|
||||
|
||||
@rx.var
|
||||
def get_sidebar_icon_nav_button(self) -> SidebarIconNavButton:
|
||||
"""获取当前侧边栏图标导航按钮"""
|
||||
return self.SIDEBAR_ICON_NAV_BUTTONS[self.sidebar_icon_nav_button]
|
||||
|
||||
@rx.event
|
||||
def set_active_sidebar_tab(self, icon_id: str):
|
||||
"""切换侧边栏当前标签名称"""
|
||||
self.sidebar_tab_name = icon_id
|
||||
Loading…
Reference in New Issue