+
{text}
+ {metric ? (
+
+
+
+ ) : null}
{isUser ? (
canEdit ? (
-
-
-
+ {supports_visualization ? (
+
+
+
+ ) : null}
bodyRef.current?.innerHTML}
content={text}
@@ -223,3 +264,5 @@ function MsgDuration({ duration }: { duration: number }) {
);
}
+
+export default observer(ChatMsg);
diff --git a/frontend/app/components/Kai/components/ChatsModal.tsx b/frontend/app/components/Kai/components/ChatsModal.tsx
new file mode 100644
index 000000000..b6175a978
--- /dev/null
+++ b/frontend/app/components/Kai/components/ChatsModal.tsx
@@ -0,0 +1,121 @@
+import React from 'react';
+import { splitByDate } from '../utils';
+import { useQuery } from '@tanstack/react-query';
+import { MessagesSquare, Trash } from 'lucide-react';
+import { kaiService } from 'App/services';
+import { toast } from 'react-toastify';
+import { useTranslation } from 'react-i18next';
+
+function ChatsModal({
+ onSelect,
+ projectId,
+}: {
+ onSelect: (threadId: string, title: string) => void;
+ projectId: string;
+}) {
+ const { t } = useTranslation();
+ const {
+ data = [],
+ isPending,
+ refetch,
+ } = useQuery({
+ queryKey: ['kai', 'chats', projectId],
+ queryFn: () => kaiService.getKaiChats(projectId),
+ staleTime: 1000 * 60,
+ });
+
+ const datedCollections = React.useMemo(() => {
+ return data.length ? splitByDate(data) : [];
+ }, [data.length]);
+
+ const onDelete = async (id: string) => {
+ try {
+ await kaiService.deleteKaiChat(projectId, id);
+ } catch (e) {
+ toast.error("Something wen't wrong. Please try again later.");
+ }
+ refetch();
+ };
+ return (
+
+
+
+ {t('Chats')}
+
+ {isPending ? (
+
{t('Loading chats')}...
+ ) : (
+
+ {datedCollections.map((col) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+function ChatCollection({
+ data,
+ onSelect,
+ onDelete,
+ date,
+}: {
+ data: { title: string; thread_id: string }[];
+ onSelect: (threadId: string, title: string) => void;
+ onDelete: (threadId: string) => void;
+ date: string;
+}) {
+ return (
+
+ );
+}
+
+function ChatsList({
+ data,
+ onSelect,
+ onDelete,
+}: {
+ data: { title: string; thread_id: string }[];
+ onSelect: (threadId: string, title: string) => void;
+ onDelete: (threadId: string) => void;
+}) {
+ return (
+
+ {data.map((chat) => (
+
+
+
onSelect(chat.thread_id, chat.title)}
+ className="cursor-pointer hover:underline truncate"
+ >
+ {chat.title}
+
+
+
onDelete(chat.thread_id)}
+ className="cursor-pointer opacity-0 group-hover:opacity-100 rounded-r min-h-7 h-full px-2 flex items-center group-hover:bg-active-blue"
+ >
+
+
+
+ ))}
+
+ );
+}
+
+export default ChatsModal;
diff --git a/frontend/app/components/Kai/utils.ts b/frontend/app/components/Kai/utils.ts
new file mode 100644
index 000000000..691ad785f
--- /dev/null
+++ b/frontend/app/components/Kai/utils.ts
@@ -0,0 +1,36 @@
+import { DateTime } from 'luxon';
+
+type DatedEntry = {
+ date: string;
+ entries: { datetime: string }[];
+}
+
+export function splitByDate(entries: { datetime: string }[]) {
+ const today = DateTime.now().startOf('day');
+ const yesterday = today.minus({ days: 1 });
+
+ const result: DatedEntry[] = [
+ { date: 'Today', entries: [] },
+ { date: 'Yesterday', entries: [] },
+ ];
+
+ entries.forEach((ent) => {
+ const entryDate = DateTime.fromISO(ent.datetime).startOf('day');
+
+ if (entryDate.toMillis() === today.toMillis()) {
+ result[0].entries.push(ent);
+ } else if (entryDate.toMillis() === yesterday.toMillis()) {
+ result[1].entries.push(ent);
+ } else {
+ const date = entryDate.toFormat('dd LLL, yyyy')
+ const existingEntry = result.find((r) => r.date === date);
+ if (existingEntry) {
+ existingEntry.entries.push(ent);
+ } else {
+ result.push({ entries: [ent], date });
+ }
+ }
+ });
+
+ return result;
+}