This commit is contained in:
liubiren 2026-08-04 20:13:08 +08:00
parent 41a42808d2
commit bc14d3aa16
12 changed files with 395 additions and 245 deletions

View File

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
from application.agents.chat_agent import chat_agent
__all__ = [
"chat_agent",
]

View File

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
"""
开放式对话智能体
"""
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider
instructions: str = """
# 角色
专业友好AI助手结构化解答各类问题
# 输出硬性规则
1. 全文强制标准Markdown禁止纯文本不要额外说明排版格式直接输出内容
2. 层级使用 `#/##/###`,列表用 `-` 无序列表或数字有序列表;
3. 代码块用 ```语言名``` 包裹
4. 重点内容标注 **粗体**/*斜体*
5. 思考工具日志仅输出文本适配前端折叠面板禁止输出HTML标签
6. 内容分点拆分排版整洁适配前端Markdown渲染
# 行文要求
语言通俗逻辑完整简洁无多余废话
"""
# 实例化智能体
chat_agent: Agent = Agent(
name="chat_agent",
model=OpenAIChatModel(
model_name="deepseek-v4-flash",
provider=OpenAIProvider(
base_url="https://tokenhub.tencentmaas.com/v1",
api_key="sk-D9Y1mCe8VlvNqLuSC4mAjqEwxJ2nW4C0h8a7EPn8kg9RLsHq",
),
),
instructions=instructions,
capabilities=None,
output_type=str,
retries=1,
)

View File

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
"""
生成产品需求文档智能体
"""
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider
instructions: str = """
# 角色
专业友好AI助手结构化解答各类问题
# 输出硬性规则
1. 全文强制标准Markdown禁止纯文本不要额外说明排版格式直接输出内容
2. 层级使用 `#/##/###`,列表用 `-` 无序列表或数字有序列表;
3. 代码块用 ```语言名``` 包裹
4. 重点内容标注 **粗体**/*斜体*
5. 思考工具日志仅输出文本适配前端折叠面板禁止输出HTML标签
6. 内容分点拆分排版整洁适配前端Markdown渲染
# 行文要求
语言通俗逻辑完整简洁无多余废话
"""
# 实例化智能体
chat_agent: Agent = Agent(
name="chat_agent",
model=OpenAIChatModel(
model_name="deepseek-v4-flash",
provider=OpenAIProvider(
base_url="https://tokenhub.tencentmaas.com/v1",
api_key="sk-D9Y1mCe8VlvNqLuSC4mAjqEwxJ2nW4C0h8a7EPn8kg9RLsHq",
),
),
instructions=instructions,
capabilities=None,
output_type=str,
retries=1,
)

View File

@ -7,6 +7,7 @@ from typing import Dict, List
from pydantic import BaseModel, Field
from pydantic_ai._uuid import uuid7
from enum import StrEnum
class Thought(BaseModel):
@ -24,7 +25,7 @@ class Dialog(BaseModel):
"""
id: str = Field(default_factory=lambda: str(uuid7()), description="对话唯一标识")
user_prompt: str = Field(..., description="用户提示词")
user_prompt: str = Field(default="", description="用户提示词")
thoughts: Dict[int, Thought] = Field(default_factory=dict, description="思考列表")
result_output: str = Field(default="", description="结果输出")
is_thinking: bool = Field(
@ -50,3 +51,12 @@ class Conversation(BaseModel):
...,
description="创建时间",
)
class TaskType(StrEnum):
"""
任务类型枚举
"""
CHAT = "chat"
GENERATING_PRD = "generating_prd"

View File

@ -288,7 +288,8 @@ def greeting_showing() -> rx.Component:
),
rx.hstack(
rx.box(
"智能客服",
"生成产品需求文档",
on_click=ConversationState.generate_prd,
padding="10px 16px",
background_color="var(--prismui-background-color-6)",
border_radius="var(--prismui-border-radius-9)",
@ -372,96 +373,122 @@ def dialog_showing(item: Tuple[str, Dialog]) -> rx.Component:
is_expanded = dialog.is_expanded
return rx.vstack(
rx.hstack(
rx.spacer(),
rx.vstack(
# 用户提示词
rx.text(
dialog.user_prompt,
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",
),
# 思考折叠面板
rx.vstack(
# 标题
rx.box(
rx.hstack(
# 用户提示词
rx.cond(
dialog.user_prompt,
rx.hstack(
rx.spacer(),
rx.vstack(
rx.text(
rx.cond(is_thinking, "思考中", "思考完成"),
font_size="var(--prismui-font-size-1)",
color="var(--prismui-color-3)",
dialog.user_prompt,
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.icon(
"chevron-right",
width="14px",
height="14px",
color="var(--prismui-color-3)",
transform=rx.cond(is_expanded, "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: ConversationState.toggle_collapse(dialog.id),
cursor="pointer",
),
# 思考
rx.box(
rx.box(
rx.auto_scroll(
rx.foreach(
dialog.thoughts,
thought_showing,
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_self="start",
),
style={"overflow": "hidden", "min-height": "0"},
),
display="grid",
grid_template_rows=rx.cond(is_expanded, "1fr", "0fr"),
align_items="flex-start",
gap="4px",
width="100%",
margin_top="8px",
),
rx.fragment(),
),
# 思考折叠面板
rx.cond(
dialog.thoughts,
rx.vstack(
# 标题
rx.box(
rx.hstack(
rx.text(
rx.cond(is_thinking, "思考中", "思考完成"),
font_size="var(--prismui-font-size-1)",
color="var(--prismui-color-3)",
),
rx.icon(
"chevron-right",
width="14px",
height="14px",
color="var(--prismui-color-3)",
transform=rx.cond(
is_expanded, "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: ConversationState.toggle_collapse(dialog.id),
cursor="pointer",
),
# 思考
rx.box(
rx.box(
rx.auto_scroll(
rx.foreach(
dialog.thoughts,
thought_showing,
),
width="100%",
align_self="start",
),
style={"overflow": "hidden", "min-height": "0"},
),
display="grid",
grid_template_rows=rx.cond(is_expanded, "1fr", "0fr"),
width="100%",
opacity=rx.cond(is_expanded, "1", "0"),
overflow="hidden",
transition="grid-template-rows 0.18s ease-in-out, opacity 0.18s ease-in-out",
),
margin_bottom="8px",
opacity=rx.cond(is_expanded, "1", "0"),
overflow="hidden",
transition="grid-template-rows 0.18s ease-in-out, opacity 0.18s ease-in-out",
),
# 若思考列表为空且正在运行则显示加载动效
rx.cond(
ConversationState.running_status,
rx.box(
rx.box(
class_name="loadin",
flex_shrink="0",
),
display="flex",
justify_content="center",
align_items="center",
width="32px",
height="18px",
margin_bottom="8px",
overflow="hidden",
),
rx.fragment(),
),
),
# 结果输出
@ -643,58 +670,34 @@ def conversation_creating() -> rx.Component:
"""
),
rx.spacer(),
rx.dialog.root(
rx.dialog.trigger(
rx.box(
rx.tooltip(
rx.icon(
"plus",
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",
)
),
rx.dialog.content(
rx.form(
rx.hstack(
rx.input(
name="description",
placeholder="请输入会话描述",
flex="auto",
min_width="20ch",
),
rx.button("新建"),
spacing="2",
wrap="wrap",
width="100%",
),
# 提交事件:新建对话
on_submit=ConversationState.create_conversation,
rx.box(
rx.tooltip(
rx.icon(
"plus",
on_click=ConversationState.create_conversation,
width="14px",
height="14px",
style={
"_hover": {
"color": "var(--prismui-color-5)",
}
},
),
background_color=rx.color("mauve", 1),
content="创建会话",
side="top",
side_offset=9,
background_color="var(--prismui-background-color-1)",
box_shadow="var(--prismui-box-shadow-3)",
),
open=ConversationState.is_conversation_creating,
on_open_change=ConversationState.toggle_conversation_creating,
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",

View File

@ -6,7 +6,7 @@ from asyncio import sleep
from email.header import Header
from email.mime.text import MIMEText
import re
from typing import AsyncGenerator
from typing import AsyncGenerator, Optional
from aiosmtplib import SMTP, SMTPException
import reflex as rx
@ -50,6 +50,9 @@ class AuthState(rx.State):
# 激活的导航按钮
activated_nav_button: str = "conversation"
# 数据库状态
_db_state: Optional[DatabaseState] = None
@rx.event
def reset_login_error_message(self) -> None:
"""
@ -91,6 +94,7 @@ class AuthState(rx.State):
):
self.login_error_message = "邮箱格式不正确,请重新输入"
return False
self.login_error_message = ""
return True
@ -123,10 +127,12 @@ class AuthState(rx.State):
发送验证码后台任务
:return: None
"""
if not self._db_state:
return
async with self:
# 创建验证码记录并获取验证码
database_state = await self.get_state(DatabaseState)
captcha = await database_state.create_captcha_record(email=self.email)
captcha = await self._db_state.create_captcha_record(email=self.email)
# 构建验证码邮件
message = MIMEText(
@ -166,6 +172,9 @@ class AuthState(rx.State):
self.is_captcha_sent = True
# 获取数据库状态
self._db_state = await self.get_state(DatabaseState)
# 将倒计时和发送验证码事件添加至后台任务队列
yield type(self).countdown()
yield type(self).send_captcha_background()
@ -221,19 +230,20 @@ class AuthState(rx.State):
self.login_error_message = "请先阅读并同意协议和政策"
return
if not self._db_state:
return
self.is_logging_in = True
# 获取数据库状态
database_state = await self.get_state(DatabaseState)
# 核验验证码
if not await database_state.verify_captcha(
if not await self._db_state.verify_captcha(
email=self.email, captcha=self.captcha
):
self.is_logging_in = False
self.login_error_message = "验证码错误"
return
# 创建用户记录
user_id = await database_state.create_user_record(email=self.email)
user_id = await self._db_state.create_user_record(email=self.email)
# 恢复当前用户会话状态
conversation_state = await self.get_state(ConversationState)
@ -271,7 +281,6 @@ class AuthState(rx.State):
conversation_state.conversations = {}
conversation_state.conversation_id = ""
conversation_state.is_conversation_history_shown = False
conversation_state.is_conversation_creating = False
self.user_id = ""

View File

@ -3,8 +3,8 @@
会话状态
"""
from datetime import datetime
from typing import Any, AsyncGenerator, Dict, List
from pydantic_ai import Agent, ThinkingPartDelta
from typing import Any, AsyncGenerator, Dict, List, Optional
from pydantic_ai import Agent, ThinkingPartDelta, ModelMessage
from pydantic_ai.messages import (
FunctionToolCallEvent,
FunctionToolResultEvent,
@ -18,48 +18,19 @@ from pydantic_ai.messages import (
ToolCallPart,
ToolSearchCallPart,
)
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.states.database import DatabaseState
from application.domain_models import (
Conversation,
Dialog,
Thought,
TaskType,
)
instructions: str = """
# 角色
专业友好AI助手结构化解答各类问题
# 输出硬性规则
1. 全文强制标准Markdown禁止纯文本不要额外说明排版格式直接输出内容
2. 层级使用 `#/##/###`,列表用 `-` 无序列表或数字有序列表;
3. 代码块用 ```语言名``` 包裹
4. 重点内容标注 **粗体**/*斜体*
5. 思考工具日志仅输出文本适配前端折叠面板禁止输出HTML标签
6. 内容分点拆分排版整洁适配前端Markdown渲染
# 行文要求
语言通俗逻辑完整简洁无多余废话
"""
# 实例化智能体(因无法序列化故剥离出状态管理)
agent: Agent = Agent(
model=OpenAIChatModel(
model_name="deepseek-v4-flash",
provider=OpenAIProvider(
base_url="https://tokenhub.tencentmaas.com/v1",
api_key="sk-D9Y1mCe8VlvNqLuSC4mAjqEwxJ2nW4C0h8a7EPn8kg9RLsHq",
),
),
instructions=instructions,
capabilities=None,
output_type=str,
retries=1,
)
from application.agents import chat_agent
class ConversationState(rx.State):
@ -79,12 +50,14 @@ class ConversationState(rx.State):
# 显示更多的会话唯一标识
shown_more_conversation_id: str = ""
# 会话创建状态True表示正在创建False表示未正在创建
is_conversation_creating: bool = False
# 任务类型
task_type: TaskType = TaskType.CHAT
# 用户提示词
user_prompt: str = ""
# 数据库状态(私有变量,不予序列化)
_db_state: Optional[DatabaseState] = None
async def resume(self, user_id: str) -> None:
"""
恢复当前用户会话状态
@ -95,18 +68,19 @@ class ConversationState(rx.State):
if not self.user_id:
return
database_state = await self.get_state(DatabaseState)
self._db_state = await self.get_state(DatabaseState)
# 获取会话字典
self.conversations = await database_state.get_conversations(
self.conversations = await self._db_state.get_conversations(
user_id=self.user_id
)
# 若会话字典为空则先创建会话记录再在会话字典中添加会话实例
if not self.conversations:
self.conversations.update(
await database_state.create_conversations_record(user_id=self.user_id)
await self._db_state.create_conversations_record(user_id=self.user_id)
)
# 将最后一个会话作为当前会话并更新会话唯一标识
self.conversation_id = next(reversed(self.conversations))
self.conversation_id = next(reversed(self.conversations.keys()))
@rx.event
def toggle_conversation_history_shown(self) -> None:
@ -133,9 +107,11 @@ class ConversationState(rx.State):
:param conversation_id: 需删除的会话唯一标识
:return: None
"""
if not self._db_state:
return
# 先设置会话记录为已删除再在会话字典中删除会话实例
database_state = await self.get_state(DatabaseState)
await database_state.set_conversations_record_deleted(
await self._db_state.set_conversations_record_deleted(
conversation_id=conversation_id
)
del self.conversations[conversation_id]
@ -143,12 +119,12 @@ class ConversationState(rx.State):
# 删除后,若会话字典为空则先创建会话记录再添加会话实例
if not self.conversations:
self.conversations.update(
await database_state.create_conversations_record(user_id=self.user_id)
await self._db_state.create_conversations_record(user_id=self.user_id)
)
# 删除后,若当前会话不存在则将最后一个会话作为当前会话并更新会话唯一标识
if self.conversation_id not in self.conversations:
self.conversation_id = next(reversed(self.conversations))
self.conversation_id = next(reversed(self.conversations.keys()))
@rx.event
def switch_conversation(self, conversation_id: str) -> None:
@ -160,34 +136,22 @@ class ConversationState(rx.State):
self.conversation_id = conversation_id
@rx.event
async def create_conversation(self, form_data: Dict[str, Any]) -> None:
async def create_conversation(self) -> None:
"""
创建会话
:param form_data: 表单数据
:return: None
"""
# 解析会话描述
description = form_data["description"].strip() or "新会话"
# 创建会话记录再添加会话实例
database_state = await self.get_state(DatabaseState)
if not self._db_state:
return
self.conversations.update(
await database_state.create_conversations_record(
user_id=self.user_id, description=description
await self._db_state.create_conversations_record(
user_id=self.user_id, description="新会话"
)
)
# 将最后一个会话作为当前会话并更新会话唯一标识
self.conversation_id = next(reversed(self.conversations))
# 会话创建状态设置为未正在创建
self.is_conversation_creating = False
@rx.event
def toggle_conversation_creating(self) -> None:
"""
切换会话创建状态
"""
self.is_conversation_creating = not self.is_conversation_creating
self.conversation_id = next(reversed(self.conversations.keys()))
@rx.event
def set_user_prompt(self, user_prompt: str) -> None:
@ -234,46 +198,30 @@ class ConversationState(rx.State):
return {}
return conversation.dialogs
@rx.event
async def handle_user_prompt(self) -> AsyncGenerator[None]:
async def run_stream_events(
self,
conversation: Conversation,
dialog: Dialog,
agent: Agent,
message_history: Optional[List[ModelMessage]] = None,
) -> AsyncGenerator[None]:
"""
处理用户提示词
运行并处理流式事件
:param agent: 代理
:param conversation: 当前会话
:param dialog: 当前对话
:param message_history: 消息历史列表
:return: AsyncGenerator[None]
"""
if not self.user_prompt:
if not self._db_state:
return
# 清空前端用户提示词
user_prompt = self.user_prompt
self.user_prompt = ""
# 当前会话
conversation = self.conversations[self.conversation_id]
# 将运行状态设置为正在运行
conversation.is_running = True
database_state = await self.get_state(DatabaseState)
# 创建对话记录再添加对话实例
conversation.dialogs.update(
await database_state.create_dialog_record(
conversation_id=self.conversation_id, user_prompt=user_prompt
)
)
# 强制更新会话并推送前端
self.conversations[self.conversation_id] = conversation
yield
# 将最后一个对话作为当前对话
dialog = next(reversed(conversation.dialogs.values()))
# 初始化工具调用唯一标识和片段索引映射字典
tool_call_ids: Dict[str, int] = {}
async with agent.run_stream_events(
conversation_id=self.conversation_id,
user_prompt=user_prompt,
message_history=await database_state.get_message_history(
conversation_id=self.conversation_id
),
user_prompt=dialog.user_prompt,
message_history=message_history,
) as events:
async for event in events:
match event:
@ -397,25 +345,91 @@ class ConversationState(rx.State):
# ========== 智能体运行结果事件 ==========
case AgentRunResultEvent(result=result):
# 获取数据库状态
# 补全对话记录
await database_state.complete_dialog_record(
await self._db_state.complete_dialog_record(
id=dialog.id,
thoughts=dialog.thoughts,
result_output=dialog.result_output,
)
# 创建结果记录
await database_state.create_result_record(
await self._db_state.create_result_record(
conversation_id=self.conversation_id,
dialog_id=dialog.id,
new_messages=result.new_messages(),
)
# 将当前会话的运行状态设置为运行完成
# 将正在运行设置为否
conversation.is_running = False
# 强制更新会话并推送前端
self.conversations[self.conversation_id] = conversation
yield
@rx.event
async def handle_user_prompt(self) -> AsyncGenerator[None]:
"""
处理用户提示词
:return: AsyncGenerator[None]
"""
if not self.user_prompt:
return
if not self._db_state:
return
# 当前会话
conversation = self.conversations[self.conversation_id]
# 将正在运行设置为是
conversation.is_running = True
# 创建对话记录再添加对话实例
conversation.dialogs.update(
await self._db_state.create_dialog_record(
conversation_id=self.conversation_id, user_prompt=self.user_prompt
)
)
# 将最后一个对话作为当前对话
dialog = next(reversed(conversation.dialogs.values()))
# 清空前端用户提示词
self.user_prompt = ""
# 强制更新会话并推送前端
self.conversations[self.conversation_id] = conversation
yield
match self.task_type:
# 开放式对话
case TaskType.CHAT:
async for _ in self.run_stream_events(
conversation=conversation,
dialog=dialog,
agent=chat_agent,
message_history=await self._db_state.get_message_history(
conversation_id=self.conversation_id
),
):
yield
case TaskType.GENERATING_PRD:
...
@rx.event
async def generate_prd(self) -> AsyncGenerator[None]:
"""
生成产品需求文档
"""
if not self._db_state:
return
# 当前会话
conversation = self.conversations[self.conversation_id]
# 创建对话记录再添加对话实例
conversation.dialogs.update(
await self._db_state.create_dialog_record(
conversation_id=self.conversation_id, result_output="请输入产品需求"
)
)
yield
@rx.event
def toggle_collapse(self, dialog_id: str) -> None:
"""

View File

@ -298,23 +298,29 @@ class DatabaseState(rx.State):
await session.commit()
async def create_dialog_record(
self, conversation_id: str, user_prompt: str
self, conversation_id: str, user_prompt: str = "", result_output: str = ""
) -> Dict[str, Dialog]:
"""
创建对话记录
:param conversation_id: 会话唯一标识
:param user_prompt: 用户提示词
:param result_output: 结果输出
:return: 创建对话记录的唯一标识
"""
async with rx.asession() as session:
record = DialogRecord(
conversation_id=conversation_id,
user_prompt=user_prompt,
result_output=result_output,
)
session.add(record)
await session.commit()
await session.refresh(record)
return {record.id: Dialog(id=record.id, user_prompt=user_prompt)}
return {
record.id: Dialog(
id=record.id, user_prompt=user_prompt, result_output=result_output
)
}
async def complete_dialog_record(
self,
@ -366,7 +372,7 @@ class DatabaseState(rx.State):
async def get_message_history(self, conversation_id: str) -> List[ModelMessage]:
"""
获取消息历史
获取消息历史列表
:param conversation_id: 会话唯一标识
:return: 消息历史
"""

View File

@ -60,3 +60,28 @@ pre {
pre, pre code {
font-family: Consolas, "Microsoft YaHei", sans-serif !important;
}
.loadin {
width: 6px;
aspect-ratio: 1;
border-radius: 50%;
animation: loadin 0.9s infinite ease-in-out;
}
@keyframes loadin {
0% {
box-shadow: 12px 0 var(--prismui-color-5), -12px 0 var(--prismui-color-9);
background: var(--prismui-color-5);
}
33% {
box-shadow: 12px 0 var(--prismui-color-5), -12px 0 var(--prismui-color-9);
background: var(--prismui-color-9);
}
66% {
box-shadow: 12px 0 var(--prismui-color-9), -12px 0 var(--prismui-color-5);
background: var(--prismui-color-9);
}
100% {
box-shadow: 12px 0 var(--prismui-color-9), -12px 0 var(--prismui-color-5);
background: var(--prismui-color-5);
}
}

Binary file not shown.

View File

@ -5,7 +5,6 @@
"": {
"name": "reflex",
"dependencies": {
"@radix-ui/react-form": "0.1.8",
"@radix-ui/themes": "3.3.0",
"@react-router/node": "7.15.0",
"isbot": "5.1.40",

View File

@ -6,7 +6,6 @@
"export": "react-router build"
},
"dependencies": {
"@radix-ui/react-form": "0.1.8",
"@radix-ui/themes": "3.3.0",
"@react-router/node": "7.15.0",
"isbot": "5.1.40",