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

View file

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

View file

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

View file

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

View file

@ -26,11 +26,13 @@ function ChatMsg({
siteId, siteId,
canEdit, canEdit,
message, message,
chatTitle,
}: { }: {
message: Message; message: Message;
userName?: string; userName?: string;
canEdit?: boolean; canEdit?: boolean;
siteId: string; siteId: string;
chatTitle: string | null;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [metric, setMetric] = React.useState<Widget | null>(null); const [metric, setMetric] = React.useState<Widget | null>(null);
@ -47,6 +49,7 @@ function ChatMsg({
const isEditing = kaiStore.replacing && messageId === kaiStore.replacing; const isEditing = kaiStore.replacing && messageId === kaiStore.replacing;
const [isProcessing, setIsProcessing] = React.useState(false); const [isProcessing, setIsProcessing] = React.useState(false);
const bodyRef = React.useRef<HTMLDivElement>(null); const bodyRef = React.useRef<HTMLDivElement>(null);
const chartRef = React.useRef<HTMLDivElement>(null);
const onEdit = () => { const onEdit = () => {
kaiStore.editMessage(text, messageId); kaiStore.editMessage(text, messageId);
}; };
@ -65,19 +68,68 @@ function ChatMsg({
setIsProcessing(false); setIsProcessing(false);
return; return;
} }
const userPrompt = kaiStore.getPreviousMessage(message.messageId);
import('jspdf') import('jspdf')
.then(({ jsPDF }) => { .then(async ({ jsPDF }) => {
const doc = new jsPDF(); const doc = new jsPDF();
doc.addImage('/assets/img/logo-img.png', 80, 3, 30, 5); const blockWidth = 170; // mm
doc.html(bodyRef.current!, { 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) { 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, x: 0,
y: 0, y: 15,
width: 190, // Target width // Target width
windowWidth: 675, // Window width for rendering width: blockWidth,
// Window width for rendering
windowWidth: 675,
}); });
}) })
.catch((e) => { .catch((e) => {
@ -138,7 +190,10 @@ function ChatMsg({
<Markdown remarkPlugins={[remarkGfm]}>{text}</Markdown> <Markdown remarkPlugins={[remarkGfm]}>{text}</Markdown>
</div> </div>
{metric ? ( {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} /> <WidgetChart metric={metric} isPreview height={360} />
</div> </div>
) : null} ) : null}