turash/bugulma/frontend/components/chatbot/Chatbot.tsx
Damir Mukimov 6347f42e20
Consolidate repositories: Remove nested frontend .git and merge into main repository
- 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.
2025-11-25 06:02:57 +01:00

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;