ui: add duration for msgs

This commit is contained in:
nick-delirium 2025-05-09 17:19:53 +02:00
parent f1e9546429
commit 3d3079fece
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
7 changed files with 60 additions and 13 deletions

View file

@ -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',

View file

@ -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);

View file

@ -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 {

View file

@ -72,10 +72,11 @@ function ChatLog({
userName={userLetter}
messageId={msg.messageId}
isLast={index === lastHumanMsgInd}
duration={msg.duration}
/>
))}
{processingStage ? (
<ChatNotice content={processingStage.content} />
<ChatNotice content={processingStage.content} duration={processingStage.duration} />
) : null}
</div>
<div className={'sticky bottom-0 pt-6 w-2/3'}>

View file

@ -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<HTMLDivElement>(null);
@ -78,7 +81,7 @@ export function ChatMsg({
<Icon name={'kai_colored'} size={18} />
</div>
)}
<div className={'mt-1'}>
<div className={'mt-1 flex flex-col'}>
<div className='markdown-body' ref={bodyRef}>
<Markdown remarkPlugins={[remarkGfm]}>{text}</Markdown>
</div>
@ -96,6 +99,10 @@ export function ChatMsg({
) : 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>
@ -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 (
<div className="flex items-center gap-2 p-2 rounded-lg bg-gray-lightest border-gray-light w-fit">
<div className={'animate-spin'}>
<Loader size={14} />
<div 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>
<div className={'animate-pulse text-disabled-text'}>{content}</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>
)
}

View file

@ -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) }

View file

@ -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)',