From 329940433c0067384cc79cd9b8579272ae0093fc Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Tue, 20 May 2025 17:12:52 +0200 Subject: [PATCH] ui: chats list modal fixes, signal editing visually, map charting response --- frontend/app/components/Kai/KaiChat.tsx | 68 +--------- frontend/app/components/Kai/KaiService.ts | 23 +++- frontend/app/components/Kai/KaiStore.ts | 62 ++++++++- .../components/Kai/components/ChatInput.tsx | 93 ++++++++++---- .../app/components/Kai/components/ChatLog.tsx | 2 +- .../app/components/Kai/components/ChatMsg.tsx | 79 +++++++++--- .../components/Kai/components/ChatsModal.tsx | 121 ++++++++++++++++++ frontend/app/components/Kai/utils.ts | 36 ++++++ 8 files changed, 364 insertions(+), 120 deletions(-) create mode 100644 frontend/app/components/Kai/components/ChatsModal.tsx create mode 100644 frontend/app/components/Kai/utils.ts diff --git a/frontend/app/components/Kai/KaiChat.tsx b/frontend/app/components/Kai/KaiChat.tsx index 904181460..0a53ab16d 100644 --- a/frontend/app/components/Kai/KaiChat.tsx +++ b/frontend/app/components/Kai/KaiChat.tsx @@ -1,16 +1,15 @@ import React from 'react'; import { useModal } from 'App/components/Modal'; -import { MessagesSquare, Trash } from 'lucide-react'; import ChatHeader from './components/ChatHeader'; import { PANEL_SIZES } from 'App/constants/panelSizes'; import ChatLog from './components/ChatLog'; import IntroSection from './components/IntroSection'; -import { useQuery } from '@tanstack/react-query'; import { kaiService } from 'App/services'; import { toast } from 'react-toastify'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; import { useHistory, useLocation } from 'react-router-dom'; +import ChatsModal from './components/ChatsModal'; function KaiChat() { const { userStore, projectsStore } = useStore(); @@ -133,69 +132,4 @@ function KaiChat() { ); } -function ChatsModal({ - onSelect, - projectId, -}: { - onSelect: (threadId: string, title: string) => void; - projectId: string; -}) { - const { - data = [], - isPending, - refetch, - } = useQuery({ - queryKey: ['kai', 'chats', projectId], - queryFn: () => kaiService.getKaiChats(projectId), - staleTime: 1000 * 60, - }); - - 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 ( -
-
- - Chats -
- {isPending ? ( -
Loading chats...
- ) : ( -
- {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 h-full px-2 flex items-center group-hover:bg-active-blue" - > - -
-
- ))} -
- )} -
- ); -} - export default observer(KaiChat); diff --git a/frontend/app/components/Kai/KaiService.ts b/frontend/app/components/Kai/KaiService.ts index 50b1ccf9a..468b42550 100644 --- a/frontend/app/components/Kai/KaiService.ts +++ b/frontend/app/components/Kai/KaiService.ts @@ -3,7 +3,7 @@ import AiService from '@/services/AiService'; export default class KaiService extends AiService { getKaiChats = async ( projectId: string, - ): Promise<{ title: string; thread_id: string }[]> => { + ): Promise<{ title: string; thread_id: string, datetime: string }[]> => { const r = await this.client.get(`/kai/${projectId}/chats`); if (!r.ok) { throw new Error('Failed to fetch chats'); @@ -80,4 +80,25 @@ export default class KaiService extends AiService { const data = await r.json(); return data; }; + + getMsgChart = async (messageId: string, projectId: string): Promise<{ filters: any[], chart: string, eventsOrder: string }> => { + const r = await this.client.get(`/kai/${projectId}/chats/data/${messageId}`); + if (!r.ok) { + throw new Error('Failed to fetch chart data'); + } + const data = await r.json(); + return data; + } + + saveChartData = async (messageId: string, projectId: string, chartData: any) => { + const r = await this.client.post(`/kai/${projectId}/chats/data/${messageId}`, { + chart_data: JSON.stringify(chartData), + }); + if (!r.ok) { + throw new Error('Failed to save chart data'); + } + + const data = await r.json(); + return data; + } } diff --git a/frontend/app/components/Kai/KaiStore.ts b/frontend/app/components/Kai/KaiStore.ts index 3208a403c..b5660faf0 100644 --- a/frontend/app/components/Kai/KaiStore.ts +++ b/frontend/app/components/Kai/KaiStore.ts @@ -2,6 +2,7 @@ import { makeAutoObservable, runInAction } from 'mobx'; import { BotChunk, ChatManager } from './SocketManager'; import { kaiService as aiService, kaiService } from 'App/services'; import { toast } from 'react-toastify'; +import Widget from 'App/mstore/types/widget'; export interface Message { text: string; @@ -15,7 +16,11 @@ export interface Message { feedback: boolean | null; duration: number; } -export interface SentMessage extends Omit { +export interface SentMessage + extends Omit< + Message, + 'duration' | 'feedback' | 'chart' | 'supports_visualization' + > { replace: boolean; } @@ -25,7 +30,7 @@ class KaiStore { messages: Array = []; queryText = ''; loadingChat = false; - replacing = false; + replacing: string | null = null; constructor() { makeAutoObservable(this); @@ -83,9 +88,9 @@ class KaiStore { this.messages.push(message); }; - editMessage = (text: string) => { + editMessage = (text: string, messageId: string) => { this.setQueryText(text); - this.setReplacing(true); + this.setReplacing(messageId); }; replaceAtIndex = (message: Message, index: number) => { @@ -192,13 +197,13 @@ class KaiStore { } }; - setReplacing = (replacing: boolean) => { + setReplacing = (replacing: string | null) => { this.replacing = replacing; }; sendMessage = (message: string) => { if (this.chatManager) { - this.chatManager.sendMessage(message, this.replacing); + this.chatManager.sendMessage(message, !!this.replacing); } if (this.replacing) { console.log( @@ -280,7 +285,50 @@ class KaiStore { } }; - getMessageChart = (msgId: string, projectId: string) => { } + getMessageChart = async (msgId: string, projectId: string) => { + this.setProcessingStage({ + content: 'Generating visualization...', + stage: 'chart', + messageId: msgId, + duration: 0, + type: 'chunk', + supports_visualization: false, + }); + try { + const filters = await kaiService.getMsgChart(msgId, projectId); + const data = { + metricId: undefined, + dashboardId: undefined, + widgetId: undefined, + metricOf: undefined, + metricType: undefined, + metricFormat: undefined, + viewType: undefined, + name: 'Kai Visualization', + series: [ + { + name: 'Kai Visualization', + filter: { + eventsOrder: filters.eventsOrder, + filters: filters.filters, + }, + }, + ], + }; + const metric = new Widget().fromJson(data); + return metric; + } catch (e) { + console.error(e); + throw new Error('Failed to generate visualization'); + } finally { + this.setProcessingStage(null); + } + }; + + getParsedChart = (data: string) => { + const parsedData = JSON.parse(data); + return new Widget().fromJson(parsedData); + }; } export const kaiStore = new KaiStore(); diff --git a/frontend/app/components/Kai/components/ChatInput.tsx b/frontend/app/components/Kai/components/ChatInput.tsx index 94bfeb89f..49c8739f6 100644 --- a/frontend/app/components/Kai/components/ChatInput.tsx +++ b/frontend/app/components/Kai/components/ChatInput.tsx @@ -1,55 +1,96 @@ -import React from 'react' -import { Button, Input } from "antd"; -import { SendHorizonal, OctagonX } from "lucide-react"; -import { kaiStore } from "../KaiStore"; -import { observer } from "mobx-react-lite"; +import React from 'react'; +import { Button, Input, Tooltip } from 'antd'; +import { SendHorizonal, OctagonX } from 'lucide-react'; +import { kaiStore } from '../KaiStore'; +import { observer } from 'mobx-react-lite'; -function ChatInput({ isLoading, onSubmit, threadId }: { isLoading?: boolean, onSubmit: (str: string) => void, threadId: string }) { +function ChatInput({ + isLoading, + onSubmit, + threadId, +}: { + isLoading?: boolean; + onSubmit: (str: string) => void; + threadId: string; +}) { const inputRef = React.useRef(null); const inputValue = kaiStore.queryText; - const isProcessing = kaiStore.processingStage !== null + const isProcessing = kaiStore.processingStage !== null; const setInputValue = (text: string) => { - kaiStore.setQueryText(text) - } + kaiStore.setQueryText(text); + }; const submit = () => { if (isProcessing) { - const settings = { projectId: '2325', userId: '0', threadId, }; - void kaiStore.cancelGeneration(settings) + const settings = { projectId: '2325', userId: '0', threadId }; + void kaiStore.cancelGeneration(settings); } else { if (inputValue.length > 0) { - onSubmit(inputValue) - setInputValue('') + onSubmit(inputValue); + setInputValue(''); } } - } + }; + + const cancelReplace = () => { + setInputValue(''); + kaiStore.setReplacing(null); + }; React.useEffect(() => { if (inputRef.current) { - inputRef.current.focus() + inputRef.current.focus(); } - }, [inputValue]) + }, [inputValue]); + + const isReplacing = kaiStore.replacing !== null; return ( { + if (e.key === 'Escape') { + cancelReplace(); + } + }} ref={inputRef} placeholder={'Ask anything about your product and users...'} size={'large'} value={inputValue} onChange={(e) => setInputValue(e.target.value)} suffix={ -