diff --git a/frontend/app/components/Kai/KaiService.ts b/frontend/app/components/Kai/KaiService.ts index 0363aa1b3..633d4c3a5 100644 --- a/frontend/app/components/Kai/KaiService.ts +++ b/frontend/app/components/Kai/KaiService.ts @@ -34,7 +34,7 @@ export default class KaiService extends AiService { return true; } - getKaiChat = async (projectId: string, userId: string, threadId: string): Promise<{ role: string, content: string, message_id: any }[]> => { + getKaiChat = async (projectId: string, userId: string, threadId: string): Promise<{ role: string, content: string, message_id: any, duration?: number }[]> => { const jwt = window.env.KAI_TESTING // this.client.getJwt() const r = await fetch(`http://localhost:8700/kai/${projectId}/chats/${threadId}?user_id=${userId}`, { method: 'GET', diff --git a/frontend/app/components/Kai/KaiStore.ts b/frontend/app/components/Kai/KaiStore.ts index 4bb04399d..a3a1583c6 100644 --- a/frontend/app/components/Kai/KaiStore.ts +++ b/frontend/app/components/Kai/KaiStore.ts @@ -99,6 +99,7 @@ class KaiStore { text: m.content, isUser: isUser, messageId: m.message_id, + duration: m.duration, }; }), ); @@ -121,7 +122,12 @@ class KaiStore { msgCallback: (msg) => { if ('state' in msg) { if (msg.state === 'running') { - this.setProcessingStage({ content: 'Processing your request...', stage: 'chart', messageId: Date.now().toPrecision() }) + 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) } @@ -140,6 +146,7 @@ class KaiStore { text: msg.content, isUser: false, messageId: msg.messageId, + duration: msg.duration } this.addMessage(msgObj); this.setProcessingStage(null); diff --git a/frontend/app/components/Kai/SocketManager.ts b/frontend/app/components/Kai/SocketManager.ts index 7014f4f29..fd4c409e1 100644 --- a/frontend/app/components/Kai/SocketManager.ts +++ b/frontend/app/components/Kai/SocketManager.ts @@ -52,7 +52,7 @@ export class ChatManager { msgCallback, titleCallback, }: { - msgCallback: (msg: BotChunk | { state: string, type: 'state' }) => void; + msgCallback: (msg: BotChunk | { state: string, type: 'state', start_time?: number }) => void; titleCallback: (title: string) => void; }) => { this.socket.on('chunk', (msg: BotChunk) => { @@ -63,8 +63,8 @@ export class ChatManager { console.log('Received title:', msg); titleCallback(msg.content); }); - this.socket.on('state', (state: { message: 'idle' | 'running' }) => { - msgCallback({ state: state.message, type: 'state' }) + this.socket.on('state', (state: { message: 'idle' | 'running', start_time: number }) => { + msgCallback({ state: state.message, type: 'state', start_time: state.start_time }) }) }; @@ -77,11 +77,13 @@ 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 { diff --git a/frontend/app/components/Kai/components/ChatLog.tsx b/frontend/app/components/Kai/components/ChatLog.tsx index 283e37f43..8876af12b 100644 --- a/frontend/app/components/Kai/components/ChatLog.tsx +++ b/frontend/app/components/Kai/components/ChatLog.tsx @@ -72,10 +72,11 @@ function ChatLog({ userName={userLetter} messageId={msg.messageId} isLast={index === lastHumanMsgInd} + duration={msg.duration} /> ))} {processingStage ? ( - + ) : null}
diff --git a/frontend/app/components/Kai/components/ChatMsg.tsx b/frontend/app/components/Kai/components/ChatMsg.tsx index db9e249cc..cc8668369 100644 --- a/frontend/app/components/Kai/components/ChatMsg.tsx +++ b/frontend/app/components/Kai/components/ChatMsg.tsx @@ -3,10 +3,11 @@ 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 } from 'lucide-react'; +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, @@ -14,12 +15,14 @@ export function ChatMsg({ 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(null); @@ -78,7 +81,7 @@ export function ChatMsg({
)} -
+
{text}
@@ -96,6 +99,10 @@ export function ChatMsg({ ) : null ) : (
+ {duration ? ( + + ) : null} +
onFeedback('like', messageId)}> @@ -131,13 +138,36 @@ function IconButton({ ); } -export function ChatNotice({ content }: { content: string }) { +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 ( -
-
- +
+
+
+ +
+
{content}
-
{content}
+
); } + +function MsgDuration({ duration }: { duration: number }) { + return ( +
+ + + {durationFormatted(duration)} + +
+ ) +} diff --git a/frontend/app/styles/colors-autogen.css b/frontend/app/styles/colors-autogen.css index b323c9adc..148df49f3 100644 --- a/frontend/app/styles/colors-autogen.css +++ b/frontend/app/styles/colors-autogen.css @@ -52,6 +52,7 @@ .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) } @@ -112,6 +113,7 @@ .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) } @@ -174,6 +176,7 @@ .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) } @@ -236,6 +239,7 @@ .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) } @@ -298,6 +302,7 @@ .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) } @@ -360,6 +365,7 @@ .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) } diff --git a/frontend/app/theme/colors.js b/frontend/app/theme/colors.js index c8dd23eb6..17d472398 100644 --- a/frontend/app/theme/colors.js +++ b/frontend/app/theme/colors.js @@ -51,6 +51,7 @@ module.exports = { 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)',