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
This commit is contained in:
parent
4e331b70a4
commit
efe16a7065
47 changed files with 2655 additions and 909 deletions
|
|
@ -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}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
strict
|
||||
path={withSiteId(KAI_PATH, siteIdList)}
|
||||
component={enhancedComponents.Kai}
|
||||
/>
|
||||
{Object.entries(routes.redirects).map(([fr, to]) => (
|
||||
<Redirect key={fr} exact strict from={fr} to={to} />
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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', '');
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="w-full mx-auto" style={{ maxWidth: '1360px' }}>
|
||||
<div className="w-full mx-auto" style={{ maxWidth: PANEL_SIZES.maxWidth }}>
|
||||
<AssistSearchActions />
|
||||
<LiveSessionSearch />
|
||||
<div className="my-4" />
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div
|
||||
style={{ maxWidth: '1360px', margin: 'auto' }}
|
||||
style={{ maxWidth: PANEL_SIZES.maxWidth, margin: 'auto' }}
|
||||
className="bg-white rounded-lg py-4 border h-screen overflow-y-scroll"
|
||||
>
|
||||
<div className="flex items-center mb-4 justify-between px-6">
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="w-full mx-auto mb-8" style={{ maxWidth: '1360px' }}>
|
||||
<div className="w-full mx-auto mb-8" style={{ maxWidth: PANEL_SIZES.maxWidth }}>
|
||||
{activeTab && this.renderActiveTab()}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div
|
||||
style={{ maxWidth: '1360px', margin: 'auto' }}
|
||||
style={{ maxWidth: PANEL_SIZES.maxWidth, margin: 'auto' }}
|
||||
className="bg-white rounded-lg shadow-sm py-4 border"
|
||||
>
|
||||
<div className="flex items-center mb-4 justify-between px-6">
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div style={{ maxWidth: '1360px', margin: 'auto' }}>
|
||||
<div style={{ maxWidth: PANEL_SIZES.maxWidth, margin: 'auto' }}>
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,11 +2,12 @@ import React from 'react';
|
|||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import DashboardList from './DashboardList';
|
||||
import Header from './Header';
|
||||
import { PANEL_SIZES } from 'App/constants/panelSizes'
|
||||
|
||||
function DashboardsView({ history, siteId }: { history: any; siteId: string }) {
|
||||
function DashboardsView() {
|
||||
return (
|
||||
<div
|
||||
style={{ maxWidth: '1360px', margin: 'auto' }}
|
||||
style={{ maxWidth: PANEL_SIZES.maxWidth, margin: 'auto' }}
|
||||
className="bg-white rounded-lg py-4 border shadow-sm"
|
||||
>
|
||||
<Header />
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}}
|
||||
>
|
||||
<div className="mb-6 flex items-end justify-between">
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Loader loading={loading}>
|
||||
<div
|
||||
style={{ maxWidth: '1360px', margin: 'auto' }}
|
||||
style={{ maxWidth: PANEL_SIZES.maxWidth, margin: 'auto' }}
|
||||
className="rounded-lg shadow-sm overflow-hidden bg-white border"
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div
|
||||
style={{ maxWidth: '1360px', margin: 'auto' }}
|
||||
style={{ maxWidth: PANEL_SIZES.maxWidth, margin: 'auto' }}
|
||||
className="bg-white rounded-lg shadow-sm pt-4 border"
|
||||
>
|
||||
<MetricViewHeader siteId={siteId} />
|
||||
|
|
|
|||
|
|
@ -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?'
|
||||
}
|
||||
/>
|
||||
<div style={{ maxWidth: '1360px', margin: 'auto' }}>
|
||||
<div style={{ maxWidth: PANEL_SIZES.maxWidth, margin: 'auto' }}>
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import Select from 'Shared/Select';
|
|||
import usePageTitle from '@/hooks/usePageTitle';
|
||||
import FFlagItem from './FFlagItem';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PANEL_SIZES } from 'App/constants/panelSizes';
|
||||
|
||||
function FFlagsList({ siteId }: { siteId: string }) {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -24,7 +25,7 @@ function FFlagsList({ siteId }: { siteId: string }) {
|
|||
return (
|
||||
<div
|
||||
className="mb-5 w-full mx-auto bg-white rounded pb-10 pt-4 widget-wrapper"
|
||||
style={{ maxWidth: '1360px' }}
|
||||
style={{ maxWidth: PANEL_SIZES.maxWidth }}
|
||||
>
|
||||
<FFlagsListHeader siteId={siteId} />
|
||||
<div className="border-y px-3 py-2 mt-2 flex items-center w-full justify-end gap-4">
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="w-full mx-auto mb-4" style={{ maxWidth: '1360px' }}>
|
||||
<div className="w-full mx-auto mb-4" style={{ maxWidth: PANEL_SIZES.maxWidth }}>
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{ label: t('Feature Flags'), to: withSiteId(fflags(), siteId) },
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import Header from './Header';
|
|||
import Multivariant from './Multivariant';
|
||||
import { Payload } from './Helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PANEL_SIZES } from 'App/constants/panelSizes';
|
||||
|
||||
function NewFFlag({ siteId, fflagId }: { siteId: string; fflagId?: string }) {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -40,7 +41,7 @@ function NewFFlag({ siteId, fflagId }: { siteId: string; fflagId?: string }) {
|
|||
if (featureFlagsStore.isLoading) return <Loader loading />;
|
||||
if (!current) {
|
||||
return (
|
||||
<div className="w-full mx-auto mb-4" style={{ maxWidth: '1360px' }}>
|
||||
<div className="w-full mx-auto mb-4" style={{ maxWidth: PANEL_SIZES.maxWidth }}>
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{ label: 'Feature Flags', to: withSiteId(fflags(), siteId) },
|
||||
|
|
@ -90,7 +91,7 @@ function NewFFlag({ siteId, fflagId }: { siteId: string; fflagId?: string }) {
|
|||
|
||||
const showDescription = Boolean(current.description?.length);
|
||||
return (
|
||||
<div className="w-full mx-auto mb-4" style={{ maxWidth: '1360px' }}>
|
||||
<div className="w-full mx-auto mb-4" style={{ maxWidth: PANEL_SIZES.maxWidth }}>
|
||||
<Prompt
|
||||
when={current.hasChanged}
|
||||
message={() =>
|
||||
|
|
|
|||
174
frontend/app/components/Kai/KaiChat.tsx
Normal file
174
frontend/app/components/Kai/KaiChat.tsx
Normal file
|
|
@ -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<string | null>(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<string | null>(null);
|
||||
const [initialMsg, setInitialMsg] = React.useState<string | null>(null);
|
||||
const { showModal, hideModal } = useModal();
|
||||
const location = useLocation();
|
||||
const params = new URLSearchParams(location.search);
|
||||
const threadIdFromUrl = params.get('threadId');
|
||||
|
||||
const openChats = () => {
|
||||
showModal(
|
||||
<ChatsModal onSelect={(threadId: string, title: string) => {
|
||||
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 (
|
||||
<div className="w-full mx-auto" style={{ maxWidth: PANEL_SIZES.maxWidth }}>
|
||||
<div className={'w-full rounded-lg overflow-hidden border shadow'}>
|
||||
<ChatHeader chatTitle={chatTitle} openChats={openChats} goBack={goBack} />
|
||||
<div
|
||||
className={
|
||||
'w-full bg-active-blue flex flex-col items-center justify-center py-4 relative'
|
||||
}
|
||||
style={{
|
||||
height: '70svh',
|
||||
background:
|
||||
'radial-gradient(50% 50% at 50% 50%, rgba(255, 255, 255, 0.50) 0%, rgba(248, 255, 254, 0.50) 46%, rgba(243, 241, 255, 0.50) 100%)',
|
||||
}}
|
||||
>
|
||||
{section === 'intro' ? (
|
||||
<IntroSection onAsk={onCreate} />
|
||||
) : (
|
||||
<ChatLog
|
||||
threadId={threadId}
|
||||
projectId={activeSiteId}
|
||||
userId={userId}
|
||||
userLetter={userLetter}
|
||||
onTitleChange={setTitle}
|
||||
initialMsg={initialMsg}
|
||||
setInitialMsg={setInitialMsg}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={'h-screen w-full flex flex-col gap-2 p-4'}>
|
||||
<div className={'flex items-center font-semibold text-lg gap-2'}>
|
||||
<MessagesSquare size={16} />
|
||||
<span>Chats</span>
|
||||
</div>
|
||||
{isPending ? (
|
||||
<div className="animate-pulse text-disabled-text">Loading chats...</div>
|
||||
) : (
|
||||
<div className="flex flex-col overflow-y-auto -mx-4 px-4">
|
||||
{data.map((chat) => (
|
||||
<div key={chat.thread_id} className="flex items-center relative group min-h-8">
|
||||
<div
|
||||
style={{ width: 270 - 28 - 4 }}
|
||||
className="rounded-l pl-2 h-full w-full hover:bg-active-blue flex items-center"
|
||||
>
|
||||
<div
|
||||
onClick={() => onSelect(chat.thread_id, chat.title)}
|
||||
className="cursor-pointer hover:underline truncate"
|
||||
>
|
||||
{chat.title}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => 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"
|
||||
>
|
||||
<Trash size={14} className="text-disabled-text" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(KaiChat);
|
||||
75
frontend/app/components/Kai/SocketManager.ts
Normal file
75
frontend/app/components/Kai/SocketManager.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import io from 'socket.io-client';
|
||||
|
||||
export class ChatManager {
|
||||
socket: ReturnType<typeof io>;
|
||||
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;
|
||||
}
|
||||
54
frontend/app/components/Kai/components/ChatHeader.tsx
Normal file
54
frontend/app/components/Kai/components/ChatHeader.tsx
Normal file
|
|
@ -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 (
|
||||
<div
|
||||
className={
|
||||
'px-4 py-2 flex items-center bg-white border-b border-b-gray-lighter'
|
||||
}
|
||||
>
|
||||
<div className={'flex-1'}>
|
||||
{goBack ? (
|
||||
<div
|
||||
className={'flex items-center gap-2 font-semibold cursor-pointer'}
|
||||
onClick={goBack}
|
||||
>
|
||||
<ArrowLeft size={14} />
|
||||
<div>Back</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className={'flex items-center gap-2 mx-auto max-w-[80%]'}>
|
||||
{chatTitle ? (
|
||||
<div className="font-semibold text-xl whitespace-nowrap truncate">{chatTitle}</div>
|
||||
) : (
|
||||
<>
|
||||
<Icon name={'kai_colored'} size={18} />
|
||||
<div className={'font-semibold text-xl'}>Kai</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
'font-semibold cursor-pointer flex items-center gap-2 flex-1 justify-end'
|
||||
}
|
||||
onClick={openChats}
|
||||
>
|
||||
<MessagesSquare size={14} />
|
||||
<div>Chats</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChatHeader;
|
||||
37
frontend/app/components/Kai/components/ChatInput.tsx
Normal file
37
frontend/app/components/Kai/components/ChatInput.tsx
Normal file
|
|
@ -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<string>('');
|
||||
|
||||
const submit = () => {
|
||||
onSubmit(inputValue)
|
||||
setInputValue('')
|
||||
}
|
||||
return (
|
||||
<Input
|
||||
placeholder={'Ask anything about your product and users...'}
|
||||
size={'large'}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && inputValue) {
|
||||
submit()
|
||||
}
|
||||
}}
|
||||
suffix={
|
||||
<Button
|
||||
loading={isLoading}
|
||||
onClick={submit}
|
||||
icon={<SendHorizonal size={16} />}
|
||||
type={'text'}
|
||||
size={'small'}
|
||||
shape={'circle'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatInput
|
||||
149
frontend/app/components/Kai/components/ChatLog.tsx
Normal file
149
frontend/app/components/Kai/components/ChatLog.tsx
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import React from 'react';
|
||||
import ChatInput from './ChatInput';
|
||||
import { ChatMsg, ChatNotice } from './ChatMsg';
|
||||
import { ChatManager } from '../SocketManager';
|
||||
import type { BotChunk, Message } from '../SocketManager';
|
||||
import { aiService } from 'App/services';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Loader } from 'UI';
|
||||
|
||||
function ChatLog({
|
||||
projectId,
|
||||
userId,
|
||||
threadId,
|
||||
userLetter,
|
||||
onTitleChange,
|
||||
initialMsg,
|
||||
setInitialMsg,
|
||||
}: {
|
||||
projectId: string;
|
||||
userId: string;
|
||||
threadId: any;
|
||||
userLetter: string;
|
||||
onTitleChange: (title: string | null) => void;
|
||||
initialMsg: string | null;
|
||||
setInitialMsg: (msg: string | null) => void;
|
||||
}) {
|
||||
const chatManager = React.useRef<ChatManager | null>(null);
|
||||
const chatRef = React.useRef<HTMLDivElement>(null);
|
||||
const [messages, setMessages] = React.useState<Message[]>(
|
||||
initialMsg ? [{ text: initialMsg, isUser: true }] : [],
|
||||
);
|
||||
const [processingStage, setProcessing] = React.useState<BotChunk | null>(
|
||||
null,
|
||||
);
|
||||
const [isLoading, setLoading] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
console.log(threadId, initialMsg);
|
||||
if (threadId && !initialMsg) {
|
||||
setLoading(true);
|
||||
//const settings = { projectId: projectId ?? 2325, userId: userId ?? 65 };
|
||||
const settings = { projectId: '2325', userId: '0' };
|
||||
aiService
|
||||
.getKaiChat(settings.projectId, settings.userId, threadId)
|
||||
.then((res) => {
|
||||
if (res && res.length) {
|
||||
setMessages(
|
||||
res.map((m) => {
|
||||
const isUser = m.role === 'human';
|
||||
return {
|
||||
text: m.content,
|
||||
isUser: isUser,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
toast.error("Couldn't load chat history. Please try again later.");
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
chatManager.current = new ChatManager(settings);
|
||||
chatManager.current.setOnMsgHook({
|
||||
msgCallback: (msg) => {
|
||||
if (msg.stage === 'chart') {
|
||||
setProcessing(msg);
|
||||
}
|
||||
if (msg.stage === 'final') {
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
text: msg.content,
|
||||
isUser: false,
|
||||
userName: 'Kai',
|
||||
},
|
||||
]);
|
||||
setProcessing(null);
|
||||
}
|
||||
},
|
||||
titleCallback: (title) => onTitleChange(title),
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
chatManager.current?.disconnect();
|
||||
setInitialMsg(null);
|
||||
};
|
||||
}, [threadId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (initialMsg) {
|
||||
chatManager.current?.sendMesage(initialMsg);
|
||||
}
|
||||
}, [initialMsg]);
|
||||
|
||||
const onSubmit = (text: string) => {
|
||||
chatManager.current?.sendMesage(text);
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
text,
|
||||
isUser: true,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
chatRef.current?.scrollTo({
|
||||
top: chatRef.current.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}, [messages.length]);
|
||||
|
||||
const newChat = messages.length === 1 && processingStage === null;
|
||||
return (
|
||||
<Loader loading={isLoading} className={'w-full h-full'}>
|
||||
<div
|
||||
ref={chatRef}
|
||||
className={
|
||||
'overflow-y-auto relative flex flex-col items-center justify-between w-full h-full'
|
||||
}
|
||||
>
|
||||
<div className={'flex flex-col gap-4 w-2/3 min-h-max'}>
|
||||
{messages.map((msg, index) => (
|
||||
<ChatMsg
|
||||
key={index}
|
||||
text={msg.text}
|
||||
isUser={msg.isUser}
|
||||
userName={userLetter}
|
||||
/>
|
||||
))}
|
||||
{processingStage ? (
|
||||
<ChatNotice content={processingStage.content} />
|
||||
) : null}
|
||||
{newChat ? (
|
||||
<ChatNotice content={'Processing your question...'} />
|
||||
) : null}
|
||||
</div>
|
||||
<div className={'sticky bottom-0 pt-6 w-2/3'}>
|
||||
<ChatInput onSubmit={onSubmit} />
|
||||
</div>
|
||||
</div>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChatLog;
|
||||
94
frontend/app/components/Kai/components/ChatMsg.tsx
Normal file
94
frontend/app/components/Kai/components/ChatMsg.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import cn from 'classnames';
|
||||
import Markdown from 'react-markdown';
|
||||
import { Loader, ThumbsUp, ThumbsDown, ListRestart } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
export function ChatMsg({
|
||||
text,
|
||||
isUser,
|
||||
userName,
|
||||
}: {
|
||||
text: string;
|
||||
isUser: boolean;
|
||||
userName?: string;
|
||||
}) {
|
||||
const onClick = () => {
|
||||
toast.info('I do nothing!');
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-start gap-2',
|
||||
isUser ? 'flex-row-reverse' : 'flex-row',
|
||||
)}
|
||||
>
|
||||
{isUser ? (
|
||||
<div
|
||||
className={
|
||||
'rounded-full bg-main text-white min-w-8 min-h-8 flex items-center justify-center sticky top-0'
|
||||
}
|
||||
>
|
||||
<span className={'font-semibold'}>{userName}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={
|
||||
'rounded-full bg-white shadow min-w-8 min-h-8 flex items-center justify-center sticky top-0'
|
||||
}
|
||||
>
|
||||
<Icon name={'kai_colored'} size={18} />
|
||||
</div>
|
||||
)}
|
||||
<div className={'mt-1'}>
|
||||
<Markdown>{text}</Markdown>
|
||||
{isUser ? null : (
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<IconButton onClick={onClick}>
|
||||
<ThumbsUp size={16} />
|
||||
</IconButton>
|
||||
<IconButton onClick={onClick}>
|
||||
<ThumbsDown size={16} />
|
||||
</IconButton>
|
||||
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={
|
||||
'flex items-center gap-2 px-2 rounded-lg border border-gray-medium text-sm cursor-pointer hover:border-main hover:text-main'
|
||||
}
|
||||
>
|
||||
<ListRestart size={16} />
|
||||
<div>Retry</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function IconButton({
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className={'cursor-pointer hover:text-main'} onClick={onClick}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChatNotice({ content }: { content: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 p-2 rounded-lg bg-gray-lightest border-gray-light w-fit">
|
||||
<div className={'animate-spin'}>
|
||||
<Loader size={14} />
|
||||
</div>
|
||||
<div className={'animate-pulse text-disabled-text'}>{content}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
frontend/app/components/Kai/components/Ideas.tsx
Normal file
32
frontend/app/components/Kai/components/Ideas.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
import { Lightbulb, MoveRight } from 'lucide-react';
|
||||
|
||||
function Ideas({ onClick }: { onClick: (query: string) => void }) {
|
||||
return (
|
||||
<>
|
||||
<div className={'flex items-center gap-2 mb-1 text-gray-dark'}>
|
||||
<Lightbulb size={16} />
|
||||
<b>Ideas:</b>
|
||||
</div>
|
||||
<IdeaItem onClick={onClick} title={'Top user journeys'} />
|
||||
<IdeaItem onClick={onClick} title={'Where do users drop off'} />
|
||||
<IdeaItem onClick={onClick} title={'Failed network requests today'} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function IdeaItem({ title, onClick }: { title: string, onClick: (query: string) => void }) {
|
||||
return (
|
||||
<div
|
||||
onClick={() => onClick(title)}
|
||||
className={
|
||||
'flex items-center gap-2 cursor-pointer text-gray-dark hover:text-black'
|
||||
}
|
||||
>
|
||||
<MoveRight size={16} />
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Ideas;
|
||||
27
frontend/app/components/Kai/components/IntroSection.tsx
Normal file
27
frontend/app/components/Kai/components/IntroSection.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
import ChatInput from './ChatInput';
|
||||
import Ideas from './Ideas';
|
||||
|
||||
function IntroSection({ onAsk }: { onAsk: (query: string) => void }) {
|
||||
const isLoading = false;
|
||||
return (
|
||||
<>
|
||||
<div className={'text-disabled-text text-xl absolute top-4'}>
|
||||
Kai is your AI assistant, delivering smart insights in response to your
|
||||
queries.
|
||||
</div>
|
||||
<div className={'relative w-2/3'} style={{ height: 44 }}>
|
||||
{/*<GradientBorderInput placeholder={'Ask anything about your product and users...'} onButtonClick={() => null} />*/}
|
||||
<ChatInput isLoading={isLoading} onSubmit={onAsk} />
|
||||
<div className={'absolute top-full flex flex-col gap-2 mt-4'}>
|
||||
<Ideas onClick={(query) => onAsk(query)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={'text-disabled-text absolute bottom-4'}>
|
||||
OpenReplay AI can make mistakes. Verify its outputs.
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default IntroSection;
|
||||
|
|
@ -9,6 +9,7 @@ import ManageUsersTab from './components/ManageUsersTab';
|
|||
import SideMenu from './components/SideMenu';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Smartphone, AppWindow } from 'lucide-react';
|
||||
import { PANEL_SIZES } from 'App/constants/panelSizes';
|
||||
|
||||
interface Props {
|
||||
match: {
|
||||
|
|
@ -66,7 +67,7 @@ function Onboarding(props: Props) {
|
|||
<div className="w-full">
|
||||
<div
|
||||
className="bg-white w-full rounded-lg mx-auto mb-8 border"
|
||||
style={{ maxWidth: '1360px' }}
|
||||
style={{ maxWidth: PANEL_SIZES.maxWidth }}
|
||||
>
|
||||
<Switch>
|
||||
<Route exact strict path={route(OB_TABS.INSTALLING)}>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import FlagView from 'Components/FFlags/FlagView/FlagView';
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from '@/mstore';
|
||||
import Bookmarks from 'Shared/SessionsTabOverview/components/Bookmarks/Bookmarks';
|
||||
import { PANEL_SIZES } from 'App/constants/panelSizes';
|
||||
|
||||
// @ts-ignore
|
||||
interface IProps extends RouteComponentProps {
|
||||
|
|
@ -42,12 +43,12 @@ function Overview({ match: { params } }: IProps) {
|
|||
return (
|
||||
<Switch>
|
||||
<Route exact strict path={withSiteId(sessions(), siteId)}>
|
||||
<div className="mb-5 w-full mx-auto" style={{ maxWidth: '1360px' }}>
|
||||
<div className="mb-5 w-full mx-auto" style={{ maxWidth: PANEL_SIZES.maxWidth }}>
|
||||
<SessionsTabOverview />
|
||||
</div>
|
||||
</Route>
|
||||
<Route exact strict path={withSiteId(bookmarks(), siteId)}>
|
||||
<div className="mb-5 w-full mx-auto" style={{ maxWidth: '1360px' }}>
|
||||
<div className="mb-5 w-full mx-auto" style={{ maxWidth: PANEL_SIZES.maxWidth }}>
|
||||
<Bookmarks />
|
||||
</div>
|
||||
</Route>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { toast } from 'react-toastify';
|
|||
import StepsModal from './StepsModal';
|
||||
import SidePanel from './SidePanel';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { PANEL_SIZES } from 'App/constants/panelSizes';
|
||||
const menuItems = [
|
||||
{
|
||||
key: '1',
|
||||
|
|
@ -160,7 +160,7 @@ function TestEdit() {
|
|||
const isStartingPointValid = isValidUrl(uxtestingStore.instance.startingPath);
|
||||
|
||||
return (
|
||||
<div className="w-full mx-auto" style={{ maxWidth: '1360px' }}>
|
||||
<div className="w-full mx-auto" style={{ maxWidth: PANEL_SIZES.maxWidth }}>
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import { toast } from 'react-toastify';
|
|||
import ResponsesOverview from './ResponsesOverview';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TFunction } from 'i18next';
|
||||
import { PANEL_SIZES } from 'App/constants/panelSizes';
|
||||
|
||||
function StatusItem({
|
||||
iconName,
|
||||
|
|
@ -155,7 +156,7 @@ function TestOverview() {
|
|||
return (
|
||||
<div
|
||||
className="w-full mx-auto"
|
||||
style={{ maxWidth: '1360px' }}
|
||||
style={{ maxWidth: PANEL_SIZES.maxWidth }}
|
||||
id="pdf-anchor"
|
||||
>
|
||||
<Breadcrumb
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
} from 'App/routes';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PANEL_SIZES } from 'App/constants/panelSizes';
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
|
|
@ -76,7 +77,7 @@ function TestsTable() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="w-full mx-auto" style={{ maxWidth: '1360px' }}>
|
||||
<div className="w-full mx-auto" style={{ maxWidth: PANEL_SIZES.maxWidth }}>
|
||||
<Modal
|
||||
title={t('Create Usability Test')}
|
||||
open={isModalVisible}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ import React from 'react';
|
|||
import NotesList from './NoteList';
|
||||
import NoteTags from './NoteTags';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PANEL_SIZES } from 'App/constants/panelSizes';
|
||||
|
||||
function NotesRoute() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="mb-5 w-full mx-auto" style={{ maxWidth: '1360px' }}>
|
||||
<div className="mb-5 w-full mx-auto" style={{ maxWidth: PANEL_SIZES.maxWidth }}>
|
||||
<div className="widget-wrapper">
|
||||
<div className="flex items-center px-4 py-2 justify-between w-full border-b">
|
||||
<div className="flex items-center justify-end w-full">
|
||||
|
|
|
|||
|
|
@ -353,6 +353,8 @@ export { default as Integrations_teams } from './integrations_teams';
|
|||
export { default as Integrations_vuejs } from './integrations_vuejs';
|
||||
export { default as Integrations_zustand } from './integrations_zustand';
|
||||
export { default as Journal_code } from './journal_code';
|
||||
export { default as Kai } from './kai';
|
||||
export { default as Kai_colored } from './kai_colored';
|
||||
export { default as Key } from './key';
|
||||
export { default as Keyboard } from './keyboard';
|
||||
export { default as Layers_half } from './layers_half';
|
||||
|
|
|
|||
18
frontend/app/components/ui/Icons/kai.tsx
Normal file
18
frontend/app/components/ui/Icons/kai.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/* Auto-generated, do not edit */
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
size?: number | string;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
fill?: string;
|
||||
}
|
||||
|
||||
function Kai(props: Props) {
|
||||
const { size = 14, width = size, height = size, fill = '' } = props;
|
||||
return (
|
||||
<svg viewBox="0 0 14 15" fill="none" width={ `${ width }px` } height={ `${ height }px` } ><g stroke="#394EFF" strokeLinecap="round" strokeLinejoin="round"><path d="m7 1.862-1.115 3.39a1.167 1.167 0 0 1-.744.744L1.75 7.112l3.39 1.115a1.167 1.167 0 0 1 .745.744L7 12.36l1.115-3.39a1.167 1.167 0 0 1 .744-.744l3.391-1.115-3.39-1.116a1.167 1.167 0 0 1-.745-.743L7 1.862Z" fill="#fff"/><path d="M2.917 1.862v2.333M11.083 10.028v2.334M1.75 3.028h2.333M9.917 11.195h2.333"/></g></svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default Kai;
|
||||
64
frontend/app/components/ui/Icons/kai_colored.tsx
Normal file
64
frontend/app/components/ui/Icons/kai_colored.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/* Auto-generated, do not edit */
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
size?: number | string;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
fill?: string;
|
||||
}
|
||||
|
||||
function Kai_colored(props: Props) {
|
||||
const { size = 14, width = size, height = size, fill = '' } = props;
|
||||
return (
|
||||
<svg viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon <- change size here" clipPath="url(#clip0_142_2721)">
|
||||
<path id="Vector" d="M11.5 2.86172L9.74733 8.19031C9.65764 8.46302 9.50514 8.71086 9.30214 8.91387C9.09914 9.11687 8.8513 9.26936 8.57858 9.35906L3.25 11.1117L8.57858 12.8644C8.8513 12.9541 9.09914 13.1066 9.30214 13.3096C9.50514 13.5126 9.65764 13.7604 9.74733 14.0331L11.5 19.3617L13.2527 14.0331C13.3424 13.7604 13.4949 13.5126 13.6979 13.3096C13.9009 13.1066 14.1487 12.9541 14.4214 12.8644L19.75 11.1117L14.4214 9.35906C14.1487 9.26936 13.9009 9.11687 13.6979 8.91387C13.4949 8.71086 13.3424 8.46302 13.2527 8.19031L11.5 2.86172Z" fill="white" stroke="url(#paint0_linear_142_2721)" strokeWidth="2"/>
|
||||
<g id="Vector_2">
|
||||
<path d="M5.08331 2.86172V6.52839V2.86172Z" fill="white"/>
|
||||
<path d="M5.08331 2.86172V6.52839" stroke="url(#paint1_linear_142_2721)" strokeWidth="2"/>
|
||||
</g>
|
||||
<g id="Vector_3">
|
||||
<path d="M17.9167 15.695V19.3617V15.695Z" fill="white"/>
|
||||
<path d="M17.9167 15.695V19.3617" stroke="url(#paint2_linear_142_2721)" strokeWidth="2"/>
|
||||
</g>
|
||||
<g id="Vector_4">
|
||||
<path d="M3.25 4.69504H6.91667H3.25Z" fill="white"/>
|
||||
<path d="M3.25 4.69504H6.91667" stroke="url(#paint3_linear_142_2721)" strokeWidth="2"/>
|
||||
</g>
|
||||
<g id="Vector_5">
|
||||
<path d="M16.0833 17.5284H19.75H16.0833Z" fill="white"/>
|
||||
<path d="M16.0833 17.5284H19.75" stroke="url(#paint4_linear_142_2721)" strokeWidth="2"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_142_2721" x1="3.25" y1="11.1117" x2="19.75" y2="11.1117" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#6F5CEC"/>
|
||||
<stop offset="1" stopColor="#3CC377"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_142_2721" x1="5.08331" y1="4.69506" x2="6.08331" y2="4.69506" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#6F5CEC"/>
|
||||
<stop offset="1" stopColor="#3CC377"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_142_2721" x1="17.9167" y1="17.5284" x2="18.9167" y2="17.5284" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#6F5CEC"/>
|
||||
<stop offset="1" stopColor="#3CC377"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_142_2721" x1="3.25" y1="5.19504" x2="6.91667" y2="5.19504" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#6F5CEC"/>
|
||||
<stop offset="1" stopColor="#3CC377"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_142_2721" x1="16.0833" y1="18.0284" x2="19.75" y2="18.0284" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#6F5CEC"/>
|
||||
<stop offset="1" stopColor="#3CC377"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_142_2721">
|
||||
<rect width="22" height="22" fill="white" transform="translate(0.5 0.111725)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default Kai_colored;
|
||||
File diff suppressed because one or more lines are too long
3
frontend/app/constants/panelSizes.ts
Normal file
3
frontend/app/constants/panelSizes.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export const PANEL_SIZES = {
|
||||
maxWidth: '1360px'
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import {
|
|||
import { MODULES } from 'Components/Client/Modules';
|
||||
import { Icon } from 'UI';
|
||||
import SVG from 'UI/SVG';
|
||||
import { hasAi } from 'App/utils/split-utils'
|
||||
|
||||
import { useStore } from 'App/mstore';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
|
@ -41,7 +42,7 @@ function SideMenu(props: Props) {
|
|||
|
||||
const isPreferencesActive = location.pathname.includes('/client/');
|
||||
const [supportOpen, setSupportOpen] = React.useState(false);
|
||||
const { searchStore, projectsStore, userStore } = useStore();
|
||||
const { projectsStore, userStore } = useStore();
|
||||
const spotOnly = userStore.scopeState === 1;
|
||||
const { account } = userStore;
|
||||
const modules = account.settings?.modules ?? [];
|
||||
|
|
@ -103,6 +104,7 @@ function SideMenu(props: Props) {
|
|||
modules.includes(MODULES.USABILITY_TESTS),
|
||||
item.isAdmin && !isAdmin,
|
||||
item.isEnterprise && !isEnterprise,
|
||||
item.key === MENU.KAI && !hasAi
|
||||
].some((cond) => cond);
|
||||
|
||||
return { ...item, hidden: isHidden };
|
||||
|
|
@ -145,6 +147,7 @@ function SideMenu(props: Props) {
|
|||
[PREFERENCES_MENU.BILLING]: () => client(CLIENT_TABS.BILLING),
|
||||
[PREFERENCES_MENU.MODULES]: () => client(CLIENT_TABS.MODULES),
|
||||
[MENU.HIGHLIGHTS]: () => withSiteId(routes.highlights(''), siteId),
|
||||
[MENU.KAI]: () => withSiteId(routes.kai(), siteId),
|
||||
};
|
||||
|
||||
const handleClick = (item: any) => {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { TFunction } from 'i18next';
|
||||
import { IconNames } from "../components/ui/SVG";
|
||||
import React from 'react';
|
||||
|
||||
export interface MenuItem {
|
||||
label: React.ReactNode;
|
||||
key: React.Key;
|
||||
icon?: string;
|
||||
icon?: IconNames;
|
||||
children?: MenuItem[];
|
||||
route?: string;
|
||||
hidden?: boolean;
|
||||
|
|
@ -53,6 +54,7 @@ export const enum MENU {
|
|||
SUPPORT = 'support',
|
||||
EXIT = 'exit',
|
||||
SPOTS = 'spots',
|
||||
KAI = 'kai',
|
||||
}
|
||||
|
||||
export const categories: (t: TFunction) => Category[] = (t) => [
|
||||
|
|
@ -93,6 +95,13 @@ export const categories: (t: TFunction) => Category[] = (t) => [
|
|||
{ label: t('Co-Browse'), key: MENU.LIVE_SESSIONS, icon: 'broadcast' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
key: 'kai',
|
||||
items: [
|
||||
{ label: t('Kai'), key: MENU.KAI, icon: 'kai' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t('Analytics'),
|
||||
key: 'analytics',
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ import UiPlayerStore from './uiPlayerStore';
|
|||
import userStore from './userStore';
|
||||
import UxtestingStore from './uxtestingStore';
|
||||
import WeeklyReportStore from './weeklyReportConfigStore';
|
||||
import KaiStore from './kaiStore';
|
||||
|
||||
import logger from '@/logger';
|
||||
|
||||
const projectStore = new ProjectsStore();
|
||||
|
|
@ -81,64 +83,36 @@ const client = new APIClient();
|
|||
|
||||
export class RootStore {
|
||||
dashboardStore: DashboardStore;
|
||||
|
||||
metricStore: MetricStore;
|
||||
|
||||
funnelStore: FunnelStore;
|
||||
|
||||
settingsStore: SettingsStore;
|
||||
|
||||
userStore: typeof userStore;
|
||||
|
||||
roleStore: RoleStore;
|
||||
|
||||
auditStore: AuditStore;
|
||||
|
||||
errorStore: ErrorStore;
|
||||
|
||||
notificationStore: NotificationStore;
|
||||
|
||||
sessionStore: SessionStore;
|
||||
|
||||
notesStore: NotesStore;
|
||||
|
||||
recordingsStore: RecordingsStore;
|
||||
|
||||
assistMultiviewStore: AssistMultiviewStore;
|
||||
|
||||
weeklyReportStore: WeeklyReportStore;
|
||||
|
||||
alertsStore: AlertStore;
|
||||
|
||||
featureFlagsStore: FeatureFlagsStore;
|
||||
|
||||
uxtestingStore: UxtestingStore;
|
||||
|
||||
tagWatchStore: TagWatchStore;
|
||||
|
||||
aiSummaryStore: AiSummaryStore;
|
||||
|
||||
aiFiltersStore: AiFiltersStore;
|
||||
|
||||
spotStore: SpotStore;
|
||||
|
||||
loginStore: LoginStore;
|
||||
|
||||
filterStore: FilterStore;
|
||||
|
||||
uiPlayerStore: UiPlayerStore;
|
||||
|
||||
issueReportingStore: IssueReportingStore;
|
||||
|
||||
customFieldStore: CustomFieldStore;
|
||||
|
||||
searchStore: SearchStore;
|
||||
|
||||
searchStoreLive: SearchStoreLive;
|
||||
|
||||
integrationsStore: IntegrationsStore;
|
||||
|
||||
projectsStore: ProjectsStore;
|
||||
kaiStore: KaiStore;
|
||||
|
||||
constructor() {
|
||||
this.dashboardStore = new DashboardStore();
|
||||
|
|
@ -171,6 +145,7 @@ export class RootStore {
|
|||
this.searchStore = searchStore;
|
||||
this.searchStoreLive = searchStoreLive;
|
||||
this.integrationsStore = new IntegrationsStore();
|
||||
this.kaiStore = new KaiStore();
|
||||
}
|
||||
|
||||
initClient() {
|
||||
|
|
|
|||
75
frontend/app/mstore/kaiStore.ts
Normal file
75
frontend/app/mstore/kaiStore.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { makeAutoObservable } from 'mobx';
|
||||
|
||||
type threadId = number | string;
|
||||
|
||||
export interface RecentChat {
|
||||
title: string;
|
||||
thread_id: threadId;
|
||||
}
|
||||
|
||||
export interface ChartData {
|
||||
timestamp: number,
|
||||
[key: string]: number
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
role: 'human' | 'kai';
|
||||
content: string;
|
||||
chart?: { type: string, data?: ChartData[] }
|
||||
message_id: number;
|
||||
}
|
||||
|
||||
export default class KaiStore {
|
||||
recentChats: RecentChat[] = [];
|
||||
chats: Record<threadId, Message[]> = {};
|
||||
activeThreadId: threadId = 0;
|
||||
loadingHistory = false;
|
||||
loadingAnswer = false;
|
||||
status = 'thinking';
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
setActiveThreadId = (threadId: threadId) => {
|
||||
this.activeThreadId = threadId;
|
||||
}
|
||||
|
||||
setStatus = (status: string) => {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
setLoadingHistory = (loading: boolean) => {
|
||||
this.loadingHistory = loading;
|
||||
}
|
||||
|
||||
setLoadingAnswer = (loading: boolean) => {
|
||||
this.loadingAnswer = loading;
|
||||
}
|
||||
|
||||
addRecentChat = (chat: RecentChat) => {
|
||||
this.recentChats.push(chat);
|
||||
}
|
||||
|
||||
setRecentChats = (chats: RecentChat[]) => {
|
||||
this.recentChats = chats;
|
||||
}
|
||||
|
||||
addMessage = (threadId: number, message: Message) => {
|
||||
const id = `_${threadId}` as threadId;
|
||||
if (!this.chats[id]) {
|
||||
this.chats[id] = [];
|
||||
}
|
||||
this.chats[id].push(message);
|
||||
}
|
||||
|
||||
setMessages = (threadId: number, messages: Message[]) => {
|
||||
const id = `_${threadId}` as threadId;
|
||||
this.chats[id] = messages;
|
||||
}
|
||||
|
||||
getMessages = (threadId: number) => {
|
||||
const id = `_${threadId}` as threadId;
|
||||
return this.chats[id] || [];
|
||||
}
|
||||
}
|
||||
|
|
@ -200,6 +200,8 @@ export const scopeSetup = (): string => '/scope-setup';
|
|||
|
||||
export const highlights = (): string => '/highlights';
|
||||
|
||||
export const kai = (): string => '/kai';
|
||||
|
||||
const REQUIRED_SITE_ID_ROUTES = [
|
||||
liveSession(''),
|
||||
session(''),
|
||||
|
|
@ -240,6 +242,8 @@ const REQUIRED_SITE_ID_ROUTES = [
|
|||
usabilityTestingView(''),
|
||||
|
||||
highlights(),
|
||||
|
||||
kai(),
|
||||
];
|
||||
const routeNeedsSiteId = (path: string): boolean =>
|
||||
REQUIRED_SITE_ID_ROUTES.some((r) => path.startsWith(r));
|
||||
|
|
|
|||
|
|
@ -84,4 +84,71 @@ export default class AiService extends BaseService {
|
|||
const { data } = await r.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
getKaiChats = async (userId: string, projectId: string): Promise<{ title: string, threadId: string }[]> => {
|
||||
// const r = await this.client.get('/kai/PROJECT_ID/chats');
|
||||
const jwt = window.env.KAI_TESTING // this.client.getJwt()
|
||||
const r = await fetch(`http://localhost:8700/kai/${projectId}/chats?user_id=${userId}`, {
|
||||
headers: new Headers({
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${jwt}`
|
||||
}),
|
||||
});
|
||||
if (!r.ok) {
|
||||
throw new Error('Failed to fetch chats');
|
||||
}
|
||||
const data = await r.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
deleteKaiChat = async (projectId: string, userId: string, threadId: string): Promise<boolean> => {
|
||||
const jwt = window.env.KAI_TESTING // this.client.getJwt()
|
||||
const r = await fetch(`http://localhost:8700/kai/${projectId}/chats/${threadId}?user_id=${userId}`, {
|
||||
method: 'DELETE',
|
||||
headers: new Headers({
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${jwt}`
|
||||
}),
|
||||
});
|
||||
if (!r.ok) {
|
||||
throw new Error('Failed to delete chat');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
getKaiChat = async (projectId: string, userId: string, threadId: string): Promise<{ role: string, content: string, message_id: any }[]> => {
|
||||
const jwt = window.env.KAI_TESTING // this.client.getJwt()
|
||||
const r = await fetch(`http://localhost:8700/kai/${projectId}/chats/${threadId}?user_id=${userId}`, {
|
||||
method: 'GET',
|
||||
headers: new Headers({
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${jwt}`
|
||||
}),
|
||||
});
|
||||
if (!r.ok) {
|
||||
throw new Error('Failed to fetch chat');
|
||||
}
|
||||
const data = await r.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
createKaiChat = async (projectId: string, userId: string): Promise<number> => {
|
||||
const jwt = window.env.KAI_TESTING // this.client.getJwt()
|
||||
const r = await fetch(`http://localhost:8700/kai/${projectId}/chat/new?user_id=${userId}`, {
|
||||
method: 'GET',
|
||||
headers: new Headers({
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${jwt}`
|
||||
}),
|
||||
})
|
||||
if (!r.ok) {
|
||||
throw new Error('Failed to create chat');
|
||||
}
|
||||
const data = await r.json();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
3
frontend/app/styles/import.css
vendored
3
frontend/app/styles/import.css
vendored
|
|
@ -1,5 +1,4 @@
|
|||
@import 'react-toastify/dist/ReactToastify.css';
|
||||
@import "react-daterange-picker/dist/css/react-calendar.css";
|
||||
@import 'rc-time-picker/assets/index.css';
|
||||
@import 'react-daterange-picker.css';
|
||||
@import 'rc-time-picker.css';
|
||||
|
|
@ -8,4 +7,4 @@
|
|||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind utilities;
|
||||
|
|
|
|||
9
frontend/app/svg/icons/kai.svg
Normal file
9
frontend/app/svg/icons/kai.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<svg viewBox="0 0 14 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon <- change size here">
|
||||
<path id="Vector" d="M7 1.86172L5.88467 5.25264C5.82759 5.42619 5.73055 5.5839 5.60136 5.71309C5.47218 5.84227 5.31446 5.93931 5.14092 5.99639L1.75 7.11172L5.14092 8.22706C5.31446 8.28414 5.47218 8.38118 5.60136 8.51036C5.73055 8.63954 5.82759 8.79726 5.88467 8.97081L7 12.3617L8.11533 8.97081C8.17241 8.79726 8.26945 8.63954 8.39864 8.51036C8.52782 8.38118 8.68554 8.28414 8.85908 8.22706L12.25 7.11172L8.85908 5.99639C8.68554 5.93931 8.52782 5.84227 8.39864 5.71309C8.26945 5.5839 8.17241 5.42619 8.11533 5.25264L7 1.86172Z" stroke="#394EFF" stroke-linecap="round" stroke-linejoin="round" fill="white"/>
|
||||
<path id="Vector_2" d="M2.91663 1.86172V4.19506" stroke="#394EFF" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_3" d="M11.0834 10.0284V12.3617" stroke="#394EFF" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_4" d="M1.75 3.02838H4.08333" stroke="#394EFF" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_5" d="M9.91663 11.1951H12.25" stroke="#394EFF" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
46
frontend/app/svg/icons/kai_colored.svg
Normal file
46
frontend/app/svg/icons/kai_colored.svg
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<svg viewBox="0 0 23 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="icon <- change size here" clip-path="url(#clip0_142_2721)">
|
||||
<path id="Vector" d="M11.5 2.86172L9.74733 8.19031C9.65764 8.46302 9.50514 8.71086 9.30214 8.91387C9.09914 9.11687 8.8513 9.26936 8.57858 9.35906L3.25 11.1117L8.57858 12.8644C8.8513 12.9541 9.09914 13.1066 9.30214 13.3096C9.50514 13.5126 9.65764 13.7604 9.74733 14.0331L11.5 19.3617L13.2527 14.0331C13.3424 13.7604 13.4949 13.5126 13.6979 13.3096C13.9009 13.1066 14.1487 12.9541 14.4214 12.8644L19.75 11.1117L14.4214 9.35906C14.1487 9.26936 13.9009 9.11687 13.6979 8.91387C13.4949 8.71086 13.3424 8.46302 13.2527 8.19031L11.5 2.86172Z" fill="white" stroke="url(#paint0_linear_142_2721)" stroke-width="2"/>
|
||||
<g id="Vector_2">
|
||||
<path d="M5.08331 2.86172V6.52839V2.86172Z" fill="white"/>
|
||||
<path d="M5.08331 2.86172V6.52839" stroke="url(#paint1_linear_142_2721)" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Vector_3">
|
||||
<path d="M17.9167 15.695V19.3617V15.695Z" fill="white"/>
|
||||
<path d="M17.9167 15.695V19.3617" stroke="url(#paint2_linear_142_2721)" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Vector_4">
|
||||
<path d="M3.25 4.69504H6.91667H3.25Z" fill="white"/>
|
||||
<path d="M3.25 4.69504H6.91667" stroke="url(#paint3_linear_142_2721)" stroke-width="2"/>
|
||||
</g>
|
||||
<g id="Vector_5">
|
||||
<path d="M16.0833 17.5284H19.75H16.0833Z" fill="white"/>
|
||||
<path d="M16.0833 17.5284H19.75" stroke="url(#paint4_linear_142_2721)" stroke-width="2"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_142_2721" x1="3.25" y1="11.1117" x2="19.75" y2="11.1117" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6F5CEC"/>
|
||||
<stop offset="1" stop-color="#3CC377"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_142_2721" x1="5.08331" y1="4.69506" x2="6.08331" y2="4.69506" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6F5CEC"/>
|
||||
<stop offset="1" stop-color="#3CC377"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_142_2721" x1="17.9167" y1="17.5284" x2="18.9167" y2="17.5284" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6F5CEC"/>
|
||||
<stop offset="1" stop-color="#3CC377"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_142_2721" x1="3.25" y1="5.19504" x2="6.91667" y2="5.19504" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6F5CEC"/>
|
||||
<stop offset="1" stop-color="#3CC377"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_142_2721" x1="16.0833" y1="18.0284" x2="19.75" y2="18.0284" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6F5CEC"/>
|
||||
<stop offset="1" stop-color="#3CC377"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_142_2721">
|
||||
<rect width="22" height="22" fill="white" transform="translate(0.5 0.111725)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
5
frontend/app/utils/split-utils.ts
Normal file
5
frontend/app/utils/split-utils.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
/**
|
||||
* can be overwritten in saas or ee editions
|
||||
* */
|
||||
|
||||
export const hasAi = true //DEBUG false;
|
||||
|
|
@ -68,13 +68,13 @@
|
|||
"rc-time-picker": "^3.7.3",
|
||||
"react": "^19.1.0",
|
||||
"react-confirm": "^0.3.0",
|
||||
"react-daterange-picker": "^2.0.1",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-google-recaptcha": "^2.1.0",
|
||||
"react-i18next": "^15.5.1",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^5.3.3",
|
||||
"react-router-dom": "^5.3.3",
|
||||
"react-select": "^5.10.1",
|
||||
|
|
|
|||
2480
frontend/yarn.lock
2480
frontend/yarn.lock
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +0,0 @@
|
|||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
Loading…
Add table
Reference in a new issue