ui: improvements for kai file exports
This commit is contained in:
parent
235364b968
commit
be9ef3bd18
5 changed files with 120 additions and 24 deletions
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue