Skip to content

Commit

Permalink
grouping
Browse files Browse the repository at this point in the history
  • Loading branch information
amcdnl committed Jul 24, 2024
1 parent a08d96c commit dcc1121
Show file tree
Hide file tree
Showing 13 changed files with 259 additions and 73 deletions.
20 changes: 1 addition & 19 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"typings": "dist/index.d.ts",
"dependencies": {
"@radix-ui/react-slot": "^1.1.0",
"date-fns": "^3.6.0",
"framer-motion": "^10.16.16",
"reablocks": "^8.4.1",
"react-markdown": "^9.0.1",
Expand Down Expand Up @@ -87,7 +88,6 @@
"vite-plugin-checker": "^0.6.4",
"vite-plugin-css-injected-by-js": "^3.5.0",
"vite-plugin-dts": "^3.7.3",
"vite-plugin-static-copy": "^1.0.4",
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.4.0"
Expand Down
1 change: 1 addition & 0 deletions src/SessionListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const SessionListItem: FC<SessionListItemProps> = ({
deleteIcon = <TrashIcon className={cn(theme.sessions.session.delete)} />
}) => (
<ListItem
dense
disableGutters
active={isActive}
className={cn(theme.sessions.session.base)}
Expand Down
5 changes: 2 additions & 3 deletions src/Sessions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,13 @@ export const Sessions: FC<SessionsProps> = ({
const contextValue = useMemo(() => ({
sessions,
activeSession,
theme,
activeSessionId: internalActiveSessionID,
selectSession: handleSelectSession,
deleteSession: handleDeleteSession,
createSession: handleCreateNewSession
}), [
theme,
sessions,
activeSession,
internalActiveSessionID,
Expand All @@ -176,10 +178,7 @@ export const Sessions: FC<SessionsProps> = ({
})}>
<>
<SessionsList
sessions={sessions}
theme={theme}
newSessionText={newSessionText}
activeSessionId={internalActiveSessionID}
onSelectSession={handleSelectSession}
onDeleteSession={onDeleteSession ? handleDeleteSession : null}
onCreateNewSession={handleCreateNewSession}
Expand Down
2 changes: 2 additions & 0 deletions src/SessionsContext.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { createContext } from 'react';
import { Session } from './types';
import { ChatTheme } from './theme';

export interface SessionContextProps {
sessions: Session[];
activeSessionId: string | null;
theme?: ChatTheme;
activeSession?: Session | null;
selectSession?: (sessionId: string) => void;
deleteSession?: (sessionId: string) => void;
Expand Down
50 changes: 22 additions & 28 deletions src/SessionsList.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,12 @@
import { FC } from 'react';
import { FC, Fragment, useContext, useMemo } from 'react';
import { SessionListItem } from './SessionListItem';
import { Session } from './types';
import { List, ListItem, Button, cn } from 'reablocks';
import { ChatTheme } from './theme';
import { groupSessionsByDate } from './utils';
import { SessionsContext } from './SessionsContext';

interface SessionsListProps {
/**
* Sessions to display.
*/
sessions: Session[];

/**
* ID of the currently active session.
*/
activeSessionId?: string;

/**
* Theme to use for the sessions list.
*/
theme?: ChatTheme;

/**
* Text to show for the new session button.
*/
Expand All @@ -42,14 +29,14 @@ interface SessionsListProps {
}

export const SessionsList: FC<SessionsListProps> = ({
sessions,
theme,
newSessionText = 'New Session',
activeSessionId,
onSelectSession,
onDeleteSession,
onCreateNewSession
}) => {
const { theme, activeSessionId, sessions } = useContext(SessionsContext);
const groups = useMemo(() => groupSessionsByDate(sessions), [sessions]);

return (
<List className={cn(theme.sessions.base)}>
<ListItem disableGutters disablePadding>
Expand All @@ -62,15 +49,22 @@ export const SessionsList: FC<SessionsListProps> = ({
{newSessionText}
</Button>
</ListItem>
{sessions?.map((session) => (
<SessionListItem
key={session.id}
session={session}
theme={theme}
isActive={session.id === activeSessionId}
onSelectSession={onSelectSession}
onDeleteSession={onDeleteSession}
/>
{Object.keys(groups).map(k => (
<Fragment key={k}>
<ListItem disableGutters disablePadding className={cn(theme.sessions.group)}>
{k}
</ListItem>
{groups[k].map(s => (
<SessionListItem
key={s.id}
session={s}
theme={theme}
isActive={s.id === activeSessionId}
onSelectSession={onSelectSession}
onDeleteSession={onDeleteSession}
/>
))}
</Fragment>
))}
</List>
);
Expand Down
6 changes: 4 additions & 2 deletions src/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface ChatTheme {
sessions: {
base: string;
create: string;
group: string;
session: {
base: string;
delete: string;
Expand All @@ -30,10 +31,11 @@ export const chatTheme: ChatTheme = {
base: 'text-white',
empty: 'text-center flex-1',
sessions: {
base: '',
base: 'overflow-auto',
group: 'text-xs text-gray-400 mt-4',
create: 'mb-4',
session: {
base: 'mb-4',
base: '',
delete: 'w-4 h-4'
}
},
Expand Down
8 changes: 4 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ export interface Conversation {
question: string;
response?: string;
sources?: ConversationSource[];
files: string[]; // TODO
files?: string[]; // TODO
user?: User; // TODO
}

export interface Session {
id: string;
title: string;
createdAt: Date;
updatedAt: Date;
title?: string;
createdAt?: Date;
updatedAt?: Date;
conversations: Conversation[];
}

Expand Down
69 changes: 69 additions & 0 deletions src/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, it, expect } from 'vitest';
import { groupSessionsByDate } from './utils';
import { Session } from './types';
import { subDays, subWeeks, subMonths, subYears } from 'date-fns';

describe('groupSessionsByDate', () => {
const createSession = (daysAgo: number): Session => ({
id: `session-${daysAgo}`,
createdAt: subDays(new Date(), daysAgo),
updatedAt: subDays(new Date(), daysAgo),
name: `Test Session ${daysAgo} days ago`,
conversations: [],
});

it('groups sessions correctly', () => {
const sessions: Session[] = [
createSession(0), // Today
createSession(1), // Yesterday
createSession(3), // Last Week
createSession(20), // Last Month
createSession(60), // A few months ago
createSession(400) // Last Year
];

const grouped = groupSessionsByDate(sessions);

expect(Object.keys(grouped)).toEqual(['Today', 'Yesterday', 'Last Week', 'Last Month', expect.stringMatching(/^[A-Z][a-z]+$/), 'Last Year']);
expect(grouped['Today'].length).toBe(1);
expect(grouped['Yesterday'].length).toBe(1);
expect(grouped['Last Week'].length).toBe(1);
expect(grouped['Last Month'].length).toBe(1);
expect(Object.values(grouped).flat().length).toBe(sessions.length);
});

it('handles empty input', () => {
const grouped = groupSessionsByDate([]);
expect(Object.keys(grouped)).toHaveLength(0);
});

it('groups multiple sessions in the same category', () => {
const sessions: Session[] = [
createSession(0),
createSession(0),
createSession(1),
createSession(1),
];

const grouped = groupSessionsByDate(sessions);

expect(grouped['Today'].length).toBe(2);
expect(grouped['Yesterday'].length).toBe(2);
});

it('sorts groups in the correct order', () => {
const sessions: Session[] = [
createSession(400), // Last Year
createSession(0), // Today
createSession(60), // A few months ago
createSession(1), // Yesterday
];

const grouped = groupSessionsByDate(sessions);
const groupOrder = Object.keys(grouped);

expect(groupOrder[0]).toBe('Today');
expect(groupOrder[1]).toBe('Yesterday');
expect(groupOrder[groupOrder.length - 1]).toBe('Last Year');
});
});
77 changes: 77 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { format, isToday, isYesterday, isThisWeek, isThisMonth, isThisYear, parseISO } from 'date-fns';
import { Session } from './types';

interface GroupedSessions {
[key: string]: Session[];
}

const sortOrder = [
'Today',
'Yesterday',
'Last Week',
'Last Month',
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
'Last Year'
];

export function groupSessionsByDate(sessions: Session[]): GroupedSessions {
const grouped: GroupedSessions = {};

sessions.forEach(session => {
const createdAt = new Date(session.createdAt);

if (isToday(createdAt)) {
if (!grouped['Today']) grouped['Today'] = [];
grouped['Today'].push(session);
} else if (isYesterday(createdAt)) {
if (!grouped['Yesterday']) grouped['Yesterday'] = [];
grouped['Yesterday'].push(session);
} else if (isThisWeek(createdAt)) {
if (!grouped['Last Week']) grouped['Last Week'] = [];
grouped['Last Week'].push(session);
} else if (isThisMonth(createdAt)) {
if (!grouped['Last Month']) grouped['Last Month'] = [];
grouped['Last Month'].push(session);
} else if (isThisYear(createdAt)) {
const monthName = format(createdAt, 'MMMM');
if (!grouped[monthName]) grouped[monthName] = [];
grouped[monthName].push(session);
} else {
if (!grouped['Last Year']) grouped['Last Year'] = [];
grouped['Last Year'].push(session);
}
});

// Remove empty groups
Object.keys(grouped).forEach(key => {
if (grouped[key].length === 0) {
delete grouped[key];
}
});

// Sort groups
const sortedGroups = Object.keys(grouped).sort((a, b) =>
sortOrder.indexOf(a) - sortOrder.indexOf(b)
);

const sortedGrouped: GroupedSessions = {};
sortedGroups.forEach(key => {
// Sort sessions within each group by createdAt date (most recent first)
sortedGrouped[key] = grouped[key].sort((a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
});

return sortedGrouped;
}
Loading

0 comments on commit dcc1121

Please sign in to comment.