mirror of
https://github.com/hanzoai/shadcn.git
synced 2026-11-03 03:18:52 +00:00
14 lines
9.0 KiB
JSON
14 lines
9.0 KiB
JSON
{
|
|
"name": "ai-assistant",
|
|
"description": "Full-featured AI assistant component",
|
|
"dependencies": [
|
|
"@hanzo/ui"
|
|
],
|
|
"files": [
|
|
{
|
|
"name": "ai-assistant.tsx",
|
|
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Cell, Grid } from \"@hanzo/ui/grid\"\nimport { Bot, MessageSquare, Settings, Sparkles, User } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\"\nimport { Badge } from \"@/registry/default/ui/badge\"\nimport { Button } from \"@/registry/default/ui/button\"\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n} from \"@/registry/default/ui/card\"\nimport { ScrollArea } from \"@/registry/default/ui/scroll-area\"\nimport { Separator } from \"@/registry/default/ui/separator\"\nimport { Textarea } from \"@/registry/default/ui/textarea\"\n\nexport interface AIAssistantProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n \"onError\"\n> {\n provider?: string\n model?: string\n apiKey?: string\n systemPrompt?: string\n tools?: Array<{ name: string; description: string; parameters?: any }>\n onResponse?: (response: any) => void\n onError?: (error: Error) => void\n messages?: Array<{\n id: string\n role: \"user\" | \"assistant\" | \"system\"\n content: string\n timestamp: Date\n }>\n onSendMessage?: (message: string) => void\n isLoading?: boolean\n placeholder?: string\n showAvatar?: boolean\n showTimestamp?: boolean\n}\n\nconst AIAssistant = React.forwardRef<HTMLDivElement, AIAssistantProps>(\n (\n {\n className,\n children,\n provider = \"openai\",\n model = \"gpt-4\",\n apiKey,\n systemPrompt,\n onResponse,\n onError,\n messages = [],\n onSendMessage,\n isLoading = false,\n placeholder = \"Ask the AI assistant anything...\",\n showAvatar = true,\n showTimestamp = true,\n tools = [],\n ...props\n },\n ref\n ) => {\n const [input, setInput] = React.useState(\"\")\n const scrollRef = React.useRef<HTMLDivElement>(null)\n\n // Auto-scroll to bottom when new messages arrive\n React.useEffect(() => {\n if (scrollRef.current) {\n scrollRef.current.scrollTop = scrollRef.current.scrollHeight\n }\n }, [messages])\n\n const handleSend = () => {\n if (!input.trim() || isLoading) return\n onSendMessage?.(input)\n setInput(\"\")\n }\n\n const handleKeyPress = (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault()\n handleSend()\n }\n }\n\n return (\n // Header, transcript, composer: three rows, the middle one taking the\n // slack. The transcript scrolls because the 1fr track bounds it, which a\n // column of flex children only does once every one of them agrees to\n // shrink.\n <Grid\n ref={ref}\n columns={1}\n rows=\"auto minmax(0, 1fr) auto\"\n gap={0}\n className={className}\n style={{ height: \"100%\", width: \"100%\" }}\n {...props}\n >\n <div className=\"flex items-center justify-between border-b p-4\">\n <div className=\"flex items-center gap-3\">\n <div className=\"flex h-10 w-10 items-center justify-center rounded-full bg-primary/10\">\n <Sparkles className=\"h-5 w-5 text-primary\" />\n </div>\n <div>\n <h3 className=\"font-semibold\">AI Assistant</h3>\n <p className=\"text-sm text-muted-foreground\">\n {provider} • {model}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n {tools.length > 0 && (\n <Badge variant=\"secondary\">{tools.length} tools</Badge>\n )}\n <Button variant=\"ghost\" size=\"icon\">\n <Settings className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n\n <ScrollArea ref={scrollRef} className=\"p-4\">\n <Grid columns={1} gap={16}>\n {messages.length === 0 ? (\n <Grid\n columns={1}\n gap={16}\n className=\"py-8 text-center\"\n style={{ justifyItems: \"center\" }}\n >\n <MessageSquare className=\"h-12 w-12 text-muted-foreground/50\" />\n <div>\n <h4 className=\"text-lg font-medium text-muted-foreground\">\n Start a conversation\n </h4>\n <p className=\"text-sm text-muted-foreground\">\n Ask me anything and I'll do my best to help!\n </p>\n </div>\n </Grid>\n ) : (\n messages.map((message) => {\n const mine = message.role === \"user\"\n // The avatar track is content-sized and the bubble track takes\n // the rest; which side each sits on is a placement, so the two\n // sides read the same way instead of one being a reversal of\n // the other.\n return (\n <Grid\n key={message.id}\n columns={\n mine ? \"minmax(0, 1fr) auto\" : \"auto minmax(0, 1fr)\"\n }\n gap={12}\n >\n {showAvatar && (\n <Cell col={mine ? 2 : 1}>\n <Avatar className=\"h-8 w-8\">\n <AvatarFallback>\n {mine ? (\n <User className=\"h-4 w-4\" />\n ) : (\n <Bot className=\"h-4 w-4\" />\n )}\n </AvatarFallback>\n </Avatar>\n </Cell>\n )}\n <Cell col={mine ? 1 : 2}>\n <Grid\n columns={1}\n gap={4}\n className=\"max-w-[80%]\"\n style={{\n justifyItems: mine ? \"end\" : \"start\",\n marginLeft: mine ? \"auto\" : undefined,\n }}\n >\n {showTimestamp && (\n <span className=\"text-xs text-muted-foreground\">\n {message.timestamp.toLocaleTimeString()}\n </span>\n )}\n <div\n className={cn(\n \"rounded-lg px-3 py-2 text-sm whitespace-pre-wrap\",\n mine\n ? \"bg-primary text-primary-foreground\"\n : \"bg-muted\"\n )}\n >\n {message.content}\n </div>\n </Grid>\n </Cell>\n </Grid>\n )\n })\n )}\n {isLoading && (\n <Grid columns=\"auto minmax(0, 1fr)\" gap={12}>\n <Avatar className=\"h-8 w-8\">\n <AvatarFallback>\n <Bot className=\"h-4 w-4\" />\n </AvatarFallback>\n </Avatar>\n <div className=\"bg-muted w-fit rounded-lg px-3 py-2 text-sm\">\n <div className=\"flex items-center gap-2\">\n <div className=\"flex gap-1\">\n <div className=\"h-2 w-2 animate-bounce rounded-full bg-current [animation-delay:-0.3s]\" />\n <div className=\"h-2 w-2 animate-bounce rounded-full bg-current [animation-delay:-0.15s]\" />\n <div className=\"h-2 w-2 animate-bounce rounded-full bg-current\" />\n </div>\n <span>AI is thinking...</span>\n </div>\n </div>\n </Grid>\n )}\n </Grid>\n </ScrollArea>\n\n <div className=\"border-t p-4\">\n <Grid columns=\"minmax(0, 1fr) auto\" gap={8}>\n <Textarea\n value={input}\n onChange={(e) => setInput(e.target.value)}\n onKeyPress={handleKeyPress}\n placeholder={placeholder}\n disabled={isLoading}\n className=\"min-h-[60px] resize-none\"\n rows={2}\n />\n <Button\n onClick={handleSend}\n disabled={!input.trim() || isLoading}\n size=\"icon\"\n >\n <MessageSquare className=\"h-4 w-4\" />\n </Button>\n </Grid>\n </div>\n\n {children}\n </Grid>\n )\n }\n)\nAIAssistant.displayName = \"AIAssistant\"\n\nexport { AIAssistant }\n"
|
|
}
|
|
],
|
|
"type": "components:ai"
|
|
} |