From 0b4b02a43d4a8438c38e004c5930a424cddcb4f3 Mon Sep 17 00:00:00 2001 From: liubiren Date: Mon, 19 Oct 2026 20:12:52 +0800 Subject: [PATCH] 1 --- agent/application/pages/login.py | 56 ++++--- agent/application/pages/task.py | 224 +++++++++++++++------------ agent/application/pages/workbench.py | 224 ++++++++++++++++----------- agent/application/states/task.py | 37 +++-- agent/assets/styles.css | 21 ++- agent/database.db | Bin 561152 -> 614400 bytes 6 files changed, 334 insertions(+), 228 deletions(-) diff --git a/agent/application/pages/login.py b/agent/application/pages/login.py index c120339..6b918d7 100644 --- a/agent/application/pages/login.py +++ b/agent/application/pages/login.py @@ -6,6 +6,7 @@ import reflex as rx from application.states import AuthState + def login_page() -> rx.Component: """ 登录页面 @@ -32,12 +33,12 @@ def login_page() -> rx.Component: on_change=AuthState.set_email, # 获取焦点事件:重置错误信息 on_focus=AuthState.reset_error, + line_height="16px", width="100%", height="45px", padding="4px", margin_bottom="24px", border_radius="var(--prismui-border-radius-4)", - line_height="16px", ), # 验证码输入框 rx.hstack( @@ -58,30 +59,40 @@ def login_page() -> rx.Component: border="none !important", ), rx.button( - # 若再次发送验证码倒计时大于0则显示倒计时,否则显示获取验证码 - rx.cond( - AuthState.resend_captcha_code_countdown > 0, - f"{AuthState.resend_captcha_code_countdown} s", - "获取验证码", + rx.text( + rx.cond( + AuthState.resend_captcha_code_countdown > 0, + f"{AuthState.resend_captcha_code_countdown} s", + "获取验证码", + ), + line_height="16px", + white_space="nowrap", + # 若验证码发送禁用状态为是则颜色设置为 color-4,否则设置为 color-5(主题色) + color=rx.cond( + AuthState.is_captcha_code_sending_button_disabled, + "var(--prismui-color-4)", + "var(--prismui-color-5)", + ), + padding="0 12px", ), + # 若再次发送验证码倒计时大于0则显示倒计时,否则显示获取验证码 # 点击事件:发送验证码 on_click=AuthState.send_captcha_code, # 是否禁用绑定验证码发送禁用状态 disabled=AuthState.is_captcha_code_sending_button_disabled, margin="0 8px", background_color="var(--prismui-background-color-1)", - line_height="16px", - # 若验证码发送禁用状态为是则颜色设置为 color-4,否则设置为 color-5(主题色) - color=rx.cond( - AuthState.is_captcha_code_sending_button_disabled, - "var(--prismui-color-4)", - "var(--prismui-color-5)", - ), + border="none", cursor=rx.cond( AuthState.is_captcha_code_sending_button_disabled, "not-allowed", "pointer", ), + style={ + "_hover": { + "background-color": "var(--prismui-background-color-1)" + } + }, ), width="100%", height="45px", @@ -161,14 +172,19 @@ def login_page() -> rx.Component: ), rx.button( "登录", - width="100%", - height="38px", - border_radius="var(--prismui-border-radius-4)", - background_color="var(--prismui-color-5)", - color="var(--prismui-color-9)", - cursor="pointer", on_click=AuthState.login, loading=AuthState.is_logging_in, + color="var(--prismui-color-9)", + width="100%", + height="38px", + background_color="var(--prismui-background-color-4)", + border_radius="var(--prismui-border-radius-4)", + cursor="pointer", + style={ + "_hover": { + "background-color": "var(--prismui-background-color-5)" + } + }, ), rx.text( "登录遇到问题?加他微信去吐槽", @@ -218,4 +234,4 @@ def login_page() -> rx.Component: width="100vw", height="100vh", backdrop_filter="blur(4px)", - ) \ No newline at end of file + ) diff --git a/agent/application/pages/task.py b/agent/application/pages/task.py index 9a84d37..b87bc70 100644 --- a/agent/application/pages/task.py +++ b/agent/application/pages/task.py @@ -8,9 +8,9 @@ from application.states import AuthState, TaskState from application.states.models import MessageHistoryItem, MessageType, WorkflowType -def workflow_button(workflow_type: WorkflowType): +def shortcut(workflow_type: WorkflowType): """ - 工作流按钮 + 快捷方式 :param workflow_type: 工作类型 :return: rx.Component """ @@ -19,17 +19,16 @@ def workflow_button(workflow_type: WorkflowType): # 点击事件:设置工作流 on_click=lambda: TaskState.set_workflow(workflow_type), padding="10px 16px", - background_color="var(--prismui-background-color-6)", + background_color="var(--prismui-background-color-9)", border_radius="var(--prismui-border-radius-9)", - color="var(--prismui-color-10)", cursor="pointer", ) -def use_guidance() -> rx.Component: +def welcome() -> rx.Component: """ - 使用指引 - :return: Component + 欢迎 + :return: rx.Component """ return rx.vstack( # 品牌图标、名称和介绍 @@ -77,13 +76,13 @@ def use_guidance() -> rx.Component: ), rx.hstack( # 工作流按钮:预定航班 - workflow_button(WorkflowType.BOOK_FLIGHT), + shortcut(WorkflowType.BOOK_FLIGHT), # 工作流按钮:银行客户服务 - workflow_button(WorkflowType.BANK_CUSTOMER_SERVICE), + shortcut(WorkflowType.BANK_CUSTOMER_SERVICE), # 工作流按钮:联网检索 - workflow_button(WorkflowType.WEB_RETRIEVE), + shortcut(WorkflowType.WEB_RETRIEVE), # 工作流按钮:知识库检索 - workflow_button(WorkflowType.KNOWLEDGE_BASE_RETRIEVE), + shortcut(WorkflowType.KNOWLEDGE_BASE_RETRIEVE), flex_wrap="wrap", align_items="center", gap="12px", @@ -194,7 +193,7 @@ def thinking(item: MessageHistoryItem) -> rx.Component: width="100%", line_height="1.6", font_size="var(--prismui-font-size-1)", - color="var(--prismui-color-3)", + color="var(--prismui-color-2)", ), rx.fragment(), ), @@ -305,7 +304,7 @@ def tool_call(item: MessageHistoryItem) -> rx.Component: True, item.id ), padding="0 12px", - background_color="var(--prismui-background-color-7)", + background_color="var(--prismui-background-color-9)", border_radius="var(--prismui-border-radius-3)", line_height="18px", font_size="var(--prismui-font-size-1)", @@ -368,7 +367,7 @@ def text(item: MessageHistoryItem) -> rx.Component: "code": lambda text: rx.text( text, display="inline", - background_color="var(--prismui-background-color-6)", + background_color="var(--prismui-background-color-9)", border_radius="var(--prismui-border-radius-1)", color="var(--prismui-color-5)", ), @@ -415,7 +414,7 @@ def text(item: MessageHistoryItem) -> rx.Component: width="4.5px", height="4.5px", margin_right="8px", - background_color="var(--prismui-background-color-7)", + background_color="var(--prismui-background-color-4)", border_radius="100%", ), rx.text( @@ -445,7 +444,7 @@ def text(item: MessageHistoryItem) -> rx.Component: "th": lambda children: rx.el.th( children, padding="8px 12px", - background_color="var(--prismui-background-color-6)", + background_color="var(--prismui-background-color-9)", border="1px solid var(--prismui-background-color-2)", text_align="left", ), @@ -487,10 +486,10 @@ def message_history() -> rx.Component: :return: Component """ return rx.vstack( - # 若消息历史为空则显示使用指引,否则渲染消息历史项 + # 若消息历史为空则渲染欢迎,否则渲染消息历史项 rx.cond( TaskState.message_history.length() == 0, - use_guidance(), + welcome(), rx.vstack( rx.hstack( rx.hstack( @@ -518,8 +517,9 @@ def message_history() -> rx.Component: TaskState.task_last_run_at, line_height="20px", font_size="var(--prismui-font-size-1)", - color="var(--prismui-color-3)", + color="var(--prismui-color-2)", ), + # 渲染产品面板按钮 rx.tooltip( rx.box( rx.icon( @@ -544,91 +544,117 @@ def message_history() -> rx.Component: ), content="产物面板", ), - rx.hstack( - # 渲染更多下拉菜单 - rx.menu.root( - rx.menu.trigger( - rx.box( - rx.icon( - "ellipsis", - width="16px", - height="16px", - object_fit="contain", - color="var(--prismui-color-2)", - ), - display="flex", - align_items="center", - justify_content="center", - width="32px", - height="32px", - border_radius="var(--prismui-border-radius-3)", - cursor="pointer", - style={ - "_hover": { - "background_color": "var(--prismui-background-color-9)" - } - }, - ), - ), - rx.menu.content( - # 渲染删除项 - rx.menu.item( - rx.hstack( - rx.icon( - "trash", - width="12px", - height="12px", - object_fit="contain", - color="var(--prismui-color-1)", - ), - rx.text("删除", line_height="20px"), - align_items="center", - gap="8px", - ), - height="28px", - width="100%", - cursor="pointer", - on_click=TaskState.show_task_delete_confirm, - style={ - "_hover": { - "background_color": "var(--prismui-background-color-9)" - } - }, + # 渲染更多按钮 + rx.menu.root( + rx.menu.trigger( + rx.box( + rx.icon( + "ellipsis", + width="16px", + height="16px", + object_fit="contain", + color="var(--prismui-color-2)", ), + display="flex", + align_items="center", + justify_content="center", + width="32px", + height="32px", + border_radius="var(--prismui-border-radius-3)", + cursor="pointer", + style={ + "_hover": { + "background_color": "var(--prismui-background-color-9)" + } + }, ), ), - # 渲染删除确认弹窗 - rx.popover.root( - rx.popover.trigger(rx.box(width="0", height="32px")), - rx.popover.content( + rx.menu.content( + # 渲染删除项 + rx.menu.item( + rx.hstack( + rx.icon( + "trash", + width="12px", + height="12px", + object_fit="contain", + color="var(--prismui-color-1)", + ), + rx.text("删除", line_height="20px"), + align_items="center", + gap="8px", + ), + height="28px", + width="100%", + cursor="pointer", + # 点击事件:显示当前任务删除确认 + on_select=TaskState.toggle_task_delete_confirm( + True + ).stop_propagation, + style={ + "_hover": { + "background_color": "var(--prismui-background-color-9)" + } + }, + ), + ), + ), + # 渲染任务删除确认 + rx.popover.root( + rx.popover.trigger(rx.box(width="0", height="32px")), + rx.popover.content( + rx.vstack( rx.vstack( - rx.text("删除任务?", line_height="20px"), + rx.text( + "删除任务?", + line_height="20px", + font_weight="var(--prismui-font-weight-3)", + ), rx.text( "删除后,任务将不可恢复。", line_height="20px", - font_size="var(--prismui-font-size-1)", - ), - rx.hstack( - rx.spacer(), - rx.hstack( - rx.popover.close( - rx.button( - "取消", - # 点击事件:隐藏当前任务删除确认 - on_click=TaskState.hide_task_delete_confirm, - ) - ), - rx.popover.close(rx.button("确认")), - ), - align_items="space-between", - width="100%", + font_size="var(--prismui-font-size-1) !important", ), + gap="8px", width="100%", - ) + ), + rx.hstack( + rx.spacer(), + rx.hstack( + rx.popover.close( + rx.button( + "取消", + style={ + "_hover": { + "background_color": "var(--prismui-background-color-9)" + } + }, + ) + ), + rx.button( + "确认", + color="var(--prismui-color-9)", + background_color="var(--prismui-background-color-6)", + style={ + "_hover": { + "background_color": "var(--prismui-background-color-7)" + } + }, + on_click=TaskState.delete_task, + ), + ), + align_items="space-between", + width="100%", + ), + gap="24px", + width="100%", ), - # 显示绑定:显示当前任务删除确认 - open=TaskState.is_task_delete_confirm_shown, + width="240px", + padding="16px", ), + # 显示绑定:显示当前任务删除确认 + open=TaskState.is_task_delete_confirm_shown, + on_open_change=TaskState.toggle_task_delete_confirm, ), align_items="center", gap="4px", @@ -748,8 +774,8 @@ def user_prompt_sending_box() -> rx.Component: padding="0 12px", background_color=rx.cond( TaskState.is_user_prompt_sending_button_disabled, - "var(--prismui-color-11)", - "var(--prismui-color-5)", + "var(--prismui-background-color-8)", + "var(--prismui-background-color-4)", ), border="none", border_radius="var(--prismui-border-radius-5)", @@ -761,6 +787,14 @@ def user_prompt_sending_box() -> rx.Component: "not-allowed", "pointer", ), + style={ + "_hover": { + "background-color": "var(--prismui-background-color-5)", + }, + "&:disabled:hover": { + "background-color": "var(--prismui-background-color-8)", + }, + }, ), justify_content="flex-end", align_items="center", diff --git a/agent/application/pages/workbench.py b/agent/application/pages/workbench.py index 6f6493a..665f824 100644 --- a/agent/application/pages/workbench.py +++ b/agent/application/pages/workbench.py @@ -103,7 +103,7 @@ def task_history_item( item.last_run_at, line_height="20px", font_size="var(--prismui-font-size-1)", - color="var(--prismui-color-3)", + color="var(--prismui-color-2)", flex_shrink=0, # 禁止缩放 ), align_items="center", @@ -148,91 +148,6 @@ def task_history() -> rx.Component: ) -def settings_button(): - """ - 设置按钮 - :return: Component - """ - return rx.box( - rx.hover_card.root( - # 触发事件:点击自定义按钮 - rx.hover_card.trigger( - rx.box( - rx.icon( - "settings", - stroke_width=1.5, - width="20px", - height="20px", - ), - display="flex", - flex_flow="column", - justify_content="center", - align_items="center", - width="36px", - height="36px", - border_radius="var(--prismui-border-radius-3)", - style={ - "&:hover": { - "background": "var(--prismui-background-color-5)", - }, # 鼠标悬停背景颜色 - }, - cursor="pointer", - ) - ), - # 悬停卡片 - rx.hover_card.content( - rx.box( - rx.box( - transform="translateX(-50%) rotate(45deg)", - position="absolute", - top="calc(50% - 4px)", - left="0", - width="8px", - height="8px", - background_color="var(--prismui-background-color-1)", - ), - rx.box( - "退出登录", - display="flex", - justify_content="center", - align_items="center", - width="100%", - height="24px", - line_height="16px", - border_radius="var(--prismui-border-radius-1)", - style={ - "_hover": { - "background_color": "var(--prismui-background-color-6)", - }, - }, - # 点击事件:退出登录 - on_click=lambda: AuthState.logout(), - ), - cursor="pointer", - ), - side="left", - side_offset=8, - position="relative", - align="center", - padding="4px 12px", - border_radius="var(--prismui-border-radius-1)", - box_shadow="var(--prismui-box-shadow-3)", - font_family="var(--prismui-font-family)", - overflow="visible", - style={ - "font-size": "var(--prismui-font-size-1) !important", - "color": "var(--prismui-color-1) !important", - }, - ), - open=AuthState.is_settings_hover_card_open, - # 悬停卡片打开状态变化事件:设置设置悬停卡片打开状态 - on_open_change=AuthState.set_settings_hover_card_open, - ), - min_width="14px", - cursor="pointer", - ) - - def sidebar() -> rx.Component: """ 侧边栏 @@ -241,6 +156,17 @@ def sidebar() -> rx.Component: return rx.vstack( rx.vstack( + rx.alert_dialog.root( + rx.alert_dialog.trigger(rx.box(height="0", width="100%")), + rx.alert_dialog.content( + rx.alert_dialog.title("确认退出登录?"), + rx.alert_dialog.description( + "退出登录不会丢失任何数据,你仍可以登录此账号。" + ), + rx.alert_dialog.cancel("取消"), + rx.alert_dialog.action("退出登录"), + ), + ), # 渲染系统图标和名称 rx.hstack( # 系统图标 @@ -291,12 +217,128 @@ def sidebar() -> rx.Component: width="100%", ), rx.vstack( - rx.vstack( - # 设置按钮 - settings_button(), + rx.divider( + width="100%", background_color="var(--prismui-background-color-3)" ), - padding_bottom="16px", - gap="8px", + rx.hstack( + rx.popover.root( + rx.popover.trigger( + rx.hstack( + rx.avatar( + fallback="M", + height="28px", + width="28px", + color="var(--prismui-color-5)", + border_radius="100%", + ), + rx.text( + "刘弼仁", + line_height="20px", + font_size="var(--prismui-font-size-1)", + font_weight="var(--prismui-font-weight-3)", + ), + flex="1", + align_items="center", + gap="10px", + padding="6px 8px", + margin="8px 8px 8px 0", + border_radius="var(--prismui-border-radius-3)", + cursor="pointer", + style={ + "_hover": { + "background": "var(--prismui-background-color-2)", + }, + }, + ), + ), + rx.popover.content( + rx.vstack( + rx.hstack( + rx.avatar( + fallback="M", + height="40px", + width="40px", + color="var(--prismui-color-5)", + border_radius="100%", + ), + rx.text( + "刘弼仁", + line_height="24px", + font_size="var(--prismui-font-size-3) !important", + font_weight="var(--prismui-font-weight-3)", + ), + align_items="center", + width="100%", + padding="16px", + background_color="var(--prismui-background-color-1)", + border_radius="var(--prismui-border-radius-3)", + ), + rx.vstack( + rx.popover.close( + rx.hstack( + rx.icon( + "log-out", + height="16px", + width="16px", + object_fit="contain", + color="var(--prismui-color-1)", + ), + rx.text("退出登录", line_height="20px"), + align_items="center", + gap="8px", + width="100%", + padding="5px 8px", + border_radius="var(--prismui-border-radius-3)", + cursor="pointer", + style={ + "_hover": { + "background": "var(--prismui-background-color-2)", + }, + }, + ), + ), + width="100%", + padding="8px", + background_color="var(--prismui-background-color-1)", + border_radius="var(--prismui-border-radius-3)", + ), + ), + width="320px", + padding="8px", + background_color="var(--prismui-background-color-9)", + ), + ), + rx.tooltip( + rx.box( + rx.icon( + "bell", + height="16px", + width="16px", + object_fit="contain", + color="var(--prismui-color-1)", + ), + display="flex", + align_items="center", + justify_content="center", + height="32px", + flex_shrink="0", + width="32px", + border_radius="var(--prismui-border-radius-3)", + cursor="pointer", + style={ + "_hover": { + "background": "var(--prismui-background-color-2)", + }, + }, + ), + content="消息中心", + ), + align_items="center", + justify_content="space-between", + gap="4px", + width="100%", + ), + width="100%", ), flex_shrink="0", justify_content="space-between", diff --git a/agent/application/states/task.py b/agent/application/states/task.py index b891c19..f062645 100644 --- a/agent/application/states/task.py +++ b/agent/application/states/task.py @@ -4,7 +4,7 @@ """ from datetime import date as Date, datetime from typing import AsyncGenerator - +from reflex_base.event import EventSpec from pydantic_ai import DeferredToolRequests, DeferredToolResults, ToolApproved from pydantic_ai.messages import ( FunctionToolResultEvent, @@ -51,8 +51,10 @@ class TaskState(rx.State): # 临时用户提示词 temporary_user_prompt: str = "" - # 显示当前任务删除确认 + # 当前任务删除确认显示 is_task_delete_confirm_shown: bool = False + # 上一次任务删除确认显示日期时间 + last_task_delete_confirm_show_at: float = 0 # 当前数据库状态(私有变量) # 私有变量:reflex 约定以 _ 开头的变量为私有变量,后端不序列化,前端不可使用 @@ -155,26 +157,26 @@ class TaskState(rx.State): return f"更新于 {task.last_run_at.strftime("%Y-%m-%d %H:%M:%S")}" @rx.event - async def show_task_delete_confirm(self) -> None: + async def toggle_task_delete_confirm(self, is_shown: bool) -> None: """ - 显示当前任务删除确认 + 切换当前任务删除确认 + :param is_shown: 显示当前任务删除确认 :return: None """ - self.is_task_delete_confirm_shown = True + # 当前日期时间 + now = datetime.now().timestamp() + # 根据当前时间与上一次任务删除确认显示日期时间间隔防抖 + if now - self.last_task_delete_confirm_show_at < 0.2: + return + + self.is_task_delete_confirm_shown = is_shown + self.last_task_delete_confirm_show_at = now @rx.event - async def hide_task_delete_confirm(self) -> None: + async def delete_task(self) -> EventSpec | None: """ - 隐藏当前任务删除确认 - :return: None - """ - self.is_task_delete_confirm_shown = False - - @rx.event - async def confirm_task_delete(self) -> None: - """ - 确认当前任务删除 - :return: None + 删除当前任务 + :return: RX事件 """ from application.states import SidebarState @@ -195,6 +197,9 @@ class TaskState(rx.State): sidebar_state.nav_button = "" # 将空设置为当前任务唯一标识 self.task_id = "" + # 将任务删除确认显示设置为否 + self.is_task_delete_confirm_shown = False + return rx.toast.success("删除成功") @rx.event def set_task_id(self, task_id: str) -> None: diff --git a/agent/assets/styles.css b/agent/assets/styles.css index 872d8c7..603fc1b 100644 --- a/agent/assets/styles.css +++ b/agent/assets/styles.css @@ -2,10 +2,10 @@ --prismui-background-color-1: #ffffff; --prismui-background-color-2: #e6e6e6; --prismui-background-color-3: rgb(230, 230, 230); - --prismui-background-color-4: #babbc0; - --prismui-background-color-5: #ffffff33; - --prismui-background-color-6: #f2f2f3; - --prismui-background-color-7: #5e7ce0; + --prismui-background-color-4: #5e7ce0; + --prismui-background-color-5: #758ee0; + --prismui-background-color-6: rgb(232, 92, 92); + --prismui-background-color-7: rgb(236, 116, 116); --prismui-background-color-8: #beccfa; --prismui-background-color-9: rgb(242, 242, 242); /* @@ -26,7 +26,7 @@ --prismui-font-weight-4: 700; --prismui-color-1: #252b3a; --prismui-color-2: #575d6c; - --prismui-color-3: #babbc0; + --prismui-color-3: rgb(222, 222, 222); --prismui-color-4: rgba(37, 43, 58, 0.5); --prismui-color-5: #5e7ce0; --prismui-color-6: #f66f6a; @@ -147,7 +147,16 @@ pre, pre code { /* Button */ .rt-Button { - font-family: var(--prismui-font-family) !important; + font-family: var(--prismui-font-family); font-size: var(--prismui-font-size-2); color: var(--prismui-color-1); + height: 32px; + width: 68px; + background-color: var(--prismui-background-color-1); + border: 1px solid var(--prismui-color-3); + border-radius: var(--prismui-border-radius-3); + cursor: pointer; +} +.rt-Button:hover { + background-color: inherit; } \ No newline at end of file diff --git a/agent/database.db b/agent/database.db index 88a907c67674e70279b426e3514025c7a9290c89..5c996555df8a7475e05ba36b3c16dc66571229af 100644 GIT binary patch delta 39541 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 DR&WB} delta 866 zcmZXRTSyd97{}+#oZUI2>l{BVzyCS^^IbTdBi7DH zR^J9QyT~w1&$)~umR%h3UCT2AJkH>8eNEyB*2Xs%Qa!=o3984iHnH1H^$LSmsGh-E zMq;waae@#>Yy5MruY^!yiXiqecnY7!gZL0uaJ)Dx-WU7m;|@_P>P7m+-lO2RX^`6f zH}%`}d;Pnp2~X0t_%mL_@9--;ho9qVJV`4J<0Qx-AVyO{G+AiU<_FVJDz0Ip%W0O| zZfcwYMrteAnxNM_xSoNmxQyO0-MZz4g898p$`y)MwlHiKXPF$dr12m2L$rjVn;$`R zSbmeewk}vFd+Pmu*=1Kfvd2^4k^_pz?f2xXfq+urv2k8N4qkF0T{Gf{XWj0x$&C81d;6Vm@`k-6H!Q6CJ*E=94+wLy1K_AB*Dfy1S^ z0&W*6w*W`CRRAk ziOB~w^283;k~tqtHpYA~C7$IDkkKJdGQKxLH!!BX@C)e94M7?v8fGIm#6OE9gK9>s zwA_m8;lG)h;Q*-SO!RNI0ijE>#Cg3NOOTP5gl+)6q2)5r2f~%{