mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
- Remove nested git repository from bugulma/frontend/.git - Add all frontend files to main repository tracking - Convert from separate frontend/backend repos to unified monorepo - Preserve all frontend code and development history as tracked files - Eliminate nested repository complexity for simpler development workflow This creates a proper monorepo structure with frontend and backend coexisting in the same repository for easier development and deployment.
172 lines
6.1 KiB
TypeScript
172 lines
6.1 KiB
TypeScript
import { AnimatePresence, motion } from 'framer-motion';
|
|
import { useCallback, useMemo, useState } from 'react';
|
|
import { useChatbot } from '@/hooks/features/useChatbot.ts';
|
|
import { useTranslation } from '@/hooks/useI18n.tsx';
|
|
import { MessageSquare, Trash2, X } from 'lucide-react';
|
|
import Button from '@/components/ui/Button.tsx';
|
|
import ChatHistory from '@/components/chatbot/ChatHistory.tsx';
|
|
import ChatInput from '@/components/chatbot/ChatInput.tsx';
|
|
|
|
const useCopyToClipboard = () => {
|
|
const [copiedText, setCopiedText] = useState<string | null>(null);
|
|
|
|
const copy = useCallback(async (text: string) => {
|
|
if (!navigator?.clipboard) {
|
|
console.warn('Clipboard not supported');
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
setCopiedText(text);
|
|
setTimeout(() => setCopiedText(null), 2000);
|
|
return true;
|
|
} catch (error) {
|
|
console.warn('Copy failed', error);
|
|
setCopiedText(null);
|
|
return false;
|
|
}
|
|
}, []);
|
|
|
|
return { copiedText, copy };
|
|
};
|
|
|
|
const Chatbot = () => {
|
|
const { t } = useTranslation();
|
|
const chatbot = useChatbot();
|
|
const { copiedText, copy } = useCopyToClipboard();
|
|
|
|
// Memoize suggested prompts to prevent recreation
|
|
const suggestedPrompts = useMemo(
|
|
() => [t('chatbot.prompt1'), t('chatbot.prompt2'), t('chatbot.prompt3')],
|
|
[t]
|
|
);
|
|
|
|
const handleSuggestionClick = useCallback(
|
|
(prompt: string) => {
|
|
chatbot.setInputValue(prompt);
|
|
chatbot.handleSendMessage();
|
|
},
|
|
[chatbot]
|
|
);
|
|
|
|
return (
|
|
<>
|
|
<AnimatePresence>
|
|
{chatbot.isOpen && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
exit={{ opacity: 0, y: 20, scale: 0.95 }}
|
|
transition={{ duration: 0.2, ease: 'easeOut' }}
|
|
className="fixed bottom-20 right-4 md:right-6 w-[calc(100vw-2rem)] max-w-sm h-[70vh] max-h-[32rem] bg-background rounded-2xl shadow-2xl border flex flex-col z-40"
|
|
>
|
|
{/* Header */}
|
|
<div className="p-4 border-b flex items-center justify-between shrink-0">
|
|
<div className="flex items-center gap-2">
|
|
<div className="relative">
|
|
<div className="h-8 w-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center font-bold text-sm shrink-0">
|
|
{t('chatbot.aiAcronym')}
|
|
</div>
|
|
<span className="absolute -bottom-0.5 -right-0.5 h-2.5 w-2.5 rounded-full bg-success ring-2 ring-background" />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-base font-semibold">{t('chatbot.header')}</h3>
|
|
<p className="text-xs text-muted-foreground">{t('chatbot.online')}</p>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="p-2 h-8 w-8"
|
|
onClick={chatbot.handleClearChat}
|
|
aria-label={t('chatbot.clearLabel')}
|
|
>
|
|
<Trash2 className="h-4 text-current w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="p-2 h-8 w-8"
|
|
onClick={chatbot.toggleChat}
|
|
aria-label={t('chatbot.closeLabel')}
|
|
>
|
|
<X className="h-4 text-current w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<ChatHistory
|
|
messages={chatbot.messages}
|
|
messagesEndRef={chatbot.messagesEndRef}
|
|
copiedText={copiedText}
|
|
onCopy={copy}
|
|
/>
|
|
|
|
<div className="p-3 border-t shrink-0 space-y-2">
|
|
{chatbot.showSuggestions && (
|
|
<div className="flex flex-wrap gap-2">
|
|
{suggestedPrompts.map((prompt, i) => (
|
|
<Button
|
|
key={i}
|
|
variant="outline"
|
|
size="sm"
|
|
className="text-xs h-auto py-1.5"
|
|
onClick={() => handleSuggestionClick(prompt)}
|
|
>
|
|
{prompt}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
)}
|
|
<ChatInput
|
|
inputValue={chatbot.inputValue}
|
|
onInputChange={chatbot.setInputValue}
|
|
onSendMessage={chatbot.handleSendMessage}
|
|
attachedImage={chatbot.attachedImage}
|
|
onClearAttachment={() => chatbot.setAttachedImage(null)}
|
|
onFileChange={chatbot.handleFileChange}
|
|
fileInputRef={chatbot.fileInputRef}
|
|
inputRef={chatbot.inputRef}
|
|
isListening={chatbot.isListening}
|
|
onStartListening={chatbot.startListening}
|
|
onStopListening={chatbot.stopListening}
|
|
isSpeechSupported={chatbot.isSpeechSupported}
|
|
isLoading={chatbot.isLoading}
|
|
/>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
<Button
|
|
variant="primary"
|
|
size="lg"
|
|
className="rounded-full h-16 w-16 shadow-lg shadow-primary/30"
|
|
onClick={chatbot.toggleChat}
|
|
aria-label={chatbot.isOpen ? t('chatbot.closeLabel') : t('chatbot.openLabel')}
|
|
aria-expanded={chatbot.isOpen}
|
|
>
|
|
<AnimatePresence mode="wait">
|
|
<motion.div
|
|
key={chatbot.isOpen ? 'close' : 'chat'}
|
|
initial={{ opacity: 0, rotate: -30, scale: 0.8 }}
|
|
animate={{ opacity: 1, rotate: 0, scale: 1 }}
|
|
exit={{ opacity: 0, rotate: 30, scale: 0.8 }}
|
|
transition={{ duration: 0.15 }}
|
|
>
|
|
{chatbot.isOpen ? (
|
|
<X className="h-4 h-6 text-current w-4 w-6" />
|
|
) : (
|
|
<MessageSquare className="h-4 h-7 text-current w-4 w-7" />
|
|
)}
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
</Button>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default Chatbot;
|