This commit is contained in:
liubiren 2026-07-07 20:27:34 +08:00
parent a6b2bcf81c
commit 51fd0a18ea
21 changed files with 872 additions and 483 deletions

149
智能体/alembic.ini Normal file
View File

@ -0,0 +1,149 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts.
# this is typically a path given in POSIX (e.g. forward slashes)
# format, relative to the token %(here)s which refers to the location of this
# ini file
script_location = %(here)s/alembic
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory. for multiple paths, the path separator
# is defined by "path_separator" below.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the tzdata library which can be installed by adding
# `alembic[tz]` to the pip requirements.
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to <script_location>/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "path_separator"
# below.
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
# path_separator; This indicates what character is used to split lists of file
# paths, including version_locations and prepend_sys_path within configparser
# files such as alembic.ini.
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
# to provide os-dependent path splitting.
#
# Note that in order to support legacy alembic.ini files, this default does NOT
# take place if path_separator is not present in alembic.ini. If this
# option is omitted entirely, fallback logic is as follows:
#
# 1. Parsing of the version_locations option falls back to using the legacy
# "version_path_separator" key, which if absent then falls back to the legacy
# behavior of splitting on spaces and/or commas.
# 2. Parsing of the prepend_sys_path option falls back to the legacy
# behavior of splitting on spaces, commas, or colons.
#
# Valid values for path_separator are:
#
# path_separator = :
# path_separator = ;
# path_separator = space
# path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
path_separator = os
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# database URL. This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py
# file.
sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
# hooks = ruff
# ruff.type = module
# ruff.module = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Alternatively, use the exec runner to execute a binary found on your PATH
# hooks = ruff
# ruff.type = exec
# ruff.executable = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Logging configuration. This is also consumed by the user-maintained
# env.py script only.
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

1
智能体/alembic/README Normal file
View File

@ -0,0 +1 @@
Generic single-database configuration.

78
智能体/alembic/env.py Normal file
View File

@ -0,0 +1,78 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = None
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@ -0,0 +1,28 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
"""Upgrade schema."""
${upgrades if upgrades else "pass"}
def downgrade() -> None:
"""Downgrade schema."""
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,46 @@
"""empty message
Revision ID: a7e21164c990
Revises:
Create Date: 2026-07-07 15:00:23.572644
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import sqlmodel
# revision identifiers, used by Alembic.
revision: str = 'a7e21164c990'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('messagehistory',
sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('conversation_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('run_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('new_message', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('messagehistory', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_messagehistory_conversation_id'), ['conversation_id'], unique=False)
batch_op.create_index(batch_op.f('ix_messagehistory_run_id'), ['run_id'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('messagehistory', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_messagehistory_run_id'))
batch_op.drop_index(batch_op.f('ix_messagehistory_conversation_id'))
op.drop_table('messagehistory')
# ### end Alembic commands ###

View File

@ -3,8 +3,8 @@
应用主入口
"""
import reflex as rx
from application.pages.index import index_page
from application.pages.index import render_index_page
app = rx.App() # 此处变量名需使用 app ,具体原因目前尚不清楚
# 注册首页路由
app.add_page(component=index_page, route="/")
app.add_page(component=render_index_page, route="/")

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@ -4,39 +4,114 @@
"""
import reflex as rx
from application.models import Run, Reasoning
from application.models import Run, Reasoning, Conversation
from application.state.conversation import ConversationState
from application.state.create_conversation_modal import CreateConversationModalState
def render_user_prompt(run: Run) -> rx.Component:
def render_delete_conversation_component(
conversation_id: str,
):
"""
渲染用户提示词
:param run: 运行实例
:return: Component
渲染删除对话组件
:param conversation_id: 对话唯一标识
:return: 删除对话组件
"""
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", # 圆角
return rx.popover.root(
rx.popover.trigger(
rx.button(
rx.icon("ellipsis", size=16),
variant="ghost",
padding="4px",
height="auto",
)
),
text_align="right",
width="100%",
margin_bottom="8px",
rx.popover.content(
rx.popover.close(
rx.button(
"删除",
color_scheme="red",
variant="ghost",
width="100%",
justify_content="center",
on_click=lambda: ConversationState.delete_conversation(
conversation_id
),
)
),
padding="4px",
),
open_delay=0,
)
def render_reasoning(reasoning_id: int, reasoning: Reasoning):
def render_conversation_item_component(
conversation_id: str, conversation: Conversation
):
"""
渲染推理
:param reasoning_id: 对话唯一标识
渲染对话项组件
"""
# 对话描述
description = conversation.description
# 若当前对话唯一标识非指定对话唯一标识则正常渲染,否则高亮渲染
return rx.cond(
ConversationState.conversation_id != conversation_id,
# 正常渲染
rx.button(
rx.hstack(
rx.text(
description,
flex=1,
overflow="hidden",
text_overflow="ellipsis",
white_space="nowrap",
text_align="left",
),
render_delete_conversation_component(conversation_id),
align_items="center",
width="100%",
spacing="2",
),
width="100%",
justify_content="flex-start",
padding_x="12px",
padding_y="10px",
variant="surface",
color_scheme="blue",
on_click=lambda: ConversationState.switch_conversation(conversation_id),
),
# 高亮渲染
rx.button(
rx.hstack(
rx.text(
description,
flex=1,
overflow="hidden",
text_overflow="ellipsis",
white_space="nowrap",
text_align="left",
),
render_delete_conversation_component(conversation_id),
align_items="center",
width="100%",
spacing="2",
),
width="100%",
justify_content="flex-start",
padding_x="12px",
padding_y="10px",
variant="soft",
color_scheme="blue",
),
)
def render_reasoning_item_component(reasoning_id: int, reasoning: Reasoning):
"""
渲染推理项组件
:param reasoning_id: 推理唯一标识
:param reasoning: 推理实例
:return: Component
"""
@ -60,26 +135,45 @@ def render_reasoning(reasoning_id: int, reasoning: Reasoning):
)
def render_reasoning_panel(run_id: str, run: Run) -> rx.Component:
def render_run_component(run_id: str, run: Run) -> rx.Component:
"""
渲染推理面板
:param run_id: 运行唯一标识
:param run: 运行实例
渲染运行组件
:param run: 运行包含用户提示词推理字典和回复正文
:return: Component
"""
# 推理字典
reasonings = run.reasonings
# 推理状态
is_reasoning = run.is_reasoning
# 推理面板展开状态
is_reasoning_panel_open = run.is_reasoning_panel_open
return rx.cond(
not reasonings,
rx.fragment(),
return rx.box(
# 渲染用户提示词组件
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",
),
# 渲染推理面板组件
rx.box(
# 标题栏
rx.hstack(
# 若推理状态为正在推理则渲染正在推理,否则渲染推理完成
rx.cond(
run.is_reasoning,
is_reasoning,
rx.badge("正在推理"),
rx.badge("推理完成"),
),
@ -96,14 +190,12 @@ def render_reasoning_panel(run_id: str, run: Run) -> rx.Component:
run_id
), # 点击事件:展开/折叠指定运行唯一标识的推理面板
),
# 若推理面板展开则遍历渲染推理,否则不渲染
# 若推理面板展开则遍历渲染推理项组件,否则不渲染
rx.cond(
is_reasoning_panel_open,
rx.foreach(
reasonings,
lambda reasoning_id, reasoning: render_reasoning(
reasoning_id=reasoning_id, reasoning=reasoning
),
lambda i, _: render_reasoning_item_component(i[0], i[1]),
),
rx.fragment(),
),
@ -111,103 +203,72 @@ def render_reasoning_panel(run_id: str, run: Run) -> rx.Component:
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", # 圆角
# 渲染回复正文组件,若正在推理则不渲染,否则渲染回复正文组件
rx.cond(
is_reasoning,
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:
def render_conversation_component() -> 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(),
ConversationState.is_runs_empty,
# 渲染空运行字典组件
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",
),
# 遍历渲染运行组件
rx.foreach(
runs,
lambda run_id, run: render_run(run_id=run_id, run=run),
lambda i, _: render_run_component(i[0], i[1]),
),
),
flex="1",
@ -216,62 +277,90 @@ def render_conversation() -> rx.Component:
)
def render_custom_input_box() -> rx.Component:
def render_input_component() -> 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.hstack(
rx.spacer(), # 占位符
rx.dialog.root(
# 触发事件:点击创建对话图标,支持鼠标悬停提示
rx.dialog.trigger(
rx.box(rx.tooltip(rx.icon("circle-plus"), content="创建对话"))
),
# 内容层
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,
),
background_color=rx.color("mauve", 1),
),
open=CreateConversationModalState.is_open,
on_open_change=CreateConversationModalState.toggle,
),
width="100%",
),
# 渲染自定义输入组件
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", # 子元素间距
on_submit=ConversationState.run,
reset_on_submit=True,
),
# 渲染底部文案
rx.text(
"内容由大模型生成,无法确保准确性和完整性,仅供参考",
text_align="center",
@ -292,4 +381,4 @@ def render_input_box() -> rx.Component:
background_color=rx.color("mauve", 2),
align="stretch",
width="100%",
) # rx.center 等价 rx.box(display="flex", align_items="center", justify_content="center")
)

View File

@ -5,104 +5,75 @@
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
# 初始化侧边栏图标导航按钮字典
sidebar_icon_nav_buttons = {
"conversation": {"icon": "message-circle-more", "text": "对话"},
"knowledge_base": {"icon": "book-open", "text": "知识库"},
}
def render_conversation(chat_id: str, chat_description: str) -> rx.Component:
def render_brand_logo(width: str = "24px", height: str = "24px") -> rx.Component:
"""
渲染对话
:param chat_id: 聊天唯一标识
:param description: 聊天描述
:return: Component
渲染品牌 Logo
:param width: 宽度默认为 24px
:param height: 高度默认为 24px
"""
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",
return rx.image(
src="/assets/logo.png",
width=width,
height=height,
object_fit="contain", # 等比缩放
)
def render_sidebar_icon_nav_button(
sidebar_icon_nav_button: str,
) -> rx.Component:
"""
渲染侧边栏图标导航按钮
:param sidebar_icon_nav_button: 侧边栏图标导航按钮
"""
# 侧边栏图标导航按钮图标
icon = sidebar_icon_nav_buttons[sidebar_icon_nav_button]["icon"]
# 侧边栏图标导航按钮标签
text = sidebar_icon_nav_buttons[sidebar_icon_nav_button]["text"]
# 若选中侧边栏图标导航按钮图标非指定图标导航按钮图标则正常渲染,否则高亮渲染
return rx.cond(
FrameState.sidebar_icon_nav_button != sidebar_icon_nav_button,
# 正常渲染
rx.button(
rx.vstack(
rx.icon(icon, size=18),
rx.text(text, font_size="10px"),
spacing="2",
align_items="center",
),
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,
justify_content="center",
variant="surface",
color_scheme="blue",
on_click=lambda: FrameState.switch_active_sidebar_tab(
sidebar_icon_nav_button
),
background_color=rx.color("mauve", 1),
), # 模态窗内容容器
open=CreateChatState.is_open,
on_open_change=CreateChatState.toggle,
padding_y="10px",
),
# 高亮渲染
rx.button(
rx.vstack(
rx.icon(icon, size=18),
rx.text(text, size="8"),
spacing="2",
align_items="center",
),
width="100%",
justify_content="center",
variant="soft",
color_scheme="blue",
padding_y="10px",
),
)
@ -112,124 +83,39 @@ def render_sidebar() -> rx.Component:
"""
return rx.box(
rx.vstack(
# 顶部
rx.vstack(
# 智能体图标和名称
# 品牌LOGO和名称
rx.vstack(
rx.icon("info"),
rx.text("智能体"),
spacing="2",
# 渲染品牌 Logo
render_brand_logo(),
rx.text(
"MarsHelper",
font_size="18px",
weight="bold",
color=rx.color("mauve", 12),
),
spacing="4",
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",
rx.divider(color=rx.color("mauve", 3), margin_bottom="16px"),
# 渲染侧边栏图标导航按钮:对话
render_sidebar_icon_nav_button("conversation"),
# 渲染侧边栏图标导航按钮:知识库
render_sidebar_icon_nav_button("knowledge_base"),
spacing="6", # 垂直间距
),
width="80px",
height="100vh",
width="64px",
padding_y="20px",
padding_x="12px",
padding_y="20px", # 水平内边距
padding_x="8px", # 垂直内边距
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),
width="80px",
min_width="80px",
background="transparent", # 透明背景
position="sticky", # 粘性定位
top=0, # 页面滚动时侧边栏固定于顶部
)

View File

@ -4,7 +4,7 @@
"""
from datetime import datetime
from enum import StrEnum
from typing import Dict, List, Optional
from typing import Dict, List
from pydantic import BaseModel, Field
from pydantic_ai._uuid import uuid7
@ -12,22 +12,7 @@ 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):
id: str = SqlField(
default_factory=lambda: str(uuid7()),
@ -105,4 +90,3 @@ class Conversation(BaseModel):
description: str = Field(default="新对话", description="描述")
runs: Dict[str, Run] = Field(default_factory=dict, description="运行字典")
created_at: datetime = Field(default_factory=datetime.now, description="创建时间")

View File

@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
"""
对话页面
"""
import reflex as rx
from application.components.conversation import (
render_conversation_component,
render_conversation_item_component,
render_input_component,
)
from application.state.conversation import ConversationState
from application.state.conversation_history_collapse import (
ConversationHistoryCollapseState,
)
def render_conversation_page() -> rx.Component:
"""
渲染对话页面左侧为对话历史折叠面板右侧为对话和输入等组件
"""
# 对话历史折叠面板打开状态
is_open = ConversationHistoryCollapseState.is_open
return rx.hstack(
# 渲染对话历史组件(聊天容器左侧部分),若对话历史列表
rx.hstack(
# 若对话历史折叠面板关闭则不渲染,否则渲染
rx.cond(
is_open,
rx.fragment(),
# 渲染对话历史组件
rx.box(
rx.vstack(
# 标题
rx.text(
"对话历史", font_size="16px", weight="bold", width="100%"
),
# 分割线
rx.divider(),
# 遍历渲染对话字典
rx.foreach(
ConversationState.get_conversations,
lambda i, _: render_conversation_item_component(i[0], i[1]),
),
rx.vstack(
rx.icon("box", size=64, color=rx.color("mauve", 8)),
rx.text("无数据", size="3", color=rx.color("mauve", 10)),
align_items="center",
justify_content="center",
flex="1",
margin_top="60px",
),
align_items="stretch",
width="100%",
spacing="2",
padding="2em",
),
width="20em",
height="100vh",
background_color=rx.color("mauve", 1),
),
),
rx.button(
rx.cond(
is_open,
rx.icon("chevron-left", size=16),
rx.icon("chevron-right", size=16),
),
width="24px",
height="80px",
border_radius=rx.cond(is_open, "0 8px 8px 0", "9999px"),
background=rx.color("mauve", 2),
variant="ghost",
box_shadow="0 2px 8px rgba(0,0,0,0.08)",
on_click=ConversationHistoryCollapseState.toggle,
),
align_items="center",
height="100vh",
spacing="0",
),
# 渲染对话、输入和其它组件(聊天容器右侧部分)
rx.vstack(
render_conversation_component(),
render_input_component(),
flex=1,
height="100vh",
spacing="0",
),
width="100vw",
height="100vh",
spacing="0",
align_items="stretch",
)

View File

@ -1,23 +1,36 @@
# -*- coding: utf-8 -*-
"""
会话页面
页面
"""
import reflex as rx
from ..components.navbar import render_navbar
from ..components.chat import dialog_list_component, input_component
from application.components.frame import render_sidebar
from application.pages.conversation import render_conversation_page
from application.pages.knowledge_base import render_knowledge_base_page
from application.state.frame import FrameState
def index_page() -> rx.Component:
"""根页面(聊天页面)"""
return rx.vstack(
render_navbar(), # 导航栏组件
dialog_list_component(), # 对话列表组件
input_component(), # 输入组件
color=rx.color(color="mauve", shade=12),
background_color=rx.color(color="mauve", shade=1),
height="100dvh",
align_items="stretch",
spacing="0",
def render_index_page() -> rx.Component:
"""
渲染根页面
"""
return rx.hstack(
# 渲染侧边栏
render_sidebar(),
# 根据选中侧边栏图标导航按钮渲染相应页面
rx.match(
FrameState.sidebar_icon_nav_button,
# 渲染对话页面
("conversation", render_conversation_page()),
# 渲染知识库页面
("knowledge_base", render_knowledge_base_page()),
),
width="100vw", # 铺满可视窗口
height="100vh",
spacing="0", # 水平方向间距
align_items="stretch", # 垂直方向拉伸至父元素高度
style={
"background": "linear-gradient(rgb(208, 201, 255) 0%, rgb(230, 214, 240) 8%, rgb(241, 219, 234) 12%, rgb(200, 220, 251) 40%, rgb(171, 198, 246) 60%, rgb(135, 174, 254) 90%)"
}, # 背景渐变
)

View File

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
"""
知识库页面
"""
import reflex as rx
def render_knowledge_base_page() -> rx.Component:
"""
渲染知识库页面暂时为空
"""
return rx.fragment()

View File

@ -45,6 +45,20 @@ instructions: str = """
语言通俗逻辑完整简洁无多余废话
"""
# 初始化智能体
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,
)
class ConversationState(rx.State):
"""
@ -56,21 +70,6 @@ class ConversationState(rx.State):
# 当前对话唯一标识
conversation_id: str = next(reversed(conversations.keys()))
# 初始化智能体
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,
)
@rx.var
def get_conversations(self) -> Dict[str, Conversation]:
"""
@ -143,6 +142,18 @@ class ConversationState(rx.State):
conversation = self.conversations.get(self.conversation_id)
return conversation.description if conversation else "新对话"
@rx.var
def is_runs_empty(self) -> bool:
"""
运行字典是否为空
:return: 运行字典是否为空
"""
# 当前对话
conversation = self.conversations.get(self.conversation_id)
if not conversation:
return True
return not conversation.runs
@rx.var
def get_runs(self) -> Dict[str, Run]:
"""
@ -203,7 +214,7 @@ class ConversationState(rx.State):
run.is_running = True
yield # 通知前端渲染
async with self.agent.run_stream_events(
async with agent.run_stream_events(
conversation_id=self.conversation_id,
user_prompt=user_prompt,
message_history=message_history,
@ -235,7 +246,8 @@ class ConversationState(rx.State):
tool_call_ids[tool_call_id] = index
run.reasonings[index] = Reasoning(
kind=ReasoningKind.TOOL_SEARCH, content="正在生成检索关键词"
kind=ReasoningKind.TOOL_SEARCH,
content="正在生成检索关键词",
)
yield
@ -254,7 +266,8 @@ class ConversationState(rx.State):
tool_call_ids[tool_call_id] = index
run.reasonings[index] = Reasoning(
kind=ReasoningKind.TOOL_CALL, content="正在生成调用参数"
kind=ReasoningKind.TOOL_CALL,
content="正在生成调用参数",
)
yield
@ -363,3 +376,4 @@ class ConversationState(rx.State):
# 指定运行
run = self.conversations[self.conversation_id].runs[run_id]
run.is_reasoning_panel_open = not run.is_reasoning_panel_open

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
"""
对话历史折叠面板状态
"""
import reflex as rx
class ConversationHistoryCollapseState(rx.State):
"""
对话历史折叠面板状态
"""
# 对话历史折叠面板打开状态True表示打开False表示关闭
is_open: bool = True
@rx.event
def toggle(self) -> None:
"""
打开/关闭对话历史折叠面板
:param is_open: 打开状态
:return: None
"""
self.is_open = not self.is_open

View File

@ -10,12 +10,12 @@ class CreateConversationModalState(rx.State):
创建对话模态窗状态
"""
# 打开状态True表示打开False表示关闭
# 创建对话模态窗打开状态True表示打开False表示关闭
is_open: bool = False
@rx.event
def toggle(self, is_open: bool) -> None:
def toggle(self) -> None:
"""
打开/关闭
打开/关闭创建对话模态窗
"""
self.is_open = is_open
self.is_open = not self.is_open

View File

@ -13,7 +13,10 @@ from application.models import MessageHistory, ModelMessage, ModelMessagesTypeAd
class DatabaseState(rx.State):
"""数据库状态"""
"""
数据库状态
重新初始化先手动删除 alembic 相关配置和文件夹再使用 reflex db init 初始化数据库表
"""
async def get_message_history(self, conversation_id: str) -> List[ModelMessage]:
"""

View File

@ -3,13 +3,6 @@
框架状态
"""
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):
@ -17,25 +10,15 @@ 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 =
# 选中侧边栏图标导航按钮
sidebar_icon_nav_button: str = "conversation"
@rx.var
def get_sidebar_icon_nav_button(self) -> SidebarIconNavButton:
"""获取当前侧边栏图标导航按钮"""
return self.SIDEBAR_ICON_NAV_BUTTONS[self.sidebar_icon_nav_button]
def get_sidebar_icon_nav_button(self) -> str:
"""获取选中侧边栏图标导航按钮"""
return self.sidebar_icon_nav_button
@rx.event
def set_active_sidebar_tab(self, icon_id: str):
"""切换侧边栏当前标签名称"""
self.sidebar_tab_name = icon_id
def switch_active_sidebar_tab(self, sidebar_icon_nav_button: str):
"""将指定侧边栏图标导航按钮设置为选中侧边栏图标导航按钮"""
self.sidebar_icon_nav_button = sidebar_icon_nav_button

View File

@ -5,7 +5,6 @@
"": {
"name": "reflex",
"dependencies": {
"@radix-ui/react-dialog": "1.1.15",
"@radix-ui/react-form": "0.1.8",
"@radix-ui/themes": "3.3.0",
"@react-router/node": "7.15.0",
@ -27,7 +26,6 @@
"socket.io-client": "4.8.3",
"sonner": "2.0.7",
"universal-cookie": "7.2.2",
"vaul": "1.1.2",
},
"devDependencies": {
"@emotion/react": "11.14.0",
@ -36,7 +34,7 @@
"autoprefixer": "10.5.0",
"postcss": "8.5.14",
"postcss-import": "16.1.1",
"vite": "8.0.14",
"vite": "8.0.16",
},
},
},
@ -204,7 +202,7 @@
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
"@oxc-project/types": ["@oxc-project/types@0.132.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.132.0.tgz", {}, "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ=="],
"@oxc-project/types": ["@oxc-project/types@0.133.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.133.0.tgz", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="],
"@radix-ui/colors": ["@radix-ui/colors@3.0.0", "https://registry.npmmirror.com/@radix-ui/colors/-/colors-3.0.0.tgz", {}, "sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg=="],
@ -338,35 +336,35 @@
"@remix-run/node-fetch-server": ["@remix-run/node-fetch-server@0.13.3", "https://registry.npmmirror.com/@remix-run/node-fetch-server/-/node-fetch-server-0.13.3.tgz", {}, "sha512-UfjOXed/DQteaM5VyTfqTeGpHwyL2J5aoRGY6cydip4tt1ehNNeSwuXCC7AEGE0RWBs/7bgKxYkL/B/+UDe4AA=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", { "os": "android", "cpu": "arm64" }, "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.3", "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", { "os": "android", "cpu": "arm64" }, "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.3", "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.3", "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.3", "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", { "os": "linux", "cpu": "arm" }, "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.3", "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", { "os": "linux", "cpu": "arm" }, "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.3", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.3", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.3", "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.3", "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", { "os": "linux", "cpu": "x64" }, "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.3", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", { "os": "linux", "cpu": "x64" }, "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", { "os": "linux", "cpu": "x64" }, "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.3", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", { "os": "linux", "cpu": "x64" }, "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", { "os": "none", "cpu": "arm64" }, "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.3", "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", { "os": "none", "cpu": "arm64" }, "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.3", "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.3", "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.2", "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", { "os": "win32", "cpu": "x64" }, "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.3", "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", { "os": "win32", "cpu": "x64" }, "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
@ -870,7 +868,7 @@
"resolve-from": ["resolve-from@4.0.0", "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"rolldown": ["rolldown@1.0.2", "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.2.tgz", { "dependencies": { "@oxc-project/types": "=0.132.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.2", "@rolldown/binding-darwin-arm64": "1.0.2", "@rolldown/binding-darwin-x64": "1.0.2", "@rolldown/binding-freebsd-x64": "1.0.2", "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", "@rolldown/binding-linux-arm64-gnu": "1.0.2", "@rolldown/binding-linux-arm64-musl": "1.0.2", "@rolldown/binding-linux-ppc64-gnu": "1.0.2", "@rolldown/binding-linux-s390x-gnu": "1.0.2", "@rolldown/binding-linux-x64-gnu": "1.0.2", "@rolldown/binding-linux-x64-musl": "1.0.2", "@rolldown/binding-openharmony-arm64": "1.0.2", "@rolldown/binding-wasm32-wasi": "1.0.2", "@rolldown/binding-win32-arm64-msvc": "1.0.2", "@rolldown/binding-win32-x64-msvc": "1.0.2" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g=="],
"rolldown": ["rolldown@1.0.3", "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.3.tgz", { "dependencies": { "@oxc-project/types": "=0.133.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.3", "@rolldown/binding-darwin-arm64": "1.0.3", "@rolldown/binding-darwin-x64": "1.0.3", "@rolldown/binding-freebsd-x64": "1.0.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", "@rolldown/binding-linux-arm64-gnu": "1.0.3", "@rolldown/binding-linux-arm64-musl": "1.0.3", "@rolldown/binding-linux-ppc64-gnu": "1.0.3", "@rolldown/binding-linux-s390x-gnu": "1.0.3", "@rolldown/binding-linux-x64-gnu": "1.0.3", "@rolldown/binding-linux-x64-musl": "1.0.3", "@rolldown/binding-openharmony-arm64": "1.0.3", "@rolldown/binding-wasm32-wasi": "1.0.3", "@rolldown/binding-win32-arm64-msvc": "1.0.3", "@rolldown/binding-win32-x64-msvc": "1.0.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g=="],
"rollup": ["rollup@4.61.1", "https://registry.npmmirror.com/rollup/-/rollup-4.61.1.tgz", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.61.1", "@rollup/rollup-android-arm64": "4.61.1", "@rollup/rollup-darwin-arm64": "4.61.1", "@rollup/rollup-darwin-x64": "4.61.1", "@rollup/rollup-freebsd-arm64": "4.61.1", "@rollup/rollup-freebsd-x64": "4.61.1", "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", "@rollup/rollup-linux-arm-musleabihf": "4.61.1", "@rollup/rollup-linux-arm64-gnu": "4.61.1", "@rollup/rollup-linux-arm64-musl": "4.61.1", "@rollup/rollup-linux-loong64-gnu": "4.61.1", "@rollup/rollup-linux-loong64-musl": "4.61.1", "@rollup/rollup-linux-ppc64-gnu": "4.61.1", "@rollup/rollup-linux-ppc64-musl": "4.61.1", "@rollup/rollup-linux-riscv64-gnu": "4.61.1", "@rollup/rollup-linux-riscv64-musl": "4.61.1", "@rollup/rollup-linux-s390x-gnu": "4.61.1", "@rollup/rollup-linux-x64-gnu": "4.61.1", "@rollup/rollup-linux-x64-musl": "4.61.1", "@rollup/rollup-openbsd-x64": "4.61.1", "@rollup/rollup-openharmony-arm64": "4.61.1", "@rollup/rollup-win32-arm64-msvc": "4.61.1", "@rollup/rollup-win32-ia32-msvc": "4.61.1", "@rollup/rollup-win32-x64-gnu": "4.61.1", "@rollup/rollup-win32-x64-msvc": "4.61.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA=="],
@ -938,15 +936,13 @@
"valibot": ["valibot@1.4.1", "https://registry.npmmirror.com/valibot/-/valibot-1.4.1.tgz", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-klCmFTz2jeDluy9RwX+F884TCiogtdBJ/YaxSx1EOBYXa3NXNWj8kR1jjN8rzluwojJVWWaHJ4r1U5LfICnM3g=="],
"vaul": ["vaul@1.1.2", "https://registry.npmmirror.com/vaul/-/vaul-1.1.2.tgz", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="],
"vfile": ["vfile@6.0.3", "https://registry.npmmirror.com/vfile/-/vfile-6.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
"vfile-location": ["vfile-location@5.0.3", "https://registry.npmmirror.com/vfile-location/-/vfile-location-5.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
"vfile-message": ["vfile-message@4.0.3", "https://registry.npmmirror.com/vfile-message/-/vfile-message-4.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"vite": ["vite@8.0.14", "https://registry.npmmirror.com/vite/-/vite-8.0.14.tgz", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.2", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw=="],
"vite": ["vite@8.0.16", "https://registry.npmmirror.com/vite/-/vite-8.0.16.tgz", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw=="],
"vite-node": ["vite-node@3.2.4", "https://registry.npmmirror.com/vite-node/-/vite-node-3.2.4.tgz", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],

View File

@ -6,7 +6,6 @@
"export": "react-router build"
},
"dependencies": {
"@radix-ui/react-dialog": "1.1.15",
"@radix-ui/react-form": "0.1.8",
"@radix-ui/themes": "3.3.0",
"@react-router/node": "7.15.0",
@ -27,8 +26,7 @@
"remark-math": "6.0.0",
"socket.io-client": "4.8.3",
"sonner": "2.0.7",
"universal-cookie": "7.2.2",
"vaul": "1.1.2"
"universal-cookie": "7.2.2"
},
"devDependencies": {
"@emotion/react": "11.14.0",
@ -37,7 +35,7 @@
"autoprefixer": "10.5.0",
"postcss": "8.5.14",
"postcss-import": "16.1.1",
"vite": "8.0.14"
"vite": "8.0.16"
},
"overrides": {
"cookie": "1.1.1"

View File

@ -6,7 +6,6 @@
from pathlib import Path
import reflex as rx
from reflex.plugins import RadixThemesPlugin
from reflex_base.plugins.sitemap import SitemapPlugin
# 构建数据库路径(使用 SQLite 作为数据库)
@ -17,11 +16,5 @@ config = rx.Config(
db_url=f"sqlite:///{database_path.as_posix()}",
async_db_url=f"sqlite+aiosqlite:///{database_path.as_posix()}",
disable_plugins=[SitemapPlugin],
plugins=[
RadixThemesPlugin(
theme=rx.theme(
appearance="dark", accent_color="purple" # 暗黑模式 # 主题色
)
)
],
plugins=[],
)