From 3f8917c803ceb70f73fc4b61cf6d425a3ff18893 Mon Sep 17 00:00:00 2001 From: liubiren Date: Thu, 15 Oct 2026 17:53:14 +0800 Subject: [PATCH] 1 --- agent/application/pages/task.py | 330 ++++++++++++++------------- agent/application/pages/workbench.py | 129 +++++------ agent/application/states/__init__.py | 6 +- agent/application/states/auth.py | 16 -- agent/application/states/database.py | 9 +- agent/application/states/models.py | 2 +- agent/application/states/sidebar.py | 42 ++++ agent/application/states/task.py | 138 ++++++----- agent/assets/conversation.svg | 22 -- agent/assets/spinner.svg | 1 + agent/assets/styles.css | 8 +- agent/database.db | Bin 487424 -> 544768 bytes 12 files changed, 364 insertions(+), 339 deletions(-) create mode 100644 agent/application/states/sidebar.py delete mode 100644 agent/assets/conversation.svg create mode 100644 agent/assets/spinner.svg diff --git a/agent/application/pages/task.py b/agent/application/pages/task.py index b7476c7..fdcac76 100644 --- a/agent/application/pages/task.py +++ b/agent/application/pages/task.py @@ -114,7 +114,7 @@ def use_guidance() -> rx.Component: def user_prompt(item: MessageHistoryItem) -> rx.Component: """ 用户提示词 - :return: Component + :return: rx.Component """ return rx.hstack( rx.spacer(), @@ -491,14 +491,80 @@ def message_history() -> rx.Component: 消息历史 :return: Component """ - return rx.box( + return rx.vstack( # 若消息历史为空则显示使用指引,否则渲染消息历史项 rx.cond( TaskState.message_history.length() == 0, use_guidance(), rx.vstack( + rx.hstack( + rx.hstack( + # 渲染任务描述 + rx.text( + TaskState.task_description, + line_height="32px", + font_size="var(--prismui-font-size-3)", + font_weight="var(--prismui-font-weight-3)", + ), + rx.icon( + "pencil-line", + width="14px", + height="14px", + object_fit="contain", + color="var(--prismui-color-2)", + ), + align_items="center", + gap="4px", + height="56px", + ), + rx.hstack( + rx.text( + TaskState.task_last_run_at, + line_height="20px", + font_size="var(--prismui-font-size-1)", + color="var(--prismui-color-3)", + ), + rx.box( + rx.icon( + "panel-right", + 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)", + ), + 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)", + ), + align_items="center", + gap="4px", + ), + align_items="center", + justify_content="space-between", + width="100%", + padding="0 12px", + ), rx.auto_scroll( rx.vstack( + # 渲染消息历史项 rx.foreach(TaskState.message_history, message_history_item), # 若等待流式输出则显示加载动效 rx.cond( @@ -533,188 +599,103 @@ def message_history() -> rx.Component: 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%", + flex="1", min_height="0", ) -def conversation_creating() -> rx.Component: +def user_prompt_sending_box() -> 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%", - ), + rx.text_area( + # 值绑定:用户提示词 + value=TaskState.user_prompt, + # 输入事件:设置用户提示词 + on_change=TaskState.set_user_prompt, + placeholder="请输入您的问题,按Enter换行", + vertical_align="middle", width="100%", - padding="12px 16px", + min_height="64px", + max_height="224px", + padding="4px 0", background_color="var(--prismui-background-color-1)", - border_radius="var(--prismui-border-radius-6)", - box_shadow="var(--prismui-box-shadow-9)", + 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.text( - "内容由大模型生成,无法确保准确性和完整性,仅供参考", + 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%", - 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", + padding="12px 16px", + background_color="var(--prismui-background-color-1)", + border_radius="var(--prismui-border-radius-6)", + box_shadow="var(--prismui-box-shadow-9)", ) @@ -735,7 +716,28 @@ def task_page() -> rx.Component: width="100%", overflow_y="auto", ), - user_prompt_box(), + rx.vstack( + # 渲染用户提示词发送框 + user_prompt_sending_box(), + # 渲染免责声明 + 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", + ), width="100%", height="100%", background="var(--prismui-background-2)", diff --git a/agent/application/pages/workbench.py b/agent/application/pages/workbench.py index af03297..2e42060 100644 --- a/agent/application/pages/workbench.py +++ b/agent/application/pages/workbench.py @@ -5,7 +5,7 @@ import reflex as rx from application.pages import task_page, knowledge_base_page, login_page -from application.states import AuthState, TaskState +from application.states import AuthState, TaskState, SidebarState from application.states.models import TaskHistoryItem @@ -29,8 +29,8 @@ def nav_button( :return : Component """ - # 导航按钮选中状态 - is_selected = nav_button == AuthState.selected_nav_button + # 选中状态 + is_selected = nav_button == SidebarState.nav_button return rx.hstack( # 导航按钮的图标 @@ -48,7 +48,6 @@ def nav_button( font_weight=rx.cond( is_selected, "var(--prismui-font-weight-3)", None ), # 通过加粗字体区分是否选中 - color="var(--prismui-color-1)", ), align_items="center", gap="8px", @@ -59,8 +58,8 @@ def nav_button( ), # 通过背景颜色区分是否选中 border_radius="var(--prismui-border-radius-3)", cursor="pointer", - # 点击事件:设置选中导航按钮 - on_click=AuthState.set_selected_nav_button(nav_button), + # 点击事件:将指定导航按钮设置为选中的导航按钮 + on_click=SidebarState.set_nav_button(nav_button), ) @@ -73,59 +72,55 @@ def task_history_item( :return: Component """ - # 悬停状态 - is_hovered = item.id == TaskState.hovered_task_id + # 选中状态 + is_selected = item.id == TaskState.task_id - # 若悬停状态为是则渲染任务描述和更多图标,否则:若运行状态为是则渲染正在运行图标,否则不渲染;同时渲染任务描述和上一次运行日期时间 - return rx.list.item( + return rx.hstack( rx.cond( - is_hovered, - rx.hstack( - rx.text( - item.description, - line_height="20px", - color="var(--prismui-color-2)", - text_overflow="ellipsis", - white_space="nowrap", - ), - rx.icon("ellipsis"), - width="100%", - padding="5px 12px", - ), - rx.hstack( - rx.cond( - item.is_running, - rx.image("/is_running.svg"), - rx.image("/is_running.svg"), - ), - # 渲染任务描述 - rx.text( - item.description, - flex=1, - min_width="0", # 自适应宽度 - line_height="20px", - color="var(--prismui-color-2)", - white_space="nowrap", # 禁止换行 - overflow="hidden", - mask_image="linear-gradient(to right, white calc(100% - 24px), transparent 100%)", # 添加蒙版 - ), - # 渲染上一次运行日期时间 - rx.text( - item.last_run_at, - line_height="20px", - font_size="calc(var(--prismui-font-size-1) - 1px)", - color="var(--prismui-color-2)", - flex_shrink=0, # 禁止缩放 - ), - gap="8px", - width="100%", - padding="5px 12px", + item.is_running, + rx.image( + "/spinner.svg", + width="16px", + height="16px", + object_fit="contain", + animation="spin 1.6s linear infinite", ), ), + # 渲染任务描述 + rx.text( + item.description, + font_weight=rx.cond(is_selected, "var(--prismui-font-weight-3)", None), + flex=1, + min_width="0", # 自适应宽度 + line_height="20px", + white_space="nowrap", # 禁止换行 + overflow="hidden", + mask_image="linear-gradient(to right, white calc(100% - 24px), transparent 100%)", # 添加蒙版 + ), + # 渲染上一次运行日期时间 + rx.text( + item.last_run_at, + line_height="20px", + font_size="var(--prismui-font-size-1)", + color="var(--prismui-color-3)", + flex_shrink=0, # 禁止缩放 + ), + align_items="center", + gap="8px", width="100%", + padding="5px 12px", + background_color=rx.cond( + is_selected, "var(--prismui-background-color-2)", "transparent" + ), # 通过背景颜色区分是否选中 + border_radius="var(--prismui-border-radius-3)", cursor="pointer", # 点击事件:将指定任务唯一标识设置为当前任务唯一标识 - on_click=lambda: TaskState.set_task_id(item.id), + on_click=SidebarState.set_task_id(item.id), + style={ + "_hover": { + "background": "var(--prismui-background-color-2)", + }, # 鼠标悬停背景颜色 + }, ) @@ -135,23 +130,20 @@ def task_history() -> rx.Component: :return: rx.Component """ return rx.auto_scroll( - rx.list( - rx.foreach( - TaskState.task_history, - task_history_item, - ), - display="flex", - flex_direction="column", - gap="2px", - width="100%", - style={ - "&::-webkit-scrollbar": { - "display": "none", - }, - }, - overflow="auto", + rx.foreach( + TaskState.task_history, + task_history_item, ), + display="flex", + flex_direction="column", + gap="2px", width="100%", + overflow="auto", + style={ + "&::-webkit-scrollbar": { + "display": "none", + }, + }, ) @@ -170,7 +162,6 @@ def settings_button(): stroke_width=1.5, width="20px", height="20px", - color="var(--prismui-color-1)", ), display="flex", flex_flow="column", @@ -333,7 +324,7 @@ def workbench() -> rx.Component: sidebar(), # 根据选中的导航按钮渲染对应的页面 rx.match( - AuthState.selected_nav_button, + SidebarState.nav_button, # 知识库页面 ("knowledge_base", knowledge_base_page()), # 任务页面 diff --git a/agent/application/states/__init__.py b/agent/application/states/__init__.py index 7f2c655..785d232 100644 --- a/agent/application/states/__init__.py +++ b/agent/application/states/__init__.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- from application.states.auth import AuthState -from application.states.task import TaskState from application.states.database import DatabaseState +from application.states.sidebar import SidebarState +from application.states.task import TaskState __all__ = [ "AuthState", - "TaskState", "DatabaseState", + "SidebarState", + "TaskState", ] diff --git a/agent/application/states/auth.py b/agent/application/states/auth.py index d373f76..b36ba17 100644 --- a/agent/application/states/auth.py +++ b/agent/application/states/auth.py @@ -44,8 +44,6 @@ class AuthState(rx.State): # 登录中状态 is_logging_in: bool = False - # 选中的导航按钮,默认为创建任务 - selected_nav_button: str = "create_task" # 设置悬停卡片打开状态 is_settings_hover_card_open: bool = False @@ -278,20 +276,6 @@ class AuthState(rx.State): """ self.is_settings_hover_card_open = is_settings_hover_card_open - @rx.event - async def set_selected_nav_button(self, nav_button: str): - """ - 设置选中导航按钮 - """ - self.selected_nav_button = nav_button - - # 若选中创建任务导航按钮则将空设置为当前任务唯一标识 - if nav_button == "create_task": - # 获取任务状态 - task_state = await self.get_state(TaskState) - # 将空设置为当前任务唯一标识 - task_state.set_task_id("") - @rx.event async def logout(self) -> None: """ diff --git a/agent/application/states/database.py b/agent/application/states/database.py index f483134..ab2f741 100644 --- a/agent/application/states/database.py +++ b/agent/application/states/database.py @@ -17,9 +17,9 @@ from sqlmodel import Field, SQLModel, select, update from application.states.models import ( Approval, - Task, Message, MessageType, + Task, Workflow, approval_dump_json, approval_validate_json, @@ -208,10 +208,13 @@ class DatabaseState(rx.State): await session.refresh(record) return record.id - async def create_task_record(self, user_id: str) -> dict[str, Task]: + async def create_task_record( + self, user_id: str, user_prompt: str = "" + ) -> dict[str, Task]: """ 创建任务记录 :param user_id: 用户唯一标识 + :param user_prompt: 用户提示词,默认为空 :return: 任务实例 """ async with rx.asession() as session: @@ -223,6 +226,7 @@ class DatabaseState(rx.State): record.id: Task( id=record.id, description=record.description, + user_prompt=user_prompt, usage=usage_validate_json( record.usage ), # 将 record.usage 由 JSON 字符串反序列化为 RunUsage @@ -377,7 +381,6 @@ class DatabaseState(rx.State): ) await session.commit() - async def update_message_record( self, message_id: str, diff --git a/agent/application/states/models.py b/agent/application/states/models.py index 5c6cb31..83ab0e9 100644 --- a/agent/application/states/models.py +++ b/agent/application/states/models.py @@ -85,10 +85,10 @@ class Task(BaseModel): id: str = Field(..., description="任务唯一标识") description: str = Field(..., description="任务描述") last_run_at: datetime = Field(..., description="上一次运行日期时间") + user_prompt: str = Field(default="", description="用户提示词") usage: RunUsage = Field(..., description="任务使用量") messages: dict[str, Message] = Field(default_factory=dict, description="消息字典") workflow: Workflow = Field(default_factory=Workflow, description="工作,默认为对话") - user_prompt: str = Field(default="", description="用户提示词") is_running: bool = Field( default=False, description="正在运行,True 表示正在运行, False 表示运行结束" ) diff --git a/agent/application/states/sidebar.py b/agent/application/states/sidebar.py new file mode 100644 index 0000000..131fc23 --- /dev/null +++ b/agent/application/states/sidebar.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +""" +侧边栏状态 +""" +import reflex as rx + +from application.states.task import TaskState + + +class SidebarState(rx.State): + """ + 侧边栏状态 + """ + + # 选中的导航按钮,默认为创建任务 + nav_button: str = "create_task" + + @rx.event + async def set_nav_button(self, nav_button: str): + """ + 将指定导航按钮设置为选中的导航按钮 + """ + self.nav_button = nav_button + + # 若选中创建任务导航按钮则将空设置为当前任务唯一标识 + if nav_button == "create_task": + # 获取任务状态 + task_state = await self.get_state(TaskState) + # 将空设置为当前任务唯一标识 + task_state.set_task_id("") + + @rx.event + async def set_task_id(self, task_id: str): + """ + 将指定任务唯一标识设置为当前选中任务 + """ + self.nav_button = "" + + # 获取任务状态 + task_state = await self.get_state(TaskState) + # 将指定任务唯一标识设置为当前任务唯一标识 + task_state.set_task_id(task_id) diff --git a/agent/application/states/task.py b/agent/application/states/task.py index ee8db0b..74716ac 100644 --- a/agent/application/states/task.py +++ b/agent/application/states/task.py @@ -48,8 +48,8 @@ class TaskState(rx.State): # 当前任务唯一标识 task_id: str = "" - # 鼠标悬停的任务唯一标识 - hovered_task_id: str = "" + # 临时用户提示词 + temporary_user_prompt: str = "" # 当前数据库状态(私有变量) # 私有变量:reflex 约定以 _ 开头的变量为私有变量,后端不序列化,前端不可使用 @@ -71,7 +71,7 @@ class TaskState(rx.State): :param user_id: 用户唯一标识 :return: None """ - self.user_id = user_id.strip() + self.user_id = user_id if not self.user_id: return @@ -79,24 +79,19 @@ class TaskState(rx.State): db_state = await self.get_db_state() # 查询当前用户的任务字典 self.tasks = await db_state.read_tasks(self.user_id) - # 若当前用户的任务字典为空则先创建任务记录再添加任务 - if not self.tasks: - self.tasks.update(await db_state.create_task_record(self.user_id)) - # 将最后一个任务的唯一标识作为当前任务唯一标识 - self.task_id = next(reversed(self.tasks.keys())) - # 当前任务 - task = self.tasks[self.task_id] - # 最后一个消息 - message = next(iter(reversed(task.messages.values())), None) - # 若最后一个消息包含审批且审批结果为 None 则任务正在运行设置为是、消息正在运行设置为是 - if ( - message is not None - and isinstance(approval := message.approval, Approval) - and approval.result is None - ): - task.is_running = True - message.is_running = True + # 遍历所有任务若最后一条消息未审批则将该任务和该消息正在运行设置为是 + for task in self.tasks.values(): + # 最后一个消息 + message = next(iter(reversed(task.messages.values())), None) + # 若最后一个消息包含审批且审批结果为 None 则任务正在运行设置为是、消息正在运行设置为是 + if ( + message + and isinstance(approval := message.approval, Approval) + and approval.result is None + ): + task.is_running = True + message.is_running = True @rx.var def task_history(self) -> list[TaskHistoryItem]: @@ -105,16 +100,16 @@ class TaskState(rx.State): :return: 任务历史项列表 """ - def formatter(datetime: datetime) -> str: + def formatter(dt: datetime) -> str: """ 格式器 - :param datetime: 日期时间 + :param dt: 日期时间 :return: 格式化后日期时间字符串 """ # 时间间隔 - delta = (datetime.now().date() - datetime.date()).total_seconds() + delta = (datetime.now() - dt).total_seconds() if delta < 60: - return f"刚刚" + return "刚刚" elif delta < 60 * 60: return f"{round(delta / 60)}分钟前" elif delta < 60 * 60 * 24: @@ -132,13 +127,29 @@ class TaskState(rx.State): for task in reversed(self.tasks.values()) ] - @rx.event - def set_task_history_item_popover_shown(self, item_id: str, is_shown: bool) -> None: + @rx.var + def task_description(self) -> str: """ - 设置会话历史项悬停气泡显示 / 隐藏 - :return: None + 获取当前任务的描述 + :return: 当前任务的描述 """ - self.popover_shown_conversation_history_item_id = item_id if is_shown else "" + # 当前任务 + task = self.tasks.get(self.task_id) + if not task: + return "" + return task.description + + @rx.var + def task_last_run_at(self) -> str: + """ + 获取当前任务的上一次运行日期时间 + :return: 当前任务的上一次运行日期时间 + """ + # 当前任务 + task = self.tasks.get(self.task_id) + if not task: + return "" + return f"更新于 {task.last_run_at.strftime("%Y-%m-%d %H:%M:%S")}" @rx.event async def delete_task(self, task_id: str) -> None: @@ -192,19 +203,6 @@ class TaskState(rx.State): ) return message_history - @rx.event - async def create_task(self) -> None: - """ - 创建任务 - :return: None - """ - # 获取当前数据库状态 - db_state = await self.get_db_state() - # 创建任务记录再添加任务 - self.tasks.update(await db_state.create_task_record(self.user_id)) - # 将最后一个任务的唯一标识作为当前任务唯一标识 - self.task_id = next(reversed(self.tasks.keys())) - @rx.var def user_prompt(self) -> str: """ @@ -213,9 +211,11 @@ class TaskState(rx.State): """ # 当前任务 task = self.tasks.get(self.task_id) - if not task: - return "" - return task.user_prompt + # 若当前任务存在则返回当前任务的用户提示词,否则返回临时用户提示词 + if task: + return task.user_prompt + else: + return self.temporary_user_prompt @rx.event def set_user_prompt(self, user_prompt: str) -> None: @@ -226,13 +226,11 @@ class TaskState(rx.State): """ # 当前任务 task = self.tasks.get(self.task_id) - if not task: - return - - if not user_prompt.strip(): - return - - task.user_prompt = user_prompt + # 若当前任务存在则将用户提示词设置为当前任务的用户提示词,否则将用户提示词设置为临时用户提示词 + if task: + task.user_prompt = user_prompt + else: + self.temporary_user_prompt = user_prompt @rx.var def is_user_prompt_sending_button_disabled(self) -> bool: @@ -242,9 +240,11 @@ class TaskState(rx.State): """ # 当前任务 task = self.tasks.get(self.task_id) - if not task: - return True - return task.is_running or not task.user_prompt + # 若当前任务存在则判断该任务是否正在运行或用户提示词非空 + if task: + return task.is_running or not task.user_prompt + else: + return not self.temporary_user_prompt @rx.event async def run( @@ -258,14 +258,30 @@ class TaskState(rx.State): :param message_id: 消息唯一标识 :return: AsyncGenerator """ - # 当前任务 - task = self.tasks.get(self.task_id) - if not task: - return + from application.states import SidebarState # 获取当前数据库状态 db_state = await self.get_db_state() + # 若当前任务唯一标识为空则创建任务 + if not self.task_id: + # 获取当前侧边栏状态 + sidebar_state = await self.get_state(SidebarState) + # 将空设置为导航按钮 + sidebar_state.nav_button = "" + + # 创建任务记录再添加任务 + self.tasks.update( + await db_state.create_task_record( + self.user_id, self.temporary_user_prompt + ) + ) + # 将最后一个任务的唯一标识作为当前任务唯一标识 + self.task_id = next(reversed(self.tasks.keys())) + + # 当前任务 + task = self.tasks[self.task_id] + user_prompt = None # 用户提示词 deferred_tool_results = None # 延迟工具结果 # 若为恢复运行 @@ -298,11 +314,11 @@ class TaskState(rx.State): else: # 若用户提示词为空则直接返回 - if not (user_prompt := task.user_prompt): + if not (user_prompt := task.user_prompt.strip()): # 去除用户提示词首尾空格 return # 清空用户提示词 - task.user_prompt = "" + self.temporary_user_prompt = task.user_prompt = "" # 将正在运行设置为是 task.is_running = True # 将等待流式输出设置为是 diff --git a/agent/assets/conversation.svg b/agent/assets/conversation.svg deleted file mode 100644 index 1277a20..0000000 --- a/agent/assets/conversation.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/agent/assets/spinner.svg b/agent/assets/spinner.svg new file mode 100644 index 0000000..3d1ed36 --- /dev/null +++ b/agent/assets/spinner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/agent/assets/styles.css b/agent/assets/styles.css index d0d417d..a679a96 100644 --- a/agent/assets/styles.css +++ b/agent/assets/styles.css @@ -26,7 +26,7 @@ --prismui-color-1: #252b3a; --prismui-color-2: #575d6c; --prismui-color-3: #babbc0; - --prismui-color-4: #060a263d; + --prismui-color-4: rgba(37, 43, 58, 0.5); --prismui-color-5: #5e7ce0; --prismui-color-6: #f66f6a; --prismui-color-7: #f2f2f3; @@ -107,7 +107,13 @@ pre, pre code { color: transparent !important; animation: shimmer 2.7s infinite linear; } + @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } } \ No newline at end of file diff --git a/agent/database.db b/agent/database.db index 411ad0b9eb0be1f5f49b20ef91cb66bf2bf75056..cda96db68cb0631052d656a81c967c9753f19c84 100644 GIT binary patch delta 38074 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 px`Td?BTx&UpkW#{2y7sp6mbs delta 2191 zcmah~eQXrR72l7!+r7QLTL)sGm=EI~$TrMrclK-c0t6y8QJNIRAx0H8nEk|tD0T=Y z6~=X_fuyYfx1)5SQcJ2@75^ARj&N3#C^R5SB&Y%;&`1p>v`tf*fJ>URQXA;(88;IB zBO2}AXz%yl`@Q#mZ|1p|<~?_1_E25wJi{>0{;2+b_WY4`FLlq%fo4YF(!4O1r9Y`f zxgHQmqg=oRe23`3|o!S=;crkYf4`|SAdAEj%_!Od@#Z$0}%j;#Fd`UOV99O06x z7b!(#8h10duwmjVbs`T;=dCL94M4{lCWp3sjYF6m+0;^$N7!#B;d8kpxEEp@`UZ-i zZD~HlKC&PVf>6~R-8FPPndP@>mQIN#O2gZhsv}(G+QsM-+PqpaP)S8aWUB~k<&kZ_VslheCnie0o;K}9 zCqL1&ntq|dE}67xRuN&O+LNW+xk9+pn#z6D*|4WNTUD|C=* zsDUpw@~tcdeq|ZtNgxV!GHvhmX+r*X!f}+coZxs{c}IDNa{O~%&hmBaow~_n=;w_@ zKX7h}pj>8@|0w@dZYrNBA1QxSE>rMng0Z;YC#jk^yT!?^bfe7ay{IG z>2+xO{Z9xvSh`YU}Qc$klmcY%TU^$lPg$c@atIR7DENNtlF zX~AAkwtaNFG%oywFU;9Wx3$$My6QvCqGz+M`8qwIBNLjHZ9e!H8;@)RP37Rtm27;b z73>H3F=m(?KG85R$ZXuIq&niZH!-eWp?s>8+Q!0=e~@=3PBt cS5N9#Pa2G;S$Q8L@1xz=BUko4DRlDx1sR%l_5c6?