Python/agent/application/pages/task.py

997 lines
34 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.states import AuthState, TaskState
from application.states.models import (
TaskHistoryItem,
MessageHistoryItem,
MessageType,
WorkflowType,
)
def task_history_item(
item: TaskHistoryItem,
) -> rx.Component:
"""
任务历史项
:param item: 任务历史项
:return: Component
"""
# 高亮
is_highlight = (item.id == TaskState.popover_shown_task_history_item_id) | (
item.id == TaskState.task_id
)
return rx.list.item(
rx.hstack(
# 会话历史项的会话描述
rx.text(
item.description,
flex=1,
height="22px",
line_height="22px",
padding_right="18px",
color="var(--prismui-color-2)",
text_overflow="ellipsis",
white_space="nowrap",
overflow="hidden",
),
display="flex",
align_items="center",
width="100%",
margin_bottom="8px",
# 点击事件:将指定任务唯一标识设置为当前任务唯一标识
on_click=lambda: TaskState.set_task_id(item.id),
),
# 会话历史项的更多按钮
rx.box(
rx.popover.root(
rx.popover.trigger(
rx.box(
rx.icon(
"ellipsis",
width="14px",
height="14px",
color="var(--prismui-color-2)",
),
cursor="pointer",
)
),
rx.popover.content(
rx.vstack(
rx.box(
position="absolute",
top="-6px",
left="50%",
transform="translateX(-50%) rotate(45deg)",
width="8px",
height="8px",
background_color="var(--prismui-background-color-1)",
),
rx.popover.close(
rx.box(
"删除",
# 点击事件:删除任务
on_click=lambda: TaskState.delete_task(item.id),
display="flex",
align_items="center",
justify_content="center",
width="100%",
height="24px",
border_radius="var(--prismui-border-radius-1)",
line_height="16px",
font_family="var(--prismui-font-family)",
font_size="var(--prismui-font-size-1)",
color="var(--prismui-color-1)",
style={
"_hover": {
"background_color": "var(--prismui-background-color-6)",
}, # 鼠标悬停显示背景颜色
},
cursor="pointer",
),
),
position="relative",
width="100%",
),
side="bottom",
side_offset=8,
position="relative",
align="center",
padding="4px 8px",
border_radius="var(--prismui-border-radius-1)",
box_shadow="var(--prismui-box-shadow-6)",
overflow="visible",
),
# 悬停气泡显示 / 隐藏变化事件:设置任务历史项悬停气泡显示 / 隐藏
on_open_change=lambda is_shown: TaskState.set_task_history_item_popover_shown(
item.id, is_shown
),
open_delay=0,
),
position="absolute",
top="27px",
right="16px",
transform="translateY(-50%)",
z_index="99",
min_width="14px",
cursor="pointer",
opacity=rx.cond(is_highlight, "1", "0"),
transition="opacity 0.18s ease-in-out",
pointer_events=rx.cond(is_highlight, "auto", "none"),
),
display="flex",
position="relative",
align_items="flex-start",
width="100%",
padding="16px",
margin_bottom="8px",
background=rx.cond(
is_highlight,
"var(--prismui-background-3)",
"var(--prismui-background-color-1)",
),
border_radius="var(--prismui-border-radius-3)",
box_shadow=rx.cond(is_highlight, "var(--prismui-box-shadow-7)", "none"),
cursor="pointer",
style={
"&:hover": {
"background": "var(--prismui-background-3)",
"box_shadow": "var(--prismui-box-shadow-7)",
}, # 鼠标悬停背景颜色和阴影
"&:hover > div:last-child": {
"opacity": "1",
"pointer_events": "auto",
}, # 鼠标悬停显示更多按钮
},
)
def task_history(
is_conversation_history_shown: bool,
) -> rx.Component:
"""
任务历史
:param is_conversation_history_shown: 展示 / 隐藏
:return: Component
"""
return rx.box(
rx.vstack(
# 会话历史的标题
rx.hstack(
rx.text(
"会话历史",
margin_bottom="8px",
font_weight="var(--prismui-font-weight-3)",
white_space="nowrap",
),
align_items="center",
justify_content="space-between",
),
rx.auto_scroll(
rx.foreach(
TaskState.task_history,
task_history_item,
),
flex="1",
align_items="stretch",
width="100%",
margin_top="8px",
style={
"&::-webkit-scrollbar": {
"display": "none",
},
},
overflow="auto",
),
display="flex",
flex_direction="column",
height="100%",
padding="12px",
background_color="#f9f9f9cc",
backdrop_filter="blur(50px)",
gap="12px",
),
width=rx.cond(is_conversation_history_shown, "25%", "0px"),
min_width=rx.cond(is_conversation_history_shown, "240px", "0px"),
max_width=rx.cond(is_conversation_history_shown, "380px", "0px"),
height="100%",
transition="all 0.18s ease-in-out",
overflow="hidden",
)
def conversation_history(
is_conversation_history_shown: bool,
) -> rx.Component:
"""
会话历史
:param is_conversation_history_shown: 展示 / 隐藏
:return: Component
"""
return rx.box(
rx.vstack(
# 会话历史的标题
rx.hstack(
rx.text(
"会话历史",
margin_bottom="8px",
font_weight="var(--prismui-font-weight-3)",
white_space="nowrap",
),
align_items="center",
justify_content="space-between",
),
rx.auto_scroll(
rx.foreach(
TaskState.task_history,
task_history_item,
),
flex="1",
align_items="stretch",
width="100%",
margin_top="8px",
style={
"&::-webkit-scrollbar": {
"display": "none",
},
},
overflow="auto",
),
display="flex",
flex_direction="column",
height="100%",
padding="12px",
background_color="#f9f9f9cc",
backdrop_filter="blur(50px)",
gap="12px",
),
width=rx.cond(is_conversation_history_shown, "25%", "0px"),
min_width=rx.cond(is_conversation_history_shown, "240px", "0px"),
max_width=rx.cond(is_conversation_history_shown, "380px", "0px"),
height="100%",
transition="all 0.18s ease-in-out",
overflow="hidden",
)
def workflow_button(workflow_type: WorkflowType):
"""
工作流按钮
:param workflow_type: 工作类型
:return: rx.Component
"""
return rx.box(
workflow_type.value,
# 点击事件:设置工作流
on_click=lambda: TaskState.set_workflow(workflow_type),
padding="10px 16px",
background_color="var(--prismui-background-color-6)",
border_radius="var(--prismui-border-radius-9)",
color="var(--prismui-color-10)",
cursor="pointer",
)
def use_guidance() -> rx.Component:
"""
使用指引
:return: Component
"""
return rx.vstack(
# 品牌图标、名称和介绍
rx.vstack(
rx.hstack(
rx.image(
src="/logo.png",
width="64px",
height="64px",
),
rx.text(
"棱镜球",
font_size="calc(var(--prismui-font-size-3) * 2)",
font_weight="var(--prismui-font-weight-3)",
letter_spacing="1px",
),
align_items="center",
gap="8px",
),
rx.vstack(
rx.text(
"产品汪的棱镜球,精准捕捉各类业务需求",
width="100%",
text_align="center",
),
rx.text(
"可帮助检索业务资料,协助生成产品方案、输出 PRD 、流程图和原型图等",
width="100%",
text_align="center",
),
line_height="1.5",
gap="4px",
),
align_items="center",
gap="12px",
),
# 演示案例
rx.vstack(
rx.text(
"演示案例",
margin_bottom="16px",
line_height="24px",
font_size="var(--prismui-font-size-3)",
font_weight="var(--prismui-font-weight-3)",
),
rx.hstack(
# 工作流按钮:预定航班
workflow_button(WorkflowType.BOOK_FLIGHT),
# 工作流按钮:银行客户服务
workflow_button(WorkflowType.BANK_CUSTOMER_SERVICE),
# 工作流按钮:联网检索
workflow_button(WorkflowType.WEB_RETRIEVE),
# 工作流按钮:知识库检索
workflow_button(WorkflowType.KNOWLEDGE_BASE_RETRIEVE),
flex_wrap="wrap",
align_items="center",
gap="12px",
),
width="100%",
padding="24px",
background_color="var(--prismui-background-color-1)",
border_radius="var(--prismui-border-radius-7)",
),
flex="1",
justify_content="center",
align_items="center",
gap="24px",
width="100%",
max_width="1200px",
height="100%",
padding="0 12px",
margin="auto",
color="var(--prismui-color-text)",
)
def user_prompt(item: MessageHistoryItem) -> rx.Component:
"""
用户提示词
:return: Component
"""
return rx.hstack(
rx.spacer(),
rx.vstack(
rx.text(
item.content,
max_width="600px",
padding="12px 16px",
background_color="var(--prismui-background-color-3)",
border_radius="var(--prismui-border-radius-3)",
line_height="1.5",
word_wrap="break-word",
word_break="break-all",
white_space="pre-line",
),
rx.hstack(
# 再次发送
rx.box(
rx.icon(
"rotate-ccw",
width="14px",
height="14px",
color="var(--prismui-color-2)",
),
padding="4px",
border_radius="var(--prismui-border-radius-1)",
cursor="pointer",
style={
"_hover": {
"background_color": "var(--prismui-background-color-2)",
}
},
),
width="100%",
margin_top="8px",
),
),
align_items="flex-start",
gap="4px",
width="100%",
margin_top="8px",
)
def thinking(item: MessageHistoryItem) -> rx.Component:
"""
思考
:return: Component
"""
return rx.vstack(
# 标题栏
rx.box(
rx.hstack(
rx.text(
item.title,
font_size="var(--prismui-font-size-1)",
class_name=rx.cond(item.is_running, "is_running", None),
),
rx.icon(
"chevron-right",
width="14px",
height="14px",
transform=rx.cond(item.is_shown, "rotate(90deg)", "rotate(0deg)"),
transition="transform 0.18s ease-in-out",
),
align_items="center",
gap="4px",
margin_bottom="8px",
line_height="22px",
),
# 点击事件,展示 / 隐藏消息历史项
on_click=lambda: TaskState.toggle_message_history_item_shown(item.id),
cursor="pointer",
),
# 思考的内容
rx.box(
rx.box(
rx.cond(
item.is_shown,
rx.markdown(
item.content,
width="100%",
line_height="1.6",
font_size="var(--prismui-font-size-1)",
color="var(--prismui-color-3)",
),
rx.fragment(),
),
min_height="0",
),
display="grid",
grid_template_rows=rx.cond(item.is_shown, "1fr", "0fr"),
width="100%",
opacity=rx.cond(item.is_shown, "1", "0"),
transition="grid-template-rows 0.18s ease-in-out, opacity 0.18s ease-in-out",
overflow="hidden",
),
)
def tool_call(item: MessageHistoryItem) -> rx.Component:
"""
工具调用
:return: Component
"""
return rx.vstack(
# 标题栏
rx.box(
rx.hstack(
rx.icon(
"square-function",
width="14px",
height="14px",
),
rx.text(
item.title,
font_size="var(--prismui-font-size-1)",
class_name=rx.cond(item.is_running, "is_running", None),
),
rx.icon(
"chevron-right",
width="14px",
height="14px",
transform=rx.cond(item.is_shown, "rotate(90deg)", "rotate(0deg)"),
transition="transform 0.18s ease-in-out",
),
align_items="center",
gap="4px",
margin_bottom="8px",
line_height="22px",
),
# 点击事件,展示 / 隐藏消息历史项
on_click=lambda: TaskState.toggle_message_history_item_shown(item.id),
cursor="pointer",
),
rx.box(
rx.box(
rx.cond(
item.is_shown,
rx.hstack(
# 垂线
rx.box(
width="1px",
margin="12px 7px",
background_color="var(--prismui-background-color-2)",
),
rx.vstack(
# 内容
rx.markdown(
item.content,
allow_html=True,
width="100%",
margin="4px 0",
line_height="1.6",
font_size="var(--prismui-font-size-1)",
),
# 若有审批
rx.cond(
item.has_approval,
rx.vstack(
rx.text_area(
# 值绑定审批内容
value=item.approval_content,
# 值变化事件:设置审批内容
on_change=lambda content: TaskState.set_approval_content(
item.id, content
),
placeholder="请输入",
vertical_align="middle",
width="100%",
height="auto",
padding="4px 0",
outline="none !important",
box_shadow="none !important",
style={
".rt-TextAreaInput": {
"font-size": "var(--prismui-font-size-1) !important",
"border": "none !important",
"overflow-y": "hidden !important",
},
".rt-TextAreaInput:disabled": {
"background-color": "var(--prismui-background-color-1) !important",
"font-size": "var(--prismui-font-size-1) !important",
"cursor": "not-allowed !important",
},
},
),
rx.hstack(
rx.button(
"提交",
# 点击事件:审批
on_click=lambda: TaskState.run(
True, item.id
),
padding="0 12px",
background_color="var(--prismui-background-color-7)",
border_radius="var(--prismui-border-radius-3)",
line_height="18px",
font_size="var(--prismui-font-size-1)",
color="var(--prismui-color-9)",
cursor="pointer",
),
justify_content="flex-end",
width="100%",
),
width="100%",
padding="12px 16px",
background_color="var(--prismui-background-color-1)",
border="var(--prismui-border-1)",
border_radius="var(--prismui-border-radius-4)",
),
rx.fragment(),
),
width="100%",
gap="4px",
),
align_items="stretch",
width="100%",
margin_bottom="8px",
gap="4px",
),
rx.fragment(),
),
min_height="0",
),
display="grid",
grid_template_rows=rx.cond(item.is_shown, "1fr", "0fr"),
width="100%",
opacity=rx.cond(item.is_shown, "1", "0"),
transition="grid-template-rows 0.18s ease-in-out, opacity 0.18s ease-in-out",
overflow="hidden",
),
)
def text(item: MessageHistoryItem) -> rx.Component:
"""
文本
:return: Component
"""
return rx.markdown(
item.content,
component_map={
"p": lambda text: rx.text(
text,
margin="4px 0",
line_height="1.75",
word_wrap="break-word",
word_break="break-all",
),
"strong": lambda text: rx.text(
text,
display="inline",
font_weight="var(--prismui-font-weight-3)",
),
"code": lambda text: rx.text(
text,
display="inline",
background_color="var(--prismui-background-color-6)",
border_radius="var(--prismui-border-radius-1)",
color="var(--prismui-color-5)",
),
"h1": lambda text: rx.text(
text,
margin="8px 0 4px",
line_height="1.75",
font_size="var(--prismui-font-size-4)",
font_weight="var(--prismui-font-weight-3)",
word_wrap="break-word",
word_break="break-all",
),
"h2": lambda text: rx.text(
text,
margin="6px 0 4px",
line_height="1.75",
font_size="var(--prismui-font-size-3)",
font_weight="var(--prismui-font-weight-3)",
word_wrap="break-word",
word_break="break-all",
),
"h3": lambda text: rx.text(
text,
margin="4px 0",
line_height="1.75",
font_weight="var(--prismui-font-weight-3)",
word_wrap="break-word",
word_break="break-all",
),
"ul": lambda children: rx.vstack(
children,
align_items="flex-start",
margin="4px 0",
style={"padding": "0 !important"},
),
"ol": lambda children: rx.vstack(
children,
align_items="flex-start",
margin="4px 0",
style={"padding": "0 !important"},
),
"li": lambda text: rx.hstack(
rx.box(
width="4.5px",
height="4.5px",
margin_right="8px",
background_color="var(--prismui-background-color-7)",
border_radius="100%",
),
rx.text(
text,
line_height="1.75",
word_wrap="break-word",
word_break="break-all",
),
align_items="center",
justify_content="flex-start",
margin_bottom="4px",
),
"hr": lambda _: rx.divider(
height="1px",
margin="12px 0",
background_color="var(--prismui-background-color-2)",
),
"table": lambda children: rx.el.table(
children,
width="100%",
margin="8px 0",
border_collapse="collapse",
),
"thead": lambda children: rx.el.thead(children),
"tbody": lambda children: rx.el.tbody(children),
"tr": lambda children: rx.el.tr(children),
"th": lambda children: rx.el.th(
children,
padding="8px 12px",
background_color="var(--prismui-background-color-6)",
border="1px solid var(--prismui-background-color-2)",
text_align="left",
),
"td": lambda children: rx.el.td(
children,
padding="8px 12px",
border="1px solid var(--prismui-background-color-2)",
text_align="left",
),
},
width="100%",
margin_bottom="8px",
)
def message_history_item(item: MessageHistoryItem) -> rx.Component:
"""
消息历史项
:param item: 对话实例
:return: Component
"""
return rx.vstack(
rx.match(
item.type,
(MessageType.USER_PROMPT, user_prompt(item)),
(MessageType.THINKING, thinking(item)),
(MessageType.TOOL_CALL, tool_call(item)),
(MessageType.TEXT, text(item)),
),
padding="0 16px",
width="100%",
)
def message_history() -> rx.Component:
"""
消息历史
:return: Component
"""
return rx.box(
# 若消息历史为空则显示使用指引,否则渲染消息历史项
rx.cond(
TaskState.message_history.length() == 0,
use_guidance(),
rx.vstack(
rx.auto_scroll(
rx.vstack(
rx.foreach(TaskState.message_history, message_history_item),
# 若等待流式输出则显示加载动效
rx.cond(
TaskState.awaiting_stream,
rx.box(
rx.box(
class_name="awaiting_stream",
flex_shrink="0",
),
display="flex",
justify_content="flex‑start",
align_items="center",
width="100%",
padding="0 16px",
margin="8px 0",
overflow="hidden",
),
rx.fragment(),
),
width="100%",
),
width="100%",
padding="0 12px",
padding_bottom="180px",
style={
"&::-webkit-scrollbar": {
"display": "none",
},
},
),
flex="1",
width="100%",
max_width="1200px",
margin_x="auto",
padding_top="20px",
justify_content="flex-start",
overflow_x="hidden",
overflow_y="auto",
),
),
display="flex",
flex_direction="column",
width="100%",
height="100%",
min_height="0",
)
def conversation_creating() -> rx.Component:
"""
创建会话
:return: Component
"""
return rx.hstack(
rx.el.style(
"""
.rt-TooltipArrow polygon {
fill: var(--prismui-background-color-1) !important;
}
.rt-TooltipText {
font-family: var(--prismui-font-family) !important;
color: var(--prismui-color-1) !important;
opacity: 1 !important;
}
"""
),
rx.spacer(),
rx.box(
rx.tooltip(
rx.icon(
"plus",
on_click=TaskState.create_task,
width="14px",
height="14px",
style={
"_hover": {
"color": "var(--prismui-color-5)",
}
},
),
content="创建任务",
side="top",
side_offset=9,
background_color="var(--prismui-background-color-1)",
box_shadow="var(--prismui-box-shadow-3)",
),
display="flex",
justify_content="center",
align_items="center",
width="24px",
height="24px",
background_color="var(--prismui-background-color-1)",
box_shadow="var(--prismui-box-shadow-8)",
border_radius="var(--prismui-border-radius-9)",
cursor="pointer",
),
display="flex",
justify_content="flex-end",
align_items="center",
gap="4px",
padding="0 12px",
margin_bottom="8px",
width="100%",
max_width="1200px",
height="39px",
background="transparent",
)
def user_prompt_box() -> rx.Component:
"""
用户提示词框
:return: rx.Component
"""
return rx.vstack(
# 创建对话
conversation_creating(),
rx.vstack(
rx.text_area(
# 值绑定:用户提示词
value=TaskState.user_prompt,
# 输入事件:设置用户提示词
on_change=TaskState.set_user_prompt,
placeholder="请输入您的问题,按Enter换行",
vertical_align="middle",
width="100%",
min_height="64px",
max_height="224px",
padding="4px 0",
background_color="var(--prismui-background-color-1)",
border="none !important",
outline="none !important",
box_shadow="none !important",
style={
".rt-TextAreaInput": {
"border": "none !important",
"outline": "none !important",
"box-shadow": "none !important",
"field-sizing": "content !important",
"min-height": "64px",
"max-height": "224px",
"overflow-y": "auto",
},
},
),
rx.hstack(
rx.spacer(),
rx.button(
rx.icon(
"send",
margin_right="4px",
width="15px",
height="15px",
color="var(--prismui-color-9)",
),
rx.text(
"发送",
line_height="21px",
color="var(--prismui-color-9)",
),
# 加载状态:绑定当前任务的运行状态
loading=TaskState.is_running,
# 不可点击状态:绑定用户提示词发送按钮不可点击状态
disabled=TaskState.is_user_prompt_sending_button_disabled,
# 点击事件:运行
on_click=lambda: TaskState.run(),
display="inline-flex",
position="relative",
justify_content="center",
align_items="center",
padding="0 12px",
background_color=rx.cond(
TaskState.is_user_prompt_sending_button_disabled,
"var(--prismui-color-11)",
"var(--prismui-color-5)",
),
border="none",
border_radius="var(--prismui-border-radius-5)",
inline_height="1.5",
font_size="var(--prismui-font-size-2)",
white_space="nowrap",
cursor=rx.cond(
TaskState.is_user_prompt_sending_button_disabled,
"not-allowed",
"pointer",
),
),
justify_content="flex-end",
align_items="center",
height="32px",
width="100%",
),
width="100%",
padding="12px 16px",
background_color="var(--prismui-background-color-1)",
border_radius="var(--prismui-border-radius-6)",
box_shadow="var(--prismui-box-shadow-9)",
),
# 免责声明
rx.text(
"内容由大模型生成,无法确保准确性和完整性,仅供参考",
width="100%",
text_align="center",
margin_top="8px",
line_height="18px",
font_size="var(--prismui-font-size-1)",
color="var(--prismui-color-10)",
),
z_index="99",
display="flex",
flex_direction="column",
width="100%",
max_width="1200px",
padding="0 12px 12px",
margin_x="auto",
border="none",
)
def task_page() -> rx.Component:
"""
任务页面
:return: rx.Component
"""
return rx.hstack(
# 渲染消息历史和用户提示词框
rx.vstack(
rx.vstack(
# 渲染消息历史
message_history(),
align_items="center",
gap="8px",
flex="1",
width="100%",
overflow_y="auto",
),
user_prompt_box(),
width="100%",
height="100%",
background="var(--prismui-background-2)",
overflow="hidden",
),
# 渲染...
width="100%",
height="100%",
# 挂载事件:恢复任务状态
on_mount=AuthState.resume_task_state,
)