Spaces:
No application file
No application file
Upload 10 files
Browse files- chat-input.tsx +93 -0
- chat-interface.tsx +126 -0
- chat-messages.tsx +96 -0
- file-upload.tsx +84 -0
- navbar.tsx +60 -0
- provider-selector.tsx +84 -0
- suggestion-pills.tsx +23 -0
- theme-provider.tsx +7 -0
- theme-toggle.tsx +25 -0
- website-preview.tsx +171 -0
chat-input.tsx
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import type React from "react"
|
| 4 |
+
|
| 5 |
+
import { useState, useRef, type KeyboardEvent } from "react"
|
| 6 |
+
import { Button } from "@/components/ui/button"
|
| 7 |
+
import { Textarea } from "@/components/ui/textarea"
|
| 8 |
+
import { Send, Trash2, Paperclip } from "lucide-react"
|
| 9 |
+
|
| 10 |
+
interface ChatInputProps {
|
| 11 |
+
onSend: (message: string, file?: File) => void
|
| 12 |
+
isLoading: boolean
|
| 13 |
+
onClear: () => void
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export function ChatInput({ onSend, isLoading, onClear }: ChatInputProps) {
|
| 17 |
+
const [message, setMessage] = useState("")
|
| 18 |
+
const [file, setFile] = useState<File | null>(null)
|
| 19 |
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
| 20 |
+
|
| 21 |
+
const handleSend = () => {
|
| 22 |
+
if (message.trim() || file) {
|
| 23 |
+
onSend(message.trim(), file || undefined)
|
| 24 |
+
setMessage("")
|
| 25 |
+
setFile(null)
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
const handleKeyPress = (e: KeyboardEvent) => {
|
| 30 |
+
if (e.key === "Enter" && !e.shiftKey) {
|
| 31 |
+
e.preventDefault()
|
| 32 |
+
handleSend()
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 37 |
+
const selectedFile = e.target.files?.[0]
|
| 38 |
+
if (selectedFile) {
|
| 39 |
+
setFile(selectedFile)
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
return (
|
| 44 |
+
<div className="space-y-2">
|
| 45 |
+
{file && (
|
| 46 |
+
<div className="flex items-center gap-2 p-2 bg-muted rounded-md">
|
| 47 |
+
<Paperclip className="h-4 w-4" />
|
| 48 |
+
<span className="text-sm">{file.name}</span>
|
| 49 |
+
<Button variant="ghost" size="sm" onClick={() => setFile(null)}>
|
| 50 |
+
×
|
| 51 |
+
</Button>
|
| 52 |
+
</div>
|
| 53 |
+
)}
|
| 54 |
+
|
| 55 |
+
<div className="flex gap-2">
|
| 56 |
+
<div className="flex-1 relative">
|
| 57 |
+
<Textarea
|
| 58 |
+
value={message}
|
| 59 |
+
onChange={(e) => setMessage(e.target.value)}
|
| 60 |
+
onKeyPress={handleKeyPress}
|
| 61 |
+
placeholder="Type your message... (Shift+Enter for new line)"
|
| 62 |
+
className="min-h-[60px] resize-none pr-12"
|
| 63 |
+
disabled={isLoading}
|
| 64 |
+
/>
|
| 65 |
+
<input
|
| 66 |
+
ref={fileInputRef}
|
| 67 |
+
type="file"
|
| 68 |
+
className="hidden"
|
| 69 |
+
onChange={handleFileSelect}
|
| 70 |
+
accept=".txt,.json,.md,.pdf"
|
| 71 |
+
/>
|
| 72 |
+
<Button
|
| 73 |
+
variant="ghost"
|
| 74 |
+
size="sm"
|
| 75 |
+
className="absolute right-2 top-2 h-8 w-8 p-0"
|
| 76 |
+
onClick={() => fileInputRef.current?.click()}
|
| 77 |
+
>
|
| 78 |
+
<Paperclip className="h-4 w-4" />
|
| 79 |
+
</Button>
|
| 80 |
+
</div>
|
| 81 |
+
|
| 82 |
+
<div className="flex flex-col gap-2">
|
| 83 |
+
<Button onClick={handleSend} disabled={isLoading || (!message.trim() && !file)} size="sm">
|
| 84 |
+
<Send className="h-4 w-4" />
|
| 85 |
+
</Button>
|
| 86 |
+
<Button variant="outline" onClick={onClear} size="sm">
|
| 87 |
+
<Trash2 className="h-4 w-4" />
|
| 88 |
+
</Button>
|
| 89 |
+
</div>
|
| 90 |
+
</div>
|
| 91 |
+
</div>
|
| 92 |
+
)
|
| 93 |
+
}
|
chat-interface.tsx
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import type React from "react"
|
| 4 |
+
import { useState, useRef } from "react"
|
| 5 |
+
import { Card } from "@/components/ui/card"
|
| 6 |
+
import { Button } from "@/components/ui/button"
|
| 7 |
+
import { Badge } from "@/components/ui/badge"
|
| 8 |
+
import { ChatMessages } from "@/components/chat-messages"
|
| 9 |
+
import { ChatInput } from "@/components/chat-input"
|
| 10 |
+
import { ProviderSelector } from "@/components/provider-selector"
|
| 11 |
+
import { SuggestionPills } from "@/components/suggestion-pills"
|
| 12 |
+
import { useChat } from "@/hooks/use-chat"
|
| 13 |
+
import { Download, Upload, FileText, Copy, Globe, Zap } from "lucide-react"
|
| 14 |
+
import type { AIProvider } from "@/types/chat"
|
| 15 |
+
|
| 16 |
+
interface ChatInterfaceProps {
|
| 17 |
+
messages?: any[]
|
| 18 |
+
onSendMessage?: (message: string) => void
|
| 19 |
+
suggestions?: string[]
|
| 20 |
+
isGenerating?: boolean
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export function ChatInterface({
|
| 24 |
+
messages: externalMessages,
|
| 25 |
+
onSendMessage: externalSendMessage,
|
| 26 |
+
suggestions: externalSuggestions,
|
| 27 |
+
isGenerating: externalIsGenerating,
|
| 28 |
+
}: ChatInterfaceProps) {
|
| 29 |
+
const [selectedProvider, setSelectedProvider] = useState<AIProvider>("openai")
|
| 30 |
+
const [selectedModel, setSelectedModel] = useState<string>("gpt-4o")
|
| 31 |
+
|
| 32 |
+
const {
|
| 33 |
+
messages: internalMessages,
|
| 34 |
+
isLoading,
|
| 35 |
+
sendMessage: internalSendMessage,
|
| 36 |
+
suggestions: internalSuggestions,
|
| 37 |
+
clearChat,
|
| 38 |
+
uploadHistory,
|
| 39 |
+
downloadHistory,
|
| 40 |
+
exportHTML,
|
| 41 |
+
copyJSON,
|
| 42 |
+
} = useChat(selectedProvider)
|
| 43 |
+
|
| 44 |
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
| 45 |
+
|
| 46 |
+
// Use external props if provided, otherwise use internal state
|
| 47 |
+
const messages = externalMessages || internalMessages
|
| 48 |
+
const sendMessage = externalSendMessage || internalSendMessage
|
| 49 |
+
const suggestions = externalSuggestions || internalSuggestions
|
| 50 |
+
const isGenerating = externalIsGenerating || isLoading
|
| 51 |
+
|
| 52 |
+
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
| 53 |
+
const file = event.target.files?.[0]
|
| 54 |
+
if (file) {
|
| 55 |
+
uploadHistory(file)
|
| 56 |
+
event.target.value = ""
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
return (
|
| 61 |
+
<Card className="w-full h-[700px] flex flex-col">
|
| 62 |
+
<div className="p-4 border-b">
|
| 63 |
+
<div className="flex items-center justify-between mb-3">
|
| 64 |
+
<div className="flex items-center gap-3">
|
| 65 |
+
<Badge variant="secondary" className="gap-1">
|
| 66 |
+
<Zap className="h-3 w-3" />
|
| 67 |
+
Software Engineering Agent
|
| 68 |
+
</Badge>
|
| 69 |
+
<Badge variant="outline" className="gap-1">
|
| 70 |
+
<Globe className="h-3 w-3" />
|
| 71 |
+
Website Generator
|
| 72 |
+
</Badge>
|
| 73 |
+
</div>
|
| 74 |
+
<div className="text-sm text-muted-foreground">{messages.length} messages</div>
|
| 75 |
+
</div>
|
| 76 |
+
|
| 77 |
+
<div className="flex items-center justify-between">
|
| 78 |
+
<ProviderSelector
|
| 79 |
+
selected={selectedProvider}
|
| 80 |
+
onSelect={setSelectedProvider}
|
| 81 |
+
selectedModel={selectedModel}
|
| 82 |
+
onModelSelect={setSelectedModel}
|
| 83 |
+
/>
|
| 84 |
+
|
| 85 |
+
<div className="flex items-center gap-2">
|
| 86 |
+
<Button variant="outline" size="sm" onClick={() => fileInputRef.current?.click()} className="gap-2">
|
| 87 |
+
<Upload className="h-4 w-4" />
|
| 88 |
+
Import
|
| 89 |
+
</Button>
|
| 90 |
+
|
| 91 |
+
<Button variant="outline" size="sm" onClick={downloadHistory} className="gap-2 bg-transparent">
|
| 92 |
+
<Download className="h-4 w-4" />
|
| 93 |
+
JSON
|
| 94 |
+
</Button>
|
| 95 |
+
|
| 96 |
+
<Button variant="outline" size="sm" onClick={exportHTML} className="gap-2 bg-transparent">
|
| 97 |
+
<FileText className="h-4 w-4" />
|
| 98 |
+
HTML
|
| 99 |
+
</Button>
|
| 100 |
+
|
| 101 |
+
<Button variant="outline" size="sm" onClick={copyJSON} className="gap-2 bg-transparent">
|
| 102 |
+
<Copy className="h-4 w-4" />
|
| 103 |
+
Copy
|
| 104 |
+
</Button>
|
| 105 |
+
</div>
|
| 106 |
+
</div>
|
| 107 |
+
|
| 108 |
+
<input ref={fileInputRef} type="file" accept=".json" onChange={handleFileUpload} className="hidden" />
|
| 109 |
+
</div>
|
| 110 |
+
|
| 111 |
+
<div className="flex-1 flex flex-col overflow-hidden">
|
| 112 |
+
<ChatMessages messages={messages} isLoading={isGenerating} />
|
| 113 |
+
|
| 114 |
+
{suggestions.length > 0 && (
|
| 115 |
+
<div className="p-4 border-t">
|
| 116 |
+
<SuggestionPills suggestions={suggestions} onSelect={sendMessage} />
|
| 117 |
+
</div>
|
| 118 |
+
)}
|
| 119 |
+
|
| 120 |
+
<div className="p-4 border-t">
|
| 121 |
+
<ChatInput onSend={sendMessage} isLoading={isGenerating} onClear={clearChat} />
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
</Card>
|
| 125 |
+
)
|
| 126 |
+
}
|
chat-messages.tsx
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import { useEffect, useRef } from "react"
|
| 4 |
+
import { ScrollArea } from "@/components/ui/scroll-area"
|
| 5 |
+
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
| 6 |
+
import { Card } from "@/components/ui/card"
|
| 7 |
+
import { Button } from "@/components/ui/button"
|
| 8 |
+
import { Copy, User, Bot } from "lucide-react"
|
| 9 |
+
import type { Message } from "@/types/chat"
|
| 10 |
+
import { useToast } from "@/hooks/use-toast"
|
| 11 |
+
|
| 12 |
+
interface ChatMessagesProps {
|
| 13 |
+
messages: Message[]
|
| 14 |
+
isLoading: boolean
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export function ChatMessages({ messages, isLoading }: ChatMessagesProps) {
|
| 18 |
+
const scrollRef = useRef<HTMLDivElement>(null)
|
| 19 |
+
const { toast } = useToast()
|
| 20 |
+
|
| 21 |
+
useEffect(() => {
|
| 22 |
+
if (scrollRef.current) {
|
| 23 |
+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
|
| 24 |
+
}
|
| 25 |
+
}, [messages])
|
| 26 |
+
|
| 27 |
+
const copyToClipboard = (text: string) => {
|
| 28 |
+
navigator.clipboard.writeText(text)
|
| 29 |
+
toast({
|
| 30 |
+
title: "Copied to clipboard",
|
| 31 |
+
duration: 2000,
|
| 32 |
+
})
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
return (
|
| 36 |
+
<ScrollArea className="flex-1 p-4" ref={scrollRef}>
|
| 37 |
+
<div className="space-y-4">
|
| 38 |
+
{messages.map((message) => (
|
| 39 |
+
<div key={message.id} className={`flex gap-3 ${message.role === "user" ? "justify-end" : "justify-start"}`}>
|
| 40 |
+
{message.role === "assistant" && (
|
| 41 |
+
<Avatar className="h-8 w-8">
|
| 42 |
+
<AvatarFallback>
|
| 43 |
+
<Bot className="h-4 w-4" />
|
| 44 |
+
</AvatarFallback>
|
| 45 |
+
</Avatar>
|
| 46 |
+
)}
|
| 47 |
+
|
| 48 |
+
<Card
|
| 49 |
+
className={`max-w-[80%] p-3 ${
|
| 50 |
+
message.role === "user" ? "bg-primary text-primary-foreground" : "bg-muted"
|
| 51 |
+
}`}
|
| 52 |
+
>
|
| 53 |
+
<div className="flex items-start justify-between gap-2">
|
| 54 |
+
<div className="whitespace-pre-wrap text-sm">{message.content}</div>
|
| 55 |
+
<Button
|
| 56 |
+
variant="ghost"
|
| 57 |
+
size="sm"
|
| 58 |
+
className="h-6 w-6 p-0 opacity-50 hover:opacity-100"
|
| 59 |
+
onClick={() => copyToClipboard(message.content)}
|
| 60 |
+
>
|
| 61 |
+
<Copy className="h-3 w-3" />
|
| 62 |
+
</Button>
|
| 63 |
+
</div>
|
| 64 |
+
{message.provider && <div className="text-xs opacity-70 mt-2">via {message.provider}</div>}
|
| 65 |
+
</Card>
|
| 66 |
+
|
| 67 |
+
{message.role === "user" && (
|
| 68 |
+
<Avatar className="h-8 w-8">
|
| 69 |
+
<AvatarFallback>
|
| 70 |
+
<User className="h-4 w-4" />
|
| 71 |
+
</AvatarFallback>
|
| 72 |
+
</Avatar>
|
| 73 |
+
)}
|
| 74 |
+
</div>
|
| 75 |
+
))}
|
| 76 |
+
|
| 77 |
+
{isLoading && (
|
| 78 |
+
<div className="flex gap-3 justify-start">
|
| 79 |
+
<Avatar className="h-8 w-8">
|
| 80 |
+
<AvatarFallback>
|
| 81 |
+
<Bot className="h-4 w-4" />
|
| 82 |
+
</AvatarFallback>
|
| 83 |
+
</Avatar>
|
| 84 |
+
<Card className="bg-muted p-3">
|
| 85 |
+
<div className="flex space-x-1">
|
| 86 |
+
<div className="w-2 h-2 bg-current rounded-full animate-bounce" />
|
| 87 |
+
<div className="w-2 h-2 bg-current rounded-full animate-bounce" style={{ animationDelay: "0.1s" }} />
|
| 88 |
+
<div className="w-2 h-2 bg-current rounded-full animate-bounce" style={{ animationDelay: "0.2s" }} />
|
| 89 |
+
</div>
|
| 90 |
+
</Card>
|
| 91 |
+
</div>
|
| 92 |
+
)}
|
| 93 |
+
</div>
|
| 94 |
+
</ScrollArea>
|
| 95 |
+
)
|
| 96 |
+
}
|
file-upload.tsx
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import type React from "react"
|
| 4 |
+
|
| 5 |
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
| 6 |
+
import { Button } from "@/components/ui/button"
|
| 7 |
+
import { Input } from "@/components/ui/input"
|
| 8 |
+
import { Label } from "@/components/ui/label"
|
| 9 |
+
import { Upload, Download, FileText, Archive, Code } from "lucide-react"
|
| 10 |
+
import { useRef } from "react"
|
| 11 |
+
|
| 12 |
+
interface FileUploadProps {
|
| 13 |
+
onUpload: (file: File) => void
|
| 14 |
+
onDownload: () => void
|
| 15 |
+
onExportHTML: () => void
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export function FileUpload({ onUpload, onDownload, onExportHTML }: FileUploadProps) {
|
| 19 |
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
| 20 |
+
|
| 21 |
+
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
| 22 |
+
const file = event.target.files?.[0]
|
| 23 |
+
if (file) {
|
| 24 |
+
onUpload(file)
|
| 25 |
+
event.target.value = ""
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
const exportZip = () => {
|
| 30 |
+
// This would create a ZIP file with all generated files
|
| 31 |
+
console.log("Exporting as ZIP...")
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
return (
|
| 35 |
+
<Card>
|
| 36 |
+
<CardHeader>
|
| 37 |
+
<CardTitle className="flex items-center gap-2">
|
| 38 |
+
<Archive className="h-5 w-5" />
|
| 39 |
+
File Management
|
| 40 |
+
</CardTitle>
|
| 41 |
+
</CardHeader>
|
| 42 |
+
<CardContent className="space-y-4">
|
| 43 |
+
<div className="space-y-2">
|
| 44 |
+
<Label htmlFor="file-upload">Import Chat History</Label>
|
| 45 |
+
<div className="flex gap-2">
|
| 46 |
+
<Input
|
| 47 |
+
id="file-upload"
|
| 48 |
+
type="file"
|
| 49 |
+
accept=".json"
|
| 50 |
+
onChange={handleFileUpload}
|
| 51 |
+
ref={fileInputRef}
|
| 52 |
+
className="flex-1"
|
| 53 |
+
/>
|
| 54 |
+
<Button variant="outline" onClick={() => fileInputRef.current?.click()}>
|
| 55 |
+
<Upload className="h-4 w-4" />
|
| 56 |
+
</Button>
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
|
| 60 |
+
<div className="grid grid-cols-2 gap-2">
|
| 61 |
+
<Button variant="outline" onClick={onDownload} className="gap-2 bg-transparent">
|
| 62 |
+
<Download className="h-4 w-4" />
|
| 63 |
+
JSON History
|
| 64 |
+
</Button>
|
| 65 |
+
|
| 66 |
+
<Button variant="outline" onClick={onExportHTML} className="gap-2 bg-transparent">
|
| 67 |
+
<FileText className="h-4 w-4" />
|
| 68 |
+
HTML Export
|
| 69 |
+
</Button>
|
| 70 |
+
|
| 71 |
+
<Button variant="outline" onClick={exportZip} className="gap-2 bg-transparent">
|
| 72 |
+
<Archive className="h-4 w-4" />
|
| 73 |
+
ZIP Package
|
| 74 |
+
</Button>
|
| 75 |
+
|
| 76 |
+
<Button variant="outline" className="gap-2 bg-transparent">
|
| 77 |
+
<Code className="h-4 w-4" />
|
| 78 |
+
Source Code
|
| 79 |
+
</Button>
|
| 80 |
+
</div>
|
| 81 |
+
</CardContent>
|
| 82 |
+
</Card>
|
| 83 |
+
)
|
| 84 |
+
}
|
navbar.tsx
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import Link from "next/link"
|
| 4 |
+
import { usePathname } from "next/navigation"
|
| 5 |
+
import { Button } from "@/components/ui/button"
|
| 6 |
+
import { ThemeToggle } from "@/components/theme-toggle"
|
| 7 |
+
import { MessageSquare, History, Settings, Download, Upload } from "lucide-react"
|
| 8 |
+
import { useChatHistory } from "@/hooks/use-chat-history"
|
| 9 |
+
|
| 10 |
+
export function Navbar() {
|
| 11 |
+
const pathname = usePathname()
|
| 12 |
+
const { exportHistory, importHistory } = useChatHistory()
|
| 13 |
+
|
| 14 |
+
return (
|
| 15 |
+
<nav className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
| 16 |
+
<div className="container mx-auto px-4">
|
| 17 |
+
<div className="flex h-16 items-center justify-between">
|
| 18 |
+
<div className="flex items-center space-x-6">
|
| 19 |
+
<Link href="/" className="flex items-center space-x-2">
|
| 20 |
+
<MessageSquare className="h-6 w-6" />
|
| 21 |
+
<span className="font-bold">AI Agent</span>
|
| 22 |
+
</Link>
|
| 23 |
+
|
| 24 |
+
<div className="flex items-center space-x-4">
|
| 25 |
+
<Link href="/">
|
| 26 |
+
<Button variant={pathname === "/" ? "default" : "ghost"} size="sm">
|
| 27 |
+
Chat
|
| 28 |
+
</Button>
|
| 29 |
+
</Link>
|
| 30 |
+
<Link href="/history">
|
| 31 |
+
<Button variant={pathname === "/history" ? "default" : "ghost"} size="sm">
|
| 32 |
+
<History className="h-4 w-4 mr-2" />
|
| 33 |
+
History
|
| 34 |
+
</Button>
|
| 35 |
+
</Link>
|
| 36 |
+
<Link href="/settings">
|
| 37 |
+
<Button variant={pathname === "/settings" ? "default" : "ghost"} size="sm">
|
| 38 |
+
<Settings className="h-4 w-4 mr-2" />
|
| 39 |
+
Settings
|
| 40 |
+
</Button>
|
| 41 |
+
</Link>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
|
| 45 |
+
<div className="flex items-center space-x-2">
|
| 46 |
+
<Button variant="outline" size="sm" onClick={exportHistory}>
|
| 47 |
+
<Download className="h-4 w-4 mr-2" />
|
| 48 |
+
Export
|
| 49 |
+
</Button>
|
| 50 |
+
<Button variant="outline" size="sm" onClick={importHistory}>
|
| 51 |
+
<Upload className="h-4 w-4 mr-2" />
|
| 52 |
+
Import
|
| 53 |
+
</Button>
|
| 54 |
+
<ThemeToggle />
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
</nav>
|
| 59 |
+
)
|
| 60 |
+
}
|
provider-selector.tsx
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
| 4 |
+
import { Badge } from "@/components/ui/badge"
|
| 5 |
+
import type { AIProvider } from "@/types/chat"
|
| 6 |
+
|
| 7 |
+
interface ProviderSelectorProps {
|
| 8 |
+
selected: AIProvider
|
| 9 |
+
onSelect: (provider: AIProvider) => void
|
| 10 |
+
selectedModel?: string
|
| 11 |
+
onModelSelect?: (model: string) => void
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
const providers = [
|
| 15 |
+
{
|
| 16 |
+
id: "openai" as AIProvider,
|
| 17 |
+
name: "OpenAI",
|
| 18 |
+
badge: "GPT-4",
|
| 19 |
+
models: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-3.5-turbo"],
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
id: "openrouter" as AIProvider,
|
| 23 |
+
name: "OpenRouter",
|
| 24 |
+
badge: "Multi-Model",
|
| 25 |
+
models: ["openai/gpt-4o", "anthropic/claude-3.5-sonnet", "meta-llama/llama-3.1-405b", "google/gemini-pro-1.5"],
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
id: "groq" as AIProvider,
|
| 29 |
+
name: "Groq",
|
| 30 |
+
badge: "Fast",
|
| 31 |
+
models: ["llama-3.1-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768"],
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
id: "grok" as AIProvider,
|
| 35 |
+
name: "Grok",
|
| 36 |
+
badge: "xAI",
|
| 37 |
+
models: ["grok-beta", "grok-vision-beta"],
|
| 38 |
+
},
|
| 39 |
+
]
|
| 40 |
+
|
| 41 |
+
export function ProviderSelector({ selected, onSelect, selectedModel, onModelSelect }: ProviderSelectorProps) {
|
| 42 |
+
const currentProvider = providers.find((p) => p.id === selected)
|
| 43 |
+
|
| 44 |
+
return (
|
| 45 |
+
<div className="flex items-center gap-3">
|
| 46 |
+
<span className="text-sm font-medium">AI Provider:</span>
|
| 47 |
+
<Select value={selected} onValueChange={onSelect}>
|
| 48 |
+
<SelectTrigger className="w-48">
|
| 49 |
+
<SelectValue />
|
| 50 |
+
</SelectTrigger>
|
| 51 |
+
<SelectContent>
|
| 52 |
+
{providers.map((provider) => (
|
| 53 |
+
<SelectItem key={provider.id} value={provider.id}>
|
| 54 |
+
<div className="flex items-center gap-2">
|
| 55 |
+
<span>{provider.name}</span>
|
| 56 |
+
<Badge variant="secondary" className="text-xs">
|
| 57 |
+
{provider.badge}
|
| 58 |
+
</Badge>
|
| 59 |
+
</div>
|
| 60 |
+
</SelectItem>
|
| 61 |
+
))}
|
| 62 |
+
</SelectContent>
|
| 63 |
+
</Select>
|
| 64 |
+
|
| 65 |
+
{onModelSelect && currentProvider && (
|
| 66 |
+
<>
|
| 67 |
+
<span className="text-sm font-medium">Model:</span>
|
| 68 |
+
<Select value={selectedModel || currentProvider.models[0]} onValueChange={onModelSelect}>
|
| 69 |
+
<SelectTrigger className="w-64">
|
| 70 |
+
<SelectValue />
|
| 71 |
+
</SelectTrigger>
|
| 72 |
+
<SelectContent>
|
| 73 |
+
{currentProvider.models.map((model) => (
|
| 74 |
+
<SelectItem key={model} value={model}>
|
| 75 |
+
<span className="font-mono text-sm">{model}</span>
|
| 76 |
+
</SelectItem>
|
| 77 |
+
))}
|
| 78 |
+
</SelectContent>
|
| 79 |
+
</Select>
|
| 80 |
+
</>
|
| 81 |
+
)}
|
| 82 |
+
</div>
|
| 83 |
+
)
|
| 84 |
+
}
|
suggestion-pills.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import { Button } from "@/components/ui/button"
|
| 4 |
+
|
| 5 |
+
interface SuggestionPillsProps {
|
| 6 |
+
suggestions: string[]
|
| 7 |
+
onSelect: (suggestion: string) => void
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export function SuggestionPills({ suggestions, onSelect }: SuggestionPillsProps) {
|
| 11 |
+
return (
|
| 12 |
+
<div className="space-y-2">
|
| 13 |
+
<div className="text-sm text-muted-foreground">Suggestions:</div>
|
| 14 |
+
<div className="flex flex-wrap gap-2">
|
| 15 |
+
{suggestions.map((suggestion, index) => (
|
| 16 |
+
<Button key={index} variant="outline" size="sm" onClick={() => onSelect(suggestion)} className="text-xs">
|
| 17 |
+
{suggestion}
|
| 18 |
+
</Button>
|
| 19 |
+
))}
|
| 20 |
+
</div>
|
| 21 |
+
</div>
|
| 22 |
+
)
|
| 23 |
+
}
|
theme-provider.tsx
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
| 3 |
+
import type { ThemeProviderProps } from "next-themes"
|
| 4 |
+
|
| 5 |
+
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
| 6 |
+
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
| 7 |
+
}
|
theme-toggle.tsx
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import { Moon, Sun } from "lucide-react"
|
| 4 |
+
import { useTheme } from "next-themes"
|
| 5 |
+
import { Button } from "@/components/ui/button"
|
| 6 |
+
import { useEffect, useState } from "react"
|
| 7 |
+
|
| 8 |
+
export function ThemeToggle() {
|
| 9 |
+
const { theme, setTheme } = useTheme()
|
| 10 |
+
const [mounted, setMounted] = useState(false)
|
| 11 |
+
|
| 12 |
+
useEffect(() => {
|
| 13 |
+
setMounted(true)
|
| 14 |
+
}, [])
|
| 15 |
+
|
| 16 |
+
if (!mounted) {
|
| 17 |
+
return null
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
return (
|
| 21 |
+
<Button variant="outline" size="sm" onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
|
| 22 |
+
{theme === "light" ? <Moon className="h-4 w-4" /> : <Sun className="h-4 w-4" />}
|
| 23 |
+
</Button>
|
| 24 |
+
)
|
| 25 |
+
}
|
website-preview.tsx
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
| 4 |
+
import { Button } from "@/components/ui/button"
|
| 5 |
+
import { Badge } from "@/components/ui/badge"
|
| 6 |
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
| 7 |
+
import { ScrollArea } from "@/components/ui/scroll-area"
|
| 8 |
+
import { Code, Eye, Download, Copy, RefreshCw } from "lucide-react"
|
| 9 |
+
import { useState } from "react"
|
| 10 |
+
|
| 11 |
+
interface WebsitePreviewProps {
|
| 12 |
+
generatedWebsite?: {
|
| 13 |
+
html: string
|
| 14 |
+
css: string
|
| 15 |
+
js: string
|
| 16 |
+
preview: string
|
| 17 |
+
}
|
| 18 |
+
isGenerating?: boolean
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export function WebsitePreview({ generatedWebsite, isGenerating }: WebsitePreviewProps) {
|
| 22 |
+
const [activeTab, setActiveTab] = useState("preview")
|
| 23 |
+
|
| 24 |
+
const copyToClipboard = (content: string) => {
|
| 25 |
+
navigator.clipboard.writeText(content)
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
const downloadFile = (content: string, filename: string) => {
|
| 29 |
+
const blob = new Blob([content], { type: "text/plain" })
|
| 30 |
+
const url = URL.createObjectURL(blob)
|
| 31 |
+
const a = document.createElement("a")
|
| 32 |
+
a.href = url
|
| 33 |
+
a.download = filename
|
| 34 |
+
a.click()
|
| 35 |
+
URL.revokeObjectURL(url)
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
return (
|
| 39 |
+
<Card className="w-full h-[700px] flex flex-col">
|
| 40 |
+
<CardHeader className="pb-3">
|
| 41 |
+
<div className="flex items-center justify-between">
|
| 42 |
+
<CardTitle className="flex items-center gap-2">
|
| 43 |
+
<Eye className="h-5 w-5" />
|
| 44 |
+
Website Preview
|
| 45 |
+
</CardTitle>
|
| 46 |
+
<div className="flex items-center gap-2">
|
| 47 |
+
{isGenerating && (
|
| 48 |
+
<Badge variant="secondary" className="gap-1">
|
| 49 |
+
<RefreshCw className="h-3 w-3 animate-spin" />
|
| 50 |
+
Generating...
|
| 51 |
+
</Badge>
|
| 52 |
+
)}
|
| 53 |
+
{generatedWebsite && <Badge variant="default">Ready</Badge>}
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
</CardHeader>
|
| 57 |
+
|
| 58 |
+
<CardContent className="flex-1 flex flex-col overflow-hidden p-0">
|
| 59 |
+
{generatedWebsite ? (
|
| 60 |
+
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col">
|
| 61 |
+
<div className="px-6 pb-0">
|
| 62 |
+
<TabsList className="grid w-full grid-cols-4">
|
| 63 |
+
<TabsTrigger value="preview">Preview</TabsTrigger>
|
| 64 |
+
<TabsTrigger value="html">HTML</TabsTrigger>
|
| 65 |
+
<TabsTrigger value="css">CSS</TabsTrigger>
|
| 66 |
+
<TabsTrigger value="js">JavaScript</TabsTrigger>
|
| 67 |
+
</TabsList>
|
| 68 |
+
</div>
|
| 69 |
+
|
| 70 |
+
<div className="flex-1 overflow-hidden">
|
| 71 |
+
<TabsContent value="preview" className="h-full m-0 p-6">
|
| 72 |
+
<div className="h-full border rounded-lg overflow-hidden bg-white">
|
| 73 |
+
<iframe
|
| 74 |
+
srcDoc={generatedWebsite.preview}
|
| 75 |
+
className="w-full h-full border-0"
|
| 76 |
+
title="Website Preview"
|
| 77 |
+
/>
|
| 78 |
+
</div>
|
| 79 |
+
</TabsContent>
|
| 80 |
+
|
| 81 |
+
<TabsContent value="html" className="h-full m-0 p-6">
|
| 82 |
+
<div className="h-full flex flex-col">
|
| 83 |
+
<div className="flex items-center justify-between mb-3">
|
| 84 |
+
<h3 className="text-sm font-medium">HTML Code</h3>
|
| 85 |
+
<div className="flex gap-2">
|
| 86 |
+
<Button size="sm" variant="outline" onClick={() => copyToClipboard(generatedWebsite.html)}>
|
| 87 |
+
<Copy className="h-4 w-4" />
|
| 88 |
+
</Button>
|
| 89 |
+
<Button
|
| 90 |
+
size="sm"
|
| 91 |
+
variant="outline"
|
| 92 |
+
onClick={() => downloadFile(generatedWebsite.html, "index.html")}
|
| 93 |
+
>
|
| 94 |
+
<Download className="h-4 w-4" />
|
| 95 |
+
</Button>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
<ScrollArea className="flex-1 border rounded-lg">
|
| 99 |
+
<pre className="p-4 text-sm">
|
| 100 |
+
<code>{generatedWebsite.html}</code>
|
| 101 |
+
</pre>
|
| 102 |
+
</ScrollArea>
|
| 103 |
+
</div>
|
| 104 |
+
</TabsContent>
|
| 105 |
+
|
| 106 |
+
<TabsContent value="css" className="h-full m-0 p-6">
|
| 107 |
+
<div className="h-full flex flex-col">
|
| 108 |
+
<div className="flex items-center justify-between mb-3">
|
| 109 |
+
<h3 className="text-sm font-medium">CSS Code</h3>
|
| 110 |
+
<div className="flex gap-2">
|
| 111 |
+
<Button size="sm" variant="outline" onClick={() => copyToClipboard(generatedWebsite.css)}>
|
| 112 |
+
<Copy className="h-4 w-4" />
|
| 113 |
+
</Button>
|
| 114 |
+
<Button
|
| 115 |
+
size="sm"
|
| 116 |
+
variant="outline"
|
| 117 |
+
onClick={() => downloadFile(generatedWebsite.css, "styles.css")}
|
| 118 |
+
>
|
| 119 |
+
<Download className="h-4 w-4" />
|
| 120 |
+
</Button>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
<ScrollArea className="flex-1 border rounded-lg">
|
| 124 |
+
<pre className="p-4 text-sm">
|
| 125 |
+
<code>{generatedWebsite.css}</code>
|
| 126 |
+
</pre>
|
| 127 |
+
</ScrollArea>
|
| 128 |
+
</div>
|
| 129 |
+
</TabsContent>
|
| 130 |
+
|
| 131 |
+
<TabsContent value="js" className="h-full m-0 p-6">
|
| 132 |
+
<div className="h-full flex flex-col">
|
| 133 |
+
<div className="flex items-center justify-between mb-3">
|
| 134 |
+
<h3 className="text-sm font-medium">JavaScript Code</h3>
|
| 135 |
+
<div className="flex gap-2">
|
| 136 |
+
<Button size="sm" variant="outline" onClick={() => copyToClipboard(generatedWebsite.js)}>
|
| 137 |
+
<Copy className="h-4 w-4" />
|
| 138 |
+
</Button>
|
| 139 |
+
<Button
|
| 140 |
+
size="sm"
|
| 141 |
+
variant="outline"
|
| 142 |
+
onClick={() => downloadFile(generatedWebsite.js, "script.js")}
|
| 143 |
+
>
|
| 144 |
+
<Download className="h-4 w-4" />
|
| 145 |
+
</Button>
|
| 146 |
+
</div>
|
| 147 |
+
</div>
|
| 148 |
+
<ScrollArea className="flex-1 border rounded-lg">
|
| 149 |
+
<pre className="p-4 text-sm">
|
| 150 |
+
<code>{generatedWebsite.js}</code>
|
| 151 |
+
</pre>
|
| 152 |
+
</ScrollArea>
|
| 153 |
+
</div>
|
| 154 |
+
</TabsContent>
|
| 155 |
+
</div>
|
| 156 |
+
</Tabs>
|
| 157 |
+
) : (
|
| 158 |
+
<div className="flex-1 flex items-center justify-center p-6">
|
| 159 |
+
<div className="text-center">
|
| 160 |
+
<Code className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
| 161 |
+
<h3 className="text-lg font-medium mb-2">No Website Generated</h3>
|
| 162 |
+
<p className="text-muted-foreground text-sm">
|
| 163 |
+
Ask the AI agent to generate a website, landing page, or web application
|
| 164 |
+
</p>
|
| 165 |
+
</div>
|
| 166 |
+
</div>
|
| 167 |
+
)}
|
| 168 |
+
</CardContent>
|
| 169 |
+
</Card>
|
| 170 |
+
)
|
| 171 |
+
}
|