{"repo": "agribeacon/agribeacon-landingpage", "files": {"src/components/ChatBot.tsx": {"3fcd14a9beaf030cc7694b3504ee4112e9f530f7": {"t": 1785846946.0062146, "n": 9883, "h": "f74d330b203c01a3", "c": "import { FormEvent, KeyboardEvent, useEffect, useRef, useState } from \"react\";\nimport { Bot, Loader2, MessageCircle, Send, Sprout, User, X } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\n\ntype ChatMessage = {\n id: string;\n role: \"user\" | \"assistant\";\n content: string;\n};\n\nconst starterMessages: ChatMessage[] = [\n {\n id: \"welcome\",\n role: \"assistant\",\n content:\n \"Xin chào, mình là Tiểu Thần Nông AI của AgriBeacon. Bạn có thể hỏi về canh tác, cảm biến IoT, UAV, robot, bản đồ nông trại, dự báo năng suất hoặc giải pháp AgriBeacon.\",\n },\n];\n\nconst quickPrompts = [\n \"Vườn sầu riêng nên đặt cảm biến gì?\",\n \"Robot phun thuốc phù hợp diện tích nào?\",\n \"Làm sao dự báo năng suất tốt hơn?\",\n];\n\nconst createId = () => `${Date.now()}-${Math.random().toString(36).slice(2)}`;\n\nconst renderInlineMarkdown = (text: string) => {\n const parts = text.split(/(\\*\\*[^*\\n]+\\*\\*)/g);\n\n return parts.map((part, index) => {\n if (part.startsWith(\"**\") && part.endsWith(\"**\") && part.length > 4) {\n return (\n \n {part.slice(2, -2)}\n \n );\n }\n\n return {part};\n });\n};\n\nconst renderAssistantMarkdown = (content: string) => {\n const elements: JSX.Element[] = [];\n let listType: \"ul\" | \"ol\" | null = null;\n let listItems: string[] = [];\n\n const flushList = () => {\n if (!listType || listItems.length === 0) return;\n\n const ListTag = listType;\n elements.push(\n \n {listItems.map((item, index) => (\n
  • {renderInlineMarkdown(item)}
  • \n ))}\n
    ,\n );\n listType = null;\n listItems = [];\n };\n\n content.split(/\\r?\\n/).forEach((line) => {\n const trimmed = line.trim();\n if (!trimmed) {\n flushList();\n return;\n }\n\n const bullet = trimmed.match(/^[-*]\\s+(.+)$/);\n const numbered = trimmed.match(/^\\d+[.)]\\s+(.+)$/);\n\n if (bullet) {\n if (listType && listType !== \"ul\") flushList();\n listType = \"ul\";\n listItems.push(bullet[1]);\n return;\n }\n\n if (numbered) {\n if (listType && listType !== \"ol\") flushList();\n listType = \"ol\";\n listItems.push(numbered[1]);\n return;\n }\n\n flushList();\n elements.push(\n

    \n {renderInlineMarkdown(trimmed)}\n

    ,\n );\n });\n\n flushList();\n\n return
    {elements}
    ;\n};\n\nconst ChatBot = () => {\n const [open, setOpen] = useState(false);\n const [messages, setMessages] = useState(starterMessages);\n const [input, setInput] = useState(\"\");\n const [loading, setLoading] = useState(false);\n const scrollRef = useRef(null);\n\n useEffect(() => {\n if (!open) return;\n scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: \"smooth\" });\n }, [messages, open]);\n\n const sendMessage = async (content: string) => {\n const trimmed = content.trim();\n if (!trimmed || loading) return;\n\n const nextMessages = [...messages, { id: createId(), role: \"user\" as const, content: trimmed }];\n setMessages(nextMessages);\n setInput(\"\");\n setLoading(true);\n\n try {\n const response = await fetch(\"/api/chat\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n messages: nextMessages.map(({ role, content }) => ({ role, content })),\n }),\n });\n\n const data = await response.json().catch(() => ({}));\n if (!response.ok) {\n throw new Error(data?.error || \"Không gọi được AI\");\n }\n\n setMessages((current) => [\n ...current,\n {\n id: createId(),\n role: \"assistant\",\n content: data.answer || \"Mình chưa có câu trả lời phù hợp lúc này.\",\n },\n ]);\n } catch (error) {\n setMessages((current) => [\n ...current,\n {\n id: createId(),\n role: \"assistant\",\n content:\n error instanceof Error && error.message.includes(\"OPENCODE_GO_API_KEY\")\n ? \"Chatbot chưa được cấu hình OPENCODE_GO_API_KEY trên server. Vui lòng thêm key trong biến môi trường deploy.\"\n : \"Mình đang không kết nối được AI. Bạn thử lại sau ít phút nhé.\",\n },\n ]);\n } finally {\n setLoading(false);\n }\n };\n\n const handleSubmit = (event: FormEvent) => {\n event.preventDefault();\n void sendMessage(input);\n };\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"Enter\" && !event.shiftKey) {\n event.preventDefault();\n void sendMessage(input);\n }\n };\n\n return (\n
    \n {open && (\n
    \n
    \n
    \n
    \n \n
    \n
    \n

    Tiểu Thần Nông AI

    \n

    Trợ lý AgriBeacon

    \n
    \n
    \n \n
    \n\n
    \n {messages.map((message) => {\n const isUser = message.role === \"user\";\n const Icon = isUser ? User : Bot;\n\n return (\n
    \n {!isUser && (\n
    \n \n
    \n )}\n \n {isUser ? message.content : renderAssistantMarkdown(message.content)}\n
    \n {isUser && (\n
    \n \n
    \n )}\n
    \n );\n })}\n {loading && (\n
    \n
    \n \n
    \n
    \n Đang trả lời...\n
    \n
    \n )}\n
    \n\n
    \n {messages.length === 1 && (\n
    \n {quickPrompts.map((prompt) => (\n void sendMessage(prompt)}\n className=\"shrink-0 rounded-full border border-border bg-background px-3 py-1.5 text-xs text-muted-foreground transition-colors hover:border-primary/50 hover:text-primary\"\n >\n {prompt}\n \n ))}\n
    \n )}\n
    \n setInput(event.target.value)}\n onKeyDown={handleKeyDown}\n placeholder=\"Hỏi Tiểu Thần Nông AI...\"\n rows={1}\n className=\"max-h-28 min-h-11 resize-none rounded-xl\"\n disabled={loading}\n />\n \n \n
    \n \n )}\n\n setOpen((current) => !current)}\n className=\"h-14 rounded-full bg-gradient-to-r from-primary to-secondary px-5 shadow-glow\"\n aria-label={open ? \"Đóng chat\" : \"Mở chat\"}\n >\n {open ? : }\n Tiểu Thần Nông AI\n \n \n );\n};\n\nexport default ChatBot;\n"}}}}