+
=> {
+ ): Promise<{ title: string; thread_id: string; datetime: string }[]> => {
const r = await this.client.get(`/kai/${projectId}/chats`);
if (!r.ok) {
throw new Error('Failed to fetch chats');
@@ -81,24 +81,45 @@ export default class KaiService extends AiService {
return data;
};
- getMsgChart = async (messageId: string, projectId: string): Promise<{ filters: any[], chart: string, eventsOrder: string }> => {
- const r = await this.client.get(`/kai/${projectId}/chats/data/${messageId}`);
+ getMsgChart = async (
+ messageId: string,
+ projectId: string,
+ ): Promise<{ filters: any[]; chart: string; eventsOrder: string }> => {
+ const r = await this.client.get(
+ `/kai/${projectId}/chats/data/${messageId}`,
+ );
if (!r.ok) {
throw new Error('Failed to fetch chart data');
}
const data = await r.json();
return data;
- }
+ };
- saveChartData = async (messageId: string, projectId: string, chartData: any) => {
- const r = await this.client.post(`/kai/${projectId}/chats/data/${messageId}`, {
- chart_data: JSON.stringify(chartData),
- });
+ saveChartData = async (
+ messageId: string,
+ projectId: string,
+ chartData: any,
+ ) => {
+ const r = await this.client.post(
+ `/kai/${projectId}/chats/data/${messageId}`,
+ {
+ chart_data: JSON.stringify(chartData),
+ },
+ );
if (!r.ok) {
throw new Error('Failed to save chart data');
}
const data = await r.json();
return data;
- }
+ };
+
+ checkUsage = async (): Promise<{ total: number; used: number }> => {
+ const r = await this.client.get(`/kai/usage`);
+ if (!r.ok) {
+ throw new Error('Failed to fetch usage');
+ }
+ const data = await r.json();
+ return data;
+ };
}
diff --git a/frontend/app/components/Kai/KaiStore.ts b/frontend/app/components/Kai/KaiStore.ts
index b5660faf0..d4d85526a 100644
--- a/frontend/app/components/Kai/KaiStore.ts
+++ b/frontend/app/components/Kai/KaiStore.ts
@@ -31,9 +31,15 @@ class KaiStore {
queryText = '';
loadingChat = false;
replacing: string | null = null;
+ usage = {
+ total: 0,
+ used: 0,
+ percent: 0,
+ };
constructor() {
makeAutoObservable(this);
+ this.checkUsage();
}
get lastHumanMessage() {
@@ -146,6 +152,7 @@ class KaiStore {
console.error('No token found');
return;
}
+ this.checkUsage();
this.chatManager = new ChatManager({ ...settings, token });
this.chatManager.setOnMsgHook({
msgCallback: (msg) => {
@@ -184,6 +191,7 @@ class KaiStore {
supports_visualization: msg.supports_visualization,
chart_data: '',
};
+ this.bumpUsage();
this.addMessage(msgObj);
this.setProcessingStage(null);
}
@@ -201,6 +209,11 @@ class KaiStore {
this.replacing = replacing;
};
+ bumpUsage = () => {
+ this.usage.used += 1;
+ this.usage.percent = (this.usage.used / this.usage.total) * 100;
+ };
+
sendMessage = (message: string) => {
if (this.chatManager) {
this.chatManager.sendMessage(message, !!this.replacing);
@@ -329,6 +342,19 @@ class KaiStore {
const parsedData = JSON.parse(data);
return new Widget().fromJson(parsedData);
};
+
+ checkUsage = async () => {
+ try {
+ const { total, used } = await kaiService.checkUsage();
+ this.usage = {
+ total,
+ used,
+ percent: (used / total) * 100,
+ };
+ } catch (e) {
+ console.error(e);
+ }
+ };
}
export const kaiStore = new KaiStore();
diff --git a/frontend/app/components/Kai/components/ChatHeader.tsx b/frontend/app/components/Kai/components/ChatHeader.tsx
index 756aa9767..ae3cb4068 100644
--- a/frontend/app/components/Kai/components/ChatHeader.tsx
+++ b/frontend/app/components/Kai/components/ChatHeader.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { Icon } from 'UI';
import { MessagesSquare, ArrowLeft } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
function ChatHeader({
openChats = () => {},
@@ -11,6 +12,7 @@ function ChatHeader({
openChats?: () => void;
chatTitle: string | null;
}) {
+ const { t } = useTranslation();
return (
{chatTitle ? (
-
{chatTitle}
+
+ {chatTitle}
+
) : (
<>
@@ -38,14 +44,14 @@ function ChatHeader({
>
)}
-
);
diff --git a/frontend/app/components/Kai/components/ChatInput.tsx b/frontend/app/components/Kai/components/ChatInput.tsx
index 49c8739f6..3ab630c87 100644
--- a/frontend/app/components/Kai/components/ChatInput.tsx
+++ b/frontend/app/components/Kai/components/ChatInput.tsx
@@ -3,6 +3,7 @@ import { Button, Input, Tooltip } from 'antd';
import { SendHorizonal, OctagonX } from 'lucide-react';
import { kaiStore } from '../KaiStore';
import { observer } from 'mobx-react-lite';
+import Usage from './Usage';
function ChatInput({
isLoading,
@@ -13,7 +14,9 @@ function ChatInput({
onSubmit: (str: string) => void;
threadId: string;
}) {
- const inputRef = React.useRef(null);
+ const inputRef = React.useRef(null);
+ const usage = kaiStore.usage;
+ const limited = usage.percent >= 100;
const inputValue = kaiStore.queryText;
const isProcessing = kaiStore.processingStage !== null;
const setInputValue = (text: string) => {
@@ -21,6 +24,9 @@ function ChatInput({
};
const submit = () => {
+ if (limited) {
+ return;
+ }
if (isProcessing) {
const settings = { projectId: '2325', userId: '0', threadId };
void kaiStore.cancelGeneration(settings);
@@ -46,50 +52,57 @@ function ChatInput({
const isReplacing = kaiStore.replacing !== null;
return (
- {
- if (e.key === 'Escape') {
- cancelReplace();
- }
- }}
- ref={inputRef}
- placeholder={'Ask anything about your product and users...'}
- size={'large'}
- value={inputValue}
- onChange={(e) => setInputValue(e.target.value)}
- suffix={
- <>
- {isReplacing ? (
-
+
+
{
+ if (e.key === 'Escape') {
+ cancelReplace();
+ }
+ }}
+ ref={inputRef}
+ placeholder={'Ask anything about your product and users...'}
+ size={'large'}
+ value={inputValue}
+ onChange={(e) => setInputValue(e.target.value)}
+ suffix={
+ <>
+ {isReplacing ? (
+
+ }
+ type={'text'}
+ size={'small'}
+ shape={'circle'}
+ disabled={limited}
+ />
+
+ ) : null}
+
}
+ loading={isLoading}
+ onClick={submit}
+ disabled={limited}
+ icon={
+ isProcessing ? (
+
+ ) : (
+
+ )
+ }
type={'text'}
size={'small'}
shape={'circle'}
/>
- ) : null}
-
-
- ) : (
-
- )
- }
- type={'text'}
- size={'small'}
- shape={'circle'}
- />
-
- >
- }
- />
+ >
+ }
+ />
+
+
+
+
);
}
diff --git a/frontend/app/components/Kai/components/ChatMsg.tsx b/frontend/app/components/Kai/components/ChatMsg.tsx
index 04406840b..27f89a660 100644
--- a/frontend/app/components/Kai/components/ChatMsg.tsx
+++ b/frontend/app/components/Kai/components/ChatMsg.tsx
@@ -19,7 +19,7 @@ import { toast } from 'react-toastify';
import { durationFormatted } from 'App/date';
import WidgetChart from '@/components/Dashboard/components/WidgetChart';
import Widget from 'App/mstore/types/widget';
-import cn from 'classnames';
+import { useTranslation } from 'react-i18next';
function ChatMsg({
userName,
@@ -32,6 +32,7 @@ function ChatMsg({
canEdit?: boolean;
siteId: string;
}) {
+ const { t } = useTranslation();
const [metric, setMetric] = React.useState(null);
const [loadingChart, setLoadingChart] = React.useState(false);
const {
@@ -46,9 +47,13 @@ function ChatMsg({
const isEditing = kaiStore.replacing && messageId === kaiStore.replacing;
const [isProcessing, setIsProcessing] = React.useState(false);
const bodyRef = React.useRef(null);
- const onRetry = () => {
+ const onEdit = () => {
kaiStore.editMessage(text, messageId);
};
+ const onCancelEdit = () => {
+ kaiStore.setQueryText('');
+ kaiStore.setReplacing(null);
+ }
const onFeedback = (feedback: 'like' | 'dislike', messageId: string) => {
kaiStore.sendMsgFeedback(feedback, messageId, siteId);
};
@@ -143,17 +148,31 @@ function ChatMsg({
) : null}
{isUser ? (
- canEdit ? (
+ <>
- ) : null
+
+ >
) : (
{duration ? : null}
diff --git a/frontend/app/components/Kai/components/ChatsModal.tsx b/frontend/app/components/Kai/components/ChatsModal.tsx
index b6175a978..a39d358f0 100644
--- a/frontend/app/components/Kai/components/ChatsModal.tsx
+++ b/frontend/app/components/Kai/components/ChatsModal.tsx
@@ -5,6 +5,8 @@ import { MessagesSquare, Trash } from 'lucide-react';
import { kaiService } from 'App/services';
import { toast } from 'react-toastify';
import { useTranslation } from 'react-i18next';
+import { kaiStore } from '../KaiStore';
+import { observer } from 'mobx-react-lite';
function ChatsModal({
onSelect,
@@ -14,6 +16,7 @@ function ChatsModal({
projectId: string;
}) {
const { t } = useTranslation();
+ const { usage } = kaiStore;
const {
data = [],
isPending,
@@ -24,6 +27,10 @@ function ChatsModal({
staleTime: 1000 * 60,
});
+ React.useEffect(() => {
+ kaiStore.checkUsage();
+ }, []);
+
const datedCollections = React.useMemo(() => {
return data.length ? splitByDate(data) : [];
}, [data.length]);
@@ -42,6 +49,14 @@ function ChatsModal({
{t('Chats')}
+ {usage.percent > 80 ? (
+
+ {t('You have used {{used}} out of {{total}} daily requests', {
+ used: usage.used,
+ total: usage.total,
+ })}
+
+ ) : null}
{isPending ? (
{t('Loading chats')}...
) : (
@@ -118,4 +133,4 @@ function ChatsList({
);
}
-export default ChatsModal;
+export default observer(ChatsModal);
diff --git a/frontend/app/components/Kai/components/Usage.tsx b/frontend/app/components/Kai/components/Usage.tsx
new file mode 100644
index 000000000..9870c2b87
--- /dev/null
+++ b/frontend/app/components/Kai/components/Usage.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { kaiStore } from '../KaiStore';
+import { observer } from 'mobx-react-lite';
+import { Progress, Tooltip } from 'antd';
+const getUsageColor = (percent: number) => {
+ return 'disabled-text';
+};
+
+function Usage() {
+ const { usage } = kaiStore;
+ const color = getUsageColor(usage.percent);
+
+ if (usage.total === 0) {
+ return null;
+ }
+ return (
+
+ );
+}
+
+export default observer(Usage);