fix: 第二轮修复 — 数据库启动检查、会话持久化、URL路由、设备排序等
1. DevTools 启动前检查数据库状态,失败时自动尝试启动 2. ai-core 添加数据库断线重连机制 (30秒间隔) 3. Dashboard 添加数据库状态卡片 (启动/停止/重启) 4. Gateway 会话空闲超时管理 (30分钟标记空闲) 5. 会话/消息 PostgreSQL 持久化 (SessionStore + REST API) 6. 前端服务端会话持久化 + URL hash 路由 + 侧边栏管理 7. 管理员回到主对话按钮 8. IoT 设备卡片固定排序 9. 更新相关文档
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useSession } from '@/hooks/useSession';
|
||||
import { useSessionStore } from '@/store/sessionStore';
|
||||
import { CyreneAvatar } from '@/components/persona/CyreneAvatar';
|
||||
import type { Session } from '@/types/session';
|
||||
|
||||
interface SidebarProps {
|
||||
onClose?: () => void;
|
||||
@@ -11,12 +13,25 @@ export function Sidebar({ onClose }: SidebarProps) {
|
||||
sessions,
|
||||
createSession,
|
||||
deleteSession,
|
||||
deleteAllSessions,
|
||||
clearMainSession,
|
||||
setCurrentSession,
|
||||
isAdmin,
|
||||
} = useSession();
|
||||
const currentSessionId = useSessionStore((s) => s.currentSessionId);
|
||||
|
||||
const displaySessions = sessions;
|
||||
const activeSessionId = currentSessionId;
|
||||
// 确认弹窗状态
|
||||
const [confirmAction, setConfirmAction] = useState<{
|
||||
type: 'delete' | 'clearMain' | 'deleteAll';
|
||||
sessionId?: string;
|
||||
} | null>(null);
|
||||
|
||||
// 按 updated_at 降序排列
|
||||
const displaySessions = [...sessions].sort((a, b) => {
|
||||
const ta = typeof a.updated_at === 'string' ? parseInt(a.updated_at, 10) : (a.updated_at as unknown as number);
|
||||
const tb = typeof b.updated_at === 'string' ? parseInt(b.updated_at, 10) : (b.updated_at as unknown as number);
|
||||
return (tb || 0) - (ta || 0);
|
||||
});
|
||||
|
||||
const handleNewChat = async () => {
|
||||
const session = await createSession();
|
||||
@@ -28,11 +43,43 @@ export function Sidebar({ onClose }: SidebarProps) {
|
||||
if (onClose) onClose();
|
||||
};
|
||||
|
||||
const handleDeleteSession = (e: { stopPropagation: () => void }, id: string) => {
|
||||
e.stopPropagation();
|
||||
deleteSession(id);
|
||||
const handleMainSession = async () => {
|
||||
// 找到主对话
|
||||
const mainSession = displaySessions.find((s) => s.is_main);
|
||||
if (mainSession) {
|
||||
setCurrentSession(mainSession.id);
|
||||
if (onClose) onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = (e: React.MouseEvent, id: string) => {
|
||||
e.stopPropagation();
|
||||
const session = displaySessions.find((s) => s.id === id);
|
||||
// 管理员主对话不可删除
|
||||
if (isAdmin && session?.is_main) return;
|
||||
setConfirmAction({ type: 'delete', sessionId: id });
|
||||
};
|
||||
|
||||
const handleConfirmAction = async () => {
|
||||
if (!confirmAction) return;
|
||||
switch (confirmAction.type) {
|
||||
case 'delete':
|
||||
if (confirmAction.sessionId) {
|
||||
await deleteSession(confirmAction.sessionId);
|
||||
}
|
||||
break;
|
||||
case 'clearMain':
|
||||
await clearMainSession();
|
||||
break;
|
||||
case 'deleteAll':
|
||||
await deleteAllSessions();
|
||||
break;
|
||||
}
|
||||
setConfirmAction(null);
|
||||
};
|
||||
|
||||
const cancelConfirm = () => setConfirmAction(null);
|
||||
|
||||
/** 格式化时间戳为可读字符串 */
|
||||
const formatTime = (ts: string | number): string => {
|
||||
if (!ts) return '';
|
||||
@@ -52,7 +99,28 @@ export function Sidebar({ onClose }: SidebarProps) {
|
||||
|
||||
return (
|
||||
<aside className="h-full bg-white/90 dark:bg-gray-900/90 border-r border-pink-100 dark:border-pink-900 flex flex-col">
|
||||
{/* 侧边栏头部 */}
|
||||
{/* 主对话按钮 (仅管理员可见) */}
|
||||
{isAdmin && (
|
||||
<div className="p-3 border-b border-pink-100 dark:border-pink-900 flex gap-2">
|
||||
<button
|
||||
onClick={handleMainSession}
|
||||
className="flex-1 flex items-center justify-center gap-1.5 px-3 py-2 bg-amber-50 hover:bg-amber-100 dark:bg-amber-900/20 dark:hover:bg-amber-900/30 text-amber-600 dark:text-amber-400 rounded-xl text-sm font-medium transition-colors border border-amber-200 dark:border-amber-800"
|
||||
title="回到主对话"
|
||||
>
|
||||
<span>🏠</span>
|
||||
<span>主对话</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmAction({ type: 'clearMain' })}
|
||||
className="flex items-center justify-center px-2 py-2 bg-red-50 hover:bg-red-100 dark:bg-red-900/20 dark:hover:bg-red-900/30 text-red-400 hover:text-red-500 rounded-xl text-sm transition-colors border border-red-200 dark:border-red-800"
|
||||
title="清空主对话消息"
|
||||
>
|
||||
<span>🗑️</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 新对话按钮 */}
|
||||
<div className="p-4 border-b border-pink-100 dark:border-pink-900">
|
||||
<button
|
||||
onClick={handleNewChat}
|
||||
@@ -70,46 +138,80 @@ export function Sidebar({ onClose }: SidebarProps) {
|
||||
还没有对话哦,开始和昔涟聊天吧 ♪
|
||||
</p>
|
||||
) : (
|
||||
displaySessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
onClick={() => handleSelectSession(session.id)}
|
||||
className={`
|
||||
group flex items-center justify-between px-4 py-2.5 mx-2 rounded-lg cursor-pointer transition-colors
|
||||
${
|
||||
activeSessionId === session.id
|
||||
? 'bg-pink-50 dark:bg-pink-900/30 text-pink-600'
|
||||
: 'hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-300'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<CyreneAvatar size="sm" />
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium truncate">{session.title || '新的对话'}</p>
|
||||
<p className="text-xs text-gray-400 truncate">
|
||||
{session.message_count != null ? `${session.message_count} 条消息` : ''}
|
||||
{session.updated_at ? ` · ${formatTime(session.updated_at)}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => handleDeleteSession(e, session.id)}
|
||||
className="opacity-0 group-hover:opacity-100 p-1 text-gray-400 hover:text-red-400 transition-all"
|
||||
title="删除会话"
|
||||
displaySessions.map((session) => {
|
||||
const isMainSession = session.is_main;
|
||||
const isActive = currentSessionId === session.id;
|
||||
// 管理员主对话不可删除
|
||||
const canDelete = !(isAdmin && isMainSession);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={session.id}
|
||||
onClick={() => handleSelectSession(session.id)}
|
||||
className={`
|
||||
group flex items-center justify-between px-4 py-2.5 mx-2 rounded-lg cursor-pointer transition-colors
|
||||
${
|
||||
isActive
|
||||
? 'bg-pink-50 dark:bg-pink-900/30 text-pink-600'
|
||||
: 'hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-300'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<CyreneAvatar size="sm" />
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium truncate">
|
||||
{isMainSession ? '🏠 ' : ''}
|
||||
{session.title || '新的对话'}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 truncate">
|
||||
{session.message_count != null
|
||||
? `${session.message_count} 条消息`
|
||||
: ''}
|
||||
{session.updated_at
|
||||
? `${session.message_count != null ? ' · ' : ''}${formatTime(session.updated_at)}`
|
||||
: ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{canDelete && (
|
||||
<button
|
||||
onClick={(e) => handleDeleteClick(e, session.id)}
|
||||
className="opacity-0 group-hover:opacity-100 p-1 text-gray-400 hover:text-red-400 transition-all"
|
||||
title="删除会话"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部信息 */}
|
||||
<div className="p-4 border-t border-pink-100 dark:border-pink-900">
|
||||
<div className="flex items-center gap-2 text-xs text-gray-400">
|
||||
{/* 底部:一键清空所有对话 */}
|
||||
<div className="p-3 border-t border-pink-100 dark:border-pink-900">
|
||||
<button
|
||||
onClick={() => setConfirmAction({ type: 'deleteAll' })}
|
||||
className="w-full flex items-center justify-center gap-1.5 px-3 py-2 bg-red-50 hover:bg-red-100 dark:bg-red-900/20 dark:hover:bg-red-900/30 text-red-400 hover:text-red-500 rounded-xl text-xs font-medium transition-colors border border-red-200 dark:border-red-800"
|
||||
>
|
||||
<span>🗑️</span>
|
||||
<span>清空所有对话</span>
|
||||
</button>
|
||||
<div className="flex items-center gap-2 mt-3 text-xs text-gray-400">
|
||||
<CyreneAvatar size="sm" />
|
||||
<div>
|
||||
<p className="font-medium text-pink-400">昔涟 AI</p>
|
||||
@@ -117,6 +219,42 @@ export function Sidebar({ onClose }: SidebarProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 确认弹窗 */}
|
||||
{confirmAction && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-6 m-4 max-w-sm w-full border border-pink-100 dark:border-pink-700">
|
||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-2">
|
||||
{confirmAction.type === 'delete'
|
||||
? '删除会话'
|
||||
: confirmAction.type === 'clearMain'
|
||||
? '清空主对话'
|
||||
: '清空所有对话'}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
||||
{confirmAction.type === 'delete'
|
||||
? '确定要删除这个会话吗?此操作不会删除记忆。'
|
||||
: confirmAction.type === 'clearMain'
|
||||
? '确定要清空主对话的所有消息吗?此操作不会删除记忆。'
|
||||
: '确定要删除所有对话吗?此操作不会删除记忆。管理员将回到主对话,普通用户进入新对话。'}
|
||||
</p>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
onClick={cancelConfirm}
|
||||
className="px-4 py-2 rounded-lg text-sm text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirmAction}
|
||||
className="px-4 py-2 rounded-lg text-sm bg-red-400 hover:bg-red-500 text-white font-medium transition-colors"
|
||||
>
|
||||
确定
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user