diff --git a/frontend/app/components/Kai/KaiChat.tsx b/frontend/app/components/Kai/KaiChat.tsx index 0a53ab16d..17bb4c820 100644 --- a/frontend/app/components/Kai/KaiChat.tsx +++ b/frontend/app/components/Kai/KaiChat.tsx @@ -98,7 +98,9 @@ function KaiChat() { }; return (
-
+
=> { + ): 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 (
{goBack ? (
-
Back
+
{t('Back')}
) : null}
{chatTitle ? ( -
{chatTitle}
+
+ {chatTitle} +
) : ( <> @@ -38,14 +44,14 @@ function ChatHeader({ )}
-
- -
Chats
+
+
+ +
{t('Chats')}
+
); 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 ? ( + +
); } 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 ? ( + <>
-
Edit
+
{t('Edit')}
- ) : null +
+
{t('Cancel')}
+
+ ) : (
{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);