From efe16a70656689aafd048d9b36daa08110ee40a4 Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Tue, 1 Apr 2025 17:12:05 +0200 Subject: [PATCH] ui: kai ui thing ui: fixes for picking existing chat, feedback and retry buttons ui: connect finding, creating threads ui: more ui tuning for chat window, socket manager ui: get/delete chats logic, create testing socket ui: testing ui: use on click query ui: minor fixed for chat display, rebase ui: start kai thing --- frontend/app/PrivateRoutes.tsx | 13 +- frontend/app/api_client.ts | 4 +- frontend/app/components/Assist/AssistView.tsx | 3 +- .../Assist/RecordingsList/Recordings.tsx | 3 +- frontend/app/components/Client/Client.tsx | 3 +- .../components/Alerts/AlertsView.tsx | 3 +- .../Dashboard/components/Alerts/NewAlert.tsx | 3 +- .../DashboardList/DashboardsView.tsx | 5 +- .../DashboardModal/DashboardModal.tsx | 3 +- .../DashboardView/DashboardView.tsx | 3 +- .../components/MetricsView/MetricsView.tsx | 3 +- .../components/WidgetView/WidgetView.tsx | 3 +- frontend/app/components/FFlags/FFlagsList.tsx | 3 +- .../components/FFlags/FlagView/FlagView.tsx | 3 +- .../components/FFlags/NewFFlag/NewFFlag.tsx | 5 +- frontend/app/components/Kai/KaiChat.tsx | 174 ++ frontend/app/components/Kai/SocketManager.ts | 75 + .../components/Kai/components/ChatHeader.tsx | 54 + .../components/Kai/components/ChatInput.tsx | 37 + .../app/components/Kai/components/ChatLog.tsx | 149 + .../app/components/Kai/components/ChatMsg.tsx | 94 + .../app/components/Kai/components/Ideas.tsx | 32 + .../Kai/components/IntroSection.tsx | 27 + .../app/components/Onboarding/Onboarding.tsx | 3 +- frontend/app/components/Overview/Overview.tsx | 5 +- .../components/UsabilityTesting/TestEdit.tsx | 4 +- .../UsabilityTesting/TestOverview.tsx | 3 +- .../UsabilityTesting/UsabilityTesting.tsx | 3 +- .../components/Notes/NotesRoute.tsx | 3 +- frontend/app/components/ui/Icons/index.ts | 2 + frontend/app/components/ui/Icons/kai.tsx | 18 + .../app/components/ui/Icons/kai_colored.tsx | 64 + frontend/app/components/ui/SVG.tsx | 10 +- frontend/app/constants/panelSizes.ts | 3 + frontend/app/layout/SideMenu.tsx | 5 +- frontend/app/layout/data.ts | 11 +- frontend/app/mstore/index.tsx | 33 +- frontend/app/mstore/kaiStore.ts | 75 + frontend/app/routes.ts | 4 + frontend/app/services/AiService.ts | 67 + frontend/app/styles/import.css | 3 +- frontend/app/svg/icons/kai.svg | 9 + frontend/app/svg/icons/kai_colored.svg | 46 + frontend/app/utils/split-utils.ts | 5 + frontend/package.json | 2 +- frontend/yarn.lock | 2480 +++++++++++------ yarn.lock | 4 - 47 files changed, 2655 insertions(+), 909 deletions(-) create mode 100644 frontend/app/components/Kai/KaiChat.tsx create mode 100644 frontend/app/components/Kai/SocketManager.ts create mode 100644 frontend/app/components/Kai/components/ChatHeader.tsx create mode 100644 frontend/app/components/Kai/components/ChatInput.tsx create mode 100644 frontend/app/components/Kai/components/ChatLog.tsx create mode 100644 frontend/app/components/Kai/components/ChatMsg.tsx create mode 100644 frontend/app/components/Kai/components/Ideas.tsx create mode 100644 frontend/app/components/Kai/components/IntroSection.tsx create mode 100644 frontend/app/components/ui/Icons/kai.tsx create mode 100644 frontend/app/components/ui/Icons/kai_colored.tsx create mode 100644 frontend/app/constants/panelSizes.ts create mode 100644 frontend/app/mstore/kaiStore.ts create mode 100644 frontend/app/svg/icons/kai.svg create mode 100644 frontend/app/svg/icons/kai_colored.svg create mode 100644 frontend/app/utils/split-utils.ts delete mode 100644 yarn.lock diff --git a/frontend/app/PrivateRoutes.tsx b/frontend/app/PrivateRoutes.tsx index 933627087..d72d67368 100644 --- a/frontend/app/PrivateRoutes.tsx +++ b/frontend/app/PrivateRoutes.tsx @@ -32,7 +32,8 @@ const components: any = { SpotsListPure: lazy(() => import('Components/Spots/SpotsList')), SpotPure: lazy(() => import('Components/Spots/SpotPlayer')), ScopeSetup: lazy(() => import('Components/ScopeForm')), - HighlightsPure: lazy(() => import('Components/Highlights/HighlightsList')) + HighlightsPure: lazy(() => import('Components/Highlights/HighlightsList')), + KaiPure: lazy(() => import('Components/Kai/KaiChat')), }; const enhancedComponents: any = { @@ -52,7 +53,8 @@ const enhancedComponents: any = { SpotsList: withSiteIdUpdater(components.SpotsListPure), Spot: components.SpotPure, ScopeSetup: components.ScopeSetup, - Highlights: withSiteIdUpdater(components.HighlightsPure) + Highlights: withSiteIdUpdater(components.HighlightsPure), + Kai: withSiteIdUpdater(components.KaiPure), }; const { withSiteId } = routes; @@ -97,6 +99,7 @@ const SPOT_PATH = routes.spot(); const SCOPE_SETUP = routes.scopeSetup(); const HIGHLIGHTS_PATH = routes.highlights(); +const KAI_PATH = routes.kai(); function PrivateRoutes() { const { projectsStore, userStore, integrationsStore, searchStore } = useStore(); @@ -270,6 +273,12 @@ function PrivateRoutes() { path={withSiteId(LIVE_SESSION_PATH, siteIdList)} component={enhancedComponents.LiveSession} /> + {Object.entries(routes.redirects).map(([fr, to]) => ( ))} diff --git a/frontend/app/api_client.ts b/frontend/app/api_client.ts index a2dc3cb4c..899cf904d 100644 --- a/frontend/app/api_client.ts +++ b/frontend/app/api_client.ts @@ -60,7 +60,7 @@ export default class APIClient { private siteIdCheck: (() => { siteId: string | null }) | undefined; - private getJwt: () => string | null = () => null; + public getJwt: () => string | null = () => null; private onUpdateJwt: (data: { jwt?: string; spotJwt?: string }) => void; @@ -197,7 +197,7 @@ export default class APIClient { delete init.credentials; } - const noChalice = path.includes('v1/integrations') || path.includes('/spot') && !path.includes('/login'); + const noChalice = path.includes('/kai') || path.includes('v1/integrations') || path.includes('/spot') && !path.includes('/login'); let edp = window.env.API_EDP || window.location.origin + '/api'; if (noChalice && !edp.includes('api.openreplay.com')) { edp = edp.replace('/api', ''); diff --git a/frontend/app/components/Assist/AssistView.tsx b/frontend/app/components/Assist/AssistView.tsx index c16bd0cd7..95579face 100644 --- a/frontend/app/components/Assist/AssistView.tsx +++ b/frontend/app/components/Assist/AssistView.tsx @@ -3,11 +3,12 @@ import LiveSessionList from 'Shared/LiveSessionList'; import LiveSessionSearch from 'Shared/LiveSessionSearch'; import usePageTitle from '@/hooks/usePageTitle'; import AssistSearchActions from './AssistSearchActions'; +import { PANEL_SIZES } from 'App/constants/panelSizes' function AssistView() { usePageTitle('Co-Browse - OpenReplay'); return ( -
+
diff --git a/frontend/app/components/Assist/RecordingsList/Recordings.tsx b/frontend/app/components/Assist/RecordingsList/Recordings.tsx index 074386797..a2aeb03a6 100644 --- a/frontend/app/components/Assist/RecordingsList/Recordings.tsx +++ b/frontend/app/components/Assist/RecordingsList/Recordings.tsx @@ -7,6 +7,7 @@ import { observer } from 'mobx-react-lite'; import RecordingsList from './RecordingsList'; import RecordingsSearch from './RecordingsSearch'; import { useTranslation } from 'react-i18next'; +import { PANEL_SIZES } from 'App/constants/panelSizes' function Recordings() { const { t } = useTranslation(); @@ -24,7 +25,7 @@ function Recordings() { return (
diff --git a/frontend/app/components/Client/Client.tsx b/frontend/app/components/Client/Client.tsx index cf41335a9..e24cf9f91 100644 --- a/frontend/app/components/Client/Client.tsx +++ b/frontend/app/components/Client/Client.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { withRouter } from 'react-router-dom'; import { Switch, Route, Redirect } from 'react-router'; import { CLIENT_TABS, client as clientRoute } from 'App/routes'; +import { PANEL_SIZES } from 'App/constants/panelSizes' import SessionsListingSettings from 'Components/Client/SessionsListingSettings'; import Modules from 'Components/Client/Modules'; @@ -105,7 +106,7 @@ export default class Client extends React.PureComponent { }, } = this.props; return ( -
+
{activeTab && this.renderActiveTab()}
); diff --git a/frontend/app/components/Dashboard/components/Alerts/AlertsView.tsx b/frontend/app/components/Dashboard/components/Alerts/AlertsView.tsx index 92ec68dd2..f1c980172 100644 --- a/frontend/app/components/Dashboard/components/Alerts/AlertsView.tsx +++ b/frontend/app/components/Dashboard/components/Alerts/AlertsView.tsx @@ -10,6 +10,7 @@ import { useStore } from 'App/mstore'; import AlertsList from './AlertsList'; import AlertsSearch from './AlertsSearch'; import { useTranslation } from 'react-i18next'; +import { PANEL_SIZES } from 'App/constants/panelSizes' interface IAlertsView { siteId: string; @@ -30,7 +31,7 @@ function AlertsView({ siteId }: IAlertsView) { }, [history]); return (
diff --git a/frontend/app/components/Dashboard/components/Alerts/NewAlert.tsx b/frontend/app/components/Dashboard/components/Alerts/NewAlert.tsx index b679bccdb..a1843c4e6 100644 --- a/frontend/app/components/Dashboard/components/Alerts/NewAlert.tsx +++ b/frontend/app/components/Dashboard/components/Alerts/NewAlert.tsx @@ -16,6 +16,7 @@ import NotifyHooks from './AlertForm/NotifyHooks'; import AlertListItem from './AlertListItem'; import Condition from './AlertForm/Condition'; import { useTranslation } from 'react-i18next'; +import { PANEL_SIZES } from 'App/constants/panelSizes' function Circle({ text }: { text: string }) { return ( @@ -200,7 +201,7 @@ function NewAlert(props: IProps) { const isThreshold = instance.detectionMethod === 'threshold'; return ( -
+
diff --git a/frontend/app/components/Dashboard/components/DashboardModal/DashboardModal.tsx b/frontend/app/components/Dashboard/components/DashboardModal/DashboardModal.tsx index a9c92298f..e175c8ccb 100644 --- a/frontend/app/components/Dashboard/components/DashboardModal/DashboardModal.tsx +++ b/frontend/app/components/Dashboard/components/DashboardModal/DashboardModal.tsx @@ -8,6 +8,7 @@ import { dashboardMetricCreate, withSiteId } from 'App/routes'; import DashboardForm from '../DashboardForm'; import DashboardMetricSelection from '../DashboardMetricSelection'; import { useTranslation } from 'react-i18next'; +import { PANEL_SIZES } from 'App/constants/panelSizes' interface Props extends RouteComponentProps { history: any; @@ -57,7 +58,7 @@ function DashboardModal(props: Props) { backgroundColor: '#FAFAFA', zIndex: 999, width: '100%', - maxWidth: '1360px', + maxWidth: PANEL_SIZES.maxWidth, }} >
diff --git a/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx b/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx index 4ed7bece5..85b15b17e 100644 --- a/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx +++ b/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx @@ -14,6 +14,7 @@ import DashboardHeader from '../DashboardHeader'; import DashboardModal from '../DashboardModal'; import DashboardWidgetGrid from '../DashboardWidgetGrid'; import AiQuery from './AiQuery'; +import { PANEL_SIZES } from 'App/constants/panelSizes' interface IProps { siteId: string; @@ -103,7 +104,7 @@ function DashboardView(props: Props) { return (
{/* @ts-ignore */} diff --git a/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx b/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx index 380f205ea..a885c4e0f 100644 --- a/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx +++ b/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx @@ -3,6 +3,7 @@ import withPageTitle from 'HOCs/withPageTitle'; import { observer } from 'mobx-react-lite'; import MetricsList from '../MetricsList'; import MetricViewHeader from '../MetricViewHeader'; +import { PANEL_SIZES } from 'App/constants/panelSizes' interface Props { siteId: string; @@ -10,7 +11,7 @@ interface Props { function MetricsView({ siteId }: Props) { return (
diff --git a/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx b/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx index d59e4e32a..b7d03d83e 100644 --- a/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx +++ b/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx @@ -31,6 +31,7 @@ import CardUserList from '../CardUserList/CardUserList'; import WidgetSessions from '../WidgetSessions'; import WidgetPreview from '../WidgetPreview'; import { useTranslation } from 'react-i18next'; +import { PANEL_SIZES } from 'App/constants/panelSizes'; interface Props { history: any; @@ -183,7 +184,7 @@ function WidgetView({ : 'You have unsaved changes. Are you sure you want to leave?' } /> -
+
diff --git a/frontend/app/components/FFlags/FlagView/FlagView.tsx b/frontend/app/components/FFlags/FlagView/FlagView.tsx index c12b992dd..9dfb5baf2 100644 --- a/frontend/app/components/FFlags/FlagView/FlagView.tsx +++ b/frontend/app/components/FFlags/FlagView/FlagView.tsx @@ -10,6 +10,7 @@ import Multivariant from 'Components/FFlags/NewFFlag/Multivariant'; import { toast } from 'react-toastify'; import RolloutCondition from 'Shared/ConditionSet'; import { useTranslation } from 'react-i18next'; +import { PANEL_SIZES } from "App/constants/panelSizes"; function FlagView({ siteId, fflagId }: { siteId: string; fflagId: string }) { const { t } = useTranslation(); @@ -52,7 +53,7 @@ function FlagView({ siteId, fflagId }: { siteId: string; fflagId: string }) { }; return ( -
+
; if (!current) { return ( -
+
+
diff --git a/frontend/app/components/Kai/KaiChat.tsx b/frontend/app/components/Kai/KaiChat.tsx new file mode 100644 index 000000000..fb7972198 --- /dev/null +++ b/frontend/app/components/Kai/KaiChat.tsx @@ -0,0 +1,174 @@ +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 { aiService } 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' + +function KaiChat() { + const { userStore, projectsStore } = useStore(); + const history = useHistory(); + const [chatTitle, setTitle] = React.useState(null); + const userId = userStore.account.id; + const userLetter = userStore.account.name[0].toUpperCase(); + const { activeSiteId } = projectsStore; + const [section, setSection] = React.useState<'intro' | 'chat'>('intro'); + const [threadId, setThreadId] = React.useState(null); + const [initialMsg, setInitialMsg] = React.useState(null); + const { showModal, hideModal } = useModal(); + const location = useLocation(); + const params = new URLSearchParams(location.search); + const threadIdFromUrl = params.get('threadId'); + + const openChats = () => { + showModal( + { + setTitle(title); + setThreadId(threadId) + hideModal(); + }} />, + { right: true, width: 300 }, + ); + }; + + React.useEffect(() => { + if (threadIdFromUrl) { + setThreadId(threadIdFromUrl); + setSection('chat'); + } + }, [threadIdFromUrl]) + + React.useEffect(() => { + if (threadId) { + setSection('chat'); + history.replace({ search: `?threadId=${threadId}` }); + } else { + setTitle(null); + history.replace({ search: '' }); + } + }, [threadId]); + + if (!userId || !activeSiteId) return null; + + const canGoBack = section !== 'intro'; + const goBack = canGoBack ? () => { + if (section === 'chat') { + setThreadId(null); + setSection('intro') + } + } : undefined; + + const onCreate = async (firstMsg?: string) => { + //const settings = { projectId: projectId ?? 2325, userId: userId ?? 65 }; + const settings = { projectId: '2325', userId: '0' }; + if (firstMsg) { + setInitialMsg(firstMsg); + } + const newThread = await aiService.createKaiChat(settings.projectId, settings.userId) + if (newThread) { + setThreadId(newThread.toString()); + setSection('chat'); + } else { + toast.error("Something wen't wrong. Please try again later."); + } + } + return ( +
+
+ +
+ {section === 'intro' ? ( + + ) : ( + + )} +
+
+
+ ); +} + +function ChatsModal({ onSelect }: { onSelect: (threadId: string, title: string) => void }) { + const userId = '0'; + const projectId = '2325'; + const { + data = [], + isPending, + refetch, + } = useQuery({ + queryKey: ['kai', 'chats'], + queryFn: () => aiService.getKaiChats(userId, projectId), + staleTime: 1000 * 60, + cacheTime: 1000 * 60 * 5, + }); + + const onDelete = async (id: string) => { + try { + await aiService.deleteKaiChat(projectId, userId, 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/SocketManager.ts b/frontend/app/components/Kai/SocketManager.ts new file mode 100644 index 000000000..b57fd1a84 --- /dev/null +++ b/frontend/app/components/Kai/SocketManager.ts @@ -0,0 +1,75 @@ +import io from 'socket.io-client'; + +export class ChatManager { + socket: ReturnType; + threadId: string | null = null; + + constructor({ projectId, userId, threadId }: { projectId: string; userId: string; threadId: string }) { + this.threadId = threadId; + const socket = io(`localhost:8700/kai/chat`, { + transports: ['websocket'], + autoConnect: true, + reconnection: true, + reconnectionAttempts: 5, + reconnectionDelay: 1000, + reconnectionDelayMax: 5000, + withCredentials: true, + multiplex: true, + query: { + user_id: userId, + token: window.env.KAI_TESTING, + project_id: projectId, + }, + }); + socket.on('connect', () => { + console.log('Connected to server'); + }); + socket.on('disconnect', () => { + console.log('Disconnected from server'); + }); + socket.onAny((e) => console.log('event', e)); + + this.socket = socket; + } + + sendMesage = (message: string) => { + this.socket.emit( + 'message', + JSON.stringify({ + message, + threadId: this.threadId, + }), + ); + }; + + setOnMsgHook = ({ + msgCallback, + titleCallback, + }: { + msgCallback: (msg: BotChunk) => void; + titleCallback: (title: string) => void; + }) => { + this.socket.on('chunk', (msg: BotChunk) => { + console.log('Received message:', msg); + msgCallback(msg); + }); + this.socket.on('title', (msg: { content: string }) => { + console.log('Received title:', msg); + titleCallback(msg.content); + }); + }; + + disconnect = () => { + this.socket.disconnect(); + }; +} + +export interface BotChunk { + stage: 'chart' | 'final' | 'title'; + content: string; + data?: any[]; +} +export interface Message { + text: string; + isUser: boolean; +} diff --git a/frontend/app/components/Kai/components/ChatHeader.tsx b/frontend/app/components/Kai/components/ChatHeader.tsx new file mode 100644 index 000000000..756aa9767 --- /dev/null +++ b/frontend/app/components/Kai/components/ChatHeader.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { Icon } from 'UI'; +import { MessagesSquare, ArrowLeft } from 'lucide-react'; + +function ChatHeader({ + openChats = () => {}, + goBack, + chatTitle, +}: { + goBack?: () => void; + openChats?: () => void; + chatTitle: string | null; +}) { + return ( +
+
+ {goBack ? ( +
+ +
Back
+
+ ) : null} +
+
+ {chatTitle ? ( +
{chatTitle}
+ ) : ( + <> + +
Kai
+ + )} +
+
+ +
Chats
+
+
+ ); +} + +export default ChatHeader; diff --git a/frontend/app/components/Kai/components/ChatInput.tsx b/frontend/app/components/Kai/components/ChatInput.tsx new file mode 100644 index 000000000..522228b5d --- /dev/null +++ b/frontend/app/components/Kai/components/ChatInput.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import { Button, Input } from "antd"; +import { SendHorizonal } from "lucide-react"; + +function ChatInput({ isLoading, onSubmit }: { isLoading?: boolean, onSubmit: (str: string) => void }) { + const [inputValue, setInputValue] = React.useState(''); + + const submit = () => { + onSubmit(inputValue) + setInputValue('') + } + return ( + setInputValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && inputValue) { + submit() + } + }} + suffix={ +