diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index bf0bd1d..9e09fed 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -2,13 +2,12 @@ import { Fragment, useEffect, useRef, useState } from 'react'; import MessageInput from './MessageInput'; -import { File, Message } from './ChatWindow'; import MessageBox from './MessageBox'; import MessageBoxLoading from './MessageBoxLoading'; import { useChat } from '@/lib/hooks/useChat'; const Chat = () => { - const { messages, loading, messageAppeared } = useChat(); + const { sections, chatTurns, loading, messageAppeared } = useChat(); const [dividerWidth, setDividerWidth] = useState(0); const dividerRef = useRef(null); @@ -35,30 +34,29 @@ const Chat = () => { messageEnd.current?.scrollIntoView({ behavior: 'smooth' }); }; - if (messages.length === 1) { - document.title = `${messages[0].content.substring(0, 30)} - Perplexica`; + if (chatTurns.length === 1) { + document.title = `${chatTurns[0].content.substring(0, 30)} - Perplexica`; } - if (messages[messages.length - 1]?.role == 'user') { + if (chatTurns[chatTurns.length - 1]?.role === 'user') { scroll(); } - }, [messages]); + }, [chatTurns]); return (
- {messages.map((msg, i) => { - const isLast = i === messages.length - 1; + {sections.map((section, i) => { + const isLast = i === sections.length - 1; return ( - + - {!isLast && msg.role === 'assistant' && ( + {!isLast && (
)} diff --git a/src/components/ChatWindow.tsx b/src/components/ChatWindow.tsx index 0d40c83..2078512 100644 --- a/src/components/ChatWindow.tsx +++ b/src/components/ChatWindow.tsx @@ -9,15 +9,39 @@ import Link from 'next/link'; import NextError from 'next/error'; import { useChat } from '@/lib/hooks/useChat'; -export type Message = { - messageId: string; +export interface BaseMessage { chatId: string; + messageId: string; createdAt: Date; +} + +export interface AssistantMessage extends BaseMessage { + role: 'assistant'; content: string; - role: 'user' | 'assistant'; suggestions?: string[]; - sources?: Document[]; -}; +} + +export interface UserMessage extends BaseMessage { + role: 'user'; + content: string; +} + +export interface SourceMessage extends BaseMessage { + role: 'source'; + sources: Document[]; +} + +export interface SuggestionMessage extends BaseMessage { + role: 'suggestion'; + suggestions: string[]; +} + +export type Message = + | AssistantMessage + | UserMessage + | SourceMessage + | SuggestionMessage; +export type ChatTurn = UserMessage | AssistantMessage; export interface File { fileName: string; diff --git a/src/components/MessageActions/Copy.tsx b/src/components/MessageActions/Copy.tsx index cb07b3e..eb48e30 100644 --- a/src/components/MessageActions/Copy.tsx +++ b/src/components/MessageActions/Copy.tsx @@ -1,12 +1,13 @@ import { Check, ClipboardList } from 'lucide-react'; import { Message } from '../ChatWindow'; import { useState } from 'react'; +import { Section } from '@/lib/hooks/useChat'; const Copy = ({ - message, + section, initialMessage, }: { - message: Message; + section: Section; initialMessage: string; }) => { const [copied, setCopied] = useState(false); @@ -14,7 +15,7 @@ const Copy = ({ return ( */} - -
-
- - -
-
- )} - {isLast && - message.suggestions && - message.suggestions.length > 0 && - message.role === 'assistant' && - !loading && ( - <> -
-
-
- -

Related

-
-
- {message.suggestions.map((suggestion, i) => ( -
-
-
{ - sendMessage(suggestion); - }} - className="cursor-pointer flex flex-row justify-between font-medium space-x-2 items-center" - > -

- {suggestion} -

- -
-
- ))} -
+ {section.assistantMessage && ( + <> + + {parsedMessage} + + + {loading && isLast ? null : ( +
+
+
- +
+ + +
+
)} -
-
-
- - + + {isLast && + section.suggestions && + section.suggestions.length > 0 && + section.assistantMessage && + !loading && ( + <> +
+
+
+ +

Related

+
+
+ {section.suggestions.map( + (suggestion: string, i: number) => ( +
+
+
{ + sendMessage(suggestion); + }} + className="cursor-pointer flex flex-row justify-between font-medium space-x-2 items-center" + > +

+ {suggestion} +

+ +
+
+ ), + )} +
+
+ + )} + + )}
- )} + + {section.assistantMessage && ( +
+ + +
+ )} +
); }; diff --git a/src/components/MessageInputActions/Attach.tsx b/src/components/MessageInputActions/Attach.tsx index 42722a2..5004e4d 100644 --- a/src/components/MessageInputActions/Attach.tsx +++ b/src/components/MessageInputActions/Attach.tsx @@ -133,7 +133,10 @@ const Attach = ({ showText }: { showText?: boolean }) => { className="flex flex-row items-center justify-start w-full space-x-3 p-3" >
- +

{file.fileName.length > 25 diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index b0e8ea6..9886384 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -10,7 +10,7 @@ import { Transition, } from '@headlessui/react'; import jsPDF from 'jspdf'; -import { useChat } from '@/lib/hooks/useChat'; +import { useChat, Section } from '@/lib/hooks/useChat'; const downloadFile = (filename: string, content: string, type: string) => { const blob = new Blob([content], { type }); @@ -26,19 +26,37 @@ const downloadFile = (filename: string, content: string, type: string) => { }, 0); }; -const exportAsMarkdown = (messages: Message[], title: string) => { - const date = new Date(messages[0]?.createdAt || Date.now()).toLocaleString(); +const exportAsMarkdown = (sections: Section[], title: string) => { + const date = new Date( + sections[0]?.userMessage?.createdAt || Date.now(), + ).toLocaleString(); let md = `# 💬 Chat Export: ${title}\n\n`; md += `*Exported on: ${date}*\n\n---\n`; - messages.forEach((msg, idx) => { - md += `\n---\n`; - md += `**${msg.role === 'user' ? '🧑 User' : '🤖 Assistant'}** + + sections.forEach((section, idx) => { + if (section.userMessage) { + md += `\n---\n`; + md += `**🧑 User** `; - md += `*${new Date(msg.createdAt).toLocaleString()}*\n\n`; - md += `> ${msg.content.replace(/\n/g, '\n> ')}\n`; - if (msg.sources && msg.sources.length > 0) { + md += `*${new Date(section.userMessage.createdAt).toLocaleString()}*\n\n`; + md += `> ${section.userMessage.content.replace(/\n/g, '\n> ')}\n`; + } + + if (section.assistantMessage) { + md += `\n---\n`; + md += `**🤖 Assistant** +`; + md += `*${new Date(section.assistantMessage.createdAt).toLocaleString()}*\n\n`; + md += `> ${section.assistantMessage.content.replace(/\n/g, '\n> ')}\n`; + } + + if ( + section.sourceMessage && + section.sourceMessage.sources && + section.sourceMessage.sources.length > 0 + ) { md += `\n**Citations:**\n`; - msg.sources.forEach((src: any, i: number) => { + section.sourceMessage.sources.forEach((src: any, i: number) => { const url = src.metadata?.url || ''; md += `- [${i + 1}] [${url}](${url})\n`; }); @@ -48,9 +66,11 @@ const exportAsMarkdown = (messages: Message[], title: string) => { downloadFile(`${title || 'chat'}.md`, md, 'text/markdown'); }; -const exportAsPDF = (messages: Message[], title: string) => { +const exportAsPDF = (sections: Section[], title: string) => { const doc = new jsPDF(); - const date = new Date(messages[0]?.createdAt || Date.now()).toLocaleString(); + const date = new Date( + sections[0]?.userMessage?.createdAt || Date.now(), + ).toLocaleString(); let y = 15; const pageHeight = doc.internal.pageSize.height; doc.setFontSize(18); @@ -64,57 +84,109 @@ const exportAsPDF = (messages: Message[], title: string) => { doc.line(10, y, 200, y); y += 6; doc.setTextColor(30); - messages.forEach((msg, idx) => { - if (y > pageHeight - 30) { - doc.addPage(); - y = 15; - } - doc.setFont('helvetica', 'bold'); - doc.text(`${msg.role === 'user' ? 'User' : 'Assistant'}`, 10, y); - doc.setFont('helvetica', 'normal'); - doc.setFontSize(10); - doc.setTextColor(120); - doc.text(`${new Date(msg.createdAt).toLocaleString()}`, 40, y); - y += 6; - doc.setTextColor(30); - doc.setFontSize(12); - const lines = doc.splitTextToSize(msg.content, 180); - for (let i = 0; i < lines.length; i++) { - if (y > pageHeight - 20) { + + sections.forEach((section, idx) => { + if (section.userMessage) { + if (y > pageHeight - 30) { doc.addPage(); y = 15; } - doc.text(lines[i], 12, y); + 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; - } - if (msg.sources && msg.sources.length > 0) { - doc.setFontSize(11); - doc.setTextColor(80); - if (y > pageHeight - 20) { - doc.addPage(); - y = 15; - } - doc.text('Citations:', 12, y); - y += 5; - msg.sources.forEach((src: any, i: number) => { - const url = src.metadata?.url || ''; - if (y > pageHeight - 15) { + 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(`- [${i + 1}] ${url}`, 15, y); - y += 5; - }); + 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 (y > pageHeight - 30) { + doc.addPage(); + y = 15; + } + doc.setFont('helvetica', 'bold'); + doc.text('Assistant', 10, y); + doc.setFont('helvetica', 'normal'); + doc.setFontSize(10); + doc.setTextColor(120); + doc.text( + `${new Date(section.assistantMessage.createdAt).toLocaleString()}`, + 40, + y, + ); + y += 6; doc.setTextColor(30); + doc.setFontSize(12); + const assistantLines = doc.splitTextToSize( + section.assistantMessage.content, + 180, + ); + for (let i = 0; i < assistantLines.length; i++) { + if (y > pageHeight - 20) { + doc.addPage(); + y = 15; + } + doc.text(assistantLines[i], 12, y); + y += 6; + } + + if ( + section.sourceMessage && + section.sourceMessage.sources && + section.sourceMessage.sources.length > 0 + ) { + doc.setFontSize(11); + doc.setTextColor(80); + if (y > pageHeight - 20) { + doc.addPage(); + y = 15; + } + doc.text('Citations:', 12, y); + y += 5; + section.sourceMessage.sources.forEach((src: any, i: number) => { + const url = src.metadata?.url || ''; + if (y > pageHeight - 15) { + doc.addPage(); + y = 15; + } + doc.text(`- [${i + 1}] ${url}`, 15, y); + y += 5; + }); + doc.setTextColor(30); + } + y += 6; + doc.setDrawColor(230); + if (y > pageHeight - 10) { + doc.addPage(); + y = 15; + } + doc.line(10, y, 200, y); + y += 4; } - y += 6; - doc.setDrawColor(230); - if (y > pageHeight - 10) { - doc.addPage(); - y = 15; - } - doc.line(10, y, 200, y); - y += 4; }); doc.save(`${title || 'chat'}.pdf`); }; @@ -123,29 +195,29 @@ const Navbar = () => { const [title, setTitle] = useState(''); const [timeAgo, setTimeAgo] = useState(''); - const { messages, chatId } = useChat(); + const { sections, chatId } = useChat(); useEffect(() => { - if (messages.length > 0) { + if (sections.length > 0 && sections[0].userMessage) { const newTitle = - messages[0].content.length > 20 - ? `${messages[0].content.substring(0, 20).trim()}...` - : messages[0].content; + sections[0].userMessage.content.length > 20 + ? `${sections[0].userMessage.content.substring(0, 20).trim()}...` + : sections[0].userMessage.content; setTitle(newTitle); const newTimeAgo = formatTimeDifference( new Date(), - messages[0].createdAt, + sections[0].userMessage.createdAt, ); setTimeAgo(newTimeAgo); } - }, [messages]); + }, [sections]); useEffect(() => { const intervalId = setInterval(() => { - if (messages.length > 0) { + if (sections.length > 0 && sections[0].userMessage) { const newTimeAgo = formatTimeDifference( new Date(), - messages[0].createdAt, + sections[0].userMessage.createdAt, ); setTimeAgo(newTimeAgo); } @@ -187,14 +259,14 @@ const Navbar = () => {