Files
memind/src/components/HistorySidebar.tsx
T
John 6ee6fd64dd Add MindSpace page live edit, chat skills, and H5 deploy tooling.
Introduce page edit sessions with draft preview and patch API, chat skill picker, user memory profile, h5ApiBase resolution, voice WAV transport, and scripts for 105/g2 deployment and Plaza local dev.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 22:09:38 -07:00

160 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useRef, useState } from 'react';
import { AvatarPicker } from './AvatarPicker';
import { appConfig } from '../config';
import type { Session } from '../types';
import { getSessionListLabel, groupSessionsByDate } from '../utils/sessions';
function formatSessionTime(iso?: string): string {
if (!iso) return '';
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return '';
const now = new Date();
const isToday =
date.getFullYear() === now.getFullYear() &&
date.getMonth() === now.getMonth() &&
date.getDate() === now.getDate();
if (isToday) {
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
}
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
}
function CloseIcon() {
return (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M18 6L6 18M6 6l12 12"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
);
}
type HistorySidebarProps = {
open: boolean;
sessions: Session[];
activeSessionId?: string;
loading: boolean;
onClose: () => void;
onSelect: (sessionId: string) => void;
onNew: () => void;
onDelete: (sessionId: string) => void;
};
export function HistorySidebar({
open,
sessions,
activeSessionId,
loading,
onClose,
onSelect,
onNew,
onDelete,
}: HistorySidebarProps) {
const bodyRef = useRef<HTMLDivElement>(null);
const [visibleCount, setVisibleCount] = useState(appConfig.sessionPageSize);
useEffect(() => {
if (open) {
setVisibleCount(appConfig.sessionPageSize);
}
}, [open, sessions.length]);
const handleScroll = useCallback(() => {
const el = bodyRef.current;
if (!el) return;
const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
if (nearBottom && visibleCount < sessions.length) {
setVisibleCount((count) => Math.min(count + appConfig.sessionPageSize, sessions.length));
}
}, [visibleCount, sessions.length]);
const handleDelete = useCallback(
(sessionId: string, label: string) => {
if (!window.confirm(`确定删除「${label}」?此操作不可恢复。`)) return;
onDelete(sessionId);
},
[onDelete],
);
if (!open) return null;
const visibleSessions = sessions.slice(0, visibleCount);
const sessionGroups = groupSessionsByDate(visibleSessions);
const hasMore = visibleCount < sessions.length;
return (
<>
<button type="button" className="sidebar-backdrop" aria-label="关闭历史" onClick={onClose} />
<aside className="sidebar sidebar-open">
<div className="sidebar-header">
<span className="sidebar-title"></span>
<button type="button" className="sidebar-close" aria-label="收起历史" onClick={onClose}>
</button>
</div>
<div className="sidebar-body" ref={bodyRef} onScroll={handleScroll}>
<button type="button" className="sidebar-new" onClick={onNew}>
+
</button>
{loading && sessions.length === 0 ? (
<div className="sidebar-empty"></div>
) : sessions.length === 0 ? (
<div className="sidebar-empty"></div>
) : (
<>
{sessionGroups.map((group) => (
<section key={group.dateKey || group.label} className="sidebar-date-group">
<div className="sidebar-date-divider">
<span>{group.label}</span>
</div>
<ul className="sidebar-list">
{group.sessions.map((item) => {
const label = getSessionListLabel(item);
return (
<li key={item.id} className="sidebar-item-wrap">
<button
type="button"
className={`sidebar-item ${item.id === activeSessionId ? 'sidebar-item-active' : ''}`}
onClick={() => onSelect(item.id)}
>
<span className="sidebar-item-title">{label}</span>
<span className="sidebar-item-meta">
{formatSessionTime(item.updated_at ?? item.created_at)}
</span>
</button>
<button
type="button"
className="sidebar-item-delete"
aria-label={`删除 ${label}`}
onClick={() => handleDelete(item.id, label)}
>
<CloseIcon />
</button>
</li>
);
})}
</ul>
</section>
))}
{hasMore && <div className="sidebar-more"></div>}
</>
)}
</div>
<div className="sidebar-footer">
<AvatarPicker />
</div>
</aside>
</>
);
}