ui: improvements for kai file exports

This commit is contained in:
nick-delirium 2025-05-26 13:30:38 +02:00
parent 235364b968
commit be9ef3bd18
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
5 changed files with 120 additions and 24 deletions

View file

@ -10,11 +10,13 @@ import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import { useHistory, useLocation } from 'react-router-dom';
import ChatsModal from './components/ChatsModal';
import { kaiStore } from './KaiStore';
function KaiChat() {
const { userStore, projectsStore } = useStore();
const history = useHistory();
const [chatTitle, setTitle] = React.useState<string | null>(null);
const chatTitle = kaiStore.chatTitle;
const setTitle = kaiStore.setTitle;
const userId = userStore.account.id;
const userLetter = userStore.account.name[0].toUpperCase();
const { activeSiteId } = projectsStore;
@ -123,7 +125,7 @@ function KaiChat() {
threadId={threadId}
projectId={activeSiteId}
userLetter={userLetter}
onTitleChange={setTitle}
chatTitle={chatTitle}
initialMsg={initialMsg}
setInitialMsg={setInitialMsg}
/>

View file

@ -26,8 +26,8 @@ export default class KaiService extends AiService {
getKaiChat = async (
projectId: string,
threadId: string,
): Promise<
{
): Promise<{
messages: {
role: string;
content: string;
message_id: any;
@ -36,8 +36,9 @@ export default class KaiService extends AiService {
supports_visualization: boolean;
chart: string;
chart_data: string;
}[]
> => {
}[];
title: string;
}> => {
const r = await this.client.get(`/kai/${projectId}/chats/${threadId}`);
if (!r.ok) {
throw new Error('Failed to fetch chat');

View file

@ -29,6 +29,7 @@ class KaiStore {
processingStage: BotChunk | null = null;
messages: Array<Message> = [];
queryText = '';
chatTitle: string | null = null;
loadingChat = false;
replacing: string | null = null;
usage = {
@ -56,6 +57,20 @@ class KaiStore {
return { msg, index };
}
get firstHumanMessage() {
let msg = null;
let index = null;
for (let i = 0; i < this.messages.length; i++) {
const message = this.messages[i];
if (message.isUser) {
msg = message;
index = i;
break;
}
}
return { msg, index };
}
get lastKaiMessage() {
let msg = null;
let index = null;
@ -70,6 +85,14 @@ class KaiStore {
return { msg, index };
}
getPreviousMessage = (messageId: string) => {
const index = this.messages.findIndex((msg) => msg.messageId === messageId);
if (index > 0) {
return this.messages[index - 1];
}
return null;
};
setQueryText = (text: string) => {
this.queryText = text;
};
@ -113,13 +136,21 @@ class KaiStore {
});
};
setTitle = (title: string | null) => {
this.chatTitle = title;
};
getChat = async (projectId: string, threadId: string) => {
this.setLoadingChat(true);
try {
const res = await aiService.getKaiChat(projectId, threadId);
if (res && res.length) {
const { messages, title } = await aiService.getKaiChat(
projectId,
threadId,
);
if (messages && messages.length) {
this.setTitle(title);
this.setMessages(
res.map((m) => {
messages.map((m) => {
const isUser = m.role === 'human';
return {
text: m.content,
@ -144,7 +175,6 @@ class KaiStore {
createChatManager = (
settings: { projectId: string; threadId: string },
setTitle: (title: string) => void,
initialMsg: string | null,
) => {
const token = kaiService.client.getJwt();
@ -197,7 +227,7 @@ class KaiStore {
}
}
},
titleCallback: setTitle,
titleCallback: this.setTitle,
});
if (initialMsg) {
@ -315,6 +345,9 @@ class KaiStore {
});
try {
const filtersStr = await kaiService.getMsgChart(msgId, projectId);
if (!filtersStr.length) {
throw new Error('No filters found for the message');
}
const filters = JSON.parse(filtersStr);
const data = {
...filters,

View file

@ -9,16 +9,16 @@ function ChatLog({
projectId,
threadId,
userLetter,
onTitleChange,
initialMsg,
chatTitle,
setInitialMsg,
}: {
projectId: string;
threadId: any;
userLetter: string;
onTitleChange: (title: string | null) => void;
initialMsg: string | null;
setInitialMsg: (msg: string | null) => void;
chatTitle: string | null;
}) {
const messages = kaiStore.messages;
const loading = kaiStore.loadingChat;
@ -31,7 +31,7 @@ function ChatLog({
void kaiStore.getChat(settings.projectId, threadId);
}
if (threadId) {
kaiStore.createChatManager(settings, onTitleChange, initialMsg);
kaiStore.createChatManager(settings, initialMsg);
}
return () => {
kaiStore.clearChat();
@ -66,7 +66,12 @@ function ChatLog({
userName={userLetter}
siteId={projectId}
message={msg}
canEdit={processingStage === null && msg.isUser && index === lastHumanMsgInd}
chatTitle={chatTitle}
canEdit={
processingStage === null &&
msg.isUser &&
index === lastHumanMsgInd
}
/>
</React.Fragment>
))}

View file

@ -26,11 +26,13 @@ function ChatMsg({
siteId,
canEdit,
message,
chatTitle,
}: {
message: Message;
userName?: string;
canEdit?: boolean;
siteId: string;
chatTitle: string | null;
}) {
const { t } = useTranslation();
const [metric, setMetric] = React.useState<Widget | null>(null);
@ -47,6 +49,7 @@ function ChatMsg({
const isEditing = kaiStore.replacing && messageId === kaiStore.replacing;
const [isProcessing, setIsProcessing] = React.useState(false);
const bodyRef = React.useRef<HTMLDivElement>(null);
const chartRef = React.useRef<HTMLDivElement>(null);
const onEdit = () => {
kaiStore.editMessage(text, messageId);
};
@ -65,19 +68,68 @@ function ChatMsg({
setIsProcessing(false);
return;
}
const userPrompt = kaiStore.getPreviousMessage(message.messageId);
import('jspdf')
.then(({ jsPDF }) => {
.then(async ({ jsPDF }) => {
const doc = new jsPDF();
doc.addImage('/assets/img/logo-img.png', 80, 3, 30, 5);
doc.html(bodyRef.current!, {
const blockWidth = 170; // mm
doc.addImage('/assets/img/logo-img.png', 20, 15, 30, 5);
const content = bodyRef.current!.cloneNode(true) as HTMLElement;
if (userPrompt) {
const titleHeader = document.createElement('h2');
titleHeader.textContent = userPrompt.text;
titleHeader.style.marginBottom = '10px';
content.prepend(titleHeader);
}
content.querySelectorAll('ul').forEach((ul) => {
const frag = document.createDocumentFragment();
ul.querySelectorAll('li').forEach((li) => {
const div = document.createElement('div');
div.textContent = '• ' + li.textContent;
frag.appendChild(div);
});
ul.replaceWith(frag);
});
content.querySelectorAll('h1,h2,h3,h4,h5,h6').forEach((el) => {
(el as HTMLElement).style.letterSpacing = '0.5px';
});
content.querySelectorAll('*').forEach((node) => {
node.childNodes.forEach((child) => {
if (child.nodeType === Node.TEXT_NODE) {
const txt = child.textContent || '';
const replaced = txt.replace(/-/g, '');
if (replaced !== txt) child.textContent = replaced;
}
});
});
if (metric && chartRef.current) {
const { default: html2canvas } = await import('html2canvas');
const metricContainer = chartRef.current;
const image = await html2canvas(metricContainer, {
backgroundColor: null,
scale: 2,
});
const imgData = image.toDataURL('image/png');
const imgHeight = (image.height * blockWidth) / image.width;
content.appendChild(
Object.assign(document.createElement('img'), {
src: imgData,
style: `width: ${blockWidth}mm; height: ${imgHeight}mm; margin-top: 10px;`,
}),
);
}
doc.html(content, {
callback: function (doc) {
doc.save('document.pdf');
doc.save((chatTitle ?? 'document') + '.pdf');
},
margin: [10, 10, 10, 10],
// top, bottom, ?, left
margin: [5, 10, 20, 20],
x: 0,
y: 0,
width: 190, // Target width
windowWidth: 675, // Window width for rendering
y: 15,
// Target width
width: blockWidth,
// Window width for rendering
windowWidth: 675,
});
})
.catch((e) => {
@ -138,7 +190,10 @@ function ChatMsg({
<Markdown remarkPlugins={[remarkGfm]}>{text}</Markdown>
</div>
{metric ? (
<div className="p-2 border-gray-light rounded-lg shadow bg-glassWhite mb-2">
<div
ref={chartRef}
className="p-2 border-gray-light rounded-lg shadow bg-glassWhite mb-2"
>
<WidgetChart metric={metric} isPreview height={360} />
</div>
) : null}