mirror of
https://github.com/hanzoai/shadcn.git
synced 2026-11-03 03:18:52 +00:00
14 lines
16 KiB
JSON
14 lines
16 KiB
JSON
{
|
|
"name": "ai-playground",
|
|
"description": "Interactive AI model testing environment with adjustable parameters",
|
|
"dependencies": [
|
|
"@hanzo/ui"
|
|
],
|
|
"files": [
|
|
{
|
|
"name": "ai-playground.tsx",
|
|
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Grid } from \"@hanzo/ui/grid\"\nimport {\n AlertCircle,\n Check,\n Copy,\n Loader2,\n Send,\n Settings2,\n Sparkles,\n Trash2,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Alert, AlertDescription } from \"@/registry/default/ui/alert\"\nimport { Button } from \"@/registry/default/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/default/ui/card\"\nimport { Label } from \"@/registry/default/ui/label\"\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/registry/default/ui/select\"\nimport { Slider } from \"@/registry/default/ui/slider\"\nimport { Textarea } from \"@/registry/default/ui/textarea\"\n\n// Available AI Models\nconst AI_MODELS = [\n { value: \"gpt-4\", label: \"GPT-4\", provider: \"OpenAI\" },\n { value: \"gpt-4-turbo\", label: \"GPT-4 Turbo\", provider: \"OpenAI\" },\n { value: \"gpt-3.5-turbo\", label: \"GPT-3.5 Turbo\", provider: \"OpenAI\" },\n { value: \"claude-3-opus\", label: \"Claude 3 Opus\", provider: \"Anthropic\" },\n { value: \"claude-3-sonnet\", label: \"Claude 3 Sonnet\", provider: \"Anthropic\" },\n { value: \"claude-3-haiku\", label: \"Claude 3 Haiku\", provider: \"Anthropic\" },\n { value: \"llama-3-70b\", label: \"Llama 3 70B\", provider: \"Meta\" },\n { value: \"llama-3-8b\", label: \"Llama 3 8B\", provider: \"Meta\" },\n { value: \"mixtral-8x7b\", label: \"Mixtral 8x7B\", provider: \"Mistral\" },\n { value: \"gemini-pro\", label: \"Gemini Pro\", provider: \"Google\" },\n] as const\n\ntype AIModel = (typeof AI_MODELS)[number][\"value\"]\n\ninterface Message {\n id: string\n role: \"user\" | \"assistant\" | \"system\"\n content: string\n timestamp: Date\n model?: AIModel\n}\n\ninterface AIPlaygroundProps {\n className?: string\n defaultModel?: AIModel\n defaultSystemPrompt?: string\n onSendMessage?: (message: string, config: PlaygroundConfig) => Promise<string>\n showSettings?: boolean\n}\n\ninterface PlaygroundConfig {\n model: AIModel\n temperature: number\n maxTokens: number\n systemPrompt: string\n}\n\nexport function AIPlayground({\n className,\n defaultModel = \"gpt-4\",\n defaultSystemPrompt = \"You are a helpful AI assistant.\",\n onSendMessage,\n showSettings = true,\n}: AIPlaygroundProps) {\n // State management\n const [messages, setMessages] = React.useState<Message[]>([])\n const [input, setInput] = React.useState(\"\")\n const [isLoading, setIsLoading] = React.useState(false)\n const [error, setError] = React.useState<string | null>(null)\n const [copiedId, setCopiedId] = React.useState<string | null>(null)\n const [showConfig, setShowConfig] = React.useState(false)\n\n // Configuration state\n const [config, setConfig] = React.useState<PlaygroundConfig>({\n model: defaultModel,\n temperature: 0.7,\n maxTokens: 2000,\n systemPrompt: defaultSystemPrompt,\n })\n\n // Refs\n const messagesEndRef = React.useRef<HTMLDivElement>(null)\n const textareaRef = React.useRef<HTMLTextAreaElement>(null)\n\n // Auto-scroll to bottom when new messages arrive\n const scrollToBottom = () => {\n messagesEndRef.current?.scrollIntoView({ behavior: \"smooth\" })\n }\n\n React.useEffect(() => {\n scrollToBottom()\n }, [messages])\n\n // Handle sending a message\n const handleSend = async () => {\n if (!input.trim()) return\n\n const userMessage: Message = {\n id: `msg-${Date.now()}`,\n role: \"user\",\n content: input.trim(),\n timestamp: new Date(),\n }\n\n setMessages((prev) => [...prev, userMessage])\n setInput(\"\")\n setIsLoading(true)\n setError(null)\n\n try {\n let response: string\n\n if (onSendMessage) {\n // Use custom message handler if provided\n response = await onSendMessage(userMessage.content, config)\n } else {\n // Mock response for demo purposes\n await new Promise((resolve) => setTimeout(resolve, 1000))\n response = `This is a mock response from ${config.model}. In a real implementation, this would call your AI API with the following configuration:\n\nModel: ${config.model}\nTemperature: ${config.temperature}\nMax Tokens: ${config.maxTokens}\nSystem Prompt: ${config.systemPrompt}\n\nYour message: \"${userMessage.content}\"`\n }\n\n const assistantMessage: Message = {\n id: `msg-${Date.now()}`,\n role: \"assistant\",\n content: response,\n timestamp: new Date(),\n model: config.model,\n }\n\n setMessages((prev) => [...prev, assistantMessage])\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to send message\")\n } finally {\n setIsLoading(false)\n // Focus back on textarea\n setTimeout(() => textareaRef.current?.focus(), 100)\n }\n }\n\n // Handle copying message content\n const handleCopy = async (message: Message) => {\n try {\n await navigator.clipboard.writeText(message.content)\n setCopiedId(message.id)\n setTimeout(() => setCopiedId(null), 2000)\n } catch {\n setError(\"Failed to copy to clipboard\")\n }\n }\n\n // Handle clearing all messages\n const handleClear = () => {\n setMessages([])\n setError(null)\n }\n\n // Handle Enter key press\n const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault()\n handleSend()\n }\n }\n\n return (\n <Card className={cn(\"w-full max-w-4xl\", className)}>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <div>\n <CardTitle className=\"flex items-center gap-2\">\n <Sparkles className=\"h-5 w-5\" />\n AI Playground\n </CardTitle>\n <CardDescription>\n Interact with various AI models in real-time\n </CardDescription>\n </div>\n <div className=\"flex items-center gap-2\">\n {messages.length > 0 && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleClear}\n className=\"h-8\"\n >\n <Trash2 className=\"h-4 w-4 mr-1\" />\n Clear\n </Button>\n )}\n {showSettings && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setShowConfig(!showConfig)}\n className=\"h-8\"\n >\n <Settings2 className=\"h-4 w-4 mr-1\" />\n Settings\n </Button>\n )}\n </div>\n </div>\n </CardHeader>\n\n <CardContent>\n <Grid columns={1} gap={16}>\n {/* Configuration Panel */}\n {showConfig && (\n <Grid\n columns={1}\n gap={16}\n className=\"rounded-lg border bg-muted/50 p-4\"\n >\n {/* Two controls side by side once there is room for both, one\n above the other when there is not. The floor measures the\n column, so no breakpoint says where that happens. */}\n <Grid columns={{ min: 260, max: 2 }} gap={16}>\n <Grid columns={1} gap={8}>\n <Label htmlFor=\"model\">Model</Label>\n <Select\n value={config.model}\n onValueChange={(value: AIModel) =>\n setConfig((prev) => ({ ...prev, model: value }))\n }\n >\n <SelectTrigger id=\"model\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {AI_MODELS.map((model) => (\n <SelectItem key={model.value} value={model.value}>\n <Grid columns={1} gap={0}>\n <span>{model.label}</span>\n <span className=\"text-xs text-muted-foreground\">\n {model.provider}\n </span>\n </Grid>\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </Grid>\n\n <Grid columns={1} gap={8}>\n <Label htmlFor=\"max-tokens\">\n Max Tokens: {config.maxTokens}\n </Label>\n <Slider\n id=\"max-tokens\"\n min={100}\n max={4000}\n step={100}\n value={[config.maxTokens]}\n onValueChange={(value) =>\n setConfig((prev) => ({ ...prev, maxTokens: value[0] }))\n }\n className=\"w-full\"\n />\n </Grid>\n </Grid>\n\n <Grid columns={1} gap={8}>\n <Label htmlFor=\"temperature\">\n Temperature: {config.temperature.toFixed(1)}\n </Label>\n <Slider\n id=\"temperature\"\n min={0}\n max={2}\n step={0.1}\n value={[config.temperature]}\n onValueChange={(value) =>\n setConfig((prev) => ({ ...prev, temperature: value[0] }))\n }\n className=\"w-full\"\n />\n <p className=\"text-xs text-muted-foreground\">\n Lower values make the output more focused and deterministic\n </p>\n </Grid>\n\n <Grid columns={1} gap={8}>\n <Label htmlFor=\"system-prompt\">System Prompt</Label>\n <Textarea\n id=\"system-prompt\"\n placeholder=\"Enter system prompt...\"\n value={config.systemPrompt}\n onChange={(e) =>\n setConfig((prev) => ({\n ...prev,\n systemPrompt: e.target.value,\n }))\n }\n className=\"min-h-[80px] resize-none\"\n />\n </Grid>\n </Grid>\n )}\n\n {/* Error Alert */}\n {error && (\n <Alert variant=\"destructive\">\n <AlertCircle className=\"h-4 w-4\" />\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n )}\n\n {/* Messages Container */}\n <Grid\n columns={1}\n gap={16}\n className=\"rounded-lg border bg-muted/30 min-h-[300px] max-h-[500px] overflow-y-auto p-4\"\n style={{ alignContent: \"start\" }}\n >\n {messages.length === 0 ? (\n <Grid\n columns={1}\n gap={4}\n className=\"h-[280px] text-muted-foreground\"\n style={{ justifyItems: \"center\", alignContent: \"center\" }}\n >\n <Sparkles className=\"h-8 w-8 opacity-50\" />\n <p className=\"text-sm\">Start a conversation with AI</p>\n <p className=\"text-xs\">\n Using {AI_MODELS.find((m) => m.value === config.model)?.label}\n </p>\n </Grid>\n ) : (\n <>\n {messages.map((message) => (\n // One full-width track; the bubble is placed at one end of\n // it. `justify-end` on a flex row did the same thing by\n // pushing a sibling that is not there.\n <Grid\n key={message.id}\n columns={1}\n gap={0}\n style={{\n justifyItems: message.role === \"user\" ? \"end\" : \"start\",\n }}\n >\n <div\n className={cn(\n \"rounded-lg px-4 py-2 max-w-[80%] relative group\",\n message.role === \"user\"\n ? \"bg-primary text-primary-foreground\"\n : \"bg-muted\"\n )}\n >\n {message.role === \"assistant\" && message.model && (\n <div className=\"text-xs opacity-70 mb-1\">\n {\n AI_MODELS.find((m) => m.value === message.model)\n ?.label\n }\n </div>\n )}\n <div className=\"whitespace-pre-wrap break-words\">\n {message.content}\n </div>\n <div className=\"flex items-center gap-2 mt-2\">\n <span className=\"text-xs opacity-50\">\n {message.timestamp.toLocaleTimeString()}\n </span>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => handleCopy(message)}\n className=\"h-6 w-6 p-0 opacity-0 group-hover:opacity-100 transition-opacity\"\n >\n {copiedId === message.id ? (\n <Check className=\"h-3 w-3\" />\n ) : (\n <Copy className=\"h-3 w-3\" />\n )}\n </Button>\n </div>\n </div>\n </Grid>\n ))}\n {isLoading && (\n <div className=\"w-fit rounded-lg px-4 py-2 bg-muted\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n </div>\n )}\n <div ref={messagesEndRef} />\n </>\n )}\n </Grid>\n\n {/* Input Area */}\n <Grid columns=\"minmax(0, 1fr) auto\" gap={8}>\n <Textarea\n ref={textareaRef}\n placeholder=\"Type your message...\"\n value={input}\n onChange={(e) => setInput(e.target.value)}\n onKeyDown={handleKeyDown}\n disabled={isLoading}\n className=\"min-h-[80px] resize-none\"\n />\n <Button\n onClick={handleSend}\n disabled={!input.trim() || isLoading}\n className=\"h-[80px]\"\n >\n {isLoading ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Send className=\"h-4 w-4\" />\n )}\n </Button>\n </Grid>\n </Grid>\n </CardContent>\n\n <CardFooter className=\"text-xs text-muted-foreground\">\n <p>\n Model: {AI_MODELS.find((m) => m.value === config.model)?.label} •\n Temperature: {config.temperature} • Max Tokens: {config.maxTokens}\n </p>\n </CardFooter>\n </Card>\n )\n}\n\n// Export additional types for external use\nexport type { Message, PlaygroundConfig, AIPlaygroundProps }\nexport { AI_MODELS }\n"
|
|
}
|
|
],
|
|
"type": "components:ai"
|
|
} |