This commit is contained in:
parent
cd08996e16
commit
bd1e5912a5
|
|
@ -20,18 +20,22 @@ app = rx.App(
|
||||||
"*::before, *::after": {
|
"*::before, *::after": {
|
||||||
"box_sizing": "border-box",
|
"box_sizing": "border-box",
|
||||||
},
|
},
|
||||||
"--prismui-background-color-1": "#ffffff",
|
"--prismui-background-color-1": "#ffffff", # 纯白背景色
|
||||||
"--prismui-background-color-2": "rgb(235, 235, 235)",
|
"--prismui-background-color-2": "rgb(235, 235, 235)",
|
||||||
"--prismui-background-color-3": "#f6f6f8",
|
"--prismui-background-color-3": "#f6f6f8",
|
||||||
|
"--prismui-background-color-4": "#babbc0",
|
||||||
|
"--prismui-background-1": "linear-gradient(to bottom, #D0C9FF 0%, #E6D6F0 8%, #F1DBEA 12%, #C8DCFB 40%, #ABC6F6 60%, #87AEFE 90%)",
|
||||||
"--mc-float-block-shadow": "rgba(213, 213, 213, 0.25)",
|
"--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)",
|
||||||
"--devui-shadow-length-connected-overlay": "0px 0px 12px 0px rgba(0, 0, 0, 0.1)",
|
"--devui-shadow-length-connected-overlay": "0px 0px 12px 0px rgba(0, 0, 0, 0.1)",
|
||||||
|
"--prismui-box-shadow-1": "0 4px 12px #0000001a", # 导航按钮阴影
|
||||||
"--devui-icon-fill-weak": "#babbc0",
|
"--devui-icon-fill-weak": "#babbc0",
|
||||||
"--prismui-background-color-weak": "#babbc0",
|
"--prismui-background-color-weak": "#babbc0",
|
||||||
"--devui-border-radius-feedback": "4px",
|
"--devui-border-radius-feedback": "4px",
|
||||||
"--prismui-border-radius": "8px",
|
"--prismui-border-radius": "8px",
|
||||||
|
"--prismui-border-radius-1": "8px",
|
||||||
"--devui-border-radius-full": "100px",
|
"--devui-border-radius-full": "100px",
|
||||||
"--font-family": "HuaweiFont,Helvetica,Arial,PingFangSC-Regular,Hiragino Sans GB,Microsoft YaHei,微软雅黑,Microsoft JhengHei",
|
"--font-family": "HuaweiFont,Helvetica,Arial,PingFangSC-Regular,Hiragino Sans GB,Microsoft YaHei,微软雅黑,Microsoft JhengHei",
|
||||||
"--prismui-font-size": "14px",
|
"--prismui-font-size": "14px",
|
||||||
|
|
@ -43,7 +47,7 @@ app = rx.App(
|
||||||
"--devui-font-size-lg": "14px",
|
"--devui-font-size-lg": "14px",
|
||||||
"--prismui-color-text": "#252b3a",
|
"--prismui-color-text": "#252b3a",
|
||||||
"--prismui-color-text-light": "#babbc0",
|
"--prismui-color-text-light": "#babbc0",
|
||||||
"--prismui-color-1": "#252b3a",
|
"--prismui-color-1": "#252b3a", # 文本颜色
|
||||||
"--prismui-color-2": "#575d6c",
|
"--prismui-color-2": "#575d6c",
|
||||||
"--prismui-color-3": "#babbc0",
|
"--prismui-color-3": "#babbc0",
|
||||||
"--devui-text-weak": "#575d6c",
|
"--devui-text-weak": "#575d6c",
|
||||||
|
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
渲染框架相关组件
|
|
||||||
"""
|
|
||||||
|
|
||||||
import reflex as rx
|
|
||||||
|
|
||||||
from application.states import SidebarState
|
|
||||||
|
|
||||||
# 侧边栏图标导航按钮字典
|
|
||||||
buttons = {
|
|
||||||
"conversation": {"src": "/conversation.svg", "text": "会话"},
|
|
||||||
"library": {"src": "/library.svg", "text": "知识库"},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def sidebar() -> rx.Component:
|
|
||||||
"""
|
|
||||||
侧边栏
|
|
||||||
"""
|
|
||||||
|
|
||||||
def button(
|
|
||||||
button: str,
|
|
||||||
) -> rx.Component:
|
|
||||||
"""
|
|
||||||
侧边栏图标导航按钮
|
|
||||||
:param button: 指定侧边栏图标导航按钮
|
|
||||||
"""
|
|
||||||
# 指定按钮的激活状态
|
|
||||||
is_actived = button == SidebarState.get_activated_button
|
|
||||||
|
|
||||||
return rx.box(
|
|
||||||
# 渲染图标容器
|
|
||||||
rx.box(
|
|
||||||
# 渲染图标
|
|
||||||
rx.image(
|
|
||||||
src=buttons[button]["src"],
|
|
||||||
width="24px",
|
|
||||||
height="24px",
|
|
||||||
object_fit="contain",
|
|
||||||
),
|
|
||||||
display="flex",
|
|
||||||
justify_content="center",
|
|
||||||
align_items="center",
|
|
||||||
width="36px",
|
|
||||||
height="36px",
|
|
||||||
border_radius="var(--prismui-border-radius)",
|
|
||||||
# 若激活则渲染背景颜色和阴影,否则不渲染
|
|
||||||
style={
|
|
||||||
"background-color": rx.cond(
|
|
||||||
is_actived, "var(--prismui-background-color)", "transparent"
|
|
||||||
),
|
|
||||||
"box-shadow": rx.cond(is_actived, "0 4px 12px #0000001a", "none"),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
# 渲染标签
|
|
||||||
rx.text(
|
|
||||||
buttons[button]["text"],
|
|
||||||
color="var(--prismui-color-text)",
|
|
||||||
font_size="var(--prismui-font-size-sm)",
|
|
||||||
line_height="20px",
|
|
||||||
),
|
|
||||||
display="flex",
|
|
||||||
flex_direction="column",
|
|
||||||
align_items="center",
|
|
||||||
gap="4px",
|
|
||||||
cursor="pointer",
|
|
||||||
# 点击事件:将指定图标导航按钮设置为侧边栏中激活的图标导航按钮
|
|
||||||
on_click=SidebarState.switch_activated_button(button),
|
|
||||||
)
|
|
||||||
|
|
||||||
return rx.vstack(
|
|
||||||
rx.vstack(
|
|
||||||
rx.vstack(
|
|
||||||
# 品牌图标
|
|
||||||
rx.image(
|
|
||||||
src="/logo.png",
|
|
||||||
width="34px",
|
|
||||||
height="34px",
|
|
||||||
object_fit="contain",
|
|
||||||
),
|
|
||||||
# 品牌名称
|
|
||||||
rx.text(
|
|
||||||
"棱镜球",
|
|
||||||
line_height="20px",
|
|
||||||
font_size="11px",
|
|
||||||
font_weight="700",
|
|
||||||
color="var(--prismui-color-text)",
|
|
||||||
),
|
|
||||||
display="flex",
|
|
||||||
flex_direction="column",
|
|
||||||
justify_content="center",
|
|
||||||
align_items="center",
|
|
||||||
gap="4px",
|
|
||||||
),
|
|
||||||
# 渲染分隔线
|
|
||||||
rx.divider(
|
|
||||||
width="32px",
|
|
||||||
height="1px",
|
|
||||||
margin="16px 0",
|
|
||||||
background_color="var(--prismui-background-color-weak)",
|
|
||||||
),
|
|
||||||
# 渲染侧边栏中会话图标导航按钮
|
|
||||||
button("conversation"),
|
|
||||||
# 渲染侧边栏中知识库图标导航按钮
|
|
||||||
button("library"),
|
|
||||||
display="flex",
|
|
||||||
flex_direction="column",
|
|
||||||
align_items="center",
|
|
||||||
width="100%",
|
|
||||||
margin_top="12px",
|
|
||||||
),
|
|
||||||
display="flex",
|
|
||||||
flex_direction="column",
|
|
||||||
justify_content="space-between",
|
|
||||||
align_items="center",
|
|
||||||
width="60px",
|
|
||||||
height="100%",
|
|
||||||
box_sizing="border-box",
|
|
||||||
)
|
|
||||||
|
|
@ -6,7 +6,7 @@ import reflex as rx
|
||||||
|
|
||||||
from application.models import Conversation, Dialog, ThoughtNode
|
from application.models import Conversation, Dialog, ThoughtNode
|
||||||
from application.states import (
|
from application.states import (
|
||||||
ConversationHistoryState,
|
ConversationHistoryCollapseState,
|
||||||
ConversationState,
|
ConversationState,
|
||||||
CreateConversationState,
|
CreateConversationState,
|
||||||
)
|
)
|
||||||
|
|
@ -323,6 +323,7 @@ def render_thought_node(thought_node_id: int, thought_node: ThoughtNode):
|
||||||
"thinking",
|
"thinking",
|
||||||
rx.text(
|
rx.text(
|
||||||
thought_node.content,
|
thought_node.content,
|
||||||
|
line_height="22px",
|
||||||
font_size="var(--prismui-font-size-2)",
|
font_size="var(--prismui-font-size-2)",
|
||||||
color="var(--prismui-color-3)",
|
color="var(--prismui-color-3)",
|
||||||
),
|
),
|
||||||
|
|
@ -426,27 +427,28 @@ def render_dialog_history_item(dialog_id: str, dialog: Dialog) -> rx.Component:
|
||||||
on_click=lambda: ConversationState.toggle_collapse(dialog_id),
|
on_click=lambda: ConversationState.toggle_collapse(dialog_id),
|
||||||
),
|
),
|
||||||
rx.box(
|
rx.box(
|
||||||
rx.auto_scroll(
|
rx.box(
|
||||||
rx.foreach(
|
rx.auto_scroll(
|
||||||
thought_nodes,
|
rx.foreach(
|
||||||
lambda i, _: render_thought_node(i[0], i[1]),
|
thought_nodes,
|
||||||
|
lambda i, _: render_thought_node(i[0], i[1]),
|
||||||
|
),
|
||||||
|
width="100%",
|
||||||
|
margin_top="8px",
|
||||||
|
align_self="start",
|
||||||
),
|
),
|
||||||
width="100%",
|
style={"overflow": "hidden", "min-height": "0"},
|
||||||
margin_top="8px",
|
|
||||||
align_self="start",
|
|
||||||
),
|
),
|
||||||
width="100%",
|
width="100%",
|
||||||
overflow="hidden",
|
|
||||||
style={
|
style={
|
||||||
"max-height": rx.cond(is_collapse_expanded, "1200px", "20px"),
|
"display": "grid",
|
||||||
|
"overflow": "hidden",
|
||||||
|
"grid_template_rows": rx.cond(is_collapse_expanded, "1fr", "0fr"),
|
||||||
"opacity": rx.cond(is_collapse_expanded, "1", "0"),
|
"opacity": rx.cond(is_collapse_expanded, "1", "0"),
|
||||||
"transition": "max-height 0.2s ease-out, opacity 0.2s ease-out",
|
# 双属性同步过渡,和千问一致
|
||||||
|
"transition": "grid-template-rows 0.3s ease-out, opacity 0.3s ease-out",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
width="100%",
|
|
||||||
margin_top="8px",
|
|
||||||
text_align="left",
|
|
||||||
overflow="visible",
|
|
||||||
),
|
),
|
||||||
# 渲染回答
|
# 渲染回答
|
||||||
rx.markdown(
|
rx.markdown(
|
||||||
|
|
@ -744,16 +746,16 @@ def render_conversation_workplace() -> rx.Component:
|
||||||
|
|
||||||
|
|
||||||
def render_conversation_history_collapse_button(
|
def render_conversation_history_collapse_button(
|
||||||
is_collapse_expanded: bool,
|
is_expanded: bool,
|
||||||
) -> rx.Component:
|
) -> rx.Component:
|
||||||
"""
|
"""
|
||||||
渲染会话历史折叠面板的打开/关闭按钮
|
渲染会话历史折叠面板的打开/关闭按钮
|
||||||
:param is_collapse_expanded: 会话历史折叠面板展开状态
|
:param is_expanded: 会话历史折叠面板展开状态
|
||||||
:return: Component
|
:return: Component
|
||||||
"""
|
"""
|
||||||
return rx.button(
|
return rx.button(
|
||||||
rx.cond(
|
rx.cond(
|
||||||
is_collapse_expanded,
|
is_expanded,
|
||||||
rx.icon(
|
rx.icon(
|
||||||
"chevron-left",
|
"chevron-left",
|
||||||
width="16px",
|
width="16px",
|
||||||
|
|
@ -769,17 +771,17 @@ def render_conversation_history_collapse_button(
|
||||||
),
|
),
|
||||||
position="absolute",
|
position="absolute",
|
||||||
top="50%",
|
top="50%",
|
||||||
left=rx.cond(is_collapse_expanded, "calc(clamp(240px, 25%, 380px) - 8px)", "0"),
|
left=rx.cond(is_expanded, "calc(clamp(240px, 25%, 380px) - 8px)", "0"),
|
||||||
z_index=99,
|
z_index=99,
|
||||||
width="16px",
|
width="16px",
|
||||||
height="40px",
|
height="40px",
|
||||||
background="var(--prismui-background-color-1)",
|
background="var(--prismui-background-color-1)",
|
||||||
box_shadow="2px 0 4px 0 var(--mc-float-block-shadow)",
|
box_shadow="2px 0 4px 0 var(--mc-float-block-shadow)",
|
||||||
border_radius=rx.cond(is_collapse_expanded, "6px", "0 6px 6px 0"),
|
border_radius=rx.cond(is_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=ConversationHistoryState.toggle_collapse,
|
on_click=ConversationHistoryCollapseState.toggle_expanded,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -787,14 +789,14 @@ def render_conversation() -> rx.Component:
|
||||||
"""
|
"""
|
||||||
渲染会话页面:布局参考 MetaChat,左侧为会话历史,右侧为对话历史和输入框
|
渲染会话页面:布局参考 MetaChat,左侧为会话历史,右侧为对话历史和输入框
|
||||||
"""
|
"""
|
||||||
# 会话历史折叠面板展开状态
|
# 会话历史折叠面板的展开状态
|
||||||
is_collapse_expanded = ConversationHistoryState.is_collapse_expanded
|
is_expanded = ConversationHistoryCollapseState.is_expanded
|
||||||
|
|
||||||
return rx.box(
|
return rx.box(
|
||||||
# 渲染对话历史折叠面板
|
# 渲染折叠面板
|
||||||
rx.hstack(
|
rx.hstack(
|
||||||
# 渲染对话历史折叠面板
|
# 渲染折叠面板
|
||||||
render_conversation_history_collapse(is_collapse_expanded),
|
render_conversation_history_collapse(is_expanded),
|
||||||
# 渲染会话工作区
|
# 渲染会话工作区
|
||||||
render_conversation_workplace(),
|
render_conversation_workplace(),
|
||||||
position="relative",
|
position="relative",
|
||||||
|
|
@ -806,7 +808,7 @@ def render_conversation() -> rx.Component:
|
||||||
transition="all 0.3s ease-in-out",
|
transition="all 0.3s ease-in-out",
|
||||||
),
|
),
|
||||||
# 渲染对话历史折叠面板打开/关闭按钮
|
# 渲染对话历史折叠面板打开/关闭按钮
|
||||||
render_conversation_history_collapse_button(is_collapse_expanded),
|
render_conversation_history_collapse_button(is_expanded),
|
||||||
position="relative",
|
position="relative",
|
||||||
width="100%",
|
width="100%",
|
||||||
height="100%",
|
height="100%",
|
||||||
|
|
|
||||||
|
|
@ -4,34 +4,250 @@
|
||||||
"""
|
"""
|
||||||
import reflex as rx
|
import reflex as rx
|
||||||
|
|
||||||
from application.components import sidebar
|
|
||||||
from application.pages import render_conversation, render_library
|
from application.pages import render_conversation, render_library
|
||||||
from application.pages.conversation import render_conversation
|
from application.pages.conversation import render_conversation
|
||||||
from application.states import AuthState, SidebarState
|
from application.states import AuthState, SidebarState
|
||||||
|
|
||||||
|
|
||||||
|
# 导航按钮字典
|
||||||
|
nav_buttons: dict = {
|
||||||
|
"conversation": {"src": "/conversation.svg", "text": "会话"},
|
||||||
|
"library": {"src": "/library.svg", "text": "知识库"},
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
约定:
|
||||||
|
1 若组件仅用于当前页面则无需抽象为通用组件(application.components.*)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def render_nav_button(
|
||||||
|
nav_button: str,
|
||||||
|
) -> rx.Component:
|
||||||
|
"""
|
||||||
|
渲染导航按钮
|
||||||
|
:param nav_button: 指定的导航按钮
|
||||||
|
:return : Component
|
||||||
|
"""
|
||||||
|
# 导航按钮激活状态
|
||||||
|
is_actived = nav_button == SidebarState.activated_nav_button
|
||||||
|
|
||||||
|
return rx.vstack(
|
||||||
|
# 渲染图标
|
||||||
|
rx.image(
|
||||||
|
nav_buttons[nav_button]["src"],
|
||||||
|
width="36px",
|
||||||
|
height="36px",
|
||||||
|
object_fit="contain",
|
||||||
|
padding="6px",
|
||||||
|
background_color=rx.cond(
|
||||||
|
is_actived, "var(--prismui-background-color-1)", "transparent"
|
||||||
|
),
|
||||||
|
border_radius="var(--prismui-border-radius-1)",
|
||||||
|
box_shadow=rx.cond(is_actived, "var(--prismui-box-shadow-1)", "none"),
|
||||||
|
),
|
||||||
|
# 渲染标签
|
||||||
|
rx.text(
|
||||||
|
nav_buttons[nav_button]["text"],
|
||||||
|
line_height="20px",
|
||||||
|
color="var(--prismui-color-1)",
|
||||||
|
font_size="var(--prismui-font-size-2)",
|
||||||
|
),
|
||||||
|
align_items="center",
|
||||||
|
gap="4px",
|
||||||
|
cursor="pointer",
|
||||||
|
# 点击事件:将指定导航按钮设置为侧边栏中激活的导航按钮
|
||||||
|
on_click=SidebarState.switch_nav_button(nav_button),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def render_sidebar() -> rx.Component:
|
||||||
|
"""
|
||||||
|
渲染侧边栏
|
||||||
|
:return: Component
|
||||||
|
"""
|
||||||
|
|
||||||
|
return rx.vstack(
|
||||||
|
rx.vstack(
|
||||||
|
rx.vstack(
|
||||||
|
# 品牌图标
|
||||||
|
rx.image(
|
||||||
|
src="/logo.png",
|
||||||
|
width="34px",
|
||||||
|
height="34px",
|
||||||
|
object_fit="contain",
|
||||||
|
),
|
||||||
|
# 品牌名称
|
||||||
|
rx.text(
|
||||||
|
"棱镜球",
|
||||||
|
line_height="20px",
|
||||||
|
font_size="11px",
|
||||||
|
font_weight="var(--prismui-font-weight-1)",
|
||||||
|
color="var(--prismui-color-1)",
|
||||||
|
),
|
||||||
|
align_items="center",
|
||||||
|
gap="4px",
|
||||||
|
),
|
||||||
|
# 渲染分隔线
|
||||||
|
rx.divider(
|
||||||
|
width="32px",
|
||||||
|
height="1px",
|
||||||
|
margin="16px 0",
|
||||||
|
background_color="var(--prismui-background-color-4)",
|
||||||
|
),
|
||||||
|
rx.vstack(
|
||||||
|
# 渲染会话导航按钮
|
||||||
|
render_nav_button("conversation"),
|
||||||
|
# 渲染知识库导航按钮
|
||||||
|
render_nav_button("library"),
|
||||||
|
gap="8px",
|
||||||
|
),
|
||||||
|
align_items="center",
|
||||||
|
width="100%",
|
||||||
|
margin_top="12px",
|
||||||
|
),
|
||||||
|
flex_shrink="0",
|
||||||
|
justify_content="space-between",
|
||||||
|
align_items="center",
|
||||||
|
width="60px",
|
||||||
|
min_width="60px",
|
||||||
|
max_width="60px",
|
||||||
|
height="100%",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def render_login_modal() -> rx.Component:
|
||||||
|
"""
|
||||||
|
渲染登录模态窗
|
||||||
|
:return: Component
|
||||||
|
"""
|
||||||
|
|
||||||
|
return rx.center(
|
||||||
|
rx.hstack(
|
||||||
|
rx.vstack(
|
||||||
|
rx.text(
|
||||||
|
"登录",
|
||||||
|
font_size="20px",
|
||||||
|
font_weight="600",
|
||||||
|
margin_bottom="24px",
|
||||||
|
),
|
||||||
|
rx.input(
|
||||||
|
placeholder="请输入邮箱地址",
|
||||||
|
value=AuthState.email,
|
||||||
|
on_change=AuthState.set_email,
|
||||||
|
width="100%",
|
||||||
|
),
|
||||||
|
rx.hstack(
|
||||||
|
rx.input(
|
||||||
|
placeholder="请输入验证码",
|
||||||
|
value=AuthState.captcha,
|
||||||
|
on_change=AuthState.set_captcha,
|
||||||
|
flex=1,
|
||||||
|
),
|
||||||
|
rx.button(
|
||||||
|
"获取验证码",
|
||||||
|
variant="outline",
|
||||||
|
width="120px",
|
||||||
|
on_click=AuthState.send_email_code,
|
||||||
|
),
|
||||||
|
gap="8px",
|
||||||
|
width="100%",
|
||||||
|
),
|
||||||
|
rx.cond(
|
||||||
|
AuthState.email == "",
|
||||||
|
rx.text("请输入邮箱地址", color="red", font_size="12px"),
|
||||||
|
rx.box(height="16px"),
|
||||||
|
),
|
||||||
|
rx.hstack(
|
||||||
|
rx.checkbox(
|
||||||
|
checked=AuthState.agree_policy,
|
||||||
|
on_change=AuthState.toggle_agree,
|
||||||
|
),
|
||||||
|
rx.text("我已阅读并同意 ", font_size="13px"),
|
||||||
|
rx.link("用户协议、隐私政策", font_size="13px", href="#"),
|
||||||
|
align_items="center",
|
||||||
|
),
|
||||||
|
rx.button(
|
||||||
|
"登录",
|
||||||
|
width="100%",
|
||||||
|
bg="#0a0e29",
|
||||||
|
color="white",
|
||||||
|
margin_top="12px",
|
||||||
|
on_click=AuthState.login_by_email_code,
|
||||||
|
),
|
||||||
|
rx.text(
|
||||||
|
"登录有问题?去反馈",
|
||||||
|
font_size="12px",
|
||||||
|
color="#666",
|
||||||
|
margin_top="8px",
|
||||||
|
),
|
||||||
|
width="360px",
|
||||||
|
gap="16px",
|
||||||
|
padding="40px 32px",
|
||||||
|
align_items="start",
|
||||||
|
),
|
||||||
|
rx.divider(orientation="vertical"),
|
||||||
|
rx.vstack(
|
||||||
|
rx.image(
|
||||||
|
src="/qrcode.png",
|
||||||
|
width="160px",
|
||||||
|
height="160px",
|
||||||
|
padding="8px",
|
||||||
|
border="1px solid #eee",
|
||||||
|
border_radius="8px",
|
||||||
|
bg="white",
|
||||||
|
),
|
||||||
|
rx.text(
|
||||||
|
"添加微信申请使用",
|
||||||
|
font_size="18px",
|
||||||
|
font_weight="600",
|
||||||
|
margin_top="16px",
|
||||||
|
),
|
||||||
|
align_items="center",
|
||||||
|
width="320px",
|
||||||
|
padding="40px 32px",
|
||||||
|
style={"background": "#f8f9fc"},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
position="fixed",
|
||||||
|
top="0",
|
||||||
|
left="0",
|
||||||
|
width="100vw",
|
||||||
|
height="100vh",
|
||||||
|
bg="rgba(255, 255, 255, 0.85)",
|
||||||
|
z_index="9999",
|
||||||
|
overflow="hidden",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def render_index() -> rx.Component:
|
def render_index() -> rx.Component:
|
||||||
"""
|
"""
|
||||||
首页
|
首页
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return rx.hstack(
|
return rx.fragment(
|
||||||
# 渲染侧边栏
|
rx.hstack(
|
||||||
sidebar(),
|
# 渲染侧边栏
|
||||||
# 根据用户所点中的侧边栏图标导航按钮渲染相应页面
|
render_sidebar(),
|
||||||
rx.match(
|
# 根据侧边栏状态中激活的导航按钮渲染相应页面(本项目采用卡片布局)
|
||||||
SidebarState.get_activated_button,
|
rx.match(
|
||||||
# 渲染知识库页面
|
SidebarState.activated_nav_button,
|
||||||
("library", render_library()),
|
# 渲染知识库页面
|
||||||
# 渲染会话页面
|
("library", render_library()),
|
||||||
render_conversation(),
|
# 渲染会话页面
|
||||||
|
render_conversation(),
|
||||||
|
),
|
||||||
|
width="100%",
|
||||||
|
height="100vh",
|
||||||
|
overflow="auto",
|
||||||
|
padding="8px 8px 8px 0",
|
||||||
|
background="var(--prismui-background-1)",
|
||||||
|
# 挂载事件:
|
||||||
|
on_mount=AuthState.check,
|
||||||
|
),
|
||||||
|
rx.cond(
|
||||||
|
AuthState.user_id == "",
|
||||||
|
render_login_modal(),
|
||||||
|
rx.fragment(),
|
||||||
),
|
),
|
||||||
width="100%",
|
|
||||||
height="100vh",
|
|
||||||
padding="8px 8px 8px 0",
|
|
||||||
overflow="auto",
|
|
||||||
box_sizing="border-box",
|
|
||||||
background="var(--mc-global-bg)",
|
|
||||||
# 挂载事件:因测试需模拟已认证
|
|
||||||
on_mount=AuthState.authenticate,
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# -*- 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.conversation_history import ConversationHistoryState
|
from application.states.conversation_historycollapse import ConversationHistoryCollapseState
|
||||||
from application.states.create_conversation import CreateConversationState
|
from application.states.create_conversation import CreateConversationState
|
||||||
from application.states.database import DatabaseState
|
from application.states.database import DatabaseState
|
||||||
from application.states.sidebar import SidebarState
|
from application.states.sidebar import SidebarState
|
||||||
|
|
@ -9,7 +9,7 @@ from application.states.sidebar import SidebarState
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"AuthState",
|
"AuthState",
|
||||||
"ConversationHistoryState",
|
"ConversationHistoryCollapseState",
|
||||||
"ConversationState",
|
"ConversationState",
|
||||||
"CreateConversationState",
|
"CreateConversationState",
|
||||||
"DatabaseState",
|
"DatabaseState",
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,11 @@
|
||||||
"""
|
"""
|
||||||
import reflex as rx
|
import reflex as rx
|
||||||
|
|
||||||
|
import re
|
||||||
|
from sqlmodel import select
|
||||||
|
from application.models.user import EmailVerifyCode
|
||||||
|
from application.database.base import get_async_db
|
||||||
|
|
||||||
|
|
||||||
class AuthState(rx.State):
|
class AuthState(rx.State):
|
||||||
"""
|
"""
|
||||||
|
|
@ -13,10 +18,112 @@ class AuthState(rx.State):
|
||||||
# 当前登录的用户唯一标识(通过本地存储同步)
|
# 当前登录的用户唯一标识(通过本地存储同步)
|
||||||
user_id: str = rx.LocalStorage("user_id", sync=True)
|
user_id: str = rx.LocalStorage("user_id", sync=True)
|
||||||
|
|
||||||
|
email: str = ""
|
||||||
|
captcha: str = ""
|
||||||
|
is_policies_agreed: bool = False
|
||||||
|
|
||||||
@rx.event
|
@rx.event
|
||||||
async def authenticate(self) -> None:
|
async def check(self) -> None:
|
||||||
"""
|
"""
|
||||||
认证
|
检查认证状态
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.user_id = "1"
|
self.user_id = ""
|
||||||
|
|
||||||
|
@rx.event
|
||||||
|
def set_email(self, email: str) -> None:
|
||||||
|
"""
|
||||||
|
设置邮箱
|
||||||
|
:param email: 邮箱
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.email = email.strip()
|
||||||
|
|
||||||
|
@rx.event
|
||||||
|
def set_captcha(self, captcha: str) -> None:
|
||||||
|
"""
|
||||||
|
设置验证码
|
||||||
|
:param captcha: 验证码
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.captcha = captcha.strip()
|
||||||
|
|
||||||
|
@rx.event
|
||||||
|
def toggle_policies_agreed(self) -> None:
|
||||||
|
"""
|
||||||
|
切换协议同意状态
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.is_policies_agreed = not self.is_policies_agreed
|
||||||
|
|
||||||
|
@rx.event
|
||||||
|
async def send_email_code(self):
|
||||||
|
"""生成并存储邮箱验证码(异步数据库,不阻塞事件循环)"""
|
||||||
|
|
||||||
|
email_rule = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
||||||
|
if not re.fullmatch(email_rule, self.email):
|
||||||
|
return rx.toast("请输入合法邮箱地址", color="red")
|
||||||
|
|
||||||
|
# 异步数据库会话
|
||||||
|
async for db in get_async_db():
|
||||||
|
# 清理该邮箱旧验证码(await 异步查询)
|
||||||
|
old_codes = await db.exec(
|
||||||
|
select(EmailVerifyCode).where(EmailVerifyCode.email == self.email)
|
||||||
|
)
|
||||||
|
old_list = old_codes.all()
|
||||||
|
for item in old_list:
|
||||||
|
await db.delete(item)
|
||||||
|
|
||||||
|
# 6位验证码,5分钟有效期
|
||||||
|
import random
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
code_str = "".join(random.choices("0123456789", k=6))
|
||||||
|
expire_time = datetime.utcnow() + timedelta(minutes=5)
|
||||||
|
db.add(
|
||||||
|
EmailVerifyCode(email=self.email, code=code_str, expire_at=expire_time)
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
# 本地调试打印验证码
|
||||||
|
print(f"邮箱 {self.email} 测试验证码:{code_str}")
|
||||||
|
return rx.toast("验证码已发送(控制台查看测试码)", color="green")
|
||||||
|
|
||||||
|
@rx.event
|
||||||
|
async def login_by_email_code(self):
|
||||||
|
"""邮箱验证码登录,不存在用户自动注册"""
|
||||||
|
if not self.email:
|
||||||
|
return rx.toast("请填写邮箱", color="red")
|
||||||
|
if not self.code:
|
||||||
|
return rx.toast("请填写验证码", color="red")
|
||||||
|
if not self.agree_policy:
|
||||||
|
return rx.toast("请勾选同意用户协议与隐私政策", color="red")
|
||||||
|
|
||||||
|
db = get_db_session()
|
||||||
|
# 查询有效验证码
|
||||||
|
code_record = db.exec(
|
||||||
|
select(EmailVerifyCode).where(
|
||||||
|
EmailVerifyCode.email == self.email, EmailVerifyCode.code == self.code
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
if not code_record or code_record.is_expired():
|
||||||
|
return rx.toast("验证码错误或已过期", color="red")
|
||||||
|
|
||||||
|
# 一次性验证码使用后删除
|
||||||
|
db.delete(code_record)
|
||||||
|
|
||||||
|
# 查找用户,无则新建
|
||||||
|
user = db.exec(select(User).where(User.email == self.email)).first()
|
||||||
|
if not user:
|
||||||
|
user = User(email=self.email, nickname=self.email.split("@")[0])
|
||||||
|
db.add(user)
|
||||||
|
user.last_login_at = datetime.utcnow()
|
||||||
|
db.commit()
|
||||||
|
db.refresh(user)
|
||||||
|
|
||||||
|
# 登录成功,弹窗自动消失
|
||||||
|
self.user_id = str(user.id)
|
||||||
|
self.email = ""
|
||||||
|
self.code = ""
|
||||||
|
self.agree_policy = False
|
||||||
|
return rx.toast("登录成功", color="green")
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
会话历史状态
|
|
||||||
"""
|
|
||||||
import reflex as rx
|
|
||||||
|
|
||||||
|
|
||||||
class ConversationHistoryState(rx.State):
|
|
||||||
"""
|
|
||||||
会话历史状态
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 会话历史折叠面板展开状态,True表示展开,False表示收起
|
|
||||||
is_collapse_expanded: bool = False
|
|
||||||
|
|
||||||
@rx.event
|
|
||||||
def toggle_collapse(self) -> None:
|
|
||||||
"""
|
|
||||||
打开/关闭会话历史折叠面板
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
self.is_collapse_expanded = not self.is_collapse_expanded
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
会话历史折叠面板状态
|
||||||
|
"""
|
||||||
|
import reflex as rx
|
||||||
|
|
||||||
|
|
||||||
|
class ConversationHistoryCollapseState(rx.State):
|
||||||
|
"""
|
||||||
|
会话历史折叠面板状态
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 展开状态,True表示展开,False表示折叠
|
||||||
|
is_expanded: bool = False
|
||||||
|
|
||||||
|
@rx.event
|
||||||
|
def toggle_expanded(self) -> None:
|
||||||
|
"""
|
||||||
|
切换展开状态
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.is_expanded = not self.is_expanded
|
||||||
|
|
@ -13,9 +13,9 @@ class SidebarState(rx.State):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@rx.var
|
@rx.var
|
||||||
async def generate_storage_key(self) -> str:
|
async def storage_key(self) -> str:
|
||||||
"""
|
"""
|
||||||
生成 LocalStorage 键
|
LocalStorage 键
|
||||||
"""
|
"""
|
||||||
# 获取认证状态
|
# 获取认证状态
|
||||||
auth = await self.get_state(AuthState)
|
auth = await self.get_state(AuthState)
|
||||||
|
|
@ -23,19 +23,12 @@ class SidebarState(rx.State):
|
||||||
user_id = auth.user_id or "default"
|
user_id = auth.user_id or "default"
|
||||||
return f"sidebar_{user_id}"
|
return f"sidebar_{user_id}"
|
||||||
|
|
||||||
# 侧边栏中激活的图标导航按钮
|
# 激活的导航按钮
|
||||||
activated_button: str = rx.LocalStorage(generate_storage_key, sync=True)
|
activated_nav_button: str = rx.LocalStorage(storage_key, sync=True)
|
||||||
|
|
||||||
@rx.var
|
|
||||||
def get_activated_button(self) -> str:
|
|
||||||
"""
|
|
||||||
获取侧边栏中激活的图标导航按钮
|
|
||||||
"""
|
|
||||||
return self.activated_button
|
|
||||||
|
|
||||||
@rx.event
|
@rx.event
|
||||||
def switch_activated_button(self, button: str):
|
def switch_nav_button(self, button: str):
|
||||||
"""
|
"""
|
||||||
将指定图标导航按钮设置为侧边栏中激活的图标导航按钮
|
将指定导航按钮设置为激活的导航按钮
|
||||||
"""
|
"""
|
||||||
self.activated_button = button
|
self.activated_nav_button = button
|
||||||
|
|
|
||||||
Binary file not shown.
Loading…
Reference in New Issue