mirror of
https://github.com/ItzCrazyKns/Perplexica.git
synced 2025-12-03 10:18:15 +00:00
feat(app): handle new architecture
This commit is contained in:
197
src/components/AssistantSteps.tsx
Normal file
197
src/components/AssistantSteps.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
'use client';
|
||||
|
||||
import { Brain, Search, FileText, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ResearchBlock, ResearchBlockSubStep } from '@/lib/types';
|
||||
import { useChat } from '@/lib/hooks/useChat';
|
||||
|
||||
const getStepIcon = (step: ResearchBlockSubStep) => {
|
||||
if (step.type === 'reasoning') {
|
||||
return <Brain className="w-4 h-4" />;
|
||||
} else if (step.type === 'searching') {
|
||||
return <Search className="w-4 h-4" />;
|
||||
} else if (step.type === 'reading') {
|
||||
return <FileText className="w-4 h-4" />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getStepTitle = (
|
||||
step: ResearchBlockSubStep,
|
||||
isStreaming: boolean,
|
||||
): string => {
|
||||
if (step.type === 'reasoning') {
|
||||
return isStreaming && !step.reasoning ? 'Thinking...' : 'Thinking';
|
||||
} else if (step.type === 'searching') {
|
||||
return `Searching ${step.searching.length} ${step.searching.length === 1 ? 'query' : 'queries'}`;
|
||||
} else if (step.type === 'reading') {
|
||||
return `Found ${step.reading.length} ${step.reading.length === 1 ? 'result' : 'results'}`;
|
||||
}
|
||||
return 'Processing';
|
||||
};
|
||||
|
||||
const AssistantSteps = ({
|
||||
block,
|
||||
status,
|
||||
}: {
|
||||
block: ResearchBlock;
|
||||
status: 'answering' | 'completed' | 'error';
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const { researchEnded, loading } = useChat();
|
||||
|
||||
useEffect(() => {
|
||||
if (researchEnded) {
|
||||
setIsExpanded(false);
|
||||
} else if (status === 'answering') {
|
||||
setIsExpanded(true);
|
||||
}
|
||||
}, [researchEnded, status]);
|
||||
|
||||
if (!block || block.data.subSteps.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 overflow-hidden">
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="w-full flex items-center justify-between p-3 hover:bg-light-200 dark:hover:bg-dark-200 transition duration-200"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Brain className="w-4 h-4 text-black dark:text-white" />
|
||||
<span className="text-sm font-medium text-black dark:text-white">
|
||||
Research Progress ({block.data.subSteps.length}{' '}
|
||||
{block.data.subSteps.length === 1 ? 'step' : 'steps'})
|
||||
</span>
|
||||
</div>
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="w-4 h-4 text-black/70 dark:text-white/70" />
|
||||
) : (
|
||||
<ChevronDown className="w-4 h-4 text-black/70 dark:text-white/70" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{isExpanded && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="border-t border-light-200 dark:border-dark-200"
|
||||
>
|
||||
<div className="p-3 space-y-2">
|
||||
{block.data.subSteps.map((step, index) => {
|
||||
const isLastStep = index === block.data.subSteps.length - 1;
|
||||
const isStreaming = loading && isLastStep && !researchEnded;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={step.id}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.2, delay: 0 }}
|
||||
className="flex gap-3"
|
||||
>
|
||||
{/* Timeline connector */}
|
||||
<div className="flex flex-col items-center pt-0.5">
|
||||
<div
|
||||
className={`rounded-full p-1.5 bg-light-100 dark:bg-dark-100 text-black/70 dark:text-white/70 ${isStreaming ? 'animate-pulse' : ''}`}
|
||||
>
|
||||
{getStepIcon(step)}
|
||||
</div>
|
||||
{index < block.data.subSteps.length - 1 && (
|
||||
<div className="w-0.5 flex-1 min-h-[20px] bg-light-200 dark:bg-dark-200 mt-1.5" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Step content */}
|
||||
<div className="flex-1 pb-1">
|
||||
<span className="text-sm font-medium text-black dark:text-white">
|
||||
{getStepTitle(step, isStreaming)}
|
||||
</span>
|
||||
|
||||
{step.type === 'reasoning' && (
|
||||
<>
|
||||
{step.reasoning && (
|
||||
<p className="text-xs text-black/70 dark:text-white/70 mt-0.5">
|
||||
{step.reasoning}
|
||||
</p>
|
||||
)}
|
||||
{isStreaming && !step.reasoning && (
|
||||
<div className="flex items-center gap-1.5 mt-0.5">
|
||||
<div
|
||||
className="w-1.5 h-1.5 bg-black/40 dark:bg-white/40 rounded-full animate-bounce"
|
||||
style={{ animationDelay: '0ms' }}
|
||||
/>
|
||||
<div
|
||||
className="w-1.5 h-1.5 bg-black/40 dark:bg-white/40 rounded-full animate-bounce"
|
||||
style={{ animationDelay: '150ms' }}
|
||||
/>
|
||||
<div
|
||||
className="w-1.5 h-1.5 bg-black/40 dark:bg-white/40 rounded-full animate-bounce"
|
||||
style={{ animationDelay: '300ms' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{step.type === 'searching' &&
|
||||
step.searching.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 mt-1.5">
|
||||
{step.searching.map((query, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium bg-light-100 dark:bg-dark-100 text-black/70 dark:text-white/70 border border-light-200 dark:border-dark-200"
|
||||
>
|
||||
{query}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step.type === 'reading' && step.reading.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 mt-1.5">
|
||||
{step.reading.slice(0, 4).map((result, idx) => {
|
||||
const url = result.metadata.url || '';
|
||||
const title = result.metadata.title || 'Untitled';
|
||||
const domain = url ? new URL(url).hostname : '';
|
||||
const faviconUrl = domain
|
||||
? `https://s2.googleusercontent.com/s2/favicons?domain=${domain}&sz=128`
|
||||
: '';
|
||||
|
||||
return (
|
||||
<span
|
||||
key={idx}
|
||||
className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md text-xs font-medium bg-light-100 dark:bg-dark-100 text-black/70 dark:text-white/70 border border-light-200 dark:border-dark-200"
|
||||
>
|
||||
{faviconUrl && (
|
||||
<img
|
||||
src={faviconUrl}
|
||||
alt=""
|
||||
className="w-3 h-3 rounded-sm flex-shrink-0"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className="line-clamp-1">{title}</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssistantSteps;
|
||||
@@ -1,14 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { Document } from '@langchain/core/documents';
|
||||
import Navbar from './Navbar';
|
||||
import Chat from './Chat';
|
||||
import EmptyChat from './EmptyChat';
|
||||
import { Settings } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import NextError from 'next/error';
|
||||
import { useChat } from '@/lib/hooks/useChat';
|
||||
import SettingsButtonMobile from './Settings/SettingsButtonMobile';
|
||||
import { Block, Chunk } from '@/lib/types';
|
||||
|
||||
export interface BaseMessage {
|
||||
chatId: string;
|
||||
@@ -16,20 +14,27 @@ export interface BaseMessage {
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface Message extends BaseMessage {
|
||||
backendId: string;
|
||||
query: string;
|
||||
responseBlocks: Block[];
|
||||
status: 'answering' | 'completed' | 'error';
|
||||
}
|
||||
|
||||
export interface UserMessage extends BaseMessage {
|
||||
role: 'user';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface AssistantMessage extends BaseMessage {
|
||||
role: 'assistant';
|
||||
content: string;
|
||||
suggestions?: string[];
|
||||
}
|
||||
|
||||
export interface UserMessage extends BaseMessage {
|
||||
role: 'user';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface SourceMessage extends BaseMessage {
|
||||
role: 'source';
|
||||
sources: Document[];
|
||||
sources: Chunk[];
|
||||
}
|
||||
|
||||
export interface SuggestionMessage extends BaseMessage {
|
||||
@@ -37,11 +42,12 @@ export interface SuggestionMessage extends BaseMessage {
|
||||
suggestions: string[];
|
||||
}
|
||||
|
||||
export type Message =
|
||||
export type LegacyMessage =
|
||||
| AssistantMessage
|
||||
| UserMessage
|
||||
| SourceMessage
|
||||
| SuggestionMessage;
|
||||
|
||||
export type ChatTurn = UserMessage | AssistantMessage;
|
||||
|
||||
export interface File {
|
||||
@@ -50,6 +56,11 @@ export interface File {
|
||||
fileId: string;
|
||||
}
|
||||
|
||||
export interface Widget {
|
||||
widgetType: string;
|
||||
params: Record<string, any>;
|
||||
}
|
||||
|
||||
const ChatWindow = () => {
|
||||
const { hasError, notFound, messages } = useChat();
|
||||
if (hasError) {
|
||||
|
||||
@@ -15,7 +15,14 @@ const Copy = ({
|
||||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
const contentToCopy = `${initialMessage}${section?.sourceMessage?.sources && section.sourceMessage.sources.length > 0 && `\n\nCitations:\n${section.sourceMessage.sources?.map((source: any, i: any) => `[${i + 1}] ${source.metadata.url}`).join(`\n`)}`}`;
|
||||
const contentToCopy = `${initialMessage}${
|
||||
section?.message.responseBlocks.filter((b) => b.type === 'source')
|
||||
?.length > 0 &&
|
||||
`\n\nCitations:\n${section.message.responseBlocks
|
||||
.filter((b) => b.type === 'source')
|
||||
?.map((source: any, i: any) => `[${i + 1}] ${source.metadata.url}`)
|
||||
.join(`\n`)}`
|
||||
}`;
|
||||
navigator.clipboard.writeText(contentToCopy);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1000);
|
||||
|
||||
@@ -22,6 +22,9 @@ import { useSpeech } from 'react-text-to-speech';
|
||||
import ThinkBox from './ThinkBox';
|
||||
import { useChat, Section } from '@/lib/hooks/useChat';
|
||||
import Citation from './Citation';
|
||||
import AssistantSteps from './AssistantSteps';
|
||||
import { ResearchBlock } from '@/lib/types';
|
||||
import Renderer from './Widgets/Renderer';
|
||||
|
||||
const ThinkTagProcessor = ({
|
||||
children,
|
||||
@@ -46,12 +49,21 @@ const MessageBox = ({
|
||||
dividerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||
isLast: boolean;
|
||||
}) => {
|
||||
const { loading, chatTurns, sendMessage, rewrite } = useChat();
|
||||
const { loading, sendMessage, rewrite, messages, researchEnded } = useChat();
|
||||
|
||||
const parsedMessage = section.parsedAssistantMessage || '';
|
||||
const parsedMessage = section.parsedTextBlocks.join('\n\n');
|
||||
const speechMessage = section.speechMessage || '';
|
||||
const thinkingEnded = section.thinkingEnded;
|
||||
|
||||
const sourceBlocks = section.message.responseBlocks.filter(
|
||||
(block): block is typeof block & { type: 'source' } =>
|
||||
block.type === 'source',
|
||||
);
|
||||
|
||||
const sources = sourceBlocks.flatMap((block) => block.data);
|
||||
|
||||
const hasContent = section.parsedTextBlocks.length > 0;
|
||||
|
||||
const { speechStatus, start, stop } = useSpeech({ text: speechMessage });
|
||||
|
||||
const markdownOverrides: MarkdownToJSX.Options = {
|
||||
@@ -72,7 +84,7 @@ const MessageBox = ({
|
||||
<div className="space-y-6">
|
||||
<div className={'w-full pt-8 break-words'}>
|
||||
<h2 className="text-black dark:text-white font-medium text-3xl lg:w-9/12">
|
||||
{section.userMessage.content}
|
||||
{section.message.query}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -81,21 +93,50 @@ const MessageBox = ({
|
||||
ref={dividerRef}
|
||||
className="flex flex-col space-y-6 w-full lg:w-9/12"
|
||||
>
|
||||
{section.sourceMessage &&
|
||||
section.sourceMessage.sources.length > 0 && (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<BookCopy className="text-black dark:text-white" size={20} />
|
||||
<h3 className="text-black dark:text-white font-medium text-xl">
|
||||
Sources
|
||||
</h3>
|
||||
</div>
|
||||
<MessageSources sources={section.sourceMessage.sources} />
|
||||
{sources.length > 0 && (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<BookCopy className="text-black dark:text-white" size={20} />
|
||||
<h3 className="text-black dark:text-white font-medium text-xl">
|
||||
Sources
|
||||
</h3>
|
||||
</div>
|
||||
<MessageSources sources={sources} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{section.message.responseBlocks
|
||||
.filter(
|
||||
(block): block is ResearchBlock =>
|
||||
block.type === 'research' && block.data.subSteps.length > 0,
|
||||
)
|
||||
.map((researchBlock) => (
|
||||
<div key={researchBlock.id} className="flex flex-col space-y-2">
|
||||
<AssistantSteps
|
||||
block={researchBlock}
|
||||
status={section.message.status}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{section.widgets.length > 0 && <Renderer widgets={section.widgets} />}
|
||||
|
||||
{isLast &&
|
||||
loading &&
|
||||
!researchEnded &&
|
||||
!section.message.responseBlocks.some(
|
||||
(b) => b.type === 'research' && b.data.subSteps.length > 0,
|
||||
) && (
|
||||
<div className="flex items-center gap-2 p-3 rounded-lg bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200">
|
||||
<Disc3 className="w-4 h-4 text-black dark:text-white animate-spin" />
|
||||
<span className="text-sm text-black/70 dark:text-white/70">
|
||||
Brainstorming...
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col space-y-2">
|
||||
{section.sourceMessage && (
|
||||
{sources.length > 0 && (
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<Disc3
|
||||
className={cn(
|
||||
@@ -110,7 +151,7 @@ const MessageBox = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{section.assistantMessage && (
|
||||
{hasContent && (
|
||||
<>
|
||||
<Markdown
|
||||
className={cn(
|
||||
@@ -127,14 +168,11 @@ const MessageBox = ({
|
||||
<div className="flex flex-row items-center -ml-2">
|
||||
<Rewrite
|
||||
rewrite={rewrite}
|
||||
messageId={section.assistantMessage.messageId}
|
||||
messageId={section.message.messageId}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row items-center -mr-2">
|
||||
<Copy
|
||||
initialMessage={section.assistantMessage.content}
|
||||
section={section}
|
||||
/>
|
||||
<Copy initialMessage={parsedMessage} section={section} />
|
||||
<button
|
||||
onClick={() => {
|
||||
if (speechStatus === 'started') {
|
||||
@@ -158,7 +196,7 @@ const MessageBox = ({
|
||||
{isLast &&
|
||||
section.suggestions &&
|
||||
section.suggestions.length > 0 &&
|
||||
section.assistantMessage &&
|
||||
hasContent &&
|
||||
!loading && (
|
||||
<div className="mt-6">
|
||||
<div className="flex flex-row items-center space-x-2 mb-4">
|
||||
@@ -206,17 +244,17 @@ const MessageBox = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{section.assistantMessage && (
|
||||
{hasContent && (
|
||||
<div className="lg:sticky lg:top-20 flex flex-col items-center space-y-3 w-full lg:w-3/12 z-30 h-full pb-4">
|
||||
<SearchImages
|
||||
query={section.userMessage.content}
|
||||
chatHistory={chatTurns}
|
||||
messageId={section.assistantMessage.messageId}
|
||||
query={section.message.query}
|
||||
chatHistory={messages}
|
||||
messageId={section.message.messageId}
|
||||
/>
|
||||
<SearchVideos
|
||||
chatHistory={chatTurns}
|
||||
query={section.userMessage.content}
|
||||
messageId={section.assistantMessage.messageId}
|
||||
chatHistory={messages}
|
||||
query={section.message.query}
|
||||
messageId={section.message.messageId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
Transition,
|
||||
TransitionChild,
|
||||
} from '@headlessui/react';
|
||||
import { Document } from '@langchain/core/documents';
|
||||
import { File } from 'lucide-react';
|
||||
import { Fragment, useState } from 'react';
|
||||
import { Chunk } from '@/lib/types';
|
||||
|
||||
const MessageSources = ({ sources }: { sources: Document[] }) => {
|
||||
const MessageSources = ({ sources }: { sources: Chunk[] }) => {
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
const closeModal = () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@headlessui/react';
|
||||
import jsPDF from 'jspdf';
|
||||
import { useChat, Section } from '@/lib/hooks/useChat';
|
||||
import { SourceBlock } from '@/lib/types';
|
||||
|
||||
const downloadFile = (filename: string, content: string, type: string) => {
|
||||
const blob = new Blob([content], { type });
|
||||
@@ -28,35 +29,41 @@ const downloadFile = (filename: string, content: string, type: string) => {
|
||||
|
||||
const exportAsMarkdown = (sections: Section[], title: string) => {
|
||||
const date = new Date(
|
||||
sections[0]?.userMessage?.createdAt || Date.now(),
|
||||
sections[0].message.createdAt || Date.now(),
|
||||
).toLocaleString();
|
||||
let md = `# 💬 Chat Export: ${title}\n\n`;
|
||||
md += `*Exported on: ${date}*\n\n---\n`;
|
||||
|
||||
sections.forEach((section, idx) => {
|
||||
if (section.userMessage) {
|
||||
md += `\n---\n`;
|
||||
md += `**🧑 User**
|
||||
md += `\n---\n`;
|
||||
md += `**🧑 User**
|
||||
`;
|
||||
md += `*${new Date(section.userMessage.createdAt).toLocaleString()}*\n\n`;
|
||||
md += `> ${section.userMessage.content.replace(/\n/g, '\n> ')}\n`;
|
||||
}
|
||||
md += `*${new Date(section.message.createdAt).toLocaleString()}*\n\n`;
|
||||
md += `> ${section.message.query.replace(/\n/g, '\n> ')}\n`;
|
||||
|
||||
if (section.assistantMessage) {
|
||||
if (section.message.responseBlocks.length > 0) {
|
||||
md += `\n---\n`;
|
||||
md += `**🤖 Assistant**
|
||||
`;
|
||||
md += `*${new Date(section.assistantMessage.createdAt).toLocaleString()}*\n\n`;
|
||||
md += `> ${section.assistantMessage.content.replace(/\n/g, '\n> ')}\n`;
|
||||
md += `*${new Date(section.message.createdAt).toLocaleString()}*\n\n`;
|
||||
md += `> ${section.message.responseBlocks
|
||||
.filter((b) => b.type === 'text')
|
||||
.map((block) => block.data)
|
||||
.join('\n')
|
||||
.replace(/\n/g, '\n> ')}\n`;
|
||||
}
|
||||
|
||||
const sourceResponseBlock = section.message.responseBlocks.find(
|
||||
(block) => block.type === 'source',
|
||||
) as SourceBlock | undefined;
|
||||
|
||||
if (
|
||||
section.sourceMessage &&
|
||||
section.sourceMessage.sources &&
|
||||
section.sourceMessage.sources.length > 0
|
||||
sourceResponseBlock &&
|
||||
sourceResponseBlock.data &&
|
||||
sourceResponseBlock.data.length > 0
|
||||
) {
|
||||
md += `\n**Citations:**\n`;
|
||||
section.sourceMessage.sources.forEach((src: any, i: number) => {
|
||||
sourceResponseBlock.data.forEach((src: any, i: number) => {
|
||||
const url = src.metadata?.url || '';
|
||||
md += `- [${i + 1}] [${url}](${url})\n`;
|
||||
});
|
||||
@@ -69,7 +76,7 @@ const exportAsMarkdown = (sections: Section[], title: string) => {
|
||||
const exportAsPDF = (sections: Section[], title: string) => {
|
||||
const doc = new jsPDF();
|
||||
const date = new Date(
|
||||
sections[0]?.userMessage?.createdAt || Date.now(),
|
||||
sections[0]?.message?.createdAt || Date.now(),
|
||||
).toLocaleString();
|
||||
let y = 15;
|
||||
const pageHeight = doc.internal.pageSize.height;
|
||||
@@ -86,44 +93,38 @@ const exportAsPDF = (sections: Section[], title: string) => {
|
||||
doc.setTextColor(30);
|
||||
|
||||
sections.forEach((section, idx) => {
|
||||
if (section.userMessage) {
|
||||
if (y > pageHeight - 30) {
|
||||
doc.addPage();
|
||||
y = 15;
|
||||
}
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('User', 10, y);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(120);
|
||||
doc.text(
|
||||
`${new Date(section.userMessage.createdAt).toLocaleString()}`,
|
||||
40,
|
||||
y,
|
||||
);
|
||||
y += 6;
|
||||
doc.setTextColor(30);
|
||||
doc.setFontSize(12);
|
||||
const userLines = doc.splitTextToSize(section.userMessage.content, 180);
|
||||
for (let i = 0; i < userLines.length; i++) {
|
||||
if (y > pageHeight - 20) {
|
||||
doc.addPage();
|
||||
y = 15;
|
||||
}
|
||||
doc.text(userLines[i], 12, y);
|
||||
y += 6;
|
||||
}
|
||||
y += 6;
|
||||
doc.setDrawColor(230);
|
||||
if (y > pageHeight - 10) {
|
||||
doc.addPage();
|
||||
y = 15;
|
||||
}
|
||||
doc.line(10, y, 200, y);
|
||||
y += 4;
|
||||
if (y > pageHeight - 30) {
|
||||
doc.addPage();
|
||||
y = 15;
|
||||
}
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('User', 10, y);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(120);
|
||||
doc.text(`${new Date(section.message.createdAt).toLocaleString()}`, 40, y);
|
||||
y += 6;
|
||||
doc.setTextColor(30);
|
||||
doc.setFontSize(12);
|
||||
const userLines = doc.splitTextToSize(section.message.query, 180);
|
||||
for (let i = 0; i < userLines.length; i++) {
|
||||
if (y > pageHeight - 20) {
|
||||
doc.addPage();
|
||||
y = 15;
|
||||
}
|
||||
doc.text(userLines[i], 12, y);
|
||||
y += 6;
|
||||
}
|
||||
y += 6;
|
||||
doc.setDrawColor(230);
|
||||
if (y > pageHeight - 10) {
|
||||
doc.addPage();
|
||||
y = 15;
|
||||
}
|
||||
doc.line(10, y, 200, y);
|
||||
y += 4;
|
||||
|
||||
if (section.assistantMessage) {
|
||||
if (section.message.responseBlocks.length > 0) {
|
||||
if (y > pageHeight - 30) {
|
||||
doc.addPage();
|
||||
y = 15;
|
||||
@@ -134,7 +135,7 @@ const exportAsPDF = (sections: Section[], title: string) => {
|
||||
doc.setFontSize(10);
|
||||
doc.setTextColor(120);
|
||||
doc.text(
|
||||
`${new Date(section.assistantMessage.createdAt).toLocaleString()}`,
|
||||
`${new Date(section.message.createdAt).toLocaleString()}`,
|
||||
40,
|
||||
y,
|
||||
);
|
||||
@@ -142,7 +143,7 @@ const exportAsPDF = (sections: Section[], title: string) => {
|
||||
doc.setTextColor(30);
|
||||
doc.setFontSize(12);
|
||||
const assistantLines = doc.splitTextToSize(
|
||||
section.assistantMessage.content,
|
||||
section.parsedTextBlocks.join('\n'),
|
||||
180,
|
||||
);
|
||||
for (let i = 0; i < assistantLines.length; i++) {
|
||||
@@ -154,10 +155,14 @@ const exportAsPDF = (sections: Section[], title: string) => {
|
||||
y += 6;
|
||||
}
|
||||
|
||||
const sourceResponseBlock = section.message.responseBlocks.find(
|
||||
(block) => block.type === 'source',
|
||||
) as SourceBlock | undefined;
|
||||
|
||||
if (
|
||||
section.sourceMessage &&
|
||||
section.sourceMessage.sources &&
|
||||
section.sourceMessage.sources.length > 0
|
||||
sourceResponseBlock &&
|
||||
sourceResponseBlock.data &&
|
||||
sourceResponseBlock.data.length > 0
|
||||
) {
|
||||
doc.setFontSize(11);
|
||||
doc.setTextColor(80);
|
||||
@@ -167,7 +172,7 @@ const exportAsPDF = (sections: Section[], title: string) => {
|
||||
}
|
||||
doc.text('Citations:', 12, y);
|
||||
y += 5;
|
||||
section.sourceMessage.sources.forEach((src: any, i: number) => {
|
||||
sourceResponseBlock.data.forEach((src: any, i: number) => {
|
||||
const url = src.metadata?.url || '';
|
||||
if (y > pageHeight - 15) {
|
||||
doc.addPage();
|
||||
@@ -198,15 +203,15 @@ const Navbar = () => {
|
||||
const { sections, chatId } = useChat();
|
||||
|
||||
useEffect(() => {
|
||||
if (sections.length > 0 && sections[0].userMessage) {
|
||||
if (sections.length > 0 && sections[0].message) {
|
||||
const newTitle =
|
||||
sections[0].userMessage.content.length > 20
|
||||
? `${sections[0].userMessage.content.substring(0, 20).trim()}...`
|
||||
: sections[0].userMessage.content;
|
||||
sections[0].message.query.substring(0, 30) + '...' ||
|
||||
'New Conversation';
|
||||
|
||||
setTitle(newTitle);
|
||||
const newTimeAgo = formatTimeDifference(
|
||||
new Date(),
|
||||
sections[0].userMessage.createdAt,
|
||||
sections[0].message.createdAt,
|
||||
);
|
||||
setTimeAgo(newTimeAgo);
|
||||
}
|
||||
@@ -214,10 +219,10 @@ const Navbar = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(() => {
|
||||
if (sections.length > 0 && sections[0].userMessage) {
|
||||
if (sections.length > 0 && sections[0].message) {
|
||||
const newTimeAgo = formatTimeDifference(
|
||||
new Date(),
|
||||
sections[0].userMessage.createdAt,
|
||||
sections[0].message.createdAt,
|
||||
);
|
||||
setTimeAgo(newTimeAgo);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user