chore: move list recipes and archive recipe to goose server (#4422)
This commit is contained in:
@@ -158,449 +158,457 @@ interface SessionListViewProps {
|
||||
selectedSessionId?: string | null;
|
||||
}
|
||||
|
||||
const SessionListView: React.FC<SessionListViewProps> = React.memo(({ onSelectSession, selectedSessionId }) => {
|
||||
const [sessions, setSessions] = useState<Session[]>([]);
|
||||
const [filteredSessions, setFilteredSessions] = useState<Session[]>([]);
|
||||
const [dateGroups, setDateGroups] = useState<DateGroup[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [showSkeleton, setShowSkeleton] = useState(true);
|
||||
const [showContent, setShowContent] = useState(false);
|
||||
const [isInitialLoad, setIsInitialLoad] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [searchResults, setSearchResults] = useState<{
|
||||
count: number;
|
||||
currentIndex: number;
|
||||
} | null>(null);
|
||||
const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
||||
({ onSelectSession, selectedSessionId }) => {
|
||||
const [sessions, setSessions] = useState<Session[]>([]);
|
||||
const [filteredSessions, setFilteredSessions] = useState<Session[]>([]);
|
||||
const [dateGroups, setDateGroups] = useState<DateGroup[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [showSkeleton, setShowSkeleton] = useState(true);
|
||||
const [showContent, setShowContent] = useState(false);
|
||||
const [isInitialLoad, setIsInitialLoad] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [searchResults, setSearchResults] = useState<{
|
||||
count: number;
|
||||
currentIndex: number;
|
||||
} | null>(null);
|
||||
|
||||
// Edit modal state
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [editingSession, setEditingSession] = useState<Session | null>(null);
|
||||
// Edit modal state
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [editingSession, setEditingSession] = useState<Session | null>(null);
|
||||
|
||||
// Search state for debouncing
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [caseSensitive, setCaseSensitive] = useState(false);
|
||||
const debouncedSearchTerm = useDebounce(searchTerm, 300); // 300ms debounce
|
||||
// Search state for debouncing
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [caseSensitive, setCaseSensitive] = useState(false);
|
||||
const debouncedSearchTerm = useDebounce(searchTerm, 300); // 300ms debounce
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Track session to element ref
|
||||
const sessionRefs = useRef<Record<string, HTMLElement>>({});
|
||||
const setSessionRefs = (itemId: string, element: HTMLDivElement | null) => {
|
||||
if (element) {
|
||||
sessionRefs.current[itemId] = element;
|
||||
} else {
|
||||
delete sessionRefs.current[itemId];
|
||||
}
|
||||
};
|
||||
|
||||
const loadSessions = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setShowSkeleton(true);
|
||||
setShowContent(false);
|
||||
setError(null);
|
||||
try {
|
||||
const sessions = await fetchSessions();
|
||||
// Use startTransition to make state updates non-blocking
|
||||
startTransition(() => {
|
||||
setSessions(sessions);
|
||||
setFilteredSessions(sessions);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to load sessions:', err);
|
||||
setError('Failed to load sessions. Please try again later.');
|
||||
setSessions([]);
|
||||
setFilteredSessions([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadSessions();
|
||||
}, [loadSessions]);
|
||||
|
||||
// Timing logic to prevent flicker between skeleton and content on initial load
|
||||
useEffect(() => {
|
||||
if (!isLoading && showSkeleton) {
|
||||
setShowSkeleton(false);
|
||||
// Use startTransition for non-blocking content show
|
||||
startTransition(() => {
|
||||
setTimeout(() => {
|
||||
setShowContent(true);
|
||||
if (isInitialLoad) {
|
||||
setIsInitialLoad(false);
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
return () => void 0;
|
||||
}, [isLoading, showSkeleton, isInitialLoad]);
|
||||
|
||||
// Memoize date groups calculation to prevent unnecessary recalculations
|
||||
const memoizedDateGroups = useMemo(() => {
|
||||
if (filteredSessions.length > 0) {
|
||||
return groupSessionsByDate(filteredSessions);
|
||||
}
|
||||
return [];
|
||||
}, [filteredSessions]);
|
||||
|
||||
// Update date groups when filtered sessions change
|
||||
useEffect(() => {
|
||||
startTransition(() => {
|
||||
setDateGroups(memoizedDateGroups);
|
||||
});
|
||||
}, [memoizedDateGroups]);
|
||||
|
||||
// Scroll to the selected session when returning from session history view
|
||||
useEffect(() => {
|
||||
if (selectedSessionId) {
|
||||
const element = sessionRefs.current[selectedSessionId];
|
||||
// Track session to element ref
|
||||
const sessionRefs = useRef<Record<string, HTMLElement>>({});
|
||||
const setSessionRefs = (itemId: string, element: HTMLDivElement | null) => {
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
block: "center"
|
||||
sessionRefs.current[itemId] = element;
|
||||
} else {
|
||||
delete sessionRefs.current[itemId];
|
||||
}
|
||||
};
|
||||
|
||||
const loadSessions = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
setShowSkeleton(true);
|
||||
setShowContent(false);
|
||||
setError(null);
|
||||
try {
|
||||
const sessions = await fetchSessions();
|
||||
// Use startTransition to make state updates non-blocking
|
||||
startTransition(() => {
|
||||
setSessions(sessions);
|
||||
setFilteredSessions(sessions);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to load sessions:', err);
|
||||
setError('Failed to load sessions. Please try again later.');
|
||||
setSessions([]);
|
||||
setFilteredSessions([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadSessions();
|
||||
}, [loadSessions]);
|
||||
|
||||
// Timing logic to prevent flicker between skeleton and content on initial load
|
||||
useEffect(() => {
|
||||
if (!isLoading && showSkeleton) {
|
||||
setShowSkeleton(false);
|
||||
// Use startTransition for non-blocking content show
|
||||
startTransition(() => {
|
||||
setTimeout(() => {
|
||||
setShowContent(true);
|
||||
if (isInitialLoad) {
|
||||
setIsInitialLoad(false);
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [selectedSessionId, sessions]);
|
||||
return () => void 0;
|
||||
}, [isLoading, showSkeleton, isInitialLoad]);
|
||||
|
||||
// Debounced search effect - performs actual filtering
|
||||
useEffect(() => {
|
||||
if (!debouncedSearchTerm) {
|
||||
// Memoize date groups calculation to prevent unnecessary recalculations
|
||||
const memoizedDateGroups = useMemo(() => {
|
||||
if (filteredSessions.length > 0) {
|
||||
return groupSessionsByDate(filteredSessions);
|
||||
}
|
||||
return [];
|
||||
}, [filteredSessions]);
|
||||
|
||||
// Update date groups when filtered sessions change
|
||||
useEffect(() => {
|
||||
startTransition(() => {
|
||||
setFilteredSessions(sessions);
|
||||
setSearchResults(null);
|
||||
setDateGroups(memoizedDateGroups);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}, [memoizedDateGroups]);
|
||||
|
||||
// Use startTransition to make search non-blocking
|
||||
startTransition(() => {
|
||||
const searchTerm = caseSensitive ? debouncedSearchTerm : debouncedSearchTerm.toLowerCase();
|
||||
const filtered = sessions.filter((session) => {
|
||||
const description = session.metadata.description || session.id;
|
||||
const path = session.path;
|
||||
const workingDir = session.metadata.working_dir;
|
||||
|
||||
if (caseSensitive) {
|
||||
return (
|
||||
description.includes(searchTerm) ||
|
||||
path.includes(searchTerm) ||
|
||||
workingDir.includes(searchTerm)
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
description.toLowerCase().includes(searchTerm) ||
|
||||
path.toLowerCase().includes(searchTerm) ||
|
||||
workingDir.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
// Scroll to the selected session when returning from session history view
|
||||
useEffect(() => {
|
||||
if (selectedSessionId) {
|
||||
const element = sessionRefs.current[selectedSessionId];
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
block: 'center',
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [selectedSessionId, sessions]);
|
||||
|
||||
// Debounced search effect - performs actual filtering
|
||||
useEffect(() => {
|
||||
if (!debouncedSearchTerm) {
|
||||
startTransition(() => {
|
||||
setFilteredSessions(sessions);
|
||||
setSearchResults(null);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Use startTransition to make search non-blocking
|
||||
startTransition(() => {
|
||||
const searchTerm = caseSensitive ? debouncedSearchTerm : debouncedSearchTerm.toLowerCase();
|
||||
const filtered = sessions.filter((session) => {
|
||||
const description = session.metadata.description || session.id;
|
||||
const path = session.path;
|
||||
const workingDir = session.metadata.working_dir;
|
||||
|
||||
if (caseSensitive) {
|
||||
return (
|
||||
description.includes(searchTerm) ||
|
||||
path.includes(searchTerm) ||
|
||||
workingDir.includes(searchTerm)
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
description.toLowerCase().includes(searchTerm) ||
|
||||
path.toLowerCase().includes(searchTerm) ||
|
||||
workingDir.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
setFilteredSessions(filtered);
|
||||
setSearchResults(filtered.length > 0 ? { count: filtered.length, currentIndex: 1 } : null);
|
||||
});
|
||||
}, [debouncedSearchTerm, caseSensitive, sessions]);
|
||||
|
||||
setFilteredSessions(filtered);
|
||||
setSearchResults(filtered.length > 0 ? { count: filtered.length, currentIndex: 1 } : null);
|
||||
});
|
||||
}, [debouncedSearchTerm, caseSensitive, sessions]);
|
||||
// Handle immediate search input (updates search term for debouncing)
|
||||
const handleSearch = useCallback((term: string, caseSensitive: boolean) => {
|
||||
setSearchTerm(term);
|
||||
setCaseSensitive(caseSensitive);
|
||||
}, []);
|
||||
|
||||
// Handle immediate search input (updates search term for debouncing)
|
||||
const handleSearch = useCallback((term: string, caseSensitive: boolean) => {
|
||||
setSearchTerm(term);
|
||||
setCaseSensitive(caseSensitive);
|
||||
}, []);
|
||||
// Handle search result navigation
|
||||
const handleSearchNavigation = (direction: 'next' | 'prev') => {
|
||||
if (!searchResults || filteredSessions.length === 0) return;
|
||||
|
||||
// Handle search result navigation
|
||||
const handleSearchNavigation = (direction: 'next' | 'prev') => {
|
||||
if (!searchResults || filteredSessions.length === 0) return;
|
||||
let newIndex: number;
|
||||
if (direction === 'next') {
|
||||
newIndex = (searchResults.currentIndex % filteredSessions.length) + 1;
|
||||
} else {
|
||||
newIndex =
|
||||
searchResults.currentIndex === 1
|
||||
? filteredSessions.length
|
||||
: searchResults.currentIndex - 1;
|
||||
}
|
||||
|
||||
let newIndex: number;
|
||||
if (direction === 'next') {
|
||||
newIndex = (searchResults.currentIndex % filteredSessions.length) + 1;
|
||||
} else {
|
||||
newIndex =
|
||||
searchResults.currentIndex === 1 ? filteredSessions.length : searchResults.currentIndex - 1;
|
||||
}
|
||||
setSearchResults({ ...searchResults, currentIndex: newIndex });
|
||||
|
||||
setSearchResults({ ...searchResults, currentIndex: newIndex });
|
||||
// Find the SearchView's container element
|
||||
const searchContainer =
|
||||
containerRef.current?.querySelector<SearchContainerElement>('.search-container');
|
||||
if (searchContainer?._searchHighlighter) {
|
||||
// Update the current match in the highlighter
|
||||
searchContainer._searchHighlighter.setCurrentMatch(newIndex - 1, true);
|
||||
}
|
||||
};
|
||||
|
||||
// Find the SearchView's container element
|
||||
const searchContainer =
|
||||
containerRef.current?.querySelector<SearchContainerElement>('.search-container');
|
||||
if (searchContainer?._searchHighlighter) {
|
||||
// Update the current match in the highlighter
|
||||
searchContainer._searchHighlighter.setCurrentMatch(newIndex - 1, true);
|
||||
}
|
||||
};
|
||||
// Handle modal close
|
||||
const handleModalClose = useCallback(() => {
|
||||
setShowEditModal(false);
|
||||
setEditingSession(null);
|
||||
}, []);
|
||||
|
||||
// Handle modal close
|
||||
const handleModalClose = useCallback(() => {
|
||||
setShowEditModal(false);
|
||||
setEditingSession(null);
|
||||
}, []);
|
||||
const handleModalSave = useCallback(async (sessionId: string, newDescription: string) => {
|
||||
// Update state immediately for optimistic UI
|
||||
setSessions((prevSessions) =>
|
||||
prevSessions.map((s) =>
|
||||
s.id === sessionId
|
||||
? { ...s, metadata: { ...s.metadata, description: newDescription } }
|
||||
: s
|
||||
)
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleModalSave = useCallback(async (sessionId: string, newDescription: string) => {
|
||||
// Update state immediately for optimistic UI
|
||||
setSessions((prevSessions) =>
|
||||
prevSessions.map((s) =>
|
||||
s.id === sessionId ? { ...s, metadata: { ...s.metadata, description: newDescription } } : s
|
||||
)
|
||||
);
|
||||
}, []);
|
||||
const handleEditSession = useCallback((session: Session) => {
|
||||
setEditingSession(session);
|
||||
setShowEditModal(true);
|
||||
}, []);
|
||||
|
||||
const handleEditSession = useCallback((session: Session) => {
|
||||
setEditingSession(session);
|
||||
setShowEditModal(true);
|
||||
}, []);
|
||||
const SessionItem = React.memo(function SessionItem({
|
||||
session,
|
||||
onEditClick,
|
||||
}: {
|
||||
session: Session;
|
||||
onEditClick: (session: Session) => void;
|
||||
}) {
|
||||
const handleEditClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation(); // Prevent card click
|
||||
onEditClick(session);
|
||||
},
|
||||
[onEditClick, session]
|
||||
);
|
||||
|
||||
const SessionItem = React.memo(function SessionItem({
|
||||
session,
|
||||
onEditClick,
|
||||
}: {
|
||||
session: Session;
|
||||
onEditClick: (session: Session) => void;
|
||||
}) {
|
||||
const handleEditClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation(); // Prevent card click
|
||||
onEditClick(session);
|
||||
},
|
||||
[onEditClick, session]
|
||||
);
|
||||
const handleCardClick = useCallback(() => {
|
||||
onSelectSession(session.id);
|
||||
}, [session.id]);
|
||||
|
||||
const handleCardClick = useCallback(() => {
|
||||
onSelectSession(session.id);
|
||||
}, [session.id]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
onClick={handleCardClick}
|
||||
className="session-item h-full py-3 px-4 hover:shadow-default cursor-pointer transition-all duration-150 flex flex-col justify-between relative group"
|
||||
ref={(el) => setSessionRefs(session.id, el)}
|
||||
>
|
||||
<button
|
||||
onClick={handleEditClick}
|
||||
className="absolute top-3 right-4 p-2 rounded opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer"
|
||||
title="Edit session name"
|
||||
return (
|
||||
<Card
|
||||
onClick={handleCardClick}
|
||||
className="session-item h-full py-3 px-4 hover:shadow-default cursor-pointer transition-all duration-150 flex flex-col justify-between relative group"
|
||||
ref={(el) => setSessionRefs(session.id, el)}
|
||||
>
|
||||
<Edit2 className="w-3 h-3 text-textSubtle hover:text-textStandard" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleEditClick}
|
||||
className="absolute top-3 right-4 p-2 rounded opacity-0 group-hover:opacity-100 transition-opacity hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer"
|
||||
title="Edit session name"
|
||||
>
|
||||
<Edit2 className="w-3 h-3 text-textSubtle hover:text-textStandard" />
|
||||
</button>
|
||||
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base mb-1 pr-6 break-words">
|
||||
{session.metadata.description || session.id}
|
||||
</h3>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base mb-1 pr-6 break-words">
|
||||
{session.metadata.description || session.id}
|
||||
</h3>
|
||||
|
||||
<div className="flex items-center text-text-muted text-xs mb-1">
|
||||
<Calendar className="w-3 h-3 mr-1 flex-shrink-0" />
|
||||
<span>{formatMessageTimestamp(Date.parse(session.modified) / 1000)}</span>
|
||||
</div>
|
||||
<div className="flex items-center text-text-muted text-xs mb-1">
|
||||
<Folder className="w-3 h-3 mr-1 flex-shrink-0" />
|
||||
<span className="truncate">{session.metadata.working_dir}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mt-1 pt-2">
|
||||
<div className="flex items-center space-x-3 text-xs text-text-muted">
|
||||
<div className="flex items-center">
|
||||
<MessageSquareText className="w-3 h-3 mr-1" />
|
||||
<span className="font-mono">{session.metadata.message_count}</span>
|
||||
<div className="flex items-center text-text-muted text-xs mb-1">
|
||||
<Calendar className="w-3 h-3 mr-1 flex-shrink-0" />
|
||||
<span>{formatMessageTimestamp(Date.parse(session.modified) / 1000)}</span>
|
||||
</div>
|
||||
{session.metadata.total_tokens !== null && (
|
||||
<div className="flex items-center text-text-muted text-xs mb-1">
|
||||
<Folder className="w-3 h-3 mr-1 flex-shrink-0" />
|
||||
<span className="truncate">{session.metadata.working_dir}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mt-1 pt-2">
|
||||
<div className="flex items-center space-x-3 text-xs text-text-muted">
|
||||
<div className="flex items-center">
|
||||
<Target className="w-3 h-3 mr-1" />
|
||||
<span className="font-mono">{session.metadata.total_tokens.toLocaleString()}</span>
|
||||
<MessageSquareText className="w-3 h-3 mr-1" />
|
||||
<span className="font-mono">{session.metadata.message_count}</span>
|
||||
</div>
|
||||
)}
|
||||
{session.metadata.total_tokens !== null && (
|
||||
<div className="flex items-center">
|
||||
<Target className="w-3 h-3 mr-1" />
|
||||
<span className="font-mono">
|
||||
{session.metadata.total_tokens.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
|
||||
// Render skeleton loader for session items with variations
|
||||
const SessionSkeleton = React.memo(({ variant = 0 }: { variant?: number }) => {
|
||||
const titleWidths = ['w-3/4', 'w-2/3', 'w-4/5', 'w-1/2'];
|
||||
const pathWidths = ['w-32', 'w-28', 'w-36', 'w-24'];
|
||||
const tokenWidths = ['w-12', 'w-10', 'w-14', 'w-8'];
|
||||
// Render skeleton loader for session items with variations
|
||||
const SessionSkeleton = React.memo(({ variant = 0 }: { variant?: number }) => {
|
||||
const titleWidths = ['w-3/4', 'w-2/3', 'w-4/5', 'w-1/2'];
|
||||
const pathWidths = ['w-32', 'w-28', 'w-36', 'w-24'];
|
||||
const tokenWidths = ['w-12', 'w-10', 'w-14', 'w-8'];
|
||||
|
||||
return (
|
||||
<Card className="session-skeleton h-full py-3 px-4 flex flex-col justify-between">
|
||||
<div className="flex-1">
|
||||
<Skeleton className={`h-5 ${titleWidths[variant % titleWidths.length]} mb-2`} />
|
||||
<div className="flex items-center mb-1">
|
||||
<Skeleton className="h-3 w-3 mr-1 rounded-sm" />
|
||||
<Skeleton className="h-4 w-20" />
|
||||
</div>
|
||||
<div className="flex items-center mb-1">
|
||||
<Skeleton className="h-3 w-3 mr-1 rounded-sm" />
|
||||
<Skeleton className={`h-4 ${pathWidths[variant % pathWidths.length]}`} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mt-1 pt-2">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center">
|
||||
<Skeleton className="h-3 w-3 mr-1 rounded-sm" />
|
||||
<Skeleton className="h-4 w-8" />
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Skeleton className="h-3 w-3 mr-1 rounded-sm" />
|
||||
<Skeleton className={`h-4 ${tokenWidths[variant % tokenWidths.length]}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
|
||||
SessionSkeleton.displayName = 'SessionSkeleton';
|
||||
|
||||
const renderActualContent = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full text-text-muted">
|
||||
<AlertCircle className="h-12 w-12 text-red-500 mb-4" />
|
||||
<p className="text-lg mb-2">Error Loading Sessions</p>
|
||||
<p className="text-sm text-center mb-4">{error}</p>
|
||||
<Button onClick={loadSessions} variant="default">
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sessions.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col justify-center h-full text-text-muted">
|
||||
<MessageSquareText className="h-12 w-12 mb-4" />
|
||||
<p className="text-lg mb-2">No chat sessions found</p>
|
||||
<p className="text-sm">Your chat history will appear here</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (dateGroups.length === 0 && searchResults !== null) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full text-text-muted mt-4">
|
||||
<MessageSquareText className="h-12 w-12 mb-4" />
|
||||
<p className="text-lg mb-2">No matching sessions found</p>
|
||||
<p className="text-sm">Try adjusting your search terms</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// For regular rendering in grid layout
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{dateGroups.map((group) => (
|
||||
<div key={group.label} className="space-y-4">
|
||||
<div className="sticky top-0 z-10 bg-background-default/95 backdrop-blur-sm">
|
||||
<h2 className="text-text-muted">{group.label}</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||
{group.sessions.map((session) => (
|
||||
<SessionItem key={session.id} session={session} onEditClick={handleEditSession} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="session-skeleton h-full py-3 px-4 flex flex-col justify-between">
|
||||
<div className="flex-1">
|
||||
<Skeleton className={`h-5 ${titleWidths[variant % titleWidths.length]} mb-2`} />
|
||||
<div className="flex items-center mb-1">
|
||||
<Skeleton className="h-3 w-3 mr-1 rounded-sm" />
|
||||
<Skeleton className="h-4 w-20" />
|
||||
</div>
|
||||
<div className="flex items-center mb-1">
|
||||
<Skeleton className="h-3 w-3 mr-1 rounded-sm" />
|
||||
<Skeleton className={`h-4 ${pathWidths[variant % pathWidths.length]}`} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mt-1 pt-2">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center">
|
||||
<Skeleton className="h-3 w-3 mr-1 rounded-sm" />
|
||||
<Skeleton className="h-4 w-8" />
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Skeleton className="h-3 w-3 mr-1 rounded-sm" />
|
||||
<Skeleton className={`h-4 ${tokenWidths[variant % tokenWidths.length]}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
|
||||
SessionSkeleton.displayName = 'SessionSkeleton';
|
||||
|
||||
const renderActualContent = () => {
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full text-text-muted">
|
||||
<AlertCircle className="h-12 w-12 text-red-500 mb-4" />
|
||||
<p className="text-lg mb-2">Error Loading Sessions</p>
|
||||
<p className="text-sm text-center mb-4">{error}</p>
|
||||
<Button onClick={loadSessions} variant="default">
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sessions.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col justify-center h-full text-text-muted">
|
||||
<MessageSquareText className="h-12 w-12 mb-4" />
|
||||
<p className="text-lg mb-2">No chat sessions found</p>
|
||||
<p className="text-sm">Your chat history will appear here</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (dateGroups.length === 0 && searchResults !== null) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full text-text-muted mt-4">
|
||||
<MessageSquareText className="h-12 w-12 mb-4" />
|
||||
<p className="text-lg mb-2">No matching sessions found</p>
|
||||
<p className="text-sm">Try adjusting your search terms</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// For regular rendering in grid layout
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{dateGroups.map((group) => (
|
||||
<div key={group.label} className="space-y-4">
|
||||
<div className="sticky top-0 z-10 bg-background-default/95 backdrop-blur-sm">
|
||||
<h2 className="text-text-muted">{group.label}</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||
{group.sessions.map((session) => (
|
||||
<SessionItem key={session.id} session={session} onEditClick={handleEditSession} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainPanelLayout>
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<div className="bg-background-default px-8 pb-8 pt-16">
|
||||
<div className="flex flex-col page-transition">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<h1 className="text-4xl font-light">Chat history</h1>
|
||||
<>
|
||||
<MainPanelLayout>
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<div className="bg-background-default px-8 pb-8 pt-16">
|
||||
<div className="flex flex-col page-transition">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<h1 className="text-4xl font-light">Chat history</h1>
|
||||
</div>
|
||||
<p className="text-sm text-text-muted mb-4">
|
||||
View and search your past conversations with Goose.
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-text-muted mb-4">
|
||||
View and search your past conversations with Goose.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-h-0 relative px-8">
|
||||
<ScrollArea className="h-full" data-search-scroll-area>
|
||||
<div ref={containerRef} className="h-full relative">
|
||||
<SearchView
|
||||
onSearch={handleSearch}
|
||||
onNavigate={handleSearchNavigation}
|
||||
searchResults={searchResults}
|
||||
className="relative"
|
||||
>
|
||||
{/* Skeleton layer - always rendered but conditionally visible */}
|
||||
<div
|
||||
className={`absolute inset-0 transition-opacity duration-300 ${
|
||||
isLoading || showSkeleton
|
||||
? 'opacity-100 z-10'
|
||||
: 'opacity-0 z-0 pointer-events-none'
|
||||
}`}
|
||||
<div className="flex-1 min-h-0 relative px-8">
|
||||
<ScrollArea className="h-full" data-search-scroll-area>
|
||||
<div ref={containerRef} className="h-full relative">
|
||||
<SearchView
|
||||
onSearch={handleSearch}
|
||||
onNavigate={handleSearchNavigation}
|
||||
searchResults={searchResults}
|
||||
className="relative"
|
||||
>
|
||||
<div className="space-y-8">
|
||||
{/* Today section */}
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-6 w-16" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||
<SessionSkeleton variant={0} />
|
||||
<SessionSkeleton variant={1} />
|
||||
<SessionSkeleton variant={2} />
|
||||
<SessionSkeleton variant={3} />
|
||||
<SessionSkeleton variant={0} />
|
||||
{/* Skeleton layer - always rendered but conditionally visible */}
|
||||
<div
|
||||
className={`absolute inset-0 transition-opacity duration-300 ${
|
||||
isLoading || showSkeleton
|
||||
? 'opacity-100 z-10'
|
||||
: 'opacity-0 z-0 pointer-events-none'
|
||||
}`}
|
||||
>
|
||||
<div className="space-y-8">
|
||||
{/* Today section */}
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-6 w-16" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||
<SessionSkeleton variant={0} />
|
||||
<SessionSkeleton variant={1} />
|
||||
<SessionSkeleton variant={2} />
|
||||
<SessionSkeleton variant={3} />
|
||||
<SessionSkeleton variant={0} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Yesterday section */}
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-6 w-20" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||
<SessionSkeleton variant={1} />
|
||||
<SessionSkeleton variant={2} />
|
||||
<SessionSkeleton variant={3} />
|
||||
<SessionSkeleton variant={0} />
|
||||
<SessionSkeleton variant={1} />
|
||||
<SessionSkeleton variant={2} />
|
||||
{/* Yesterday section */}
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-6 w-20" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||
<SessionSkeleton variant={1} />
|
||||
<SessionSkeleton variant={2} />
|
||||
<SessionSkeleton variant={3} />
|
||||
<SessionSkeleton variant={0} />
|
||||
<SessionSkeleton variant={1} />
|
||||
<SessionSkeleton variant={2} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Additional section */}
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-6 w-24" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||
<SessionSkeleton variant={3} />
|
||||
<SessionSkeleton variant={0} />
|
||||
<SessionSkeleton variant={1} />
|
||||
{/* Additional section */}
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-6 w-24" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
|
||||
<SessionSkeleton variant={3} />
|
||||
<SessionSkeleton variant={0} />
|
||||
<SessionSkeleton variant={1} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content layer - always rendered but conditionally visible */}
|
||||
<div
|
||||
className={`relative transition-opacity duration-300 ${
|
||||
showContent ? 'opacity-100 z-10' : 'opacity-0 z-0'
|
||||
}`}
|
||||
>
|
||||
{renderActualContent()}
|
||||
</div>
|
||||
</SearchView>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
{/* Content layer - always rendered but conditionally visible */}
|
||||
<div
|
||||
className={`relative transition-opacity duration-300 ${
|
||||
showContent ? 'opacity-100 z-10' : 'opacity-0 z-0'
|
||||
}`}
|
||||
>
|
||||
{renderActualContent()}
|
||||
</div>
|
||||
</SearchView>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MainPanelLayout>
|
||||
</MainPanelLayout>
|
||||
|
||||
<EditSessionModal
|
||||
session={editingSession}
|
||||
isOpen={showEditModal}
|
||||
onClose={handleModalClose}
|
||||
onSave={handleModalSave}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
<EditSessionModal
|
||||
session={editingSession}
|
||||
isOpen={showEditModal}
|
||||
onClose={handleModalClose}
|
||||
onSave={handleModalSave}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SessionListView.displayName = 'SessionListView';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user