Humbl3m33 commited on
Commit
0cd248c
·
verified ·
1 Parent(s): c6276fe

Upload 10 files

Browse files
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
+ }