chore: add support for rendering Markdown with citations and LICENSE
This commit is contained in:
parent
24ed100f2c
commit
d4e7269fe4
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Zaid Mukaddam
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
126
app/page.tsx
126
app/page.tsx
@ -2,6 +2,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useRef, useCallback, useState, useEffect, ReactNode } from 'react';
|
import React, { useRef, useCallback, useState, useEffect, ReactNode } from 'react';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
import { useChat } from 'ai/react';
|
import { useChat } from 'ai/react';
|
||||||
import { ToolInvocation } from 'ai';
|
import { ToolInvocation } from 'ai';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@ -15,7 +17,8 @@ import {
|
|||||||
ChevronUp,
|
ChevronUp,
|
||||||
FastForward,
|
FastForward,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
ArrowRight
|
ArrowRight,
|
||||||
|
BookCheck
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
HoverCard,
|
HoverCard,
|
||||||
@ -117,14 +120,13 @@ export default function Home() {
|
|||||||
return (
|
return (
|
||||||
<HoverCard key={index}>
|
<HoverCard key={index}>
|
||||||
<HoverCardTrigger asChild>
|
<HoverCardTrigger asChild>
|
||||||
<span className="cursor-help text-blue-500 hover:underline">
|
<span className="cursor-help text-primary py-0.5 px-2 m-0 bg-secondary rounded-full">
|
||||||
{citationText}
|
{index + 1}
|
||||||
<sup>[{index + 1}]</sup>
|
|
||||||
</span>
|
</span>
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
<HoverCardContent className="flex items-center gap-2 p-2 max-w-xs bg-card text-card-foreground">
|
<HoverCardContent className="flex items-center gap-1 !p-0 !px-0.5 max-w-xs bg-card text-card-foreground !m-0 h-6 rounded-xl">
|
||||||
<img src={faviconUrl} alt="Favicon" className="w-4 h-4 flex-shrink-0" />
|
<img src={faviconUrl} alt="Favicon" className="w-4 h-4 flex-shrink-0 rounded-full" />
|
||||||
<a href={citationLink} target="_blank" rel="noopener noreferrer" className="text-sm text-blue-500 hover:underline truncate">
|
<a href={citationLink} target="_blank" rel="noopener noreferrer" className="text-sm text-primary no-underline truncate">
|
||||||
{citationLink}
|
{citationLink}
|
||||||
</a>
|
</a>
|
||||||
</HoverCardContent>
|
</HoverCardContent>
|
||||||
@ -132,78 +134,41 @@ export default function Home() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderMarkdown = (content: string) => {
|
const CitationComponent: React.FC<{ href: string; children: ReactNode; index: number }> = ({ href, children, index }) => {
|
||||||
const citationRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
const citationText = Array.isArray(children) ? children[0] : children;
|
||||||
const boldRegex = /\*\*(.*?)\*\*/g; // Bold
|
|
||||||
const italicRegex = /\*(.*?)\*/g; // Italic
|
|
||||||
const unorderedListRegex = /^-\s+(.*)$/gm; // Unordered list
|
|
||||||
const orderedListRegex = /^\d+\.\s+(.*)$/gm; // Ordered list
|
|
||||||
const headingRegex = /^(#{1,6})\s+(.*)$/gm; // Headings
|
|
||||||
const parts: (string | ReactNode)[] = [];
|
|
||||||
let lastIndex = 0;
|
|
||||||
let match;
|
|
||||||
|
|
||||||
// Replace bold and italic
|
return renderCitation(citationText as string, href, index);
|
||||||
content = content
|
|
||||||
.replace(boldRegex, '<strong>$1</strong>')
|
|
||||||
.replace(italicRegex, '<em>$1</em>');
|
|
||||||
|
|
||||||
// Replace unordered and ordered lists
|
|
||||||
content = content
|
|
||||||
.replace(unorderedListRegex, '<li class="list-disc ml-6">$1</li>')
|
|
||||||
.replace(orderedListRegex, '<li class="list-decimal ml-6">$1</li>');
|
|
||||||
|
|
||||||
// Replace headings
|
|
||||||
content = content.replace(headingRegex, (match, hashes, headingText) => {
|
|
||||||
const level = hashes.length; // Determine heading level
|
|
||||||
return `<h${level} class="text-${level === 1 ? '3xl' : level === 2 ? '2xl' : 'xl'} font-bold mb-1">${headingText}</h${level}>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add list wrapping
|
|
||||||
const wrappedContent = content.split(/(<li.*?<\/li>)/g).map((item, index) => {
|
|
||||||
if (item.startsWith('<li')) {
|
|
||||||
return `<ul>${item}</ul>`;
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
// Parse citations and add to parts
|
|
||||||
while ((match = citationRegex.exec(wrappedContent)) !== null) {
|
|
||||||
// Add text before the citation
|
|
||||||
if (match.index > lastIndex) {
|
|
||||||
parts.push(wrappedContent.slice(lastIndex, match.index));
|
|
||||||
}
|
|
||||||
|
|
||||||
const citationText = match[1];
|
|
||||||
const citationLink = match[2];
|
|
||||||
parts.push(renderCitation(citationText, citationLink, parts.length)); // Adjusting index for key
|
|
||||||
|
|
||||||
lastIndex = match.index + match[0].length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add any remaining text after the last citation
|
|
||||||
if (lastIndex < wrappedContent.length) {
|
|
||||||
parts.push(wrappedContent.slice(lastIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
{parts.map((part, index) => {
|
|
||||||
if (typeof part === 'string') {
|
|
||||||
const lines = part.split('\n');
|
|
||||||
return lines.map((line, lineIndex) => (
|
|
||||||
<React.Fragment key={`${index}-${lineIndex}`}>
|
|
||||||
<span dangerouslySetInnerHTML={{ __html: line }} />
|
|
||||||
{lineIndex < lines.length - 1 && <br />}
|
|
||||||
</React.Fragment>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return <React.Fragment key={index}>{part}</React.Fragment>; // Render citations
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MarkdownRenderer: React.FC<{ content: string }> = ({ content }) => {
|
||||||
|
const citationLinks = [...content.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)].map(([_, text, link]) => ({
|
||||||
|
text,
|
||||||
|
link,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
className="prose text-sm sm:text-base"
|
||||||
|
components={{
|
||||||
|
a: ({ href, children }) => {
|
||||||
|
const index = citationLinks.findIndex(link => link.link === href);
|
||||||
|
return index !== -1 ? (
|
||||||
|
<CitationComponent href={href as string} index={index} >
|
||||||
|
{children}
|
||||||
|
</CitationComponent>
|
||||||
|
) : (
|
||||||
|
<a href={href} target="_blank" rel="noopener noreferrer" className="text-blue-500 hover:underline">
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (bottomRef.current) {
|
if (bottomRef.current) {
|
||||||
@ -399,9 +364,14 @@ export default function Home() {
|
|||||||
{message.role === 'assistant' && message.content && (
|
{message.role === 'assistant' && message.content && (
|
||||||
<Card className="bg-card text-card-foreground border border-muted !mb-20 sm:!mb-16">
|
<Card className="bg-card text-card-foreground border border-muted !mb-20 sm:!mb-16">
|
||||||
<CardContent className="p-3 sm:p-4">
|
<CardContent className="p-3 sm:p-4">
|
||||||
<h2 className="text-lg sm:text-xl font-semibold mb-2">Answer</h2>
|
<div
|
||||||
|
className='flex items-center gap-2 mb-2'
|
||||||
|
>
|
||||||
|
<BookCheck className="size-4 sm:size-5 text-primary" />
|
||||||
|
<h2 className="text-lg sm:text-xl font-semibold">Answer</h2>
|
||||||
|
</div>
|
||||||
<div className="text-sm sm:text-base">
|
<div className="text-sm sm:text-base">
|
||||||
{renderMarkdown(message.content)}
|
<MarkdownRenderer content={message.content} />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -26,6 +26,8 @@
|
|||||||
"next": "14.2.5",
|
"next": "14.2.5",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
"react-markdown": "^9.0.1",
|
||||||
|
"remark-gfm": "^4.0.0",
|
||||||
"sonner": "^1.5.0",
|
"sonner": "^1.5.0",
|
||||||
"tailwind-merge": "^2.4.0",
|
"tailwind-merge": "^2.4.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
|||||||
856
pnpm-lock.yaml
856
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user