This commit is contained in:
liubiren 2026-07-13 20:11:49 +08:00
parent 5a68247656
commit 7d6d1d64a8
19 changed files with 980 additions and 914 deletions

View File

@ -1,46 +0,0 @@
"""empty message
Revision ID: 54a15eecb10a
Revises:
Create Date: 2026-07-10 14:17:15.523102
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import sqlmodel
# revision identifiers, used by Alembic.
revision: str = '54a15eecb10a'
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

@ -0,0 +1,67 @@
"""empty message
Revision ID: 6cd382f48060
Revises:
Create Date: 2026-07-13 17:24:40.090382
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import sqlmodel
# revision identifiers, used by Alembic.
revision: str = '6cd382f48060'
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('conversations',
sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('user_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('description', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('is_deleted', sa.Boolean(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('conversations', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_conversations_user_id'), ['user_id'], unique=False)
op.create_table('dialogs',
sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('conversation_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('question', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('thought_nodes', sa.JSON(), nullable=False),
sa.Column('answer', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('dialogs', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_dialogs_conversation_id'), ['conversation_id'], unique=False)
op.create_table('runresults',
sa.Column('conversation_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('dialog_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('new_messages', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.PrimaryKeyConstraint('conversation_id', 'dialog_id')
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('runresults')
with op.batch_alter_table('dialogs', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_dialogs_conversation_id'))
op.drop_table('dialogs')
with op.batch_alter_table('conversations', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_conversations_user_id'))
op.drop_table('conversations')
# ### end Alembic commands ###

View File

@ -3,7 +3,7 @@
应用主入口 应用主入口
""" """
import reflex as rx import reflex as rx
from application.pages import index from application.pages.index import render_index
app = rx.App( app = rx.App(
style={ style={
@ -21,6 +21,7 @@ app = rx.App(
"box_sizing": "border-box", "box_sizing": "border-box",
}, },
"--prismui-background-color": "#ffffff", "--prismui-background-color": "#ffffff",
"--mc-float-block-shadow": "rgba(213, 213, 213, 0.25)",
"--devui-global-bg": "#f6f6f8", "--devui-global-bg": "#f6f6f8",
"--mc-global-bg": "linear-gradient(to bottom, #D0C9FF 0%, #E6D6F0 8%, #F1DBEA 12%, #C8DCFB 40%, #ABC6F6 60%, #87AEFE 90%)", "--mc-global-bg": "linear-gradient(to bottom, #D0C9FF 0%, #E6D6F0 8%, #F1DBEA 12%, #C8DCFB 40%, #ABC6F6 60%, #87AEFE 90%)",
"--mc-box-shadow": "rgba(25, 25, 25, .06)", "--mc-box-shadow": "rgba(25, 25, 25, .06)",
@ -46,4 +47,4 @@ app = rx.App(
}, # 自定义主题颜色 }, # 自定义主题颜色
) # 此处变量名需使用 app ,具体原因目前尚不清楚 ) # 此处变量名需使用 app ,具体原因目前尚不清楚
# 注册首页路由 # 注册首页路由
app.add_page(component=index, route="/") app.add_page(component=render_index(), route="/")

View File

@ -1,622 +0,0 @@
# -*- coding: utf-8 -*-
"""
渲染对话相关组件
"""
import reflex as rx
from application.models import Run, Reasoning, Conversation
from application.state.conversation import ConversationState
from application.state.create_conversation_modal import CreateConversationModalState
def render_conversation_history_item(
conversation_id: str, conversation: Conversation
) -> rx.Component:
"""
渲染对话历史的对话卡片
:param conversation_id: 对话唯一标识
:param conversation: 对话实例
:return: Component
"""
def more_button(
conversation_id: str,
) -> rx.Component:
"""
渲染更多按钮
:param conversation_id: 对话唯一标识
:return: Component
"""
return rx.popover.root(
# 触发事件:点击更多按钮
rx.popover.trigger(
rx.box(
rx.icon("ellipsis", size=14, color="var(--devui-text-weak)"),
cursor="pointer", # 鼠标光标显示为手指
)
),
# 渲染气泡卡片
rx.popover.content(
rx.vstack(
rx.box(
position="absolute", # 绝对定位
width="8px", # 宽度
height="8px", # 高度
top="-12px", # 向上移动
left="50%", # 向左移动
transform="translateX(-50%) rotate(45deg)", # 旋转45度
background_color="#ffffff", # 背景颜色
box_shadow="-2px -2px 4px rgba(0,0,0,0.05)", # 阴影
),
rx.popover.close(
rx.box(
"删除",
width="100%", # 宽度
height="24px", # 高度
padding="4px", # 内边距
border_radius="4px", # 圆角
line_height="16px", # 行高
font_family="HuaweiFont,Helvetica,Arial,PingFangSC-Regular,Hiragino Sans GB,Microsoft YaHei,微软雅黑,Microsoft JhengHei", # 字体
font_size="12px", # 字体大小
color="#252b3a", # 字体颜色
style={
"_hover": {
"background_color": "#f2f2f3", # 鼠标悬停显示背景颜色
}
},
# 点击事件:删除对话
on_click=lambda: ConversationState.delete_conversation(
conversation_id
),
)
),
position="relative", # 相对定位
width="100%", # 宽度
),
position="relative", # 相对定位
align="center", # 水平居中对齐
padding="8px", # 内边距
border_radius="4px", # 圆角
background_color="#ffffff", # 背景颜色
box_shadow="0 2px 12px rgba(0,0,0,0.1)", # 阴影
overflow="visible", # 溢出可见
side="bottom", # 气泡卡片位于底部
side_offset=9, # 偏移量
),
open_delay=0,
)
# 指定对话的激活状态
is_actived = conversation_id == ConversationState.conversation_id
return rx.box(
rx.hstack(
# 渲染对话描述
rx.text(
conversation.description,
flex=1,
height="22px",
line_height="22px",
font_size="var(--devui-font-size)",
padding_right="4px",
overflow="hidden",
text_overflow="ellipsis",
white_space="nowrap",
),
# 渲染更多按钮
rx.box(
more_button(conversation_id=conversation_id),
min_width="14px",
cursor="pointer", # 鼠标光标显示为手指
style={
"opacity": rx.cond(
is_actived, "1", "0"
), # 若已激活则不透明,否则透明
"pointer_events": rx.cond(
is_actived, "auto", "none"
), # 若已激活则可点击,否则不可点击
"transition": "opacity 0.18s ease",
},
),
display="flex", # 弹性布局
align_items="center", # 子元素垂直居中
width="100%", # 宽度
margin_bottom="8px", # 底部外边距
),
line_height="1.5", # 行高
color="var(--devui-text-weak)", # 字体颜色
cursor="pointer", # 鼠标悬停显示手指
margin_bottom="8px", # 底部外边距
width="100%", # 宽度
padding="16px", # 内边距
border_radius="8px", # 圆角
style={
"background": rx.cond(
is_actived,
"linear-gradient(to right, #f3efff, #f3efff33, #e2f1fd33, #e2f1fd)",
"var(--devui-base-bg)",
),
"box_shadow": rx.cond(is_actived, "2px 2px 8px #e9e9e9", "none"),
# 鼠标悬停时渲染背景颜色和阴影
"&:hover": {
"background": "linear-gradient(to right, #f3efff, #f3efff33, #e2f1fd33, #e2f1fd)",
"box_shadow": "2px 2px 8px #e9e9e9",
},
# 鼠标悬停时强制显示右侧三点按钮,覆盖默认隐藏
"&:hover > div > div:last-child": {
"opacity": "1 !important",
"pointer_events": "auto !important",
},
},
# 点击事件:将指定对话唯一标识设置为当前对话唯一标识
on_click=lambda: ConversationState.switch_conversation(conversation_id),
)
def render_reasoning_item_component(reasoning_id: int, reasoning: Reasoning):
"""
渲染推理项组件
:param reasoning_id: 推理唯一标识
:param reasoning: 推理实例
:return: Component
"""
return rx.hstack(
rx.vstack(
rx.box(
width="8px",
height="8px",
background_color=rx.color("blue", 7),
),
rx.box(width="2px", flex=1, background_color=rx.color("blue", 3)),
align_items="center",
spacing="0",
height="100%",
),
rx.markdown(reasoning.content, color=rx.color("gray", 11), padding_y="4px"),
align_items="flex-start",
spacing="2",
width="100%",
key=reasoning_id,
)
def render_conversation_box() -> rx.Component:
"""
渲染对话框
:return: Component
"""
def item(run_id: str, run: Run) -> rx.Component:
"""
对话项
:param run: 对话实例
:return: Component
"""
# 推理字典
reasonings = run.reasonings
# 推理状态
is_reasoning = run.is_reasoning
# 推理面板展开状态
is_reasoning_panel_open = run.is_reasoning_panel_open
return rx.vstack(
rx.hstack(
rx.spacer(),
rx.vstack(
# 用户问题
rx.markdown(
run.user_prompt,
max_width="600px",
padding="12px 16px",
background_color="var(--devui-global-bg)",
border_radius="12px",
word_wrap="break-word",
word_break="break-all",
style={
"& p.rt-Text": {
"padding": "0",
"margin": "0 !important",
},
},
),
# 再次发送按钮
rx.hstack(
rx.box(
rx.icon(
"rotate-ccw",
width="14px",
height="14px",
color="var(--devui-text-weak)",
),
padding="4px",
border_radius="4px",
cursor="pointer",
),
width="100%",
margin_top="8px",
),
),
# 用户身份标识
rx.avatar(
src="/user_avatar.svg",
align_self="flex-start",
width="32px",
height="32px",
border_radius="100%",
border="none",
),
width="100%",
margin_top="8px",
align_items="flex-start",
gap="4px",
font_size="var(--devui-font-size)",
),
# 智能体回答
rx.hstack(
# 智能体身份标识
rx.avatar(
src="/logo.svg",
align_self="flex-start",
width="32px",
height="32px",
border_radius="100%",
border="none",
),
rx.box(
rx.vstack(
# 标题栏
rx.cond(
is_reasoning,
rx.hstack(
rx.icon("loader"),
rx.text("推理中..."),
rx.icon("chevron-down"),
),
rx.hstack(
rx.icon("check"),
rx.text("已推理"),
rx.icon("chevron-up"),
),
),
)
),
# 标题栏
text_align="left",
width="100%",
margin_bottom="8px",
),
# 渲染回复正文组件,若正在推理则不渲染,否则渲染回复正文组件
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", # 圆角
),
),
),
width="100%",
key=run_id,
)
# 运行字典
runs = ConversationState.get_runs
# 若运行字典为空则渲染品牌图标、名称和介绍、预设用户提示词等,否则遍历渲染运行项
return rx.cond(
runs.length() == 0,
rx.vstack(
rx.vstack(
# 品牌图标、名称和介绍
rx.vstack(
rx.hstack(
rx.image(
src="/logo.png",
width="64px",
height="64px",
object_fit="contain",
),
rx.box(
"棱镜球",
font_size="32px",
font_weight="700",
letter_spacing="1px",
),
display="flex",
align_items="center",
gap="8px",
),
rx.vstack(
rx.text(
"产品汪的棱镜球,精准捕捉各类业务需求",
width="100%",
text_align="center",
),
rx.text(
"可帮助检索业务资料,协助生成产品方案、输出 PRD 、流程图和原型图等",
width="100%",
text_align="center",
),
width="100%",
line_height="1.5",
font_size="var(--devui-font-size)",
),
display="flex",
flex_direction="column",
width="100%",
align_items="center",
gap="12px",
color="var(--devui-text)",
),
# 猜你想问
rx.vstack(
rx.hstack(
rx.text(
"猜你想问",
line_height="24px",
font_size="16px",
font_weight="700",
),
display="flex",
width="100%",
justify_content="space-between",
align_items="center",
margin_bottom="16px",
),
rx.hstack(
rx.box(
"需求梳理",
padding="10px 16px",
background_color="var(--devui-dividing-line)",
border_radius="var(--devui-border-radius-full)",
font_size="var(--devui-font-size)",
color="var(--devui-aide-text)",
cursor="pointer",
),
display="flex",
flex_wrap="wrap",
align_items="center",
gap="12px",
),
width="100%",
padding="24px",
background_color="var(--devui-base-bg)",
border_radius="24px",
),
display="flex",
flex_direction="column",
width="100%",
min_height="0",
margin="auto 0",
align_items="center",
gap="24px",
color="var(--devui-text)",
),
display="flex",
flex="1",
flex_direction="column",
width="100%",
max_width="1200px",
padding="0 12px",
justify_content="flex-start",
gap="24px",
overflow="auto",
style={
"&::-webkit-scrollbar": {
"display": "none",
},
},
),
# 遍历渲染对话项
rx.vstack(
rx.auto_scroll(
rx.foreach(
runs,
lambda i, _: item(i[0], i[1]),
),
width="100%",
padding="0 12px",
),
flex="1",
width="100%",
max_width="1200px",
margin_x="auto",
padding_top="20px",
justify_content="flex-start",
overflow_x="hidden",
overflow_y="auto",
),
)
def render_create_conversation_button() -> rx.Component:
"""
渲染新建对话按钮
:return: Component
"""
return rx.hstack(
rx.el.style(
"""
.rt-TooltipArrow polygon {
fill: #ffffff !important;
}
.rt-TooltipText {
color: var(--devui-text) !important;
opacity: 1 !important;
}
"""
),
rx.spacer(), # 占位符
rx.dialog.root(
# 触发事件:点击图标
rx.dialog.trigger(
rx.box(
rx.tooltip(
rx.icon(
"plus",
size=14,
color="var(--devui-text)",
style={
"_hover": {
"color": "var(--devui-brand)",
}
},
),
content="新建对话",
background_color="#ffffff",
box_shadow="0 2px 12px 0 rgba(37, 43, 58, .24)",
color="#252b3a",
side="top",
side_offset=9,
),
display="flex",
width="24px",
height="24px",
background_color="var(--devui-base-bg)",
box_shadow="0 1px 8px #1919190f",
border_radius="var(--devui-border-radius-full)",
justify_content="center",
align_items="center",
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,
),
background_color=rx.color("mauve", 1),
),
open=CreateConversationModalState.is_open,
on_open_change=CreateConversationModalState.toggle,
),
display="flex",
width="100%",
max_width="1200px",
height="39px",
padding="0 12px",
justify_content="flex-end",
align_items="center",
gap="4px",
)
def render_input_box() -> rx.Component:
"""
渲染输入框
"""
return rx.vstack(
# 渲染自定义输入组件
rx.form(
rx.box(
rx.vstack(
rx.hstack(
rx.text_area(
name="user_prompt",
placeholder="请输入您的问题按Enter换行",
vertical_align="middle",
width="100%",
height="64px",
padding="4px 0",
background_color="var(--devui-base-bg)",
font_size="var(--devui-font-size)",
color="var(--devui-text)",
style={
"border": "none !important",
"outline": "none !important",
"boxShadow": "none !important",
},
),
width="100%",
padding="0 16px",
),
rx.hstack(
rx.spacer(), # 占位符
rx.button(
rx.icon(
"send",
margin_right="4px",
width="12px",
height="12px",
),
rx.text("发送"),
position="relative",
display="inline-flex",
padding="0 12px",
background_color="var(--devui-primary)",
border="none",
border_radius="20px",
align_items="center",
justify_content="center",
white_space="nowrap",
inline_height="1.5",
font_size="var(--devui-font-size)",
color="var(--devui-light-text)",
overflow="hidden",
cursor="pointer",
type="submit",
loading=ConversationState.get_running_status,
disabled=ConversationState.get_running_status,
),
width="100%",
justify_content="flex-end",
align_items="center",
height="32px",
padding="0 16px",
),
),
style={
"* textarea::placeholder": {
"fontFamily": "var(--font-family)",
"fontSize": "var(--devui-font-size)",
"color": "var(--placeholder)",
"opacity": 1,
},
},
),
display="flex",
flex_direction="column",
width="100%",
padding="12px 0",
background_color="var(--devui-base-bg)",
border="none",
border_radius="16px",
box_shadow="0 1px 8px 0 var(--mc-box-shadow)",
on_submit=ConversationState.run,
reset_on_submit=True,
),
# 渲染底部文案
rx.text(
"内容由大模型生成,无法确保准确性和完整性,仅供参考",
margin_top="8px",
text_align="center",
font_size="12px",
color="var(--devui-aide-text)",
),
width="100%",
max_width="1200px",
padding="0 12px 12px",
align_items="center",
)

View File

@ -85,7 +85,7 @@ def sidebar() -> rx.Component:
line_height="20px", line_height="20px",
font_size="11px", font_size="11px",
font_weight="700", font_weight="700",
color="var(--devui-color-text)", color="var(--prismui-color-text)",
), ),
display="flex", display="flex",
flex_direction="column", flex_direction="column",

View File

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
from application.models.datatables import Conversations, Dialogs, RunResults
from application.models.memories import Conversation, Dialog, ThoughtNode
__all__ = [
"Conversation",
"Conversations",
"Dialog",
"Dialogs",
"RunResults",
"ThoughtNode",
]

View File

@ -8,8 +8,8 @@ from pydantic_ai._uuid import uuid7
from sqlmodel import Field, JSON, SQLModel from sqlmodel import Field, JSON, SQLModel
# 会话数据表模型 # 会话数据表
class Conversation(SQLModel, table=True): class Conversations(SQLModel, table=True):
id: str = Field( id: str = Field(
default_factory=lambda: str(uuid7()), default_factory=lambda: str(uuid7()),
primary_key=True, primary_key=True,
@ -21,8 +21,15 @@ class Conversation(SQLModel, table=True):
created_at: datetime = Field(default_factory=datetime.now, description="创建时间") created_at: datetime = Field(default_factory=datetime.now, description="创建时间")
# 对话数据表模型 # 思考节点数据表(仅用于定义)
class Dialog(SQLModel, table=True): class ThoughtNodes(SQLModel):
kind: str = Field(description="思考节点类型")
content: str = Field(default="", description="思考节点内容")
# 对话数据表
class Dialogs(SQLModel, table=True):
id: str = Field( id: str = Field(
default_factory=lambda: str(uuid7()), default_factory=lambda: str(uuid7()),
primary_key=True, primary_key=True,
@ -30,21 +37,14 @@ class Dialog(SQLModel, table=True):
) )
conversation_id: str = Field(..., index=True, description="会话唯一标识") conversation_id: str = Field(..., index=True, description="会话唯一标识")
question: str = Field(..., description="问题") question: str = Field(..., description="问题")
thought_nodes: list[ThoughtNode] = Field( thought_nodes: dict[int, ThoughtNodes] = Field(
default_factory=list, sa_type=JSON, description="思考节点列表" default_factory=dict, sa_type=JSON, description="思考节点字典"
) )
answer: str = Field(default="", description="回答") answer: str = Field(default="", description="回答")
# 思考节点数据表模型 # 运行结果数据表
class ThoughtNode(SQLModel): class RunResults(SQLModel, table=True):
kind: str = Field(description="思考节点类型")
content: str = Field(default="", description="思考节点内容")
# 运行结果数据表模型
class RunResult(SQLModel, table=True):
conversation_id: str = Field(primary_key=True, description="会话唯一标识") conversation_id: str = Field(primary_key=True, description="会话唯一标识")
dialog_id: str = Field(primary_key=True, description="对话唯一标识") dialog_id: str = Field(primary_key=True, description="对话唯一标识")
new_messages: str = Field(description="新增消息") new_messages: str = Field(description="新增消息")

View File

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
"""
内存模型
"""
from datetime import datetime
from typing import Dict
from pydantic import BaseModel, Field
class ThoughtNode(BaseModel):
"""
思考节点内存模型
"""
kind: str
content: str
class Dialog(BaseModel):
"""
对话内存模型
"""
question: str = Field(..., description="问题")
is_thinking: bool = Field(
default=False, description="思考状态True 表示思考中False 表示思考完成"
)
is_collapse_expanded: bool = Field(
default=False, description="思考折叠面板展开状态True 表示展开False 表示折叠"
)
thought_nodes: dict[int, ThoughtNode] = Field(
default_factory=dict, description="思考节点字典"
)
answer: str = Field(default="", description="回答")
class Conversation(BaseModel):
"""
会话内存模型
"""
description: str = Field(default="新会话", description="会话描述")
is_running: bool = Field(
default=False, description="运行状态True 表示运行中False 表示运行完成"
)
dialogs: Dict[str, Dialog] = Field(default_factory=dict, description="对话字典")
created_at: datetime = Field(default_factory=datetime.now, description="创建时间")

View File

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from application.pages.index import index from application.pages.conversation import render_conversation
from application.pages.conversation import conversation from application.pages.library import render_library
from application.pages.library import library
__all__ = ["index", "conversation", "library"] __all__ = ["render_conversation", "render_library"]

View File

@ -4,31 +4,162 @@
""" """
import reflex as rx import reflex as rx
from application.components import ( from application.models import Conversation, Dialog, ThoughtNode
render_conversation_box, from application.states import (
render_conversation_history_item, ConversationHistoryState,
render_input_box, ConversationState,
render_create_conversation_button, CreateConversationState,
) )
from application.states import ConversationState, ConversationHistoryState
def conversation() -> rx.Component: def render_conversation_history_item(
conversation_id: str, conversation: Conversation
) -> rx.Component:
""" """
会话页面布局参考 MetaChat左侧为会话历史右侧为会话其中会话历史为折叠面板 渲染会话历史条目
:param conversation_id: 会话唯一标识
:param conversation: 会话实例
:return: Component
""" """
# 会话历史折叠面板打开状态
is_open = ConversationHistoryState.is_open # 会话激活状态
is_conversation_actived = conversation_id == ConversationState.conversation_id
return rx.box( return rx.box(
rx.hstack( rx.hstack(
# 渲染对话历史折叠面板 # 渲染对话描述
rx.text(
conversation.description,
flex=1,
height="22px",
line_height="22px",
font_size="var(--devui-font-size)",
padding_right="4px",
overflow="hidden",
text_overflow="ellipsis",
white_space="nowrap",
),
# 渲染更多按钮
rx.box( rx.box(
rx.popover.root(
# 触发事件:点击更多按钮
rx.popover.trigger(
rx.box(
rx.icon(
"ellipsis", size=14, color="var(--devui-text-weak)"
),
cursor="pointer",
)
),
# 渲染气泡卡片
rx.popover.content(
rx.vstack(
rx.box(
position="absolute",
top="-12px",
left="50%",
transform="translateX(-50%) rotate(45deg)",
width="8px",
height="8px",
background_color="#ffffff",
box_shadow="-2px -2px 4px rgba(0,0,0,0.05)",
),
rx.popover.close(
rx.box(
"删除",
width="100%",
height="24px",
padding="4px",
border_radius="4px",
line_height="16px",
font_family="HuaweiFont,Helvetica,Arial,PingFangSC-Regular,Hiragino Sans GB,Microsoft YaHei,微软雅黑,Microsoft JhengHei",
font_size="12px",
color="#252b3a",
style={
"_hover": {
"background_color": "#f2f2f3",
}
},
# 点击事件:删除对话
on_click=lambda: ConversationState.delete_conversation(
conversation_id
),
)
),
position="relative",
width="100%",
),
position="relative",
align="center",
padding="8px",
border_radius="4px",
background_color="#ffffff",
box_shadow="0 2px 12px rgba(0,0,0,0.1)",
overflow="visible",
side="bottom",
side_offset=9,
),
open_delay=0,
),
min_width="14px",
cursor="pointer",
style={
"opacity": rx.cond(
is_conversation_actived, "1", "0"
), # 若当前会话已激活则不透明,否则透明
"pointer_events": rx.cond(
is_conversation_actived, "auto", "none"
), # 若当前会话已激活则可点击,否则不可点击
"transition": "opacity 0.18s ease",
},
),
display="flex",
align_items="center",
width="100%",
margin_bottom="8px",
),
width="100%",
padding="16px",
margin_bottom="8px",
border_radius="8px",
line_height="1.5",
color="var(--devui-text-weak)",
cursor="pointer",
style={
"background": rx.cond(
is_conversation_actived,
"linear-gradient(to right, #f3efff, #f3efff33, #e2f1fd33, #e2f1fd)",
"var(--devui-base-bg)",
),
"box_shadow": rx.cond(
is_conversation_actived, "2px 2px 8px #e9e9e9", "none"
),
"&:hover": {
"background": "linear-gradient(to right, #f3efff, #f3efff33, #e2f1fd33, #e2f1fd)",
"box_shadow": "2px 2px 8px #e9e9e9",
}, # 鼠标悬停渲染背景颜色和阴影
"&:hover > div > div:last-child": {
"opacity": "1 !important",
"pointer_events": "auto !important",
}, # 鼠标悬停强制显示右侧三点按钮
},
# 点击事件:将指定对话唯一标识设置为当前对话唯一标识
on_click=ConversationState.switch_conversation(conversation_id),
)
def render_conversation_history_collapse(is_collapse_expanded: bool) -> rx.Component:
"""
渲染对话历史折叠面板
:param is_collapse_expanded: 对话历史折叠面板展开状态
:return: Component
"""
return rx.box(
rx.vstack( rx.vstack(
# 渲染标题 # 渲染标题
rx.hstack( rx.hstack(
rx.text( rx.text(
"对话历史", "话历史",
margin_bottom="8px", margin_bottom="8px",
font_size="var(--devui-font-size-lg)", font_size="var(--devui-font-size-lg)",
font_weight="700", font_weight="700",
@ -42,9 +173,7 @@ def conversation() -> rx.Component:
rx.auto_scroll( rx.auto_scroll(
rx.foreach( rx.foreach(
ConversationState.conversation_history, ConversationState.conversation_history,
lambda item, _: render_conversation_history_item( lambda item, _: render_conversation_history_item(item[0], item[1]),
item[0], item[1]
),
), ),
flex="1", flex="1",
margin_top="8px", margin_top="8px",
@ -59,29 +188,483 @@ def conversation() -> rx.Component:
), ),
display="flex", display="flex",
flex_direction="column", flex_direction="column",
padding="12px",
height="100%", height="100%",
padding="12px",
background_color="#f9f9f9cc", background_color="#f9f9f9cc",
backdrop_filter="blur(50px)", # 背景模糊 backdrop_filter="blur(50px)",
gap="12px", gap="12px",
color="var(--devui-text)", color="var(--prismui-color-text)",
), ),
width=rx.cond(is_open, "25%", "0px"), width=rx.cond(is_collapse_expanded, "25%", "0px"),
min_width=rx.cond(is_open, "240px", "0px"), min_width=rx.cond(is_collapse_expanded, "240px", "0px"),
max_width=rx.cond(is_open, "380px", "0px"), max_width=rx.cond(is_collapse_expanded, "380px", "0px"),
height="100%", height="100%",
overflow="hidden", overflow="hidden",
transition="all 0.3s ease-in-out", transition="all 0.3s ease-in-out",
)
def render_greeting() -> rx.Component:
"""
渲染问候
:return: Component
"""
return rx.vstack(
rx.vstack(
# 品牌图标、名称和介绍
rx.vstack(
rx.hstack(
rx.image(
src="/logo.png",
width="64px",
height="64px",
object_fit="contain",
), ),
# 渲染对话区域 rx.box(
"棱镜球",
font_size="32px",
font_weight="700",
letter_spacing="1px",
),
display="flex",
align_items="center",
gap="8px",
),
rx.vstack(
rx.text(
"产品汪的棱镜球,精准捕捉各类业务需求",
width="100%",
text_align="center",
),
rx.text(
"可帮助检索业务资料,协助生成产品方案、输出 PRD 、流程图和原型图等",
width="100%",
text_align="center",
),
width="100%",
line_height="1.5",
font_size="var(--devui-font-size)",
),
display="flex",
flex_direction="column",
width="100%",
align_items="center",
gap="12px",
color="var(--prismui-color-text)",
),
# 猜你想问
rx.vstack(
rx.hstack(
rx.text(
"猜你想问",
line_height="24px",
font_size="16px",
font_weight="700",
),
display="flex",
width="100%",
justify_content="space-between",
align_items="center",
margin_bottom="16px",
),
rx.hstack(
rx.box(
"需求梳理",
padding="10px 16px",
background_color="var(--devui-dividing-line)",
border_radius="var(--devui-border-radius-full)",
font_size="var(--devui-font-size)",
color="var(--devui-aide-text)",
cursor="pointer",
),
display="flex",
flex_wrap="wrap",
align_items="center",
gap="12px",
),
width="100%",
padding="24px",
background_color="var(--devui-base-bg)",
border_radius="24px",
),
display="flex",
flex_direction="column",
width="100%",
min_height="0",
margin="auto 0",
align_items="center",
gap="24px",
color="var(--prismui-color-text)",
),
display="flex",
flex="1",
flex_direction="column",
width="100%",
max_width="1200px",
padding="0 12px",
justify_content="flex-start",
gap="24px",
overflow="auto",
style={
"&::-webkit-scrollbar": {
"display": "none",
},
},
)
def render_reasoning_thought_node(thought_node_id: int, thought_node: ThoughtNode):
"""
渲染思考节点
:param thought_node_id: 思考节点唯一标识
:param thought_node: 思考节点实例
:return: Component
"""
return rx.hstack(
rx.vstack(
rx.box(
width="8px",
height="8px",
background_color=rx.color("blue", 7),
),
rx.box(width="2px", flex=1, background_color=rx.color("blue", 3)),
align_items="center",
spacing="0",
height="100%",
),
rx.markdown(thought_node.content, color=rx.color("gray", 11), padding_y="4px"),
align_items="flex-start",
spacing="2",
width="100%",
key=thought_node_id,
)
def render_dialog_history_item(dialog_id: str, dialog: Dialog) -> rx.Component:
"""
渲染对话历史条目
:param run: 对话实例
:return: Component
"""
# 推理状态
is_thinking = dialog.is_thinking
# 思考节点
thought_nodes = dialog.thought_nodes
# 推理折叠面板展开状态
is_reasoning_panel_open = dialog.is_collapse_expanded
return rx.vstack(
# 渲染问题和再次发送按钮
rx.hstack(
rx.spacer(),
rx.vstack(
# 渲染问题
rx.markdown(
dialog.question,
max_width="600px",
padding="12px 16px",
background_color="var(--devui-global-bg)",
border_radius="12px",
word_wrap="break-word",
word_break="break-all",
color="var(--prismui-color-text)",
style={
"& p.rt-Text": {
"padding": "0",
"margin": "0 !important",
},
},
),
# 再次发送按钮
rx.hstack(
rx.box(
rx.icon(
"rotate-ccw",
width="14px",
height="14px",
color="var(--devui-text-weak)",
),
padding="4px",
border_radius="4px",
cursor="pointer",
),
width="100%",
margin_top="8px",
),
),
width="100%",
margin_top="8px",
align_items="flex-start",
gap="4px",
font_size="var(--devui-font-size)",
),
# 渲染智能体思考链面板和回答
rx.vstack(
rx.auto_scroll(
rx.foreach(
thought_nodes,
lambda i, _: render_reasoning_thought_node(i[0], i[1]),
),
width="100%",
padding="0 12px",
),
# 渲染回答
rx.markdown(
dialog.answer,
display="inline-block",
max_width="85%",
padding_x="1.25em",
padding_y="0.5em",
margin_left="auto",
margin_bottom="8px",
background_color=rx.color("gray", 2),
border_radius="12px",
color="var(--prismui-color-text)",
),
text_align="left",
width="100%",
margin_bottom="8px",
),
width="100%",
key=dialog_id,
)
def render_dialog_history() -> rx.Component:
"""
渲染对话历史
:return: Component
"""
# 获取当前会话的对话历史
dialog_history = ConversationState.dialog_history
# 若运行字典为空则渲染品牌图标、名称和介绍、预设用户提示词等,否则遍历渲染运行项
return rx.cond(
dialog_history.length() == 0,
# 渲染问候
render_greeting(),
# 遍历渲染对话项
rx.vstack(
rx.auto_scroll(
rx.foreach(
dialog_history,
lambda i, _: render_dialog_history_item(i[0], i[1]),
),
width="100%",
padding="0 12px",
),
flex="1",
width="100%",
max_width="1200px",
margin_x="auto",
padding_top="20px",
justify_content="flex-start",
overflow_x="hidden",
overflow_y="auto",
),
)
def render_create_conversation_button() -> rx.Component:
"""
渲染新建对话按钮
:return: Component
"""
return rx.hstack(
rx.el.style(
"""
.rt-TooltipArrow polygon {
fill: #ffffff !important;
}
.rt-TooltipText {
color: var(--prismui-color-text) !important;
opacity: 1 !important;
}
"""
),
rx.spacer(),
rx.dialog.root(
# 触发事件:点击图标
rx.dialog.trigger(
rx.box(
rx.tooltip(
rx.icon(
"plus",
size=14,
color="var(--prismui-color-text)",
style={
"_hover": {
"color": "var(--devui-brand)",
}
},
),
content="新建对话",
background_color="#ffffff",
box_shadow="0 2px 12px 0 rgba(37, 43, 58, .24)",
color="#252b3a",
side="top",
side_offset=9,
),
display="flex",
width="24px",
height="24px",
background_color="var(--devui-base-bg)",
box_shadow="0 1px 8px #1919190f",
border_radius="var(--devui-border-radius-full)",
justify_content="center",
align_items="center",
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,
),
background_color=rx.color("mauve", 1),
),
open=CreateConversationState.is_modal_open,
on_open_change=CreateConversationState.toggle_modal,
),
display="flex",
width="100%",
max_width="1200px",
height="39px",
padding="0 12px",
justify_content="flex-end",
align_items="center",
gap="4px",
)
def render_input() -> rx.Component:
"""
渲染输入框
:return: Component
"""
return rx.vstack(
# 渲染自定义输入组件
rx.form(
rx.box( rx.box(
rx.vstack( rx.vstack(
# 渲染对话框 rx.hstack(
render_conversation_box(), rx.text_area(
name="question",
placeholder="请输入您的问题按Enter换行",
vertical_align="middle",
width="100%",
height="64px",
padding="4px 0",
background_color="var(--devui-base-bg)",
font_size="var(--devui-font-size)",
color="var(--prismui-color-text)",
style={
"border": "none !important",
"outline": "none !important",
"boxShadow": "none !important",
},
),
width="100%",
padding="0 16px",
),
rx.hstack(
rx.spacer(),
rx.button(
rx.icon(
"send",
margin_right="4px",
width="12px",
height="12px",
),
rx.text("发送"),
position="relative",
display="inline-flex",
padding="0 12px",
background_color="var(--devui-primary)",
border="none",
border_radius="20px",
align_items="center",
justify_content="center",
white_space="nowrap",
inline_height="1.5",
font_size="var(--devui-font-size)",
color="var(--devui-light-text)",
overflow="hidden",
cursor="pointer",
type="submit",
loading=ConversationState.running_status,
disabled=ConversationState.running_status,
),
width="100%",
justify_content="flex-end",
align_items="center",
height="32px",
padding="0 16px",
),
),
style={
"* textarea::placeholder": {
"fontFamily": "var(--font-family)",
"fontSize": "var(--devui-font-size)",
"color": "var(--placeholder)",
"opacity": 1,
},
},
),
display="flex",
flex_direction="column",
width="100%",
padding="12px 0",
background_color="var(--devui-base-bg)",
border="none",
border_radius="16px",
box_shadow="0 1px 8px 0 var(--mc-box-shadow)",
on_submit=ConversationState.run,
reset_on_submit=True,
),
# 渲染底部文案
rx.text(
"内容由大模型生成,无法确保准确性和完整性,仅供参考",
margin_top="8px",
text_align="center",
font_size="12px",
color="var(--devui-aide-text)",
),
width="100%",
max_width="1200px",
padding="0 12px 12px",
align_items="center",
)
def render_conversation_workplace() -> rx.Component:
"""
渲染会话工作区
:return: Component
"""
return rx.box(
rx.vstack(
# 渲染对话历史
render_dialog_history(),
# 渲染新建对话按钮 # 渲染新建对话按钮
render_create_conversation_button(), render_create_conversation_button(),
# 渲染输入框 # 渲染输入框
render_input_box(), render_input(),
display="flex", display="flex",
flex_flow="column", flex_flow="column",
width="100%", width="100%",
@ -95,19 +678,20 @@ def conversation() -> rx.Component:
width="0", width="0",
height="100%", height="100%",
background="linear-gradient(180deg, #fffffff2, #f8fafff2 99%)", background="linear-gradient(180deg, #fffffff2, #f8fafff2 99%)",
), )
position="relative",
display="flex",
flex="1", def render_conversation_history_collapse_button(
height="100%", is_collapse_expanded: bool,
min_height="0", ) -> rx.Component:
overflow="hidden", """
transition="all 0.3s ease-in-out", 渲染会话历史折叠面板的打开/关闭按钮
), :param is_collapse_expanded: 会话历史折叠面板展开状态
# 渲染打开/关闭对话历史折叠面板按钮 :return: Component
rx.button( """
return rx.button(
rx.cond( rx.cond(
is_open, is_collapse_expanded,
rx.icon( rx.icon(
"chevron-left", "chevron-left",
width="16px", width="16px",
@ -123,21 +707,49 @@ def conversation() -> rx.Component:
), ),
position="absolute", position="absolute",
top="50%", top="50%",
left=rx.cond(is_open, "calc(clamp(240px, 25%, 380px) - 8px)", "0"), left=rx.cond(is_collapse_expanded, "calc(clamp(240px, 25%, 380px) - 8px)", "0"),
z_index=99, z_index=99,
width="16px", width="16px",
height="40px", height="40px",
background="var(--devui-base-bg)", background="var(--prismui-background-color)",
box_shadow="var(--mc-float-block-shadow)", box_shadow="2px 0 4px 0 var(--mc-float-block-shadow)",
border_radius=rx.cond(is_open, "6px", "0 6px 6px 0"), border_radius=rx.cond(is_collapse_expanded, "6px", "0 6px 6px 0"),
transition="all 0.3s ease-in-out", transition="all 0.3s ease-in-out",
cursor="pointer", cursor="pointer",
# 点击事件:打开/关闭对话历史折叠面板 # 点击事件:打开/关闭对话历史折叠面板
on_click=ConversationHistoryCollapseState.toggle, on_click=ConversationHistoryState.toggle_collapse,
)
def render_conversation() -> rx.Component:
"""
渲染会话页面布局参考 MetaChat左侧为会话历史右侧为对话历史和输入框
"""
# 会话历史折叠面板展开状态
is_collapse_expanded = ConversationHistoryState.is_collapse_expanded
return rx.box(
# 渲染对话历史折叠面板
rx.hstack(
# 渲染对话历史折叠面板
render_conversation_history_collapse(is_collapse_expanded),
# 渲染会话工作区
render_conversation_workplace(),
position="relative",
display="flex",
flex="1",
height="100%",
min_height="0",
overflow="hidden",
transition="all 0.3s ease-in-out",
), ),
# 渲染对话历史折叠面板打开/关闭按钮
render_conversation_history_collapse_button(is_collapse_expanded),
position="relative", position="relative",
width="100%", width="100%",
height="100%", height="100%",
border_radius="12px", border_radius="12px",
overflow="hidden", # 溢出隐藏 overflow="hidden",
# 挂载事件:初始化会话历史
on_mount=ConversationState.on_mount,
) )

View File

@ -5,11 +5,12 @@
import reflex as rx import reflex as rx
from application.components import sidebar from application.components import sidebar
from application.pages import conversation, library from application.pages import render_conversation, render_library
from application.pages.conversation import render_conversation
from application.states import AuthState, SidebarState from application.states import AuthState, SidebarState
def index() -> rx.Component: def render_index() -> rx.Component:
""" """
首页 首页
""" """
@ -21,9 +22,9 @@ def index() -> rx.Component:
rx.match( rx.match(
SidebarState.get_activated_button, SidebarState.get_activated_button,
# 渲染知识库页面 # 渲染知识库页面
("library", library()), ("library", render_library()),
# 渲染会话页面 # 渲染会话页面
conversation(), render_conversation(),
), ),
width="100%", width="100%",
height="100vh", height="100vh",
@ -32,5 +33,5 @@ def index() -> rx.Component:
box_sizing="border-box", box_sizing="border-box",
background="var(--mc-global-bg)", background="var(--mc-global-bg)",
# 挂载事件:因测试需模拟已认证 # 挂载事件:因测试需模拟已认证
on_mount=AuthState.authenticate(), on_mount=AuthState.authenticate,
) )

View File

@ -5,7 +5,7 @@
import reflex as rx import reflex as rx
def library() -> rx.Component: def render_library() -> rx.Component:
""" """
知识库页面 知识库页面
""" """

View File

@ -1,27 +1,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from application.states.auth import AuthState from application.states.auth import AuthState
from application.states.conversation import ConversationState from application.states.conversation import ConversationState
from application.states.create_conversation import CreateConversationState
from application.states.conversation_history import ConversationHistoryState from application.states.conversation_history import ConversationHistoryState
from application.states.database import ( from application.states.create_conversation import CreateConversationState
DatabaseState, from application.states.database import DatabaseState
Conversations,
ConversationMemory as Conversation,
DialogMemory as Dialog,
ThoughtNodeMemory as ThoughtNode,
)
from application.states.sidebar import SidebarState from application.states.sidebar import SidebarState
__all__ = [ __all__ = [
"AuthState", "AuthState",
"ConversationState",
"Conversations",
"ConversationHistoryState", "ConversationHistoryState",
"ConversationState",
"CreateConversationState", "CreateConversationState",
"DatabaseState", "DatabaseState",
"SidebarState", "SidebarState",
"Conversation",
"Dialog",
"ThoughtNode",
] ]

View File

@ -11,7 +11,7 @@ class AuthState(rx.State):
""" """
# 当前登录的用户唯一标识(通过本地存储同步) # 当前登录的用户唯一标识(通过本地存储同步)
user_id = rx.LocalStorage("user_id", sync=True) user_id: str = rx.LocalStorage("user_id", sync=True)
@rx.event @rx.event
async def authenticate(self) -> None: async def authenticate(self) -> None:

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
智能体状态 会话状态
""" """
from typing import Any, AsyncGenerator, Dict from typing import Any, AsyncGenerator, Dict
from typing import Dict, List from typing import Dict
from pydantic_ai import Agent, ThinkingPartDelta from pydantic_ai import Agent, ThinkingPartDelta
from pydantic_ai.messages import ( from pydantic_ai.messages import (
@ -24,15 +24,10 @@ from pydantic_ai.providers.openai import OpenAIProvider
from pydantic_ai.run import AgentRunResultEvent from pydantic_ai.run import AgentRunResultEvent
import reflex as rx import reflex as rx
from application.states import ( from application.models import Conversation, Dialog, ThoughtNode
AuthState, from application.states.auth import AuthState
Conversation, from application.states.create_conversation import CreateConversationState
Conversations, from application.states.database import DatabaseState
CreateConversationState,
DatabaseState,
Dialog,
ThoughtNode,
)
instructions: str = """ instructions: str = """
@ -74,7 +69,7 @@ class ConversationState(rx.State):
# 用户唯一标识 # 用户唯一标识
user_id: str = "" user_id: str = ""
# 会话字典 # 会话字典
conversations: Conversations = {} conversations: Dict[str, Conversation] = {}
# 当前会话唯一标识 # 当前会话唯一标识
conversation_id: str = "" conversation_id: str = ""
@ -96,7 +91,6 @@ class ConversationState(rx.State):
self.conversations = await database_state.get_conversations( self.conversations = await database_state.get_conversations(
user_id=self.user_id user_id=self.user_id
) )
# 若会话字典为空则新建会话 # 若会话字典为空则新建会话
if not self.conversations: if not self.conversations:
# 先在数据库新建会话,再在会话字典新建会话 # 先在数据库新建会话,再在会话字典新建会话
@ -109,7 +103,7 @@ class ConversationState(rx.State):
self.conversation_id = next(reversed(self.conversations.keys())) self.conversation_id = next(reversed(self.conversations.keys()))
@rx.var @rx.var
def conversation_history(self) -> Conversations: def conversation_history(self) -> Dict[str, Conversation]:
""" """
获取会话历史用于前端渲染会话历史 获取会话历史用于前端渲染会话历史
:return: 会话历史 :return: 会话历史
@ -384,6 +378,12 @@ class ConversationState(rx.State):
# ========== 智能体运行结果事件 ========== # ========== 智能体运行结果事件 ==========
case AgentRunResultEvent(result=result): case AgentRunResultEvent(result=result):
# 完成对话
await database_state.complete_dialog(
dialog_id=dialog_id,
thought_nodes=dialog.thought_nodes,
answer=dialog.answer,
)
# 保存新增消息 # 保存新增消息
await database_state.save_new_messages( await database_state.save_new_messages(
conversation_id=self.conversation_id, conversation_id=self.conversation_id,
@ -401,4 +401,4 @@ class ConversationState(rx.State):
""" """
# 指定运行 # 指定运行
dialog = self.conversations[self.conversation_id].dialogs[dialog_id] dialog = self.conversations[self.conversation_id].dialogs[dialog_id]
dialog.is_collapse_open = not dialog.is_collapse_open dialog.is_collapse_expanded = not dialog.is_collapse_expanded

View File

@ -10,8 +10,8 @@ class ConversationHistoryState(rx.State):
会话历史状态 会话历史状态
""" """
# 会话历史折叠面板打开状态True表示打开False表示关闭 # 会话历史折叠面板展开状态True表示展开False表示收起
is_collapse_open: bool = False is_collapse_expanded: bool = False
@rx.event @rx.event
def toggle_collapse(self) -> None: def toggle_collapse(self) -> None:
@ -19,4 +19,4 @@ class ConversationHistoryState(rx.State):
打开/关闭会话历史折叠面板 打开/关闭会话历史折叠面板
:return: None :return: None
""" """
self.is_collapse_open = not self.is_collapse_open self.is_collapse_expanded = not self.is_collapse_expanded

View File

@ -2,66 +2,27 @@
""" """
数据库状态 数据库状态
""" """
from datetime import datetime from datetime import datetime
from typing import Dict, List, cast, TypeAlias from typing import Dict, List, cast
from pydantic import BaseModel, Field, TypeAdapter from pydantic import TypeAdapter
from pydantic_ai import ModelMessage, ModelMessagesTypeAdapter from pydantic_ai import ModelMessage, ModelMessagesTypeAdapter
import reflex as rx import reflex as rx
from sqlalchemy import ColumnElement, desc from sqlalchemy import ColumnElement, desc
from sqlmodel import select from sqlmodel import select
from application.models import Conversation, Dialog, RunResult from application.models import (
Conversation,
Conversations,
class ConversationMemory(BaseModel): Dialog,
""" Dialogs,
会话内存模型 RunResults,
""" ThoughtNode,
description: str = Field(default="新会话", description="会话描述")
is_running: bool = Field(
default=False, description="运行状态True 表示运行中False 表示运行完成"
) )
dialogs: Dict[str, DialogMemory] = Field(
default_factory=dict, description="对话字典"
)
created_at: datetime = Field(default_factory=datetime.now, description="创建时间")
class DialogMemory(BaseModel):
"""
对话内存模型
"""
question: str = Field(..., description="问题")
is_thinking: bool = Field(
default=False, description="思考状态True 表示思考中False 表示思考完成"
)
is_collapse_expanded: bool = Field(
default=False, description="思考折叠面板展开状态True 表示展开False 表示折叠"
)
thought_nodes: list[ThoughtNodeMemory] = Field(
default_factory=list, description="思考节点列表"
)
answer: str = Field(default="", description="回答")
class ThoughtNodeMemory(BaseModel):
"""
思考节点内存模型
"""
kind: str
content: str
# 会话字典类型别名
Conversations: TypeAlias = Dict[str, ConversationMemory]
# 思考节点内存模型类型适配器 # 思考节点内存模型类型适配器
ThoughtNodeMemoryTypeAdapter = TypeAdapter(list[ThoughtNodeMemory]) ThoughtNodeTypeAdapter = TypeAdapter(dict[int, ThoughtNode])
class DatabaseState(rx.State): class DatabaseState(rx.State):
@ -69,33 +30,33 @@ class DatabaseState(rx.State):
数据库状态 数据库状态
""" """
async def get_conversations(self, user_id: str) -> Conversations: async def get_conversations(self, user_id: str) -> Dict[str, Conversation]:
""" """
获取会话字典 获取会话字典
:param user_id: 用户唯一标识 :param user_id: 用户唯一标识
:return: 会话字典 :return: 会话字典
""" """
conversations: Conversations = {} conversations: Dict[str, Conversation] = {}
async with rx.asession() as session: async with rx.asession() as session:
result = await session.exec( result = await session.exec(
select(Conversation, Dialog) select(Conversations, Dialogs)
.outerjoin( .outerjoin(
Dialog, Dialogs,
cast( cast(
ColumnElement[bool], ColumnElement[bool],
Conversation.id == Dialog.conversation_id, Conversations.id == Dialogs.conversation_id,
), ),
) )
.where( .where(
Conversation.user_id == user_id, Conversations.user_id == user_id,
Conversation.is_deleted == False, Conversations.is_deleted == False,
) )
.order_by(Conversation.id, Dialog.id) .order_by(Conversations.id, Dialogs.id)
) )
for conversation, dialog in result.all(): for conversation, dialog in result.all():
conversation_memory = conversations.setdefault( conversation_memory = conversations.setdefault(
conversation.id, conversation.id,
ConversationMemory( Conversation(
description=conversation.description, description=conversation.description,
created_at=conversation.created_at, created_at=conversation.created_at,
), ),
@ -103,11 +64,11 @@ class DatabaseState(rx.State):
if not dialog: if not dialog:
continue continue
conversation_memory.dialogs[dialog.id] = DialogMemory( conversation_memory.dialogs[dialog.id] = Dialog(
question=dialog.question, question=dialog.question,
thought_nodes=ThoughtNodeMemoryTypeAdapter.validate_python( thought_nodes=ThoughtNodeTypeAdapter.validate_python(
dialog.thought_nodes dialog.thought_nodes
), # 反序列化为思考节点内存实例列表 ), # 反序列化为思考节点内存实例字典
answer=dialog.answer, answer=dialog.answer,
) )
return conversations return conversations
@ -122,7 +83,7 @@ class DatabaseState(rx.State):
:return: 新建会话唯一标识创建时间 :return: 新建会话唯一标识创建时间
""" """
async with rx.asession() as session: async with rx.asession() as session:
conversation = Conversation(user_id=user_id, description=description) conversation = Conversations(user_id=user_id, description=description)
session.add(conversation) session.add(conversation)
await session.commit() await session.commit()
await session.refresh(conversation) await session.refresh(conversation)
@ -135,7 +96,7 @@ class DatabaseState(rx.State):
:return: None :return: None
""" """
async with rx.asession() as session: async with rx.asession() as session:
conversation = await session.get(Conversation, conversation_id) conversation = await session.get(Conversations, conversation_id)
if not conversation: if not conversation:
return return
conversation.is_deleted = True conversation.is_deleted = True
@ -149,7 +110,7 @@ class DatabaseState(rx.State):
:return: 新建对话唯一标识 :return: 新建对话唯一标识
""" """
async with rx.asession() as session: async with rx.asession() as session:
dialog = Dialog( dialog = Dialogs(
conversation_id=conversation_id, conversation_id=conversation_id,
question=question, question=question,
) )
@ -161,7 +122,7 @@ class DatabaseState(rx.State):
async def complete_dialog( async def complete_dialog(
self, self,
dialog_id: str, dialog_id: str,
thought_nodes: list[ThoughtNodeMemory], thought_nodes: dict[int, ThoughtNode],
answer: str, answer: str,
) -> None: ) -> None:
""" """
@ -172,11 +133,11 @@ class DatabaseState(rx.State):
:return: None :return: None
""" """
async with rx.asession() as session: async with rx.asession() as session:
dialog = await session.get(Dialog, dialog_id) dialog = await session.get(Dialogs, dialog_id)
if not dialog: if not dialog:
return return
dialog.thought_nodes = ThoughtNodeMemoryTypeAdapter.dump_python( dialog.thought_nodes = ThoughtNodeTypeAdapter.dump_python(
thought_nodes {k: v for k, v in thought_nodes.items()}
) )
dialog.answer = answer dialog.answer = answer
await session.commit() await session.commit()
@ -190,9 +151,9 @@ class DatabaseState(rx.State):
message_history: List[ModelMessage] = [] message_history: List[ModelMessage] = []
async with rx.asession() as session: async with rx.asession() as session:
result = await session.exec( result = await session.exec(
select(RunResult) select(RunResults)
.where(RunResult.conversation_id == conversation_id) .where(RunResults.conversation_id == conversation_id)
.order_by(desc(RunResult.dialog_id)) .order_by(desc(RunResults.dialog_id))
) )
for record in result.all(): for record in result.all():
message_history.extend( message_history.extend(
@ -215,7 +176,7 @@ class DatabaseState(rx.State):
""" """
async with rx.asession() as session: async with rx.asession() as session:
session.add( session.add(
RunResult( RunResults(
conversation_id=conversation_id, conversation_id=conversation_id,
dialog_id=dialog_id, dialog_id=dialog_id,
new_messages=ModelMessagesTypeAdapter.dump_json( new_messages=ModelMessagesTypeAdapter.dump_json(

View File

@ -4,7 +4,7 @@
""" """
import reflex as rx import reflex as rx
from application.states import AuthState from application.states.auth import AuthState
class SidebarState(rx.State): class SidebarState(rx.State):
@ -24,7 +24,7 @@ class SidebarState(rx.State):
return f"sidebar_{user_id}" return f"sidebar_{user_id}"
# 侧边栏中激活的图标导航按钮 # 侧边栏中激活的图标导航按钮
activated_button = rx.LocalStorage(generate_storage_key, sync=True) activated_button: str = rx.LocalStorage(generate_storage_key, sync=True)
@rx.var @rx.var
def get_activated_button(self) -> str: def get_activated_button(self) -> str:

View File

@ -1 +1,43 @@
{"name": "reflex", "type": "module", "scripts": {"dev": "react-router dev --host", "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", "lucide-react": "1.14.0", "react": "19.2.6", "react-dom": "19.2.6", "react-error-boundary": "6.1.1", "react-helmet": "6.1.0", "react-markdown": "10.1.0", "react-router": "7.15.0", "react-router-dom": "7.15.0", "react-syntax-highlighter": "16.1.1", "rehype-katex": "7.0.1", "rehype-raw": "7.0.0", "rehype-unwrap-images": "1.0.0", "remark-gfm": "4.0.1", "remark-math": "6.0.0", "socket.io-client": "4.8.3", "sonner": "2.0.7", "universal-cookie": "7.2.2"}, "devDependencies": {"@emotion/react": "11.14.0", "@react-router/dev": "7.15.0", "@react-router/fs-routes": "7.15.0", "autoprefixer": "10.5.0", "postcss": "8.5.14", "postcss-import": "16.1.1", "vite": "8.0.16"}, "overrides": {"cookie": "1.1.1"}} {
"name": "reflex",
"type": "module",
"scripts": {
"dev": "react-router dev --host",
"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",
"lucide-react": "1.14.0",
"react": "19.2.6",
"react-dom": "19.2.6",
"react-error-boundary": "6.1.1",
"react-helmet": "6.1.0",
"react-markdown": "10.1.0",
"react-router": "7.15.0",
"react-router-dom": "7.15.0",
"react-syntax-highlighter": "16.1.1",
"rehype-katex": "7.0.1",
"rehype-raw": "7.0.0",
"rehype-unwrap-images": "1.0.0",
"remark-gfm": "4.0.1",
"remark-math": "6.0.0",
"socket.io-client": "4.8.3",
"sonner": "2.0.7",
"universal-cookie": "7.2.2"
},
"devDependencies": {
"@emotion/react": "11.14.0",
"@react-router/dev": "7.15.0",
"@react-router/fs-routes": "7.15.0",
"autoprefixer": "10.5.0",
"postcss": "8.5.14",
"postcss-import": "16.1.1",
"vite": "8.0.16"
},
"overrides": {
"cookie": "1.1.1"
}
}