diff --git a/package-lock.json b/package-lock.json
index 23f04aa..cf4ed8d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -35,6 +35,7 @@
"@eslint/js": "^9.9.1",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
+ "@types/react-syntax-highlighter": "^15.5.13",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.18",
"eslint": "^9.9.1",
@@ -2286,6 +2287,16 @@
"@types/react": "*"
}
},
+ "node_modules/@types/react-syntax-highlighter": {
+ "version": "15.5.13",
+ "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
+ "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
diff --git a/package.json b/package.json
index 51c7843..f3e7d45 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
"@eslint/js": "^9.9.1",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
+ "@types/react-syntax-highlighter": "^15.5.13",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.18",
"eslint": "^9.9.1",
diff --git a/src/components/chat/Message.tsx b/src/components/chat/Message.tsx
index 81e42cb..5fa26f7 100644
--- a/src/components/chat/Message.tsx
+++ b/src/components/chat/Message.tsx
@@ -2,23 +2,27 @@ import React from 'react';
import { formatDistanceToNow } from 'date-fns';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
-import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
-import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
-import { Copy, Check, ThumbsUp, ThumbsDown } from 'lucide-react';
+import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
+import { vs } from 'react-syntax-highlighter/dist/esm/styles/prism';
+import { Copy, Check } from 'lucide-react';
import { cn } from '../../lib/utils';
import { Button } from '../ui/Button';
import { Avatar, AvatarFallback } from '../ui/Avatar';
import { Tooltip } from '../ui/Tooltip';
import type { ChatMessage } from '../../types/supabase';
+import type { Components } from 'react-markdown';
interface MessageProps {
message: ChatMessage;
- onFeedback?: (messageId: string, isPositive: boolean) => void;
}
-export function Message({ message, onFeedback }: MessageProps) {
+interface CodeBlockProps {
+ language: string;
+ value: string;
+}
+
+export function Message({ message }: MessageProps) {
const [copied, setCopied] = React.useState(false);
- const [feedback, setFeedback] = React.useState<'positive' | 'negative' | null>(null);
const handleCopy = async (text: string) => {
await navigator.clipboard.writeText(text);
@@ -26,10 +30,49 @@ export function Message({ message, onFeedback }: MessageProps) {
setTimeout(() => setCopied(false), 2000);
};
- const handleFeedback = (isPositive: boolean) => {
- if (!onFeedback) return;
- setFeedback(isPositive ? 'positive' : 'negative');
- onFeedback(message.id, isPositive);
+ const CodeBlock = React.memo(({ language, value }: CodeBlockProps) => (
+
+
+
+
+
+
+
+ {value.replace(/\n$/, '')}
+
+
+ ));
+
+ const code: Components['code'] = ({ className, children, ...props }) => {
+ const match = /language-(\w+)/.exec(className || '');
+ const language = match ? match[1] : '';
+ const value = String(children).replace(/\n$/, '');
+
+ if (!className) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return ;
};
return (
@@ -40,19 +83,19 @@ export function Message({ message, onFeedback }: MessageProps) {
)}
>
{message.role === 'assistant' && (
-
-
+
+
AI
)}
-
+
@@ -63,94 +106,20 @@ export function Message({ message, onFeedback }: MessageProps) {
message.role === 'user' ? 'prose-invert' : 'prose-stone dark:prose-invert'
)}
components={{
- code({ node, inline, className, children, ...props }) {
- const match = /language-(\w+)/.exec(className || '');
- const language = match ? match[1] : '';
-
- if (inline) {
- return (
-
- {children}
-
- );
- }
-
- return (
-
-
-
-
-
-
-
- {String(children).replace(/\n$/, '')}
-
-
- );
- },
+ code
}}
>
{message.content}
-
+
{formatDistanceToNow(new Date(message.created_at), { addSuffix: true })}
-
- {message.role === 'assistant' && onFeedback && (
-
-
-
-
-
-
-
-
- )}
{message.role === 'user' && (
-
+
You
diff --git a/src/pages/dashboard/ask.tsx b/src/pages/dashboard/ask.tsx
index 8e95e9f..072d954 100644
--- a/src/pages/dashboard/ask.tsx
+++ b/src/pages/dashboard/ask.tsx
@@ -1,8 +1,7 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useRef } from 'react';
import { Send, Loader2, X, History, MessageSquarePlus, AlertCircle } from 'lucide-react';
import { Link, useParams, useNavigate } from 'react-router-dom';
import { Button } from '../../components/ui/Button';
-import { ScrollArea } from '../../components/ui/ScrollArea';
import { Message } from '../../components/chat/Message';
import { Avatar, AvatarFallback } from '../../components/ui/Avatar';
import { cn } from '../../lib/utils';
@@ -22,6 +21,8 @@ export default function AskQuestion() {
const [isNewChat, setIsNewChat] = useState(false);
const [hasExistingChats, setHasExistingChats] = useState(null);
const [isTyping, setIsTyping] = useState(false);
+ const messagesEndRef = useRef(null);
+ const scrollAreaRef = useRef(null);
// Check for existing chats and redirect to most recent if on base path
useEffect(() => {
@@ -163,6 +164,14 @@ export default function AskQuestion() {
}
};
+ // Auto-scroll when new messages arrive
+ useEffect(() => {
+ if (scrollAreaRef.current) {
+ const scrollArea = scrollAreaRef.current;
+ scrollArea.scrollTop = scrollArea.scrollHeight;
+ }
+ }, [messages, isTyping]);
+
// Show loading state while checking for existing chats
if (hasExistingChats === null) {
return (
@@ -173,8 +182,9 @@ export default function AskQuestion() {
}
return (
-
-
+
+ {/* Header */}
+
@@ -210,122 +220,177 @@ export default function AskQuestion() {
+ {/* Error Message */}
{error && (
-
+
)}
-
-
+ {/* Messages Area */}
+
+
{!isNewChat && !chatId ? (
-
-
-
-
-
-
-
Welcome to StudyAI Chat
-
- Start a new conversation to get answers to your questions
+
+
+
+
+
Welcome to StudyAI Chat
+
+ Start a new chat to begin asking questions about your study materials
+
+
+
Example questions you can ask:
+
+
+
+
+
+
-
) : messages.length === 0 ? (
-
-
-
-
-
-
-
No messages yet
-
- Start by asking a question about your studies
+
+
+
+
+
Ask Your First Question
+
+ Type your question below or choose from the examples
+
+
+
+
+
) : (
<>
- {messages.map((message, index) => (
+ {messages.map((message) => (
{
- // Implement feedback handling here
- console.log('Feedback:', messageId, isPositive);
- }}
/>
))}
- {/* Typing indicator */}
{isTyping && (
-
+
- {(isNewChat || chatId) && (
-
);
}