e1828b6f41
## PR Description ### Summary - Add AI chat thread actions: rename, archive (soft-delete via `deletedAt`), and hard-delete with confirmation. - Add chat thread filtering by status (active/archived/all), group-by mode, and last activity. - Rework drawer/side-panel thread lists to share thread sections, item menus, archive icons, and empty-state behavior. - Extend server chat thread model/API with `deletedAt`, mutations, broadcasts, and archive-aware stream guards. ### Decisions - Two-stage lifecycle: Archive sets `deletedAt` (soft); Delete is a separate action on archived threads that hard-deletes the row. Aligns with Twenty's soft-delete convention (Felix's suggestion). - `lastMessageAt` is derived from `MAX(agentMessage.createdAt)` on read, not stored. List query does inline aggregation for sort; `@ResolveField` covers single-thread / mutation paths so the schema contract is honest everywhere. Matches `timeline-messaging.service.ts` precedent and the existing `totalInputCredits` / `totalOutputCredits` `@ResolveField` pattern in the same resolver. - Replaced auto-CRUD `chatThreads` (cursor-paginated Connection) with a custom `[AgentChatThreadDTO!]` resolver. Frontend metadata-store treats threads as a flat collection and filters/sorts client-side, so cursor pagination was performative. - Sending in an archived chat unarchives it optimistically on the client and authoritatively on the server. - Grouping and last-activity filtering use `lastMessageAt ?? updatedAt` so archive/rename don't bump threads in the list. - Kept metadata-store core API unchanged; AI chat uses the same local cast pattern already used by other metadata-store partial updates. https://github.com/user-attachments/assets/1b179b7b-1a2a-4a7a-aa0a-c88f6f051a87
84 lines
2.0 KiB
TypeScript
84 lines
2.0 KiB
TypeScript
import { t } from '@lingui/core/macro';
|
|
import { differenceInCalendarDays } from 'date-fns';
|
|
|
|
import { type AgentChatThread } from '~/generated-metadata/graphql';
|
|
|
|
export type AgentChatThreadDateGroup = {
|
|
id: string;
|
|
title: string;
|
|
threads: AgentChatThread[];
|
|
};
|
|
|
|
const getLocalDayDifference = (date: Date, today: Date) =>
|
|
differenceInCalendarDays(today, date);
|
|
|
|
const getMonthGroupId = (date: Date) =>
|
|
`month:${date.getFullYear()}-${date.getMonth() + 1}`;
|
|
|
|
const formatMonthGroupTitle = (date: Date) =>
|
|
new Intl.DateTimeFormat(undefined, {
|
|
month: 'long',
|
|
year: 'numeric',
|
|
}).format(date);
|
|
|
|
const getThreadDateGroup = (
|
|
threadActivityAt: Date,
|
|
today: Date,
|
|
): Omit<AgentChatThreadDateGroup, 'threads'> => {
|
|
const localDayDifference = getLocalDayDifference(threadActivityAt, today);
|
|
|
|
if (localDayDifference === 0) {
|
|
return {
|
|
id: 'today',
|
|
title: t`Today`,
|
|
};
|
|
}
|
|
|
|
if (localDayDifference === 1) {
|
|
return {
|
|
id: 'yesterday',
|
|
title: t`Yesterday`,
|
|
};
|
|
}
|
|
|
|
if (localDayDifference >= 2 && localDayDifference <= 7) {
|
|
return {
|
|
id: 'previous-7-days',
|
|
title: t`Previous 7 days`,
|
|
};
|
|
}
|
|
|
|
return {
|
|
id: getMonthGroupId(threadActivityAt),
|
|
title: formatMonthGroupTitle(threadActivityAt),
|
|
};
|
|
};
|
|
|
|
export const groupThreadsByDate = (
|
|
threads: AgentChatThread[],
|
|
today = new Date(),
|
|
): AgentChatThreadDateGroup[] => {
|
|
const groupedThreadsByDate = new Map<string, AgentChatThreadDateGroup>();
|
|
|
|
for (const thread of threads) {
|
|
const threadDateGroup = getThreadDateGroup(
|
|
new Date(thread.lastMessageAt ?? thread.updatedAt),
|
|
today,
|
|
);
|
|
const existingThreadDateGroup = groupedThreadsByDate.get(
|
|
threadDateGroup.id,
|
|
);
|
|
|
|
if (existingThreadDateGroup !== undefined) {
|
|
existingThreadDateGroup.threads.push(thread);
|
|
} else {
|
|
groupedThreadsByDate.set(threadDateGroup.id, {
|
|
...threadDateGroup,
|
|
threads: [thread],
|
|
});
|
|
}
|
|
}
|
|
|
|
return [...groupedThreadsByDate.values()];
|
|
};
|