import { useState, useRef, useEffect, useMemo } from 'react';
import { ArrowUp, Bot, Trash2, Copy, Check, ImageUp, X, Brain, Pencil, Globe, Download, Wand2 } from 'lucide-react';
import { Sun, Moon } from 'lucide-react';
import {
GROQ_API_KEYS,
GROQ_API_URL,
GROQ_MODEL_REASONER,
POLLINATIONS_API_URL,
POLLINATIONS_MODEL_TEXT,
POLLINATIONS_MODEL_VISION,
POLLINATIONS_MODEL_FALLBACK,
POLLINATIONS_MODEL_SEARCH,
TACTIQ_API_URL
} from './config/api';
import ReactMarkdown, { type Components } from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { materialDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
import 'katex/dist/katex.min.css';
import { InlineMath, BlockMath } from 'react-katex';
import type { Message, EditingMessage } from './types/msg';
import { SYSTEM_PROMPT, STORAGE_KEY, MAX_RETRIES, RETRY_DELAY, REQUEST_TIMEOUT } from './config/const';
const CodeBlock = ({ children, inline, className }: { children: string | string[]; inline?: boolean; className?: string }) => {
const codeContent = Array.isArray(children) ? children.join('') : children?.toString() || '';
if (inline || !className) {
return {codeContent};
}
const [copied, setCopied] = useState(false);
const language = className?.replace(/language-/, '') || 'plaintext';
const handleCopy = async () => {
await navigator.clipboard.writeText(codeContent);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const customStyle = {
...materialDark,
'pre[class*="language-"]': {
...materialDark['pre[class*="language-"]'],
background: 'transparent'
}
};
return (
{codeContent}
);
};
interface ThinkingBlockProps {
id: string;
children: React.ReactNode;
isExpanded: boolean;
onToggle: (id: string) => void;
}
const ThinkingProcess: React.FC = ({ id, children, isExpanded, onToggle }) => {
return (
{isExpanded && (
)}
);
};
const MathBlock = ({ math, isInParagraph }: { math: string; isInParagraph: boolean }) => {
try {
if (isInParagraph) {
return (
{`\\displaystyle ${math}`}
);
}
return (
{math}
);
} catch (error) {
console.error('Math rendering error:', error);
return Error rendering equation: {math};
}
};
const ChatMessage = ({
message,
onEdit,
isEditing,
editContent,
onEditChange,
onEditSave,
onEditCancel,
setFullScreenImage
}: {
message: Message;
onEdit?: (message: Message) => void;
isEditing?: boolean;
editContent?: string;
onEditChange?: (content: string) => void;
onEditSave?: () => void;
onEditCancel?: () => void;
setFullScreenImage: (image: string | null) => void;
}) => {
const [expandedThinkBlocks, setExpandedThinkBlocks] = useState>(new Set());
const toggleThinkBlock = (id: string) => {
setExpandedThinkBlocks(prev => {
const newSet = new Set(prev);
if (newSet.has(id)) {
newSet.delete(id);
} else {
newSet.add(id);
}
return newSet;
});
};
const TextBlock = ({ children, isInParagraph = false }: { children: React.ReactNode; isInParagraph?: boolean }) => {
if (Array.isArray(children)) {
return <>{children.map((child, i) => {child})}>;
}
if (!children || typeof children !== 'string') {
return <>{children}>;
}
if (!children.includes('$')) {
return <>{children}>;
}
const segments: string[] = [];
let currentText = '';
let pos = 0;
let inMath = false;
let mathStart = 0;
while (pos < children.length) {
if (children[pos] === '$') {
if (pos + 1 < children.length && children[pos + 1] === '$') {
if (!inMath) {
if (currentText) segments.push(currentText);
currentText = '$$';
mathStart = pos;
inMath = true;
pos += 2;
} else if (pos > mathStart + 2) {
segments.push(currentText + '$$');
currentText = '';
inMath = false;
pos += 2;
} else {
currentText += '$$';
pos += 2;
}
} else {
if (!inMath) {
if (currentText) segments.push(currentText);
currentText = '$';
mathStart = pos;
inMath = true;
pos++;
} else if (pos > mathStart + 1) {
segments.push(currentText + '$');
currentText = '';
inMath = false;
pos++;
} else {
currentText += '$';
pos++;
}
}
} else {
currentText += children[pos];
pos++;
}
}
if (currentText) {
segments.push(currentText);
}
return (
<>
{segments.map((segment, i) => {
try {
if (segment.startsWith('$$') && segment.endsWith('$$')) {
const math = segment.slice(2, -2);
return ;
} else if (segment.startsWith('$') && segment.endsWith('$')) {
const math = segment.slice(1, -1);
return {math};
}
return {segment};
} catch (error) {
console.error('LaTeX rendering error:', error);
return {segment};
}
})}
>
);
};
const components = {
code: ({ children, inline, className }: { children: string; inline?: boolean; className?: string }) => (
{children as string}
),
think: ({ children }: { children: React.ReactNode }) => {
const thinkId = useMemo(() => {
const content = children?.toString() || '';
return `think-${message.id}-${content.slice(0, 32)}`;
}, [children, message.id]);
return (
{children}
);
},
p: ({ children }: { children: React.ReactNode }) => (
{children}
),
strong: ({ children }: { children: React.ReactNode }) => (
{children}
),
em: ({ children }: { children: React.ReactNode }) => (
{children}
),
li: ({ children }: { children: React.ReactNode }) => (
{children}
),
h1: ({ children }: { children: React.ReactNode }) => (
{children}
),
h2: ({ children }: { children: React.ReactNode }) => (
{children}
),
h3: ({ children }: { children: React.ReactNode }) => (
{children}
)
};
const processMessageContent = (content: string) => {
if (content.includes('') && !content.includes('')) {
content = content.replace(//g, '');
}
const thinkMatch = content.match(/([\s\S]*?)<\/think>/);
if (thinkMatch) {
const thinking = thinkMatch[1].trim();
const restOfContent = content.replace(/[\s\S]*?<\/think>/, '').trim();
return (
<>
{thinking}
}
>
{restOfContent}
>
);
}
return (
}
>
{content}
);
};
const downloadImage = async (imageUrl: string) => {
try {
const response = await fetch(imageUrl);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `image-${Date.now()}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('Error downloading image:', error);
}
};
return (
{isEditing ? (
) : (
<>
{message.image && (
)
setFullScreenImage(message.image || null)}
onLoad={(e) => {
if (message.image?.startsWith('https://image.pollinations.ai')) {
e.currentTarget.src = message.image!;
}
}}
/>
)}
{processMessageContent(message.content)}
{message.role === 'user' && (
)}
{message.timestamp.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
})}
>
)}
);
};
function App() {
const [messages, setMessages] = useState(() => {
const savedMessages = localStorage.getItem(STORAGE_KEY);
if (savedMessages) {
const parsed = JSON.parse(savedMessages);
return parsed.map((msg: any) => ({
...msg,
timestamp: new Date(msg.timestamp)
}));
}
return [SYSTEM_PROMPT];
});
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [showClearConfirm, setShowClearConfirm] = useState(false);
const messagesEndRef = useRef(null);
const [selectedImage, setSelectedImage] = useState(null);
const fileInputRef = useRef(null);
const [fullScreenImage, setFullScreenImage] = useState(null);
const textareaRef = useRef(null);
const [reasonerEnabled, setReasonerEnabled] = useState(false);
const [editingMessage, setEditingMessage] = useState(null);
const [searchEnabled, setSearchEnabled] = useState(false);
const [imageGenEnabled, setImageGenEnabled] = useState(false);
const [generatingImage, setGeneratingImage] = useState(false);
const [darkMode, setDarkMode] = useState(() => {
if (typeof window !== 'undefined') {
return localStorage.getItem('theme') === 'dark' ||
(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
}
return false;
});
useEffect(() => {
if (darkMode) {
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('theme', 'light');
}
}, [darkMode]);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'auto' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
// Save messages to localStorage whenever they change
useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(messages));
}, [messages]);
const handleClearHistory = () => {
setMessages([SYSTEM_PROMPT as Message]);
setShowClearConfirm(false);
};
const toggleMode = (mode: 'reasoner' | 'search' | 'image' | 'generate') => {
switch (mode) {
case 'reasoner':
if (reasonerEnabled) {
setReasonerEnabled(false);
} else {
setReasonerEnabled(true);
setSearchEnabled(false);
setImageGenEnabled(false);
setSelectedImage(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
break;
case 'search':
if (searchEnabled) {
setSearchEnabled(false);
} else {
setSearchEnabled(true);
setReasonerEnabled(false);
setImageGenEnabled(false);
setSelectedImage(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
break;
case 'image':
if (selectedImage) {
setSelectedImage(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
} else {
fileInputRef.current?.click();
setReasonerEnabled(false);
setSearchEnabled(false);
setImageGenEnabled(false);
}
break;
case 'generate':
if (imageGenEnabled) {
setImageGenEnabled(false);
} else {
setImageGenEnabled(true);
setReasonerEnabled(false);
setSearchEnabled(false);
setSelectedImage(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
break;
}
};
const generateImage = async (prompt: string) => {
if (!prompt.trim() || generatingImage) return;
setGeneratingImage(true);
try {
const encodedPrompt = encodeURIComponent(prompt.trim());
const imageUrl = `https://image.pollinations.ai/prompt/${encodedPrompt}?width=1024&height=1024&nologo=true&private=true&enhance=true&safe=true`;
const userMessage: Message = {
id: Date.now().toString(),
role: 'user',
content: `Generate an image: ${prompt}`,
timestamp: new Date()
};
setMessages(prev => [...prev, userMessage]);
const assistantMessage: Message = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: 'Here\'s your generated image!',
timestamp: new Date(),
image: imageUrl
};
setMessages(prev => [...prev, assistantMessage]);
setInput('');
} catch (error) {
console.error('Error generating image:', error);
setMessages(prev => [...prev, {
id: Date.now().toString(),
role: 'assistant',
content: 'Sorry, I encountered an error while generating the image. Please try again.',
timestamp: new Date()
}]);
} finally {
setGeneratingImage(false);
}
};
const handleImageSelect = (e: React.ChangeEvent) => {
const file = e.target.files?.[0];
if (file) {
if (file.size > 5 * 1024 * 1024) { // 5MB limit
alert('Image size should be less than 5MB');
return;
}
const reader = new FileReader();
reader.onloadend = () => {
setSelectedImage(reader.result as string);
setReasonerEnabled(false);
setSearchEnabled(false);
};
reader.readAsDataURL(file);
}
};
const removeSelectedImage = () => {
setSelectedImage(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const makeApiRequest = async (messages: Message[], retryCount = 0, useFallback = false): Promise => {
const randomApiKey = GROQ_API_KEYS[Math.floor(Math.random() * GROQ_API_KEYS.length)];
try {
const systemPrompt = messages.find(msg => msg.role === 'system');
const lastMessages = messages.filter(msg => msg.role !== 'system').slice(-5);
const apiMessages = systemPrompt ? [systemPrompt, ...lastMessages] : lastMessages;
const hasImageInHistory = apiMessages.some(msg => !!msg.image);
const filteredMessages = apiMessages.map(msg => {
// Skip messages with images if using reasoning model
if (reasonerEnabled && msg.image) {
return null;
}
if (msg.role === 'assistant' && msg.image) {
return null;
}
const lastImageMessage = hasImageInHistory ?
[...apiMessages].reverse().find((m: Message) => !!m.image) : null;
if (msg.image && msg === lastImageMessage && !reasonerEnabled) {
return {
role: msg.role,
content: [
{ type: "text", text: msg.content },
{ type: "image_url", image_url: { url: msg.image } }
]
};
} else {
return {
role: msg.role,
content: msg.content
};
}
}).filter(Boolean); // Remove any null messages (filtered image messages)
let model;
let apiUrl;
if (reasonerEnabled) {
// Use GROQ API for reasoning
model = GROQ_MODEL_REASONER;
apiUrl = GROQ_API_URL;
} else {
// Use Pollinations API for everything else
model = searchEnabled ? POLLINATIONS_MODEL_SEARCH :
(hasImageInHistory ? POLLINATIONS_MODEL_VISION :
(useFallback ? POLLINATIONS_MODEL_FALLBACK : POLLINATIONS_MODEL_TEXT));
apiUrl = POLLINATIONS_API_URL;
}
// Create AbortController for timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
try {
const headers: Record = {
'Content-Type': 'application/json'
};
if (apiUrl === GROQ_API_URL) {
headers['Authorization'] = `Bearer ${randomApiKey}`;
}
const response = await fetch(apiUrl, {
method: 'POST',
headers,
body: JSON.stringify({
model,
messages: filteredMessages,
temperature: 0.7,
max_tokens: (model === GROQ_MODEL_REASONER) ? 8192 : 4096,
stream: true
}),
signal: controller.signal
});
clearTimeout(timeoutId); // Clear timeout if request completes
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.error?.message || 'Unknown error'}`);
}
return response;
} catch (error) {
clearTimeout(timeoutId); // Clear timeout on error
if (error instanceof Error && error.name === 'AbortError') {
throw new Error('Request timed out. Please try again.');
}
throw error;
}
} catch (error) {
// If error is timeout and not using fallback, try fallback immediately
if (error instanceof Error && error.message === 'Request timed out. Please try again.' && !useFallback) {
console.log('Request timed out, switching to fallback model...');
return makeApiRequest(messages, 0, true);
}
if (!useFallback) {
console.log('Switching to fallback model...');
return makeApiRequest(messages, 0, true);
}
if (retryCount < MAX_RETRIES) {
console.log(`Retry attempt ${retryCount + 1} of ${MAX_RETRIES}`);
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY * (retryCount + 1)));
return makeApiRequest(messages, retryCount + 1, true);
}
throw error;
}
};
const fetchYouTubeTranscript = async (url: string): Promise => {
try {
const response = await fetch(TACTIQ_API_URL, {
method: 'POST',
headers: {
'accept': '*/*',
'content-type': 'application/json',
'sec-ch-ua': '"Chromium";v="134", "Not:A-Brand";v="24", "Brave";v="134"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'Referer': 'https://tactiq.io/',
},
body: JSON.stringify({ videoUrl: url })
});
if (!response.ok) {
throw new Error('Failed to fetch transcript');
}
const data = await response.json();
let text = `Title: ${data.title}\n\nTranscript:\n`;
data.captions.forEach((caption: any) => {
text += caption.text + ' ';
});
return text.trim();
} catch (error) {
console.error('Error fetching YouTube transcript:', error);
throw error;
}
};
const extractYouTubeUrl = (text: string): string | null => {
const regex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/watch\?v=|youtu\.be\/)([^\s&]+)/;
const match = text.match(regex);
return match ? match[0] : null;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if ((!input.trim() && !selectedImage) || isLoading) return;
// If image generation is enabled, use the generateImage function instead
if (imageGenEnabled || input.trim().startsWith('Generate an image')) {
const prompt = input.trim().replace('Generate an image: ', '');
const reprompt = prompt.replace('Generate an image', '');
await generateImage(reprompt.trim());
return;
}
// Create visible user message
const userMessage: Message = {
id: Date.now().toString(),
role: 'user',
content: input.trim() || "What's in this image?",
timestamp: new Date(),
image: selectedImage || undefined
};
setMessages(prev => [...prev, userMessage]);
setInput('');
setSelectedImage(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
setIsLoading(true);
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
textareaRef.current.focus();
}
try {
// Check for YouTube URL and get transcript if exists
const youtubeUrl = extractYouTubeUrl(userMessage.content);
let transcriptText = '';
if (youtubeUrl) {
try {
transcriptText = await fetchYouTubeTranscript(youtubeUrl);
} catch (error) {
console.error('Failed to fetch YouTube transcript:', error);
}
}
const apiMessages: Message[] = selectedImage ?
[{ ...SYSTEM_PROMPT, role: 'system' as const }] :
[...messages];
apiMessages.push({
...userMessage,
content: transcriptText ?
`${userMessage.content}\n\nContext from video:\n${transcriptText}` :
userMessage.content
});
const response = await makeApiRequest(apiMessages);
const reader = response.body?.getReader();
if (!reader) throw new Error('Failed to get response reader');
// Create a new message for streaming response
const assistantMessage: Message = {
id: Date.now().toString(),
role: 'assistant',
content: '',
timestamp: new Date(),
};
setMessages(prev => [...prev, assistantMessage]);
// Read the stream
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // Keep the incomplete line in buffer
for (const line of lines) {
if (line.trim() === '') continue;
if (line === 'data: [DONE]') continue;
try {
const data = JSON.parse(line.replace(/^data: /, ''));
const content = data.choices[0]?.delta?.content || '';
setMessages(prev => prev.map(msg =>
msg.id === assistantMessage.id
? { ...msg, content: msg.content + content }
: msg
));
} catch (error) {
console.error('Error parsing stream:', error);
}
}
}
} catch (error) {
console.error('Error:', error);
setMessages(prev => [...prev, {
id: Date.now().toString(),
role: 'assistant',
content: 'Sorry, I encountered an error. Please try again. If the problem persists, try refreshing the page or clearing the chat history.',
timestamp: new Date(),
}]);
} finally {
setIsLoading(false);
requestAnimationFrame(() => {
if (textareaRef.current) {
textareaRef.current.focus();
}
});
}
};
const handleEditMessage = (message: Message) => {
setEditingMessage({
id: message.id,
content: message.content,
image: message.image
});
};
const handleEditCancel = () => {
setEditingMessage(null);
};
const handleEditSave = async () => {
if (!editingMessage) return;
const messageIndex = messages.findIndex(m => m.id === editingMessage.id);
if (messageIndex === -1) return;
// Check if content has actually changed
const originalMessage = messages[messageIndex];
if (originalMessage.content === editingMessage.content &&
originalMessage.image === editingMessage.image) {
setEditingMessage(null);
return;
}
const updatedMessages = [...messages];
updatedMessages[messageIndex] = {
...updatedMessages[messageIndex],
content: editingMessage.content,
image: editingMessage.image
};
updatedMessages.splice(messageIndex + 1);
setMessages(updatedMessages);
setEditingMessage(null);
// If the edited message was an image generation prompt, regenerate the image
if (imageGenEnabled || editingMessage.content.trim().startsWith('Generate an image')) {
const prompt = editingMessage.content.trim().replace('Generate an image: ', '');
const reprompt = prompt.replace('Generate an image', '');
const encodedPrompt = encodeURIComponent(reprompt.trim());
const imageUrl = `https://image.pollinations.ai/prompt/${encodedPrompt}?width=1024&height=1024&nologo=true&private=true&enhance=true&safe=true`;
const assistantMessage: Message = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: 'Here\'s your generated image!',
timestamp: new Date(),
image: imageUrl
};
setMessages(prev => [...prev, assistantMessage]);
return;
}
setIsLoading(true);
try {
const response = await makeApiRequest(updatedMessages);
const reader = response.body?.getReader();
if (!reader) throw new Error('Failed to get response reader');
const assistantMessage: Message = {
id: Date.now().toString(),
role: 'assistant',
content: '',
timestamp: new Date(),
};
setMessages(prev => [...prev, assistantMessage]);
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim() === '' || line === 'data: [DONE]') continue;
try {
const data = JSON.parse(line.replace(/^data: /, ''));
const content = data.choices[0]?.delta?.content || '';
setMessages(prev => prev.map(msg =>
msg.id === assistantMessage.id
? { ...msg, content: msg.content + content }
: msg
));
} catch (error) {
console.error('Error parsing stream:', error);
}
}
}
} catch (error) {
console.error('Error:', error);
setMessages(prev => [...prev, {
id: Date.now().toString(),
role: 'assistant',
content: 'Sorry, I encountered an error. Please try again. If the problem persists, try refreshing the page or clearing the chat history.',
timestamp: new Date(),
}]);
} finally {
setIsLoading(false);
}
};
// Add a warning if messages fail to load from localStorage
useEffect(() => {
const savedMessages = localStorage.getItem(STORAGE_KEY);
if (savedMessages) {
try {
JSON.parse(savedMessages);
} catch (error) {
console.error('Error loading chat history:', error);
localStorage.removeItem(STORAGE_KEY);
setMessages([SYSTEM_PROMPT as Message]);
}
}
}, []);
// Add clipboard paste handler
const handlePaste = async (e: ClipboardEvent) => {
const items = e.clipboardData?.items;
if (!items) return;
for (const item of Array.from(items)) {
if (item.type.startsWith('image/')) {
e.preventDefault();
const file = item.getAsFile();
if (!file) continue;
if (file.size > 5 * 1024 * 1024) { // 5MB limit
alert('Image size should be less than 5MB');
return;
}
const reader = new FileReader();
reader.onloadend = () => {
setSelectedImage(reader.result as string);
setReasonerEnabled(false);
setSearchEnabled(false);
};
reader.readAsDataURL(file);
break;
}
}
};
useEffect(() => {
document.addEventListener('paste', handlePaste);
return () => {
document.removeEventListener('paste', handlePaste);
};
}, []);
const adjustTextareaHeight = () => {
const textarea = textareaRef.current;
if (textarea) {
textarea.style.height = 'auto';
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`; // Max height of 200px
}
};
const handleInputChange = (e: React.ChangeEvent) => {
setInput(e.target.value);
adjustTextareaHeight();
};
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.focus();
}
}, []);
return (
{/* Header */}
Study Assistant
{isLoading ? 'Typing...' : 'Online'}
{/* Messages */}
{messages.length <= 1 ? (
👋
Welcome to Study Assistant!
I'm your friendly AI study buddy—here to help you learn, solve problems, and master new topics. Just ask me anything!
- Breaks down tough topics into simple, bite-sized explanations.
- Guides you step-by-step through problems and concepts.
- Encourages critical thinking and independent learning.
- Supports you with tips, resources, and clear answers.
🚀 Get started: Type your question below and let's learn together!
) : (
{messages.slice(1).map(message => (
setEditingMessage(prev => prev ? { ...prev, content } : null)}
onEditSave={handleEditSave}
onEditCancel={handleEditCancel}
setFullScreenImage={setFullScreenImage}
/>
))}
{isLoading && (
)}
)}
{/* Input section */}
{showClearConfirm && (
Clear Chat History
Are you sure you want to clear all chat history? This action cannot be undone.
)}
{fullScreenImage && (
setFullScreenImage(null)}
>

e.stopPropagation()}
/>
)}
);
};
export default App