Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions frontend/src/components/Chat/FlashCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,33 @@ import type { FlashCard } from "../../lib/api";
type Props = {
items?: FlashCard[];
onAdd: (item: { kind: "flashcard" | "note"; title: string; content: string }) => void;
onToggleCollapse?: () => void;
};

export default function FlashCards({ items = [], onAdd }: Props) {
export default function FlashCards({ items = [], onAdd, onToggleCollapse }: Props) {
return (
<div className="hidden lg:block">
<div className="hidden lg:block w-full">
<div className="sticky top-6 h-[calc(100vh-8rem)] flex flex-col">
<div className="mb-5">
<div className="mb-4">
<div className="rounded-2xl bg-stone-950/80 border border-zinc-900 px-4 py-3 flex items-center justify-between">
<h3 className="text-stone-100 font-semibold tracking-wide">Important Topics</h3>
<span className="text-xs text-stone-400">{items.length}</span>
<div className="flex items-center gap-2">
<h3 className="text-stone-100 font-semibold tracking-wide text-sm">Important Topics</h3>
<span className="text-[11px] font-medium text-stone-400 bg-stone-900 px-2 py-0.5 rounded-full border border-zinc-800">
{items.length}
</span>
</div>
{onToggleCollapse && (
<button
onClick={onToggleCollapse}
className="p-1 rounded-lg text-stone-400 hover:text-stone-100 hover:bg-stone-900 transition-colors"
title="Collapse sidebar"
aria-label="Collapse topics sidebar"
>
<svg className="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 6 6 18M6 6l12 12" />
</svg>
</button>
)}
</div>
</div>

Expand Down
18 changes: 9 additions & 9 deletions frontend/src/components/Chat/MarkdownView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@ import rehypeHighlight from "rehype-highlight";
type Props = { md: string };

const components: Components = {
h1: (p) => <h1 className="text-3xl lg:text-4xl font-bold tracking-tight mt-2 mb-4 text-stone-100" {...p} />,
h2: (p) => <h2 className="text-2xl lg:text-3xl font-semibold tracking-tight mt-6 mb-3 text-stone-100" {...p} />,
h3: (p) => <h3 className="text-xl lg:text-2xl font-semibold tracking-tight mt-5 mb-2 text-stone-100" {...p} />,
p: (p) => <p className="my-3 text-stone-200" {...p} />,
h1: (p) => <h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold tracking-tight mt-2 mb-4 text-stone-100 break-words" {...p} />,
h2: (p) => <h2 className="text-xl sm:text-2xl lg:text-3xl font-semibold tracking-tight mt-6 mb-3 text-stone-100 break-words" {...p} />,
h3: (p) => <h3 className="text-lg sm:text-xl lg:text-2xl font-semibold tracking-tight mt-5 mb-2 text-stone-100 break-words" {...p} />,
p: (p) => <p className="my-3 text-stone-200 break-words" {...p} />,
ul: (p) => <ul className="my-3 list-disc pl-6 space-y-1 text-stone-200" {...p} />,
ol: (p) => <ol className="my-3 list-decimal pl-6 space-y-1 text-stone-200" {...p} />,
li: (p) => <li className="my-1" {...p} />,
li: (p) => <li className="my-1 break-words" {...p} />,
strong: (p) => <strong className="font-semibold text-stone-100" {...p} />,
hr: (p) => <hr className="my-6 border-zinc-800" {...p} />,
blockquote: (p) => (
<blockquote className="my-4 border-l-4 border-zinc-700 pl-4 italic text-stone-300" {...p} />
),
table: (p) => (
<div className="my-4 overflow-x-auto">
<div className="my-4 overflow-x-auto max-w-full">
<table className="min-w-full text-left border-collapse text-stone-200" {...p} />
</div>
),
Expand All @@ -31,13 +31,13 @@ const components: Components = {
code: ({ inline, className, children, ...rest }: any) => {
if (inline) {
return (
<code className="px-1.5 py-0.5 rounded bg-stone-800 text-stone-100" {...rest}>
<code className="px-1.5 py-0.5 rounded bg-stone-800 text-stone-100 text-xs sm:text-sm break-all" {...rest}>
{children}
</code>
);
}
return (
<pre className="border border-zinc-800 rounded-lg p-3 overflow-x-auto">
<pre className="border border-zinc-800 rounded-lg p-3 overflow-x-auto max-w-full text-xs sm:text-sm bg-stone-900/50 my-3">
<code className={className} {...rest}>
{children}
</code>
Expand All @@ -48,7 +48,7 @@ const components: Components = {

export default function MarkdownView({ md }: Props) {
return (
<div className="prose prose-invert max-w-3xl leading-relaxed text-stone-200">
<div className="prose prose-invert max-w-none leading-relaxed text-stone-200 w-full overflow-hidden">
<ReactMarkdown
skipHtml
remarkPlugins={[remarkGfm, remarkBreaks, remarkMath]}
Expand Down
110 changes: 89 additions & 21 deletions frontend/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,36 +16,103 @@ export default function Sidebar() {
const [isToggled, setIsToggled] = useState(false)
const [chats, setChats] = useState<ChatsResponse | null>(null);

// Fetch chats only when the drawer is opened to avoid redundant requests on mount
useEffect(() => {
getChats().then(data => setChats(data));
}, []);
if (isToggled) {
getChats().then(data => setChats(data));
}
}, [isToggled]);
Comment on lines 20 to +24

// Dismiss drawer on Escape key press
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isToggled) {
setIsToggled(false);
}
};
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [isToggled]);

const b = (t: string) => `p-2 rounded-xl duration-300 transition-all ${p === t ? 'text-white bg-stone-800' : 'hover:bg-stone-900 hover:text-stone-200'}`
const chatButtonClass = `p-2 rounded-xl duration-300 transition-all ${isToggled ? 'text-white bg-stone-800' : 'hover:bg-stone-900 hover:text-stone-200'}`
function toggleC() {
const idk = document.getElementById("idk")
idk?.classList.toggle("flex");
idk?.classList.toggle("hidden");
document.getElementById("idk0")?.classList.toggle("md:rounded-r-none")
setIsToggled(!isToggled)
}

return (
<aside className='left-0 bottom-0 w-screen md:w-fit md:h-screen fixed p-4 z-50 lg:flex'>
<div id='idk' className='w-64 md:order-2 h-full z-40 rounded-l-none rounded-2xl bg-stone-950 border border-l-transparent border-stone-900 hidden flex-col p-4 space-y-3 overflow-y-auto custom-scroll'>
{chats?.chats.map((chat) => (
<Link key={chat.id} to={`/chat/?chatId=${chat.id}`} className='p-2 hover:text-stone-200 hover:bg-stone-900 w-full rounded-xl block text-sm min-h-fit truncate'>
{chat.title || 'Untitled Chat'}
</Link>
))}
</div>
<div id='idk0' className='md:w-20 md:order-1 h-full rounded-3xl bg-stone-950/50 backdrop-blur-xl border border-stone-900 text-stone-400 flex flex-col items-center justify-between py-4'>
<img src='/logo.png' alt='logo' className='w-10 h-auto rounded-full hidden md:block' />
<>
{/* Click-outside backdrop to prevent permanent chat obstruction */}
{isToggled && (
<div
className="fixed inset-0 bg-black/40 backdrop-blur-sm z-30 transition-opacity"
onClick={() => setIsToggled(false)}
Comment on lines +44 to +46
aria-hidden="true"
/>
)}

<aside className='left-0 bottom-0 w-screen md:w-fit md:h-screen fixed p-4 z-50 lg:flex'>
<div
id='idk'
className={`w-72 md:order-2 h-full z-40 rounded-l-none rounded-2xl bg-stone-950 border border-l-transparent border-stone-900 ${
isToggled ? 'flex' : 'hidden'
} flex-col p-4 space-y-3 overflow-y-auto custom-scroll shadow-2xl transition-all duration-200`}
>
{/* Drawer header with explicit close toggle */}
<div className="flex items-center justify-between pb-3 border-b border-stone-800">
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-stone-200 tracking-wide uppercase">Chat History</span>
{chats?.chats && (
<span className="text-[10px] bg-stone-900 text-stone-400 px-1.5 py-0.5 rounded-full border border-stone-800">
{chats.chats.length}
</span>
)}
</div>
<button
onClick={() => setIsToggled(false)}
className="p-1 rounded-lg text-stone-400 hover:text-stone-100 hover:bg-stone-900 transition-colors"
title="Close history (Esc)"
aria-label="Close chat history"
>
<svg className="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 6 6 18M6 6l12 12" />
</svg>
</button>
</div>

{/* List of chats, closing drawer upon navigation */}
<div className="flex-1 overflow-y-auto space-y-1">
{chats?.chats?.length ? (
chats.chats.map((chat) => (
<Link
key={chat.id}
to={`/chat/?chatId=${chat.id}`}
onClick={() => setIsToggled(false)}
className='p-2.5 hover:text-stone-100 hover:bg-stone-900/90 rounded-xl block text-sm truncate border border-transparent hover:border-stone-800 transition-colors'
>
{chat.title || 'Untitled Chat'}
</Link>
))
) : (
<p className="text-xs text-stone-500 text-center py-6">No previous chats</p>
)}
</div>
Comment thread
recabasic marked this conversation as resolved.
Outdated
</div>

<div
id='idk0'
className={`md:w-20 md:order-1 h-full rounded-3xl ${
isToggled ? 'md:rounded-r-none' : ''
} bg-stone-950/50 backdrop-blur-xl border border-stone-900 text-stone-400 flex flex-col items-center justify-between py-4 z-40`}
>
<img src='/logo.png' alt='logo' className='w-10 h-auto rounded-full hidden md:block' />
<nav className='flex md:flex-col items-center space-x-4 md:space-x-0 md:space-y-2 my-auto'>
<Link to='/' className={b('/')}>
<svg className="size-6" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M6.29367 4.96556C3.62685 6.90311 2.29344 7.87189 1.76974 9.30291C1.72773 9.41771 1.68994 9.534 1.65645 9.65157C1.23901 11.1171 1.74832 12.6846 2.76696 15.8197C3.78559 18.9547 4.2949 20.5222 5.49405 21.4625C5.59025 21.5379 5.68918 21.6098 5.79064 21.678C7.05546 22.5279 8.70364 22.5279 12 22.5279C15.2964 22.5279 16.9446 22.5279 18.2094 21.678C18.3108 21.6098 18.4098 21.5379 18.506 21.4625C19.7051 20.5222 20.2144 18.9547 21.2331 15.8197C22.2517 12.6846 22.761 11.1171 22.3436 9.65157C22.3101 9.534 22.2723 9.41771 22.2303 9.30291C21.7066 7.87189 20.3732 6.90312 17.7064 4.96557C15.0395 3.02801 13.7061 2.05923 12.1833 2.00336C12.0611 1.99888 11.9389 1.99888 11.8167 2.00336C10.2939 2.05923 8.96048 3.02801 6.29367 4.96556ZM10 17.0697C9.58579 17.0697 9.25 17.4054 9.25 17.8197C9.25 18.2339 9.58579 18.5697 10 18.5697H14C14.4142 18.5697 14.75 18.2339 14.75 17.8197C14.75 17.4054 14.4142 17.0697 14 17.0697H10Z" fill="currentColor" /></svg>
</Link>
<button onClick={() => toggleC()} className={chatButtonClass}>
<button
onClick={() => setIsToggled(prev => !prev)}
className={chatButtonClass}
aria-label="Toggle chat history"
title="Toggle chat history"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 1.25C6.06294 1.25 1.25 6.06294 1.25 12C1.25 17.9371 6.06294 22.75 12 22.75C17.9371 22.75 22.75 17.9371 22.75 12C22.75 6.06294 17.9371 1.25 12 1.25ZM12.75 8C12.75 7.58579 12.4142 7.25 12 7.25C11.5858 7.25 11.25 7.58579 11.25 8L11.25 11.5C11.25 12.7426 12.2574 13.75 13.5 13.75H15C15.4142 13.75 15.75 13.4142 15.75 13C15.75 12.5858 15.4142 12.25 15 12.25H13.5C13.0858 12.25 12.75 11.9142 12.75 11.5L12.75 8Z" fill="currentColor" /></svg>
</button>
<Link to='/tools' className={b('/tools')}>
Expand Down Expand Up @@ -83,5 +150,6 @@ export default function Sidebar() {
</nav>
</div>
</aside>
)
</>
);
}
38 changes: 32 additions & 6 deletions frontend/src/pages/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export default function Chat() {
const [connecting, setConnecting] = useState<boolean>(!!(initialChatId || initialQuestion));
const [awaitingAnswer, setAwaitingAnswer] = useState<boolean>(false);
const [topic, setTopic] = useState<string>("");
const [showTopics, setShowTopics] = useState(true);
const { setDocument } = useCompanion();

const selPopupRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -294,17 +295,36 @@ export default function Chat() {

return (
<div className="flex flex-col min-h-screen w-full px-4 lg:pl-28 lg:pr-4">
<div className="grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-8 mt-20 lg:mt-6 mb-16">
<div className="flex-1 pr-6">
<div className="w-full max-w-5xl mx-auto p-4 pt-2 pb-28">
{!showTopics && (
<button
onClick={() => setShowTopics(true)}
className="fixed top-5 right-6 z-20 hidden lg:flex items-center gap-2 px-3 py-1.5 rounded-xl bg-stone-950/90 border border-zinc-800 text-stone-400 hover:text-stone-100 hover:bg-stone-900 text-xs font-medium shadow-lg backdrop-blur transition-colors"
title="Show Important Topics"
aria-label="Show Important Topics"
>
<svg className="size-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M15 3v18" />
</svg>
<span>Topics ({cards.length})</span>
</button>
)}

<div
className={`grid grid-cols-1 ${
showTopics ? "xl:grid-cols-[1fr_320px] lg:grid-cols-[1fr_280px]" : ""
} gap-6 mt-20 lg:mt-6 mb-16`}
>
<div className="flex-1 min-w-0 pr-0 lg:pr-2">
<div className="w-full max-w-4xl mx-auto p-2 sm:p-4 pt-2 pb-28">
<div className="space-y-6">
{list.map((m, i) => {
const userBubble = "inline-block max-w-[85%] bg-stone-900/70 border border-zinc-800 rounded-2xl px-4 py-3";
if (m.role === "assistant") {
return (
<div key={i} className="w-full flex justify-start">
<div className="w-full mx-auto rounded-3xl bg-stone-950/90 border border-zinc-900 shadow-[0_10px_30px_rgba(0,0,0,0.45)] ring-1 ring-black/10 backdrop-blur px-6 md:px-8 py-6 md:py-8 max-w-[min(100%,1000px)]">
<div className="animate-[fadeIn_300ms_ease-out] leading-7 md:leading-8">
<div className="w-full mx-auto rounded-3xl bg-stone-950/90 border border-zinc-900 shadow-[0_10px_30px_rgba(0,0,0,0.45)] ring-1 ring-black/10 backdrop-blur px-5 sm:px-8 py-5 sm:py-8 max-w-full">
<div className="animate-[fadeIn_300ms_ease-out] leading-7 md:leading-8 overflow-x-auto">
<MarkdownView md={m.content} />
</div>
</div>
Expand Down Expand Up @@ -350,7 +370,13 @@ export default function Chat() {
</div>
</div>

<FlashCards items={cards} onAdd={({ kind, title, content }) => addToBag(kind, title, content)} />
{showTopics && (
<FlashCards
items={cards}
onAdd={({ kind, title, content }) => addToBag(kind, title, content)}
onToggleCollapse={() => setShowTopics(false)}
/>
)}
</div>

<SelectionPopup
Expand Down
Loading