Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 5 additions & 1 deletion client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ask, uploadDocs, listDocs, deleteDoc, Document, SessionMetadata } from
import { Toaster } from '@/components/ui/toast';
import { useToast } from '@/hooks/useToast';
import { useSession, Message } from './hooks/useSession';
import { useTheme } from './hooks/useTheme';
import { DocumentList } from './components/documents/DocumentList';
import { ChatArea } from './components/chat/ChatArea';
import { ChatList } from './components/chat/ChatList';
Expand All @@ -22,6 +23,7 @@ export default function App() {
const [previewDocument, setPreviewDocument] = useState<Document | null>(null);
const [mobilePanel, setMobilePanel] = useState<MobilePanel>(null);
const { toasts, toast, dismiss } = useToast();
const { theme, toggleTheme } = useTheme();
const closePreview = useCallback(() => setPreviewDocument(null), []);
const closeMobilePanel = useCallback(() => setMobilePanel(null), []);

Expand Down Expand Up @@ -120,7 +122,7 @@ export default function App() {
};

return (
<div className="flex h-[100dvh] overflow-hidden bg-gray-50">
<div className="flex h-[100dvh] overflow-hidden bg-gray-50 dark:bg-gray-950">
<DocumentList {...documentListProps} className="hidden lg:flex" />

<ChatArea
Expand All @@ -133,6 +135,8 @@ export default function App() {
onNewChat={createNewSession}
onOpenDocs={() => setMobilePanel('docs')}
onOpenChats={() => setMobilePanel('chats')}
theme={theme}
onToggleTheme={toggleTheme}
/>

<ChatList
Expand Down
28 changes: 22 additions & 6 deletions client/src/components/chat/ChatArea.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FileText, MessageSquare, MessageSquarePlus } from 'lucide-react';
import { FileText, MessageSquare, MessageSquarePlus, Moon, Sun } from 'lucide-react';
import { MessageList } from './MessageList';
import { ChatInput } from './ChatInput';
import { Message } from '../../hooks/useSession';
Expand All @@ -13,6 +13,8 @@ interface ChatAreaProps {
onNewChat: () => void;
onOpenDocs?: () => void;
onOpenChats?: () => void;
theme: 'light' | 'dark';
onToggleTheme: () => void;
}

export function ChatArea({
Expand All @@ -25,39 +27,53 @@ export function ChatArea({
onNewChat,
onOpenDocs,
onOpenChats,
theme,
onToggleTheme,
}: ChatAreaProps) {
return (
<div className="flex min-h-0 flex-1 flex-col min-w-0 w-full">
<div className="h-14 shrink-0 border-b border-gray-200 bg-white flex items-center gap-2 px-3 sm:px-6">
<div className="h-14 shrink-0 border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 flex items-center gap-2 px-3 sm:px-6">
{onOpenDocs ? (
<button
type="button"
onClick={onOpenDocs}
className="lg:hidden rounded-lg p-2 text-gray-600 hover:bg-gray-100 hover:text-gray-800 transition"
className="lg:hidden rounded-lg p-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-800 dark:hover:text-gray-100 transition"
aria-label="Open documents"
>
<FileText size={20} />
</button>
) : null}

<h1 className="flex-1 truncate text-lg sm:text-xl font-bold text-gray-800">RTFM For Me</h1>
<h1 className="flex-1 truncate text-lg sm:text-xl font-bold text-gray-800 dark:text-gray-100">
RTFM For Me
</h1>

<div className="flex items-center gap-1 sm:gap-2 shrink-0">
{onOpenChats ? (
<button
type="button"
onClick={onOpenChats}
className="lg:hidden rounded-lg p-2 text-gray-600 hover:bg-gray-100 hover:text-gray-800 transition"
className="lg:hidden rounded-lg p-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-800 dark:hover:text-gray-100 transition"
aria-label="Open chats"
>
<MessageSquare size={20} />
</button>
) : null}

<button
type="button"
onClick={onToggleTheme}
className="rounded-lg p-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-800 dark:hover:text-gray-100 transition"
title="Toggle theme"
aria-label="Toggle theme"
>
{theme === 'dark' ? <Sun size={18} /> : <Moon size={18} />}
</button>

<button
type="button"
onClick={onNewChat}
className="flex items-center gap-2 rounded-lg px-2 sm:px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-800 transition"
className="flex items-center gap-2 rounded-lg px-2 sm:px-3 py-2 text-sm text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-800 dark:hover:text-gray-100 transition"
title="Start new conversation"
>
<MessageSquarePlus size={18} />
Expand Down
10 changes: 6 additions & 4 deletions client/src/components/chat/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,17 @@ export function ChatInput({ input, loading, onInputChange, onSend }: ChatInputPr
};

return (
<div className="border-t border-gray-200 bg-white p-4 pb-[max(1rem,env(safe-area-inset-bottom))]">
<div className="border-t border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-4 pb-[max(1rem,env(safe-area-inset-bottom))]">
<div className="max-w-3xl mx-auto">
<div className="relative flex items-end border border-gray-300 rounded-xl focus-within:ring-2 focus-within:ring-blue-500 bg-white">
<div className="relative flex items-end border border-gray-300 dark:border-gray-700 rounded-xl focus-within:ring-2 focus-within:ring-blue-500 bg-white dark:bg-gray-900">
<textarea
ref={textareaRef}
value={input}
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder="Ask a question..."
rows={1}
className="flex-1 resize-none px-4 py-3 bg-transparent focus:outline-none text-gray-800 placeholder-gray-400"
className="flex-1 resize-none px-4 py-3 bg-transparent focus:outline-none text-gray-800 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500"
style={{ maxHeight: '200px' }}
disabled={loading}
/>
Expand All @@ -58,7 +58,9 @@ export function ChatInput({ input, loading, onInputChange, onSend }: ChatInputPr
<Send size={18} />
</button>
</div>
<p className="text-xs text-gray-400 mt-1 ml-1">Enter to send · Shift+Enter for new line</p>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1 ml-1">
Enter to send · Shift+Enter for new line
</p>
</div>
</div>
);
Expand Down
29 changes: 18 additions & 11 deletions client/src/components/chat/ChatList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,48 +22,55 @@ export function ChatList({
return (
<div
className={cn(
'w-64 shrink-0 bg-white border-l border-gray-200 flex flex-col h-full',
'w-64 shrink-0 bg-white dark:bg-gray-900 border-l border-gray-200 dark:border-gray-800 flex flex-col h-full',
className,
)}
>
{showHeader ? (
<div className="hidden lg:block p-4 border-b border-gray-200">
<h2 className="font-semibold text-gray-700">Chats</h2>
<div className="hidden lg:block p-4 border-b border-gray-200 dark:border-gray-800">
<h2 className="font-semibold text-gray-700 dark:text-gray-200">Chats</h2>
</div>
) : null}

<div className="flex-1 overflow-y-auto">
{sessions.length === 0 ? (
<p className="text-gray-400 text-sm text-center p-4">No chats yet</p>
<p className="text-gray-400 dark:text-gray-500 text-sm text-center p-4">No chats yet</p>
) : (
<ul className="p-2 space-y-1">
{sessions.map((session) => (
<li
key={session.id}
className={`group p-3 rounded-lg cursor-pointer transition-colors ${
activeSessionId === session.id
? 'bg-blue-50 border border-blue-200'
: 'hover:bg-gray-50'
? 'bg-blue-50 dark:bg-blue-950/50 border border-blue-200 dark:border-blue-800'
: 'hover:bg-gray-50 dark:hover:bg-gray-800'
}`}
onClick={() => onSwitch(session)}
>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<MessageSquare size={16} className="text-gray-400 flex-shrink-0" />
<h3 className="text-sm font-medium text-gray-900 truncate">
<MessageSquare
size={16}
className="text-gray-400 dark:text-gray-500 flex-shrink-0"
/>
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
{session.title}
</h3>
</div>
<p className="text-xs text-gray-500 mt-1 truncate">{session.lastMessage}</p>
<p className="text-xs text-gray-400 mt-1">{session.messageCount} messages</p>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate">
{session.lastMessage}
</p>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
{session.messageCount} messages
</p>
</div>
<button
onClick={(e) => {
e.stopPropagation();
onDelete(session.id);
}}
className="opacity-100 lg:opacity-0 lg:group-hover:opacity-100 text-gray-400 hover:text-red-500 transition-opacity flex-shrink-0 ml-2 p-1 -m-1"
className="opacity-100 lg:opacity-0 lg:group-hover:opacity-100 text-gray-400 dark:text-gray-500 hover:text-red-500 transition-opacity flex-shrink-0 ml-2 p-1 -m-1"
>
<Trash2 size={14} />
</button>
Expand Down
11 changes: 7 additions & 4 deletions client/src/components/chat/MessageBubble.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ export function MessageBubble({ role, content, sources }: MessageBubbleProps) {
<div
className={`p-4 rounded-lg ${
role === 'user'
? 'bg-[#edf3fe] text-gray-800 ml-auto max-w-[85%] sm:max-w-md'
: 'bg-white border border-gray-200 max-w-full sm:max-w-2xl'
? 'bg-[#edf3fe] dark:bg-blue-950 text-gray-800 dark:text-blue-50 ml-auto max-w-[85%] sm:max-w-md'
: 'bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 max-w-full sm:max-w-2xl'
}`}
>
{role === 'user' ? (
<p className="whitespace-pre-wrap">{content}</p>
) : (
<div className="prose prose-sm max-w-none prose-p:my-1 prose-headings:my-2 prose-ul:my-1 prose-ol:my-1 prose-pre:my-2 prose-pre:bg-gray-50 prose-pre:text-gray-800 prose-pre:border prose-pre:border-gray-200 prose-pre:rounded-lg prose-pre:shadow-none prose-pre:[&_code]:bg-transparent prose-pre:[&_code]:p-0 prose-code:text-blue-600 prose-code:bg-blue-50 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none">
<div className="prose prose-sm dark:prose-invert max-w-none prose-p:my-1 prose-headings:my-2 prose-ul:my-1 prose-ol:my-1 prose-pre:my-2 prose-pre:bg-gray-50 dark:prose-pre:bg-gray-800 prose-pre:text-gray-800 dark:prose-pre:text-gray-100 prose-pre:border prose-pre:border-gray-200 dark:prose-pre:border-gray-700 prose-pre:rounded-lg prose-pre:shadow-none prose-pre:[&_code]:bg-transparent prose-pre:[&_code]:p-0 prose-code:text-blue-600 dark:prose-code:text-blue-300 prose-code:bg-blue-50 dark:prose-code:bg-blue-950 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
</div>
)}
Expand All @@ -30,7 +30,10 @@ export function MessageBubble({ role, content, sources }: MessageBubbleProps) {
{sources && sources.length > 0 ? (
<div className="mt-2 flex flex-wrap gap-2">
{sources.map((src, j) => (
<span key={j} className="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded">
<span
key={j}
className="text-xs bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 px-2 py-1 rounded"
>
{src.fileName}
{src.section && `#${src.section}`}
</span>
Expand Down
8 changes: 4 additions & 4 deletions client/src/components/chat/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function MessageList({ messages, loading, hasDocuments }: MessageListProp
<div className="flex-1 overflow-y-auto p-4 sm:p-6">
{messages.length === 0 && !hasDocuments ? (
<div className="h-full flex items-center justify-center">
<div className="text-center text-gray-400">
<div className="text-center text-gray-400 dark:text-gray-500">
<FileText size={48} className="mx-auto mb-4" />
<p>Upload your docs to get started</p>
</div>
Expand All @@ -29,7 +29,7 @@ export function MessageList({ messages, loading, hasDocuments }: MessageListProp

{messages.length === 0 && hasDocuments ? (
<div className="h-full flex items-center justify-center">
<div className="text-center text-gray-400">
<div className="text-center text-gray-400 dark:text-gray-500">
<p>Ask a question about your docs</p>
</div>
</div>
Expand All @@ -41,8 +41,8 @@ export function MessageList({ messages, loading, hasDocuments }: MessageListProp
))}

{loading ? (
<div className="bg-white border border-gray-200 p-4 rounded-lg max-w-full sm:max-w-2xl">
<div className="flex items-center gap-2 text-gray-400">
<div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 p-4 rounded-lg max-w-full sm:max-w-2xl">
<div className="flex items-center gap-2 text-gray-400 dark:text-gray-500">
<Loader2 size={16} className="animate-spin" />
Thinking...
</div>
Expand Down
20 changes: 11 additions & 9 deletions client/src/components/documents/DocumentList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,20 @@ export function DocumentList({
return (
<div
className={cn(
'w-64 shrink-0 bg-white border-r border-gray-200 flex flex-col h-full',
'w-64 shrink-0 bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 flex flex-col h-full',
className,
)}
>
{showHeader ? (
<div className="hidden lg:block p-4 border-b border-gray-200">
<h2 className="font-semibold text-gray-700">Documents</h2>
<div className="hidden lg:block p-4 border-b border-gray-200 dark:border-gray-800">
<h2 className="font-semibold text-gray-700 dark:text-gray-200">Documents</h2>
</div>
) : null}

<div className="p-4">
<button
onClick={onUploadClick}
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-[#edf3fe] text-gray-800 rounded-lg hover:bg-[#dce8fc] transition"
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-[#edf3fe] dark:bg-blue-950 text-gray-800 dark:text-blue-50 rounded-lg hover:bg-[#dce8fc] dark:hover:bg-blue-900 transition"
>
<Upload size={18} />
Upload
Expand All @@ -44,26 +44,28 @@ export function DocumentList({

<div className="flex-1 overflow-y-auto px-4">
{documents.length === 0 ? (
<p className="text-gray-400 text-sm">No files yet</p>
<p className="text-gray-400 dark:text-gray-500 text-sm">No files yet</p>
) : (
<ul className="space-y-2">
{documents.map((doc) => (
<li
key={doc.id}
className="flex items-center justify-between p-2 bg-gray-50 rounded-lg hover:bg-gray-100 transition"
className="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-800/50 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition"
>
<button
type="button"
onClick={() => onPreview(doc)}
className="flex items-center gap-2 min-w-0 flex-1 text-left"
>
<FileText size={16} className="text-gray-400 flex-shrink-0" />
<span className="text-sm text-gray-700 truncate">{doc.fileName}</span>
<FileText size={16} className="text-gray-400 dark:text-gray-500 flex-shrink-0" />
<span className="text-sm text-gray-700 dark:text-gray-200 truncate">
{doc.fileName}
</span>
</button>
<button
type="button"
onClick={() => onDelete(doc.id)}
className="text-gray-400 hover:text-red-500 flex-shrink-0 p-2 -m-1"
className="text-gray-400 dark:text-gray-500 hover:text-red-500 flex-shrink-0 p-2 -m-1"
aria-label={`Delete ${doc.fileName}`}
>
<X size={16} />
Expand Down
8 changes: 4 additions & 4 deletions client/src/components/layout/MobileDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,19 @@ export function MobileDrawer({ open, onClose, side, title, children }: MobileDra
<div className="absolute inset-0 bg-black/50" onClick={onClose} aria-hidden="true" />
<div
className={cn(
'absolute inset-y-0 flex w-[min(20rem,85vw)] flex-col bg-white shadow-xl',
'absolute inset-y-0 flex w-[min(20rem,85vw)] flex-col bg-white dark:bg-gray-900 shadow-xl',
side === 'left' ? 'left-0' : 'right-0',
)}
role="dialog"
aria-modal="true"
aria-label={title}
>
<div className="flex items-center justify-between border-b border-gray-200 p-4">
<h2 className="font-semibold text-gray-700">{title}</h2>
<div className="flex items-center justify-between border-b border-gray-200 dark:border-gray-800 p-4">
<h2 className="font-semibold text-gray-700 dark:text-gray-200">{title}</h2>
<button
type="button"
onClick={onClose}
className="rounded-lg p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
className="rounded-lg p-2 text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-600 dark:hover:text-gray-300"
aria-label="Close panel"
>
<X size={20} />
Expand Down
Loading
Loading