Kai UI (#3336)
* 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 * ui: add logs, add threadid * ui: feedback methods and ui * ui: store, replacing messages and giving feedback * ui: move retry btn to right corner * ui: move kai service out for ease of code splitting * ui: add thread id to socket connection * ui: support state messages * ui: cancel response generation method * ui: fix toast str * ui: add gfm plugin * ui: ensure md table has max sizes to prevent overflow * ui: revert tailwind styles on markdown block layer * ui: export as pdf, copy text contents of a message * ui: try to save text with formatting in secure contexts * ui: fix types * ui: fixup dark mode colors * ui: add duration for msgs * ui: take out custom jwt * ui: removing hardcode... * ui: change endpoints to prod * ui: swap socket path * ui: flip vis toggle * ui: lock file regenerate
This commit is contained in:
parent
4e331b70a4
commit
dca5e54811
55 changed files with 3532 additions and 1119 deletions
|
|
@ -2,6 +2,6 @@ compressionLevel: 1
|
|||
|
||||
enableGlobalCache: true
|
||||
|
||||
nodeLinker: pnpm
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.7.0.cjs
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { Loader } from 'UI';
|
|||
import APIClient from './api_client';
|
||||
import * as routes from './routes';
|
||||
import { debounceCall } from '@/utils';
|
||||
import { hasAi } from './utils/split-utils';
|
||||
|
||||
const components: any = {
|
||||
SessionPure: lazy(() => import('Components/Session/Session')),
|
||||
|
|
@ -32,7 +33,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 +54,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 +100,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 +274,12 @@ function PrivateRoutes() {
|
|||
path={withSiteId(LIVE_SESSION_PATH, siteIdList)}
|
||||
component={enhancedComponents.LiveSession}
|
||||
/>
|
||||
{hasAi ? <Route
|
||||
exact
|
||||
strict
|
||||
path={withSiteId(KAI_PATH, siteIdList)}
|
||||
component={enhancedComponents.Kai}
|
||||
/> : null}
|
||||
{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={() =>
|
||||
|
|
|
|||
187
frontend/app/components/Kai/KaiChat.tsx
Normal file
187
frontend/app/components/Kai/KaiChat.tsx
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
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';
|
||||
|
||||
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
|
||||
projectId={activeSiteId}
|
||||
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) => {
|
||||
if (firstMsg) {
|
||||
setInitialMsg(firstMsg);
|
||||
}
|
||||
const newThread = await kaiService.createKaiChat(activeSiteId);
|
||||
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%, var(--color-glassWhite) 0%, var(--color-glassMint) 46%, var(--color-glassLavander) 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,
|
||||
projectId,
|
||||
}: {
|
||||
onSelect: (threadId: string, title: string) => void;
|
||||
projectId: string;
|
||||
}) {
|
||||
const {
|
||||
data = [],
|
||||
isPending,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ['kai', 'chats'],
|
||||
queryFn: () => kaiService.getKaiChats(projectId),
|
||||
staleTime: 1000 * 60,
|
||||
});
|
||||
|
||||
const onDelete = async (id: string) => {
|
||||
try {
|
||||
await kaiService.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);
|
||||
61
frontend/app/components/Kai/KaiService.ts
Normal file
61
frontend/app/components/Kai/KaiService.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import AiService from "@/services/AiService";
|
||||
|
||||
export default class KaiService extends AiService {
|
||||
getKaiChats = async (projectId: string): Promise<{ title: string, threadId: string }[]> => {
|
||||
const r = await this.client.get(`/kai/${projectId}/chats`);
|
||||
if (!r.ok) {
|
||||
throw new Error('Failed to fetch chats');
|
||||
}
|
||||
const data = await r.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
deleteKaiChat = async (projectId: string, threadId: string): Promise<boolean> => {
|
||||
const r = await this.client.delete(`/kai/${projectId}/chats/${threadId}`);
|
||||
if (!r.ok) {
|
||||
throw new Error('Failed to delete chat');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
getKaiChat = async (projectId: string, threadId: string): Promise<{ role: string, content: string, message_id: any, duration?: number }[]> => {
|
||||
const r = await this.client.get(`/kai/${projectId}/chats/${threadId}`);
|
||||
if (!r.ok) {
|
||||
throw new Error('Failed to fetch chat');
|
||||
}
|
||||
const data = await r.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
createKaiChat = async (projectId: string): Promise<number> => {
|
||||
const r = await this.client.get(`/kai/${projectId}/chat/new`)
|
||||
if (!r.ok) {
|
||||
throw new Error('Failed to create chat');
|
||||
}
|
||||
const data = await r.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
feedback = async (positive: boolean | null, messageId: string, projectId: string) => {
|
||||
const r = await this.client.post(`/kai/${projectId}/messages/feedback`, {
|
||||
message_id: messageId,
|
||||
value: positive,
|
||||
user_id: userId,
|
||||
});
|
||||
if (!r.ok) {
|
||||
throw new Error('Failed to send feedback');
|
||||
}
|
||||
|
||||
return await r.json()
|
||||
}
|
||||
|
||||
cancelGeneration = async (projectId: string, threadId: string) => {
|
||||
const r = await this.client.post(`/kai/${projectId}/cancel/${threadId}`);
|
||||
if (!r.ok) {
|
||||
throw new Error('Failed to cancel generation');
|
||||
}
|
||||
|
||||
const data = await r.json();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
231
frontend/app/components/Kai/KaiStore.ts
Normal file
231
frontend/app/components/Kai/KaiStore.ts
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
import { makeAutoObservable, runInAction } from 'mobx';
|
||||
import { BotChunk, ChatManager, Message } from './SocketManager';
|
||||
import { kaiService as aiService, kaiService } from 'App/services';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
class KaiStore {
|
||||
chatManager: ChatManager | null = null;
|
||||
processingStage: BotChunk | null = null;
|
||||
messages: Message[] = [];
|
||||
queryText = '';
|
||||
loadingChat = false;
|
||||
replacing = false;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
get lastHumanMessage() {
|
||||
let msg = null;
|
||||
let index = null;
|
||||
for (let i = this.messages.length - 1; i >= 0; i--) {
|
||||
const message = this.messages[i];
|
||||
if (message.isUser) {
|
||||
msg = message;
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { msg, index };
|
||||
}
|
||||
|
||||
get lastKaiMessage() {
|
||||
let msg = null;
|
||||
let index = null;
|
||||
for (let i = this.messages.length - 1; i >= 0; i--) {
|
||||
const message = this.messages[i];
|
||||
if (!message.isUser) {
|
||||
msg = message;
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { msg, index };
|
||||
}
|
||||
|
||||
setQueryText = (text: string) => {
|
||||
this.queryText = text;
|
||||
};
|
||||
|
||||
setLoadingChat = (loading: boolean) => {
|
||||
this.loadingChat = loading;
|
||||
};
|
||||
|
||||
setChatManager = (chatManager: ChatManager) => {
|
||||
this.chatManager = chatManager;
|
||||
};
|
||||
|
||||
setProcessingStage = (stage: BotChunk | null) => {
|
||||
this.processingStage = stage;
|
||||
};
|
||||
|
||||
setMessages = (messages: Message[]) => {
|
||||
this.messages = messages;
|
||||
};
|
||||
|
||||
addMessage = (message: Message) => {
|
||||
this.messages.push(message);
|
||||
};
|
||||
|
||||
editMessage = (text: string) => {
|
||||
this.setQueryText(text);
|
||||
this.setReplacing(true);
|
||||
};
|
||||
|
||||
replaceAtIndex = (message: Message, index: number) => {
|
||||
const messages = [...this.messages];
|
||||
messages[index] = message;
|
||||
this.setMessages(messages);
|
||||
};
|
||||
|
||||
deleteAtIndex = (indexes: number[]) => {
|
||||
if (!indexes.length) return;
|
||||
const messages = this.messages.filter((_, i) => !indexes.includes(i));
|
||||
console.log(messages, indexes)
|
||||
runInAction(() => {
|
||||
this.messages = messages;
|
||||
})
|
||||
}
|
||||
|
||||
getChat = async (projectId: string, threadId: string) => {
|
||||
this.setLoadingChat(true);
|
||||
try {
|
||||
const res = await aiService.getKaiChat(projectId, threadId);
|
||||
if (res && res.length) {
|
||||
this.setMessages(
|
||||
res.map((m) => {
|
||||
const isUser = m.role === 'human';
|
||||
return {
|
||||
text: m.content,
|
||||
isUser: isUser,
|
||||
messageId: m.message_id,
|
||||
duration: m.duration,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error("Couldn't load chat history. Please try again later.");
|
||||
} finally {
|
||||
this.setLoadingChat(false);
|
||||
}
|
||||
};
|
||||
|
||||
createChatManager = (
|
||||
settings: { projectId: string; threadId: string },
|
||||
setTitle: (title: string) => void,
|
||||
initialMsg: string | null,
|
||||
) => {
|
||||
const token = kaiService.client.getJwt()
|
||||
this.chatManager = new ChatManager({ ...settings, token });
|
||||
this.chatManager.setOnMsgHook({
|
||||
msgCallback: (msg) => {
|
||||
if ('state' in msg) {
|
||||
if (msg.state === 'running') {
|
||||
this.setProcessingStage({
|
||||
content: 'Processing your request...',
|
||||
stage: 'chart',
|
||||
messageId: Date.now().toPrecision(),
|
||||
duration: msg.start_time ? Date.now() - msg.start_time : 0
|
||||
})
|
||||
} else {
|
||||
this.setProcessingStage(null)
|
||||
}
|
||||
} else {
|
||||
if (msg.stage === 'start') {
|
||||
this.setProcessingStage({
|
||||
...msg,
|
||||
content: 'Processing your request...',
|
||||
});
|
||||
}
|
||||
if (msg.stage === 'chart') {
|
||||
this.setProcessingStage(msg);
|
||||
}
|
||||
if (msg.stage === 'final') {
|
||||
const msgObj = {
|
||||
text: msg.content,
|
||||
isUser: false,
|
||||
messageId: msg.messageId,
|
||||
duration: msg.duration
|
||||
}
|
||||
this.addMessage(msgObj);
|
||||
this.setProcessingStage(null);
|
||||
}
|
||||
}
|
||||
},
|
||||
titleCallback: setTitle,
|
||||
});
|
||||
|
||||
if (initialMsg) {
|
||||
this.sendMessage(initialMsg);
|
||||
}
|
||||
};
|
||||
|
||||
setReplacing = (replacing: boolean) => {
|
||||
this.replacing = replacing;
|
||||
};
|
||||
|
||||
sendMessage = (message: string) => {
|
||||
if (this.chatManager) {
|
||||
this.chatManager.sendMessage(message, this.replacing);
|
||||
}
|
||||
if (this.replacing) {
|
||||
console.log(this.lastHumanMessage, this.lastKaiMessage, 'replacing these two')
|
||||
const deleting = []
|
||||
if (this.lastHumanMessage.index !== null) {
|
||||
deleting.push(this.lastHumanMessage.index);
|
||||
}
|
||||
if (this.lastKaiMessage.index !== null) {
|
||||
deleting.push(this.lastKaiMessage.index)
|
||||
}
|
||||
this.deleteAtIndex(deleting);
|
||||
this.setReplacing(false)
|
||||
}
|
||||
this.addMessage({
|
||||
text: message,
|
||||
isUser: true,
|
||||
messageId: Date.now().toString(),
|
||||
});
|
||||
};
|
||||
|
||||
sendMsgFeedback = (feedback: string, messageId: string) => {
|
||||
const settings = { projectId: '2325', userId: '0' };
|
||||
aiService
|
||||
.feedback(
|
||||
feedback === 'like',
|
||||
messageId,
|
||||
settings.projectId,
|
||||
)
|
||||
.then(() => {
|
||||
toast.success('Feedback saved.');
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
toast.error('Failed to send feedback. Please try again later.');
|
||||
});
|
||||
};
|
||||
|
||||
cancelGeneration = async (settings: { projectId: string; userId: string; threadId: string }) => {
|
||||
try {
|
||||
await kaiService.cancelGeneration(settings.projectId, settings.threadId, settings.userId)
|
||||
this.setProcessingStage(null)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error('Failed to cancel the response generation, please try again later.')
|
||||
}
|
||||
}
|
||||
|
||||
clearChat = () => {
|
||||
this.setMessages([]);
|
||||
this.setProcessingStage(null);
|
||||
this.setLoadingChat(false);
|
||||
this.setQueryText('');
|
||||
if (this.chatManager) {
|
||||
this.chatManager.disconnect();
|
||||
this.chatManager = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const kaiStore = new KaiStore();
|
||||
110
frontend/app/components/Kai/SocketManager.ts
Normal file
110
frontend/app/components/Kai/SocketManager.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import io from 'socket.io-client';
|
||||
|
||||
export class ChatManager {
|
||||
socket: ReturnType<typeof io>;
|
||||
threadId: string | null = null;
|
||||
|
||||
constructor({
|
||||
projectId,
|
||||
threadId,
|
||||
token,
|
||||
}: {
|
||||
projectId: string;
|
||||
threadId: string;
|
||||
token: string;
|
||||
}) {
|
||||
this.threadId = threadId;
|
||||
const urlObject = new URL(window.env.API_EDP || window.location.origin);
|
||||
const socket = io(`${urlObject.origin}/kai/chat`, {
|
||||
transports: ['websocket'],
|
||||
path: '/kai/chat/socket.io',
|
||||
autoConnect: true,
|
||||
reconnection: true,
|
||||
reconnectionAttempts: 5,
|
||||
reconnectionDelay: 1000,
|
||||
reconnectionDelayMax: 5000,
|
||||
withCredentials: true,
|
||||
multiplex: true,
|
||||
query: {
|
||||
project_id: projectId,
|
||||
thread_id: threadId,
|
||||
},
|
||||
auth: {
|
||||
token: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
socket.on('connect', () => {
|
||||
console.log('Connected to server');
|
||||
});
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Disconnected from server');
|
||||
});
|
||||
socket.on('error', (err) => {
|
||||
console.error('Socket error:', err);
|
||||
});
|
||||
socket.onAny((e) => console.log('event', e));
|
||||
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
sendMessage = (message: string, isReplace = false) => {
|
||||
this.socket.emit(
|
||||
'message',
|
||||
JSON.stringify({
|
||||
message,
|
||||
threadId: this.threadId,
|
||||
replace: isReplace,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
setOnMsgHook = ({
|
||||
msgCallback,
|
||||
titleCallback,
|
||||
}: {
|
||||
msgCallback: (
|
||||
msg: BotChunk | { state: string; type: 'state'; start_time?: number },
|
||||
) => 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);
|
||||
});
|
||||
this.socket.on(
|
||||
'state',
|
||||
(state: { message: 'idle' | 'running'; start_time: number }) => {
|
||||
msgCallback({
|
||||
state: state.message,
|
||||
type: 'state',
|
||||
start_time: state.start_time,
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
disconnect = () => {
|
||||
this.socket.disconnect();
|
||||
};
|
||||
}
|
||||
|
||||
export interface BotChunk {
|
||||
stage: 'start' | 'chart' | 'final' | 'title';
|
||||
content: string;
|
||||
messageId: string;
|
||||
duration?: number;
|
||||
}
|
||||
export interface Message {
|
||||
text: string;
|
||||
isUser: boolean;
|
||||
messageId: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export interface SentMessage extends Message {
|
||||
replace: 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;
|
||||
55
frontend/app/components/Kai/components/ChatInput.tsx
Normal file
55
frontend/app/components/Kai/components/ChatInput.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
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";
|
||||
|
||||
function ChatInput({ isLoading, onSubmit, threadId }: { isLoading?: boolean, onSubmit: (str: string) => void, threadId: string }) {
|
||||
const inputRef = React.useRef<Input>(null);
|
||||
const inputValue = kaiStore.queryText;
|
||||
const isProcessing = kaiStore.processingStage !== null
|
||||
const setInputValue = (text: string) => {
|
||||
kaiStore.setQueryText(text)
|
||||
}
|
||||
|
||||
const submit = () => {
|
||||
if (isProcessing) {
|
||||
const settings = { projectId: '2325', userId: '0', threadId, };
|
||||
void kaiStore.cancelGeneration(settings)
|
||||
} else {
|
||||
if (inputValue.length > 0) {
|
||||
onSubmit(inputValue)
|
||||
setInputValue('')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
}
|
||||
}, [inputValue])
|
||||
|
||||
return (
|
||||
<Input
|
||||
onPressEnter={submit}
|
||||
ref={inputRef}
|
||||
placeholder={'Ask anything about your product and users...'}
|
||||
size={'large'}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
suffix={
|
||||
<Button
|
||||
loading={isLoading}
|
||||
onClick={submit}
|
||||
icon={isProcessing ? <OctagonX size={16} /> : <SendHorizonal size={16} />}
|
||||
type={'text'}
|
||||
size={'small'}
|
||||
shape={'circle'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default observer(ChatInput)
|
||||
88
frontend/app/components/Kai/components/ChatLog.tsx
Normal file
88
frontend/app/components/Kai/components/ChatLog.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import React from 'react';
|
||||
import ChatInput from './ChatInput';
|
||||
import { ChatMsg, ChatNotice } from './ChatMsg';
|
||||
import { Loader } from 'UI';
|
||||
import { kaiStore } from '../KaiStore'
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
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 messages = kaiStore.messages;
|
||||
const loading = kaiStore.loadingChat;
|
||||
const chatRef = React.useRef<HTMLDivElement>(null);
|
||||
const processingStage = kaiStore.processingStage;
|
||||
|
||||
React.useEffect(() => {
|
||||
const settings = { projectId, threadId };
|
||||
if (threadId && !initialMsg) {
|
||||
void kaiStore.getChat(settings.projectId, threadId)
|
||||
}
|
||||
if (threadId) {
|
||||
kaiStore.createChatManager(settings, onTitleChange, initialMsg)
|
||||
}
|
||||
return () => {
|
||||
kaiStore.clearChat();
|
||||
setInitialMsg(null);
|
||||
};
|
||||
}, [threadId]);
|
||||
|
||||
const onSubmit = (text: string) => {
|
||||
kaiStore.sendMessage(text)
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
chatRef.current?.scrollTo({
|
||||
top: chatRef.current.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}, [messages.length, processingStage]);
|
||||
|
||||
const lastHumanMsgInd: null | number = kaiStore.lastHumanMessage.index;
|
||||
return (
|
||||
<Loader loading={loading} 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}
|
||||
messageId={msg.messageId}
|
||||
isLast={index === lastHumanMsgInd}
|
||||
duration={msg.duration}
|
||||
/>
|
||||
))}
|
||||
{processingStage ? (
|
||||
<ChatNotice content={processingStage.content} duration={processingStage.duration} />
|
||||
) : null}
|
||||
</div>
|
||||
<div className={'sticky bottom-0 pt-6 w-2/3'}>
|
||||
<ChatInput onSubmit={onSubmit} threadId={threadId} />
|
||||
</div>
|
||||
</div>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(ChatLog);
|
||||
173
frontend/app/components/Kai/components/ChatMsg.tsx
Normal file
173
frontend/app/components/Kai/components/ChatMsg.tsx
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import React from 'react';
|
||||
import { Icon, CopyButton } from 'UI';
|
||||
import cn from 'classnames';
|
||||
import Markdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { Loader, ThumbsUp, ThumbsDown, ListRestart, FileDown, Clock } from 'lucide-react';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { kaiStore } from '../KaiStore';
|
||||
import { toast } from 'react-toastify';
|
||||
import { durationFormatted } from 'App/date';
|
||||
|
||||
export function ChatMsg({
|
||||
text,
|
||||
isUser,
|
||||
userName,
|
||||
messageId,
|
||||
isLast,
|
||||
duration,
|
||||
}: {
|
||||
text: string;
|
||||
isUser: boolean;
|
||||
messageId: string;
|
||||
userName?: string;
|
||||
isLast?: boolean;
|
||||
duration?: number;
|
||||
}) {
|
||||
const [isProcessing, setIsProcessing] = React.useState(false);
|
||||
const bodyRef = React.useRef<HTMLDivElement>(null);
|
||||
const onRetry = () => {
|
||||
kaiStore.editMessage(text)
|
||||
}
|
||||
const onFeedback = (feedback: 'like' | 'dislike', messageId: string) => {
|
||||
kaiStore.sendMsgFeedback(feedback, messageId);
|
||||
};
|
||||
|
||||
const onExport = () => {
|
||||
setIsProcessing(true);
|
||||
import('jspdf').then(({ jsPDF }) => {
|
||||
const doc = new jsPDF();
|
||||
|
||||
doc.html(bodyRef.current, {
|
||||
callback: function(doc) {
|
||||
doc.save('document.pdf');
|
||||
},
|
||||
margin: [10, 10, 10, 10],
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 190, // Target width
|
||||
windowWidth: 675 // Window width for rendering
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
console.error('Error exporting message:', e);
|
||||
toast.error('Failed to export message');
|
||||
})
|
||||
.finally(() => {
|
||||
setIsProcessing(false);
|
||||
});
|
||||
}
|
||||
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 flex flex-col'}>
|
||||
<div className='markdown-body' ref={bodyRef}>
|
||||
<Markdown remarkPlugins={[remarkGfm]}>{text}</Markdown>
|
||||
</div>
|
||||
{isUser ? (
|
||||
isLast ? (
|
||||
<div
|
||||
onClick={onRetry}
|
||||
className={
|
||||
'ml-auto flex items-center gap-2 px-2 rounded-lg border border-gray-medium text-sm cursor-pointer hover:border-main hover:text-main w-fit'
|
||||
}
|
||||
>
|
||||
<ListRestart size={16} />
|
||||
<div>Edit</div>
|
||||
</div>
|
||||
) : null
|
||||
) : (
|
||||
<div className={'flex items-center gap-2'}>
|
||||
{duration ? (
|
||||
<MsgDuration duration={duration} />
|
||||
) : null}
|
||||
<div className='ml-auto' />
|
||||
<IconButton tooltip="Like this answer" onClick={() => onFeedback('like', messageId)}>
|
||||
<ThumbsUp size={16} />
|
||||
</IconButton>
|
||||
<IconButton tooltip="Dislike this answer" onClick={() => onFeedback('dislike', messageId)}>
|
||||
<ThumbsDown size={16} />
|
||||
</IconButton>
|
||||
<CopyButton getHtml={() => bodyRef.current?.innerHTML} content={text} isIcon format={'text/html'} />
|
||||
<IconButton processing={isProcessing} tooltip="Export as PDF" onClick={onExport}>
|
||||
<FileDown size={16} />
|
||||
</IconButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function IconButton({
|
||||
children,
|
||||
onClick,
|
||||
tooltip,
|
||||
processing,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
tooltip?: string;
|
||||
processing?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Tooltip title={tooltip}>
|
||||
<Button onClick={onClick} type="text" icon={children} size='small' loading={processing} />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChatNotice({ content, duration }: { content: string, duration?: number }) {
|
||||
const startTime = React.useRef(duration ? Date.now() - duration : Date.now());
|
||||
const [activeDuration, setDuration] = React.useState(duration ?? 0);
|
||||
|
||||
React.useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setDuration(Math.round((Date.now() - startTime.current)));
|
||||
}, 250);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
return (
|
||||
<div className='flex flex-col gap-1 items-start p-2 rounded-lg bg-gray-lightest border-gray-light w-fit '>
|
||||
<div className="flex gap-2 items-start">
|
||||
<div className={'animate-spin mt-1'}>
|
||||
<Loader size={14} />
|
||||
</div>
|
||||
<div className={'animate-pulse'}>{content}</div>
|
||||
</div>
|
||||
<MsgDuration duration={activeDuration} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MsgDuration({ duration }: { duration: number }) {
|
||||
return (
|
||||
<div className="text-disabled-text text-sm flex items-center gap-1">
|
||||
<Clock size={14} />
|
||||
<span className="leading-none">
|
||||
{durationFormatted(duration)}
|
||||
</span>
|
||||
</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">
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { Button } from 'antd';
|
||||
|
||||
function CopyButton({
|
||||
content,
|
||||
variant = 'text',
|
||||
className = 'capitalize mt-2 font-medium text-neutral-400',
|
||||
btnText = 'copy',
|
||||
size = 'small',
|
||||
}) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const copyHandler = () => {
|
||||
setCopied(true);
|
||||
copy(content);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
type={variant}
|
||||
onClick={copyHandler}
|
||||
size={size}
|
||||
className={className}
|
||||
>
|
||||
{copied ? 'copied' : btnText}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default CopyButton;
|
||||
83
frontend/app/components/ui/CopyButton/CopyButton.tsx
Normal file
83
frontend/app/components/ui/CopyButton/CopyButton.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import React, { useState } from 'react';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { ClipboardCopy, ClipboardCheck } from 'lucide-react';
|
||||
|
||||
interface Props {
|
||||
content: string;
|
||||
getHtml?: () => any;
|
||||
variant?: 'text' | 'primary' | 'ghost' | 'link' | 'default';
|
||||
className?: string;
|
||||
btnText?: string;
|
||||
size?: 'small' | 'middle' | 'large';
|
||||
isIcon?: boolean;
|
||||
format?: string;
|
||||
}
|
||||
|
||||
function CopyButton({
|
||||
content,
|
||||
getHtml,
|
||||
variant = 'text',
|
||||
className = 'capitalize mt-2 font-medium text-neutral-400',
|
||||
btnText = 'copy',
|
||||
size = 'small',
|
||||
isIcon = false,
|
||||
format = 'text/plain',
|
||||
}: Props) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const reset = () => {
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 1000);
|
||||
}
|
||||
const copyHandler = () => {
|
||||
setCopied(true);
|
||||
const contentIsGetter = !!getHtml
|
||||
const textContent = contentIsGetter ? getHtml() : content;
|
||||
const isHttps = window.location.protocol === 'https:';
|
||||
if (!isHttps) {
|
||||
copy(textContent);
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
const blob = new Blob([textContent], { type: format });
|
||||
const cbItem = new ClipboardItem({
|
||||
[format]: blob
|
||||
})
|
||||
navigator.clipboard.write([cbItem])
|
||||
.catch(e => {
|
||||
copy(textContent);
|
||||
})
|
||||
.finally(() => {
|
||||
reset()
|
||||
})
|
||||
};
|
||||
|
||||
if (isIcon) {
|
||||
return (
|
||||
<Tooltip title={copied ? 'Copied!' : 'Copy'}>
|
||||
<Button
|
||||
type="text"
|
||||
onClick={copyHandler}
|
||||
size={size}
|
||||
icon={
|
||||
copied ? <ClipboardCheck size={16} /> : <ClipboardCopy size={16} />
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
type={variant}
|
||||
onClick={copyHandler}
|
||||
size={size}
|
||||
className={className}
|
||||
>
|
||||
{copied ? 'copied' : btnText}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default CopyButton;
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -81,63 +81,34 @@ 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;
|
||||
|
||||
constructor() {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import IssueReportsService from './IssueReportsService';
|
|||
import CustomFieldService from './CustomFieldService';
|
||||
import IntegrationsService from './IntegrationsService';
|
||||
import ProjectsService from './ProjectsService';
|
||||
import KaiService from '@/components/Kai/KaiService';
|
||||
|
||||
export const dashboardService = new DashboardService();
|
||||
export const metricService = new MetricService();
|
||||
|
|
@ -52,6 +53,7 @@ export const customFieldService = new CustomFieldService();
|
|||
export const integrationsService = new IntegrationsService();
|
||||
export const searchService = new SearchService();
|
||||
export const projectsService = new ProjectsService();
|
||||
export const kaiService = new KaiService();
|
||||
|
||||
export const services = [
|
||||
projectsService,
|
||||
|
|
@ -80,4 +82,5 @@ export const services = [
|
|||
customFieldService,
|
||||
integrationsService,
|
||||
searchService,
|
||||
kaiService,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -47,6 +47,12 @@
|
|||
.fill-transparent { fill: var(--color-transparent) }
|
||||
.fill-cyan { fill: var(--color-cyan) }
|
||||
.fill-amber { fill: var(--color-amber) }
|
||||
.fill-glassWhite { fill: var(--color-glassWhite) }
|
||||
.fill-glassMint { fill: var(--color-glassMint) }
|
||||
.fill-glassLavander { fill: var(--color-glassLavander) }
|
||||
.fill-blueLight { fill: var(--color-blueLight) }
|
||||
.fill-offWhite { fill: var(--color-offWhite) }
|
||||
.fill-disabled-text { fill: var(--color-disabled-text) }
|
||||
.fill-figmaColors-accent-secondary { fill: var(--color-figmaColors-accent-secondary) }
|
||||
.fill-figmaColors-main { fill: var(--color-figmaColors-main) }
|
||||
.fill-figmaColors-primary-outlined-hover-background { fill: var(--color-figmaColors-primary-outlined-hover-background) }
|
||||
|
|
@ -102,6 +108,12 @@
|
|||
.hover-fill-transparent:hover svg { fill: var(--color-transparent) }
|
||||
.hover-fill-cyan:hover svg { fill: var(--color-cyan) }
|
||||
.hover-fill-amber:hover svg { fill: var(--color-amber) }
|
||||
.hover-fill-glassWhite:hover svg { fill: var(--color-glassWhite) }
|
||||
.hover-fill-glassMint:hover svg { fill: var(--color-glassMint) }
|
||||
.hover-fill-glassLavander:hover svg { fill: var(--color-glassLavander) }
|
||||
.hover-fill-blueLight:hover svg { fill: var(--color-blueLight) }
|
||||
.hover-fill-offWhite:hover svg { fill: var(--color-offWhite) }
|
||||
.hover-fill-disabled-text:hover svg { fill: var(--color-disabled-text) }
|
||||
.hover-fill-figmaColors-accent-secondary:hover svg { fill: var(--color-figmaColors-accent-secondary) }
|
||||
.hover-fill-figmaColors-main:hover svg { fill: var(--color-figmaColors-main) }
|
||||
.hover-fill-figmaColors-primary-outlined-hover-background:hover svg { fill: var(--color-figmaColors-primary-outlined-hover-background) }
|
||||
|
|
@ -159,6 +171,12 @@
|
|||
.color-transparent { color: var(--color-transparent) }
|
||||
.color-cyan { color: var(--color-cyan) }
|
||||
.color-amber { color: var(--color-amber) }
|
||||
.color-glassWhite { color: var(--color-glassWhite) }
|
||||
.color-glassMint { color: var(--color-glassMint) }
|
||||
.color-glassLavander { color: var(--color-glassLavander) }
|
||||
.color-blueLight { color: var(--color-blueLight) }
|
||||
.color-offWhite { color: var(--color-offWhite) }
|
||||
.color-disabled-text { color: var(--color-disabled-text) }
|
||||
.color-figmaColors-accent-secondary { color: var(--color-figmaColors-accent-secondary) }
|
||||
.color-figmaColors-main { color: var(--color-figmaColors-main) }
|
||||
.color-figmaColors-primary-outlined-hover-background { color: var(--color-figmaColors-primary-outlined-hover-background) }
|
||||
|
|
@ -216,6 +234,12 @@
|
|||
.hover-transparent:hover { color: var(--color-transparent) }
|
||||
.hover-cyan:hover { color: var(--color-cyan) }
|
||||
.hover-amber:hover { color: var(--color-amber) }
|
||||
.hover-glassWhite:hover { color: var(--color-glassWhite) }
|
||||
.hover-glassMint:hover { color: var(--color-glassMint) }
|
||||
.hover-glassLavander:hover { color: var(--color-glassLavander) }
|
||||
.hover-blueLight:hover { color: var(--color-blueLight) }
|
||||
.hover-offWhite:hover { color: var(--color-offWhite) }
|
||||
.hover-disabled-text:hover { color: var(--color-disabled-text) }
|
||||
.hover-figmaColors-accent-secondary:hover { color: var(--color-figmaColors-accent-secondary) }
|
||||
.hover-figmaColors-main:hover { color: var(--color-figmaColors-main) }
|
||||
.hover-figmaColors-primary-outlined-hover-background:hover { color: var(--color-figmaColors-primary-outlined-hover-background) }
|
||||
|
|
@ -273,6 +297,12 @@
|
|||
.border-transparent { border-color: var(--color-transparent) }
|
||||
.border-cyan { border-color: var(--color-cyan) }
|
||||
.border-amber { border-color: var(--color-amber) }
|
||||
.border-glassWhite { border-color: var(--color-glassWhite) }
|
||||
.border-glassMint { border-color: var(--color-glassMint) }
|
||||
.border-glassLavander { border-color: var(--color-glassLavander) }
|
||||
.border-blueLight { border-color: var(--color-blueLight) }
|
||||
.border-offWhite { border-color: var(--color-offWhite) }
|
||||
.border-disabled-text { border-color: var(--color-disabled-text) }
|
||||
.border-figmaColors-accent-secondary { border-color: var(--color-figmaColors-accent-secondary) }
|
||||
.border-figmaColors-main { border-color: var(--color-figmaColors-main) }
|
||||
.border-figmaColors-primary-outlined-hover-background { border-color: var(--color-figmaColors-primary-outlined-hover-background) }
|
||||
|
|
@ -330,6 +360,12 @@
|
|||
.bg-transparent { background-color: var(--color-transparent) }
|
||||
.bg-cyan { background-color: var(--color-cyan) }
|
||||
.bg-amber { background-color: var(--color-amber) }
|
||||
.bg-glassWhite { background-color: var(--color-glassWhite) }
|
||||
.bg-glassMint { background-color: var(--color-glassMint) }
|
||||
.bg-glassLavander { background-color: var(--color-glassLavander) }
|
||||
.bg-blueLight { background-color: var(--color-blueLight) }
|
||||
.bg-offWhite { background-color: var(--color-offWhite) }
|
||||
.bg-disabled-text { background-color: var(--color-disabled-text) }
|
||||
.bg-figmaColors-accent-secondary { background-color: var(--color-figmaColors-accent-secondary) }
|
||||
.bg-figmaColors-main { background-color: var(--color-figmaColors-main) }
|
||||
.bg-figmaColors-primary-outlined-hover-background { background-color: var(--color-figmaColors-primary-outlined-hover-background) }
|
||||
|
|
|
|||
1
frontend/app/styles/import.css
vendored
1
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';
|
||||
|
|
|
|||
|
|
@ -287,3 +287,141 @@ svg {
|
|||
.text-black {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.markdown-body table {
|
||||
table-layout: auto;
|
||||
width: 100%;
|
||||
text-indent: 0;
|
||||
border-color: inherit;
|
||||
border-collapse: collapse;
|
||||
display: block;
|
||||
max-width: 960px;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.markdown-body table::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
background-color: var(--color-gray-lightest);
|
||||
}
|
||||
|
||||
.markdown-body table::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-gray-medium);
|
||||
border-radius: 4px;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.markdown-body table::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--color-gray-dark);
|
||||
}
|
||||
|
||||
.markdown-body table::-webkit-scrollbar-thumb:active {
|
||||
background-color: var(--color-gray-darkest);
|
||||
}
|
||||
|
||||
.markdown-body table::-webkit-scrollbar-track {
|
||||
background-color: var(--color-gray-lightest);
|
||||
}
|
||||
|
||||
.markdown-body dl dt {
|
||||
padding: 0;
|
||||
margin-top: 16px;
|
||||
font-size: 1em;
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-body dl dd {
|
||||
padding: 0 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.markdown-body table th {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown-body table th,
|
||||
.markdown-body table td {
|
||||
padding: 3px 6px;
|
||||
border: 1px solid $gray-light;
|
||||
}
|
||||
|
||||
.markdown-body table td>:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.markdown-body table thead {
|
||||
background-color: var(--color-blueLight);
|
||||
}
|
||||
|
||||
.markdown-body table tbody tr {
|
||||
background-color: var(--color-offWhite);
|
||||
}
|
||||
|
||||
.markdown-body table tbody td {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 30px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.markdown-body table tr:nth-child(2n) {
|
||||
background-color: var(--color-blueLight);
|
||||
}
|
||||
|
||||
.markdown-body table img {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.markdown-body * {
|
||||
all: revert-layer;
|
||||
}
|
||||
|
||||
.markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4 {
|
||||
font-size: revert;
|
||||
font-weight: revert;
|
||||
}
|
||||
|
||||
.markdown-body h1 {
|
||||
font-size: 1.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.markdown-body h2 {
|
||||
font-size: 1.25rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.markdown-body h3 {
|
||||
font-size: 1.125rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.markdown-body h4 {
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.markdown-body a {
|
||||
color: var(--color-teal);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-body p {
|
||||
margin: 0!important;
|
||||
}
|
||||
|
||||
.markdown-body ul {
|
||||
margin-top: 4px!important;
|
||||
margin-bottom: 4px!important;
|
||||
}
|
||||
|
||||
.markdown-body ul {
|
||||
list-style-type: none; /* Remove default bullets */
|
||||
}
|
||||
.markdown-body ul li:before {
|
||||
content: "•"; /* Use standard bullet character */
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
margin-left: -1em;
|
||||
}
|
||||
|
|
|
|||
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 |
|
|
@ -46,6 +46,12 @@ module.exports = {
|
|||
transparent: 'transparent',
|
||||
cyan: '#EBF4F5',
|
||||
amber: 'oklch(98.7% 0.022 95.277)',
|
||||
glassWhite: 'rgba(255, 255, 255, 0.5)',
|
||||
glassMint: 'rgba(248, 255, 254, 0.5)',
|
||||
glassLavander: 'rgba(243, 241, 255, 0.5)',
|
||||
blueLight: 'rgba(235, 235, 255, 1)',
|
||||
offWhite: 'rgba(250, 250, 255, 1)',
|
||||
'disabled-text': 'rgba(0,0,0, 0.38)',
|
||||
|
||||
figmaColors: {
|
||||
'accent-secondary': 'rgba(62, 170, 175, 1)',
|
||||
|
|
@ -96,6 +102,12 @@ module.exports = {
|
|||
'light-blue-bg': 'oklch(39.8% 0.07 227.392)',
|
||||
'disabled-text': 'rgba(255, 255, 255, 0.38)',
|
||||
|
||||
glassWhite: 'rgba(30, 30, 30, 0.5)',
|
||||
glassMint: 'rgba(20, 27, 28, 0.5)',
|
||||
glassLavander: 'rgba(25, 23, 37, 0.5)',
|
||||
blueLight: '#343460',
|
||||
offWhite: 'rgba(30, 30, 50, 1)',
|
||||
|
||||
figmaColors: {
|
||||
'accent-secondary': 'rgba(82, 190, 195, 1)',
|
||||
'text-disabled': 'rgba(255, 255, 255, 0.38)',
|
||||
|
|
|
|||
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 = 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",
|
||||
|
|
@ -82,6 +82,7 @@
|
|||
"react-toastify": "^11.0.5",
|
||||
"react18-json-view": "^0.2.9",
|
||||
"recharts": "^2.15.3",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"syncod": "^0.0.1",
|
||||
"ts-api-utils": "^2.1.0",
|
||||
|
|
|
|||
3027
frontend/yarn.lock
3027
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