From be9ef3bd1828cd68595218d7de81158a55aa70d8 Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Mon, 26 May 2025 13:30:38 +0200 Subject: [PATCH] ui: improvements for kai file exports --- frontend/app/components/Kai/KaiChat.tsx | 6 +- frontend/app/components/Kai/KaiService.ts | 9 ++- frontend/app/components/Kai/KaiStore.ts | 43 +++++++++-- .../app/components/Kai/components/ChatLog.tsx | 13 +++- .../app/components/Kai/components/ChatMsg.tsx | 73 ++++++++++++++++--- 5 files changed, 120 insertions(+), 24 deletions(-) diff --git a/frontend/app/components/Kai/KaiChat.tsx b/frontend/app/components/Kai/KaiChat.tsx index 17bb4c820..35fb8f257 100644 --- a/frontend/app/components/Kai/KaiChat.tsx +++ b/frontend/app/components/Kai/KaiChat.tsx @@ -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(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} /> diff --git a/frontend/app/components/Kai/KaiService.ts b/frontend/app/components/Kai/KaiService.ts index 116bf4dc0..447ce1779 100644 --- a/frontend/app/components/Kai/KaiService.ts +++ b/frontend/app/components/Kai/KaiService.ts @@ -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'); diff --git a/frontend/app/components/Kai/KaiStore.ts b/frontend/app/components/Kai/KaiStore.ts index 895aeefe5..fb8a1736a 100644 --- a/frontend/app/components/Kai/KaiStore.ts +++ b/frontend/app/components/Kai/KaiStore.ts @@ -29,6 +29,7 @@ class KaiStore { processingStage: BotChunk | null = null; messages: Array = []; 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, diff --git a/frontend/app/components/Kai/components/ChatLog.tsx b/frontend/app/components/Kai/components/ChatLog.tsx index 9b015ad22..ecb4405b4 100644 --- a/frontend/app/components/Kai/components/ChatLog.tsx +++ b/frontend/app/components/Kai/components/ChatLog.tsx @@ -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 + } /> ))} diff --git a/frontend/app/components/Kai/components/ChatMsg.tsx b/frontend/app/components/Kai/components/ChatMsg.tsx index 4c3d97b8f..a1fbd7522 100644 --- a/frontend/app/components/Kai/components/ChatMsg.tsx +++ b/frontend/app/components/Kai/components/ChatMsg.tsx @@ -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(null); @@ -47,6 +49,7 @@ function ChatMsg({ const isEditing = kaiStore.replacing && messageId === kaiStore.replacing; const [isProcessing, setIsProcessing] = React.useState(false); const bodyRef = React.useRef(null); + const chartRef = React.useRef(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({ {text} {metric ? ( -
+
) : null}