ui: fixes for kai and network tooltips

This commit is contained in:
nick-delirium 2025-05-13 17:17:18 +02:00
parent ddb47631b6
commit 0a5d4413ca
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
10 changed files with 176 additions and 101 deletions

View file

@ -50,7 +50,8 @@ function KaiChat() {
React.useEffect(() => {
if (
activeSiteId &&
parseInt(activeSiteId, 10) !== parseInt(location.pathname.split('/')[1], 10)
parseInt(activeSiteId, 10) !==
parseInt(location.pathname.split('/')[1], 10)
) {
return;
}
@ -120,7 +121,6 @@ function KaiChat() {
<ChatLog
threadId={threadId}
projectId={activeSiteId}
userId={userId}
userLetter={userLetter}
onTitleChange={setTitle}
initialMsg={initialMsg}

View file

@ -3,7 +3,7 @@ import AiService from '@/services/AiService';
export default class KaiService extends AiService {
getKaiChats = async (
projectId: string,
): Promise<{ title: string; threadId: string }[]> => {
): Promise<{ title: string; thread_id: string }[]> => {
const r = await this.client.get(`/kai/${projectId}/chats`);
if (!r.ok) {
throw new Error('Failed to fetch chats');
@ -27,7 +27,13 @@ export default class KaiService extends AiService {
projectId: string,
threadId: string,
): Promise<
{ role: string; content: string; message_id: any; duration?: number }[]
{
role: string;
content: string;
message_id: any;
duration?: number;
feedback: boolean | null;
}[]
> => {
const r = await this.client.get(`/kai/${projectId}/chats/${threadId}`);
if (!r.ok) {

View file

@ -81,11 +81,10 @@ class KaiStore {
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);
@ -100,6 +99,7 @@ class KaiStore {
isUser: isUser,
messageId: m.message_id,
duration: m.duration,
feedback: m.feedback,
};
}),
);
@ -117,7 +117,11 @@ class KaiStore {
setTitle: (title: string) => void,
initialMsg: string | null,
) => {
const token = kaiService.client.getJwt()
const token = kaiService.client.getJwt();
if (!token) {
console.error('No token found');
return;
}
this.chatManager = new ChatManager({ ...settings, token });
this.chatManager.setOnMsgHook({
msgCallback: (msg) => {
@ -127,10 +131,10 @@ class KaiStore {
content: 'Processing your request...',
stage: 'chart',
messageId: Date.now().toPrecision(),
duration: msg.start_time ? Date.now() - msg.start_time : 0
})
duration: msg.start_time ? Date.now() - msg.start_time : 0,
});
} else {
this.setProcessingStage(null)
this.setProcessingStage(null);
}
} else {
if (msg.stage === 'start') {
@ -147,8 +151,9 @@ class KaiStore {
text: msg.content,
isUser: false,
messageId: msg.messageId,
duration: msg.duration
}
duration: msg.duration,
feedback: null,
};
this.addMessage(msgObj);
this.setProcessingStage(null);
}
@ -171,32 +176,46 @@ class KaiStore {
this.chatManager.sendMessage(message, this.replacing);
}
if (this.replacing) {
console.log(this.lastHumanMessage, this.lastKaiMessage, 'replacing these two')
const deleting = []
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)
deleting.push(this.lastKaiMessage.index);
}
this.deleteAtIndex(deleting);
this.setReplacing(false)
this.setReplacing(false);
}
this.addMessage({
text: message,
isUser: true,
messageId: Date.now().toString(),
feedback: null,
duration: 0,
});
};
sendMsgFeedback = (feedback: string, messageId: string) => {
const settings = { projectId: '2325', userId: '0' };
sendMsgFeedback = (
feedback: string,
messageId: string,
projectId: string,
) => {
this.messages = this.messages.map((msg) => {
if (msg.messageId === messageId) {
return {
...msg,
feedback: feedback === 'like' ? true : false,
};
}
return msg;
});
aiService
.feedback(
feedback === 'like',
messageId,
settings.projectId,
)
.feedback(feedback === 'like', messageId, projectId)
.then(() => {
toast.success('Feedback saved.');
})
@ -206,15 +225,21 @@ class KaiStore {
});
};
cancelGeneration = async (settings: { projectId: string; userId: string; threadId: string }) => {
cancelGeneration = async (settings: {
projectId: string;
userId: string;
threadId: string;
}) => {
try {
await kaiService.cancelGeneration(settings.projectId, settings.threadId, settings.userId)
this.setProcessingStage(null)
await kaiService.cancelGeneration(settings.projectId, settings.threadId);
this.setProcessingStage(null);
} catch (e) {
console.error(e)
toast.error('Failed to cancel the response generation, please try again later.')
console.error(e);
toast.error(
'Failed to cancel the response generation, please try again later.',
);
}
}
};
clearChat = () => {
this.setMessages([]);

View file

@ -100,6 +100,7 @@ export interface Message {
isUser: boolean;
messageId: string;
duration?: number;
feedback: boolean | null;
}
export interface SentMessage extends Message {

View file

@ -2,12 +2,11 @@ import React from 'react';
import ChatInput from './ChatInput';
import { ChatMsg, ChatNotice } from './ChatMsg';
import { Loader } from 'UI';
import { kaiStore } from '../KaiStore'
import { kaiStore } from '../KaiStore';
import { observer } from 'mobx-react-lite';
function ChatLog({
projectId,
userId,
threadId,
userLetter,
onTitleChange,
@ -15,7 +14,6 @@ function ChatLog({
setInitialMsg,
}: {
projectId: string;
userId: string;
threadId: any;
userLetter: string;
onTitleChange: (title: string | null) => void;
@ -30,10 +28,10 @@ function ChatLog({
React.useEffect(() => {
const settings = { projectId, threadId };
if (threadId && !initialMsg) {
void kaiStore.getChat(settings.projectId, threadId)
void kaiStore.getChat(settings.projectId, threadId);
}
if (threadId) {
kaiStore.createChatManager(settings, onTitleChange, initialMsg)
kaiStore.createChatManager(settings, onTitleChange, initialMsg);
}
return () => {
kaiStore.clearChat();
@ -42,7 +40,7 @@ function ChatLog({
}, [threadId]);
const onSubmit = (text: string) => {
kaiStore.sendMessage(text)
kaiStore.sendMessage(text);
};
React.useEffect(() => {
@ -71,10 +69,15 @@ function ChatLog({
messageId={msg.messageId}
isLast={index === lastHumanMsgInd}
duration={msg.duration}
feedback={msg.feedback}
siteId={projectId}
/>
))}
{processingStage ? (
<ChatNotice content={processingStage.content} duration={processingStage.duration} />
<ChatNotice
content={processingStage.content}
duration={processingStage.duration}
/>
) : null}
</div>
<div className={'sticky bottom-0 pt-6 w-2/3'}>

View file

@ -23,6 +23,8 @@ export function ChatMsg({
messageId,
isLast,
duration,
feedback,
siteId,
}: {
text: string;
isUser: boolean;
@ -30,6 +32,8 @@ export function ChatMsg({
userName?: string;
isLast?: boolean;
duration?: number;
feedback: boolean | null;
siteId: string;
}) {
const [isProcessing, setIsProcessing] = React.useState(false);
const bodyRef = React.useRef<HTMLDivElement>(null);
@ -37,7 +41,7 @@ export function ChatMsg({
kaiStore.editMessage(text);
};
const onFeedback = (feedback: 'like' | 'dislike', messageId: string) => {
kaiStore.sendMsgFeedback(feedback, messageId);
kaiStore.sendMsgFeedback(feedback, messageId, siteId);
};
const onExport = () => {
@ -115,12 +119,14 @@ export function ChatMsg({
{duration ? <MsgDuration duration={duration} /> : null}
<div className="ml-auto" />
<IconButton
active={feedback === true}
tooltip="Like this answer"
onClick={() => onFeedback('like', messageId)}
>
<ThumbsUp size={16} />
</IconButton>
<IconButton
active={feedback === false}
tooltip="Dislike this answer"
onClick={() => onFeedback('dislike', messageId)}
>
@ -151,17 +157,19 @@ function IconButton({
onClick,
tooltip,
processing,
active,
}: {
children: React.ReactNode;
onClick?: () => void;
tooltip?: string;
processing?: boolean;
active?: boolean;
}) {
return (
<Tooltip title={tooltip}>
<Button
onClick={onClick}
type="text"
type={active ? 'primary' : 'text'}
icon={children}
size="small"
loading={processing}

View file

@ -11,7 +11,7 @@ import React, {
useCallback,
useRef,
} from 'react';
import i18n from 'App/i18n'
import i18n from 'App/i18n';
import { useModal } from 'App/components/Modal';
import {
@ -23,10 +23,7 @@ import { useStore } from 'App/mstore';
import { formatBytes, debounceCall } from 'App/utils';
import { Icon, NoContent, Tabs } from 'UI';
import { Tooltip, Input, Switch, Form } from 'antd';
import {
SearchOutlined,
InfoCircleOutlined,
} from '@ant-design/icons';
import { SearchOutlined, InfoCircleOutlined } from '@ant-design/icons';
import FetchDetailsModal from 'Shared/FetchDetailsModal';
@ -37,7 +34,7 @@ import TimeTable from '../TimeTable';
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
import WSPanel from './WSPanel';
import { useTranslation } from 'react-i18next';
import { mergeListsWithZoom, processInChunks } from './utils'
import { mergeListsWithZoom, processInChunks } from './utils';
// Constants remain the same
const INDEX_KEY = 'network';
@ -84,9 +81,22 @@ export function renderType(r: any) {
}
export function renderName(r: any) {
const maxTtipUrlLength = 800;
const tooltipUrl =
r.url && r.url.length > maxTtipUrlLength
? `${r.url.slice(0, maxTtipUrlLength / 2)}......${r.url.slice(-maxTtipUrlLength / 2)}`
: r.url;
return (
<Tooltip style={{ width: '100%' }} title={<div>{r.url}</div>}>
<div>{r.name}</div>
<Tooltip
style={{ width: '100%', maxWidth: 1024 }}
title={<div>{tooltipUrl}</div>}
>
<div
style={{ maxWidth: 250, overflow: 'hidden', textOverflow: 'ellipsis' }}
>
{r.name}
</div>
</Tooltip>
);
}
@ -94,7 +104,7 @@ export function renderName(r: any) {
function renderSize(r: any) {
const t = i18n.t;
const notCaptured = t('Not captured');
const resSizeStr = t('Resource size')
const resSizeStr = t('Resource size');
let triggerText;
let content;
if (r.responseBodySize) {
@ -185,7 +195,6 @@ function renderStatus({
);
}
// Main component for Network Panel
function NetworkPanelCont({ panelHeight }: { panelHeight: number }) {
const { player, store } = React.useContext(PlayerContext);
@ -433,11 +442,11 @@ export const NetworkPanelComp = observer(
// Heaviest operation here, will create a final merged network list
const processData = async () => {
const fetchUrlMap: Record<string, number[]> = {}
const fetchUrlMap: Record<string, number[]> = {};
const len = fetchList.length;
for (let i = 0; i < len; i++) {
const ft = fetchList[i] as any;
const key = `${ft.name}-${Math.round(ft.time / 10)}-${Math.round(ft.duration / 10)}`
const key = `${ft.name}-${Math.round(ft.time / 10)}-${Math.round(ft.duration / 10)}`;
if (fetchUrlMap[key]) {
fetchUrlMap[key].push(i);
}
@ -445,21 +454,23 @@ export const NetworkPanelComp = observer(
}
// We want to get resources that aren't in fetch list
const filteredResources = await processInChunks(resourceList, (chunk) => {
const clearChunk = [];
for (const res of chunk) {
const key = `${res.name}-${Math.floor(res.time / 10)}-${Math.floor(res.duration / 10)}`;
const possibleRequests = fetchUrlMap[key]
if (possibleRequests && possibleRequests.length) {
for (const i of possibleRequests) {
fetchList[i].timings = res.timings;
const filteredResources = await processInChunks(
resourceList,
(chunk) => {
const clearChunk = [];
for (const res of chunk) {
const key = `${res.name}-${Math.floor(res.time / 10)}-${Math.floor(res.duration / 10)}`;
const possibleRequests = fetchUrlMap[key];
if (possibleRequests && possibleRequests.length) {
for (const i of possibleRequests) {
fetchList[i].timings = res.timings;
}
fetchUrlMap[key] = [];
} else {
clearChunk.push(res);
}
fetchUrlMap[key] = [];
} else {
clearChunk.push(res);
}
}
return clearChunk;
return clearChunk;
},
// chunk.filter((res: any) => {
// const key = `${res.name}-${Math.floor(res.time / 100)}-${Math.floor(res.duration / 100)}`;
@ -484,8 +495,12 @@ export const NetworkPanelComp = observer(
filteredResources as Timed[],
fetchList,
processedSockets as Timed[],
{ enabled: Boolean(zoomEnabled), start: zoomStartTs ?? 0, end: zoomEndTs ?? 0 }
)
{
enabled: Boolean(zoomEnabled),
start: zoomStartTs ?? 0,
end: zoomEndTs ?? 0,
},
);
originalListRef.current = mergedList;
setTotalItems(mergedList.length);
@ -509,19 +524,21 @@ export const NetworkPanelComp = observer(
const calculateResourceStats = (resourceList: Record<string, any>) => {
setTimeout(() => {
let resourcesSize = 0
let transferredSize = 0
resourceList.forEach(({ decodedBodySize, headerSize, encodedBodySize }: any) => {
resourcesSize += decodedBodySize || 0
transferredSize += (headerSize || 0) + (encodedBodySize || 0)
})
let resourcesSize = 0;
let transferredSize = 0;
resourceList.forEach(
({ decodedBodySize, headerSize, encodedBodySize }: any) => {
resourcesSize += decodedBodySize || 0;
transferredSize += (headerSize || 0) + (encodedBodySize || 0);
},
);
setSummaryStats({
resourcesSize,
transferredSize,
});
}, 0);
}
};
useEffect(() => {
if (originalListRef.current.length === 0) return;
@ -530,27 +547,33 @@ export const NetworkPanelComp = observer(
let filteredItems: any[] = originalListRef.current;
filteredItems = await processInChunks(filteredItems, (chunk) =>
chunk.filter(
(it) => {
let valid = true;
if (showOnlyErrors) {
valid = parseInt(it.status) >= 400 || !it.success || it.error
}
if (filter) {
try {
const regex = new RegExp(filter, 'i');
valid = valid && regex.test(it.status) || regex.test(it.name) || regex.test(it.type) || regex.test(it.method);
} catch (e) {
valid = valid && String(it.status).includes(filter) || it.name.includes(filter) || it.type.includes(filter) || (it.method && it.method.includes(filter));
}
}
if (activeTab !== ALL) {
valid = valid && TYPE_TO_TAB[it.type] === activeTab;
chunk.filter((it) => {
let valid = true;
if (showOnlyErrors) {
valid = parseInt(it.status) >= 400 || !it.success || it.error;
}
if (filter) {
try {
const regex = new RegExp(filter, 'i');
valid =
(valid && regex.test(it.status)) ||
regex.test(it.name) ||
regex.test(it.type) ||
regex.test(it.method);
} catch (e) {
valid =
(valid && String(it.status).includes(filter)) ||
it.name.includes(filter) ||
it.type.includes(filter) ||
(it.method && it.method.includes(filter));
}
}
if (activeTab !== ALL) {
valid = valid && TYPE_TO_TAB[it.type] === activeTab;
}
return valid;
},
),
return valid;
}),
);
// Update displayed items
@ -587,7 +610,7 @@ export const NetworkPanelComp = observer(
};
const onFilterChange = ({ target: { value } }) => {
setInputFilterValue(value)
setInputFilterValue(value);
debouncedFilter(value);
};
@ -855,11 +878,11 @@ export const NetworkPanelComp = observer(
ref={loadingRef}
className="flex justify-center items-center text-xs text-gray-500"
>
<div className="flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-600 mr-2"></div>
Loading more data ({totalItems - displayedItems.length}{' '}
remaining)
</div>
<div className="flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-600 mr-2"></div>
Loading more data ({totalItems - displayedItems.length}{' '}
remaining)
</div>
</div>
)}
</div>

View file

@ -355,7 +355,7 @@ function RowRenderer({
);
}
const RowColumns = React.memo(({ columns, row }: any) => {
const RowColumns = ({ columns, row }: any) => {
const { t } = useTranslation();
return columns.map(({ dataKey, render, width, label }: any) => (
@ -371,6 +371,6 @@ const RowColumns = React.memo(({ columns, row }: any) => {
)}
</div>
));
});
};
export default observer(TimeTable);

View file

@ -13,6 +13,11 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
const _duration = parseInt(resource.duration);
const { t } = useTranslation();
const maxUrlLength = 800;
const displayUrl =
resource.url && resource.url.length > maxUrlLength
? `${resource.url.slice(0, maxUrlLength / 2)}......${resource.url.slice(-maxUrlLength / 2)}`
: resource.url;
return (
<div>
<div className="flex items-start py-1">
@ -22,7 +27,7 @@ function FetchBasicDetails({ resource, timestamp }: Props) {
bordered={false}
style={{ maxWidth: '300px' }}
>
<div>{resource.url}</div>
<div>{displayUrl}</div>
</Tag>
</div>

View file

@ -425,3 +425,7 @@ svg {
width: 1em;
margin-left: -1em;
}
.ant-tooltip {
max-width: 640px;
}