gpt-engineer-app[bot] commited on
Commit
808fe45
·
1 Parent(s): fea8c8a
.env ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ VITE_SUPABASE_PROJECT_ID="rxrhjvwnfxtosbneybwa"
2
+ VITE_SUPABASE_PUBLISHABLE_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJ4cmhqdnduZnh0b3NibmV5YndhIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjkzMTQyNzcsImV4cCI6MjA4NDg5MDI3N30.__UwHyl1SV2LNw-NM2hMpNtfL5b_ae7Pw3Yv9oacCIs"
3
+ VITE_SUPABASE_URL="https://rxrhjvwnfxtosbneybwa.supabase.co"
index.html CHANGED
@@ -1,22 +1,41 @@
1
  <!doctype html>
2
- <html lang="en">
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <!-- TODO: Set the document title to the name of your application -->
7
- <title>Lovable App</title>
8
- <meta name="description" content="Lovable Generated Project" />
9
  <meta name="author" content="Lovable" />
 
10
 
11
- <!-- TODO: Update og:title to match your application name -->
12
- <meta property="og:title" content="Lovable App" />
13
- <meta property="og:description" content="Lovable Generated Project" />
 
 
 
 
 
 
 
14
  <meta property="og:type" content="website" />
15
  <meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
16
 
 
17
  <meta name="twitter:card" content="summary_large_image" />
18
  <meta name="twitter:site" content="@Lovable" />
19
  <meta name="twitter:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
 
 
 
 
 
 
 
 
 
 
 
20
  </head>
21
 
22
  <body>
 
1
  <!doctype html>
2
+ <html lang="en" class="dark">
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>AI Desktop Assistant</title>
7
+ <meta name="description" content="Your intelligent desktop assistant with vision and voice capabilities. Ask questions, share your screen, and get help with any task." />
 
8
  <meta name="author" content="Lovable" />
9
+ <meta name="theme-color" content="#0f172a" />
10
 
11
+ <!-- PWA Meta Tags -->
12
+ <link rel="manifest" href="/manifest.webmanifest" />
13
+ <link rel="apple-touch-icon" href="/pwa-192x192.png" />
14
+ <meta name="apple-mobile-web-app-capable" content="yes" />
15
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
16
+ <meta name="apple-mobile-web-app-title" content="AI Assistant" />
17
+
18
+ <!-- Open Graph -->
19
+ <meta property="og:title" content="AI Desktop Assistant" />
20
+ <meta property="og:description" content="Your intelligent desktop assistant with vision and voice capabilities" />
21
  <meta property="og:type" content="website" />
22
  <meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
23
 
24
+ <!-- Twitter -->
25
  <meta name="twitter:card" content="summary_large_image" />
26
  <meta name="twitter:site" content="@Lovable" />
27
  <meta name="twitter:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
28
+
29
+ <!-- Fonts -->
30
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
31
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
32
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
33
+
34
+ <style>
35
+ body {
36
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
37
+ }
38
+ </style>
39
  </head>
40
 
41
  <body>
package-lock.json CHANGED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -41,12 +41,14 @@
41
  "@radix-ui/react-toggle": "^1.1.9",
42
  "@radix-ui/react-toggle-group": "^1.1.10",
43
  "@radix-ui/react-tooltip": "^1.2.7",
 
44
  "@tanstack/react-query": "^5.83.0",
45
  "class-variance-authority": "^0.7.1",
46
  "clsx": "^2.1.1",
47
  "cmdk": "^1.1.1",
48
  "date-fns": "^3.6.0",
49
  "embla-carousel-react": "^8.6.0",
 
50
  "input-otp": "^1.4.2",
51
  "lucide-react": "^0.462.0",
52
  "next-themes": "^0.3.0",
@@ -61,13 +63,14 @@
61
  "tailwind-merge": "^2.6.0",
62
  "tailwindcss-animate": "^1.0.7",
63
  "vaul": "^0.9.9",
 
64
  "zod": "^3.25.76"
65
  },
66
  "devDependencies": {
67
  "@eslint/js": "^9.32.0",
 
68
  "@testing-library/jest-dom": "^6.6.0",
69
  "@testing-library/react": "^16.0.0",
70
- "@tailwindcss/typography": "^0.5.16",
71
  "@types/node": "^22.16.5",
72
  "@types/react": "^18.3.23",
73
  "@types/react-dom": "^18.3.7",
 
41
  "@radix-ui/react-toggle": "^1.1.9",
42
  "@radix-ui/react-toggle-group": "^1.1.10",
43
  "@radix-ui/react-tooltip": "^1.2.7",
44
+ "@supabase/supabase-js": "^2.91.1",
45
  "@tanstack/react-query": "^5.83.0",
46
  "class-variance-authority": "^0.7.1",
47
  "clsx": "^2.1.1",
48
  "cmdk": "^1.1.1",
49
  "date-fns": "^3.6.0",
50
  "embla-carousel-react": "^8.6.0",
51
+ "framer-motion": "^12.29.0",
52
  "input-otp": "^1.4.2",
53
  "lucide-react": "^0.462.0",
54
  "next-themes": "^0.3.0",
 
63
  "tailwind-merge": "^2.6.0",
64
  "tailwindcss-animate": "^1.0.7",
65
  "vaul": "^0.9.9",
66
+ "vite-plugin-pwa": "^1.2.0",
67
  "zod": "^3.25.76"
68
  },
69
  "devDependencies": {
70
  "@eslint/js": "^9.32.0",
71
+ "@tailwindcss/typography": "^0.5.16",
72
  "@testing-library/jest-dom": "^6.6.0",
73
  "@testing-library/react": "^16.0.0",
 
74
  "@types/node": "^22.16.5",
75
  "@types/react": "^18.3.23",
76
  "@types/react-dom": "^18.3.7",
public/pwa-192x192.png ADDED
public/pwa-512x512.png ADDED
src/components/ChatInput.tsx ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef, useEffect } from "react";
2
+ import { motion, AnimatePresence } from "framer-motion";
3
+ import { Send, Mic, MicOff, Monitor, MonitorOff, Volume2, VolumeX } from "lucide-react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Textarea } from "@/components/ui/textarea";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ interface ChatInputProps {
9
+ onSendMessage: (message: string) => void;
10
+ isLoading: boolean;
11
+ isListening: boolean;
12
+ isSpeaking: boolean;
13
+ isSharing: boolean;
14
+ voiceEnabled: boolean;
15
+ transcript: string;
16
+ onStartListening: () => void;
17
+ onStopListening: () => void;
18
+ onToggleVoice: () => void;
19
+ onToggleScreenShare: () => void;
20
+ }
21
+
22
+ export function ChatInput({
23
+ onSendMessage,
24
+ isLoading,
25
+ isListening,
26
+ isSpeaking,
27
+ isSharing,
28
+ voiceEnabled,
29
+ transcript,
30
+ onStartListening,
31
+ onStopListening,
32
+ onToggleVoice,
33
+ onToggleScreenShare,
34
+ }: ChatInputProps) {
35
+ const [input, setInput] = useState("");
36
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
37
+
38
+ // Update input with voice transcript
39
+ useEffect(() => {
40
+ if (transcript && !isListening) {
41
+ setInput(transcript);
42
+ }
43
+ }, [transcript, isListening]);
44
+
45
+ const handleSubmit = () => {
46
+ if (input.trim() && !isLoading) {
47
+ onSendMessage(input.trim());
48
+ setInput("");
49
+ if (textareaRef.current) {
50
+ textareaRef.current.style.height = "auto";
51
+ }
52
+ }
53
+ };
54
+
55
+ const handleKeyDown = (e: React.KeyboardEvent) => {
56
+ if (e.key === "Enter" && !e.shiftKey) {
57
+ e.preventDefault();
58
+ handleSubmit();
59
+ }
60
+ };
61
+
62
+ const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
63
+ setInput(e.target.value);
64
+ // Auto-resize
65
+ const textarea = e.target;
66
+ textarea.style.height = "auto";
67
+ textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
68
+ };
69
+
70
+ return (
71
+ <div className="p-4 border-t border-border bg-background/50 backdrop-blur-sm">
72
+ <div className="max-w-3xl mx-auto">
73
+ {/* Voice transcript indicator */}
74
+ <AnimatePresence>
75
+ {isListening && (
76
+ <motion.div
77
+ initial={{ opacity: 0, y: 10 }}
78
+ animate={{ opacity: 1, y: 0 }}
79
+ exit={{ opacity: 0, y: 10 }}
80
+ className="mb-3 px-4 py-2 rounded-lg bg-primary/10 border border-primary/20"
81
+ >
82
+ <div className="flex items-center gap-2">
83
+ <div className="flex gap-1">
84
+ {[...Array(3)].map((_, i) => (
85
+ <div
86
+ key={i}
87
+ className="w-1 h-4 bg-primary rounded-full animate-voice-wave"
88
+ style={{ animationDelay: `${i * 0.1}s` }}
89
+ />
90
+ ))}
91
+ </div>
92
+ <span className="text-sm text-primary">
93
+ {transcript || "Listening..."}
94
+ </span>
95
+ </div>
96
+ </motion.div>
97
+ )}
98
+ </AnimatePresence>
99
+
100
+ <div className="flex items-end gap-2">
101
+ {/* Screen Share Button */}
102
+ <Button
103
+ variant="ghost"
104
+ size="icon"
105
+ onClick={onToggleScreenShare}
106
+ className={cn(
107
+ "shrink-0 rounded-full transition-colors",
108
+ isSharing
109
+ ? "bg-primary/20 text-primary hover:bg-primary/30"
110
+ : "text-muted-foreground hover:text-foreground"
111
+ )}
112
+ title={isSharing ? "Stop sharing screen" : "Share screen"}
113
+ >
114
+ {isSharing ? (
115
+ <MonitorOff className="h-5 w-5" />
116
+ ) : (
117
+ <Monitor className="h-5 w-5" />
118
+ )}
119
+ </Button>
120
+
121
+ {/* Voice Toggle Button */}
122
+ <Button
123
+ variant="ghost"
124
+ size="icon"
125
+ onClick={onToggleVoice}
126
+ className={cn(
127
+ "shrink-0 rounded-full transition-colors",
128
+ voiceEnabled
129
+ ? "bg-primary/20 text-primary hover:bg-primary/30"
130
+ : "text-muted-foreground hover:text-foreground"
131
+ )}
132
+ title={voiceEnabled ? "Disable voice responses" : "Enable voice responses"}
133
+ >
134
+ {voiceEnabled ? (
135
+ <Volume2 className="h-5 w-5" />
136
+ ) : (
137
+ <VolumeX className="h-5 w-5" />
138
+ )}
139
+ </Button>
140
+
141
+ {/* Input Area */}
142
+ <div className="flex-1 relative">
143
+ <Textarea
144
+ ref={textareaRef}
145
+ value={input}
146
+ onChange={handleTextareaChange}
147
+ onKeyDown={handleKeyDown}
148
+ placeholder="Ask me anything..."
149
+ className="min-h-[48px] max-h-[200px] resize-none pr-24 rounded-2xl bg-input border-border focus-visible:ring-primary"
150
+ disabled={isLoading}
151
+ rows={1}
152
+ />
153
+
154
+ {/* Action buttons inside textarea */}
155
+ <div className="absolute right-2 bottom-2 flex items-center gap-1">
156
+ {/* Mic Button */}
157
+ <Button
158
+ variant="ghost"
159
+ size="icon"
160
+ onClick={isListening ? onStopListening : onStartListening}
161
+ className={cn(
162
+ "h-8 w-8 rounded-full transition-all",
163
+ isListening
164
+ ? "bg-red-500/20 text-red-500 hover:bg-red-500/30 animate-pulse-glow"
165
+ : "text-muted-foreground hover:text-foreground"
166
+ )}
167
+ disabled={isLoading}
168
+ >
169
+ {isListening ? (
170
+ <MicOff className="h-4 w-4" />
171
+ ) : (
172
+ <Mic className="h-4 w-4" />
173
+ )}
174
+ </Button>
175
+
176
+ {/* Send Button */}
177
+ <Button
178
+ size="icon"
179
+ onClick={handleSubmit}
180
+ disabled={!input.trim() || isLoading}
181
+ className="h-8 w-8 rounded-full bg-primary hover:bg-primary/90"
182
+ >
183
+ <Send className="h-4 w-4" />
184
+ </Button>
185
+ </div>
186
+ </div>
187
+ </div>
188
+
189
+ <p className="mt-2 text-center text-xs text-muted-foreground">
190
+ AI can make mistakes. Consider checking important information.
191
+ </p>
192
+ </div>
193
+ </div>
194
+ );
195
+ }
src/components/ChatInterface.tsx ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useRef, useEffect, useState } from "react";
2
+ import { AnimatePresence } from "framer-motion";
3
+ import { Menu, PanelLeftClose } from "lucide-react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { ScrollArea } from "@/components/ui/scroll-area";
6
+ import { ChatMessage } from "@/components/ChatMessage";
7
+ import { ChatInput } from "@/components/ChatInput";
8
+ import { WelcomeScreen } from "@/components/WelcomeScreen";
9
+ import { ScreenSharePreview } from "@/components/ScreenSharePreview";
10
+ import { ConversationSidebar } from "@/components/ConversationSidebar";
11
+ import { useChat } from "@/hooks/useChat";
12
+ import { useVoice } from "@/hooks/useVoice";
13
+ import { useScreenShare } from "@/hooks/useScreenShare";
14
+ import { cn } from "@/lib/utils";
15
+
16
+ export function ChatInterface() {
17
+ const [sidebarOpen, setSidebarOpen] = useState(true);
18
+ const [voiceEnabled, setVoiceEnabled] = useState(false);
19
+ const scrollRef = useRef<HTMLDivElement>(null);
20
+
21
+ const {
22
+ conversations,
23
+ activeConversation,
24
+ activeConversationId,
25
+ messages,
26
+ isLoading,
27
+ createConversation,
28
+ setActiveConversationId,
29
+ deleteConversation,
30
+ sendMessage,
31
+ } = useChat();
32
+
33
+ const {
34
+ isListening,
35
+ isSpeaking,
36
+ transcript,
37
+ startListening,
38
+ stopListening,
39
+ speak,
40
+ stopSpeaking,
41
+ } = useVoice();
42
+
43
+ const {
44
+ isSharing,
45
+ stream,
46
+ videoRef,
47
+ canvasRef,
48
+ startSharing,
49
+ stopSharing,
50
+ getScreenDescription,
51
+ } = useScreenShare();
52
+
53
+ // Auto-scroll to bottom when new messages arrive
54
+ useEffect(() => {
55
+ if (scrollRef.current) {
56
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
57
+ }
58
+ }, [messages]);
59
+
60
+ // Speak assistant responses if voice is enabled
61
+ useEffect(() => {
62
+ if (voiceEnabled && messages.length > 0) {
63
+ const lastMessage = messages[messages.length - 1];
64
+ if (lastMessage.role === "assistant" && !lastMessage.isStreaming) {
65
+ speak(lastMessage.content);
66
+ }
67
+ }
68
+ }, [messages, voiceEnabled, speak]);
69
+
70
+ const handleSendMessage = async (content: string) => {
71
+ let screenContext: string | undefined;
72
+
73
+ if (isSharing) {
74
+ const description = await getScreenDescription();
75
+ if (description) {
76
+ screenContext = "User is sharing their screen. The current screen shows: " + description;
77
+ }
78
+ }
79
+
80
+ sendMessage(content, screenContext);
81
+ };
82
+
83
+ const handleToggleScreenShare = async () => {
84
+ if (isSharing) {
85
+ stopSharing();
86
+ } else {
87
+ await startSharing();
88
+ }
89
+ };
90
+
91
+ const handleToggleVoice = () => {
92
+ if (voiceEnabled) {
93
+ stopSpeaking();
94
+ }
95
+ setVoiceEnabled(!voiceEnabled);
96
+ };
97
+
98
+ const handleVoiceSubmit = () => {
99
+ if (transcript) {
100
+ handleSendMessage(transcript);
101
+ }
102
+ };
103
+
104
+ // Submit voice input when user stops speaking
105
+ useEffect(() => {
106
+ if (!isListening && transcript) {
107
+ handleVoiceSubmit();
108
+ }
109
+ }, [isListening]);
110
+
111
+ return (
112
+ <div className="flex h-screen bg-background">
113
+ {/* Hidden video and canvas for screen capture */}
114
+ <video ref={videoRef} className="hidden" autoPlay muted playsInline />
115
+ <canvas ref={canvasRef} className="hidden" />
116
+
117
+ {/* Sidebar */}
118
+ <AnimatePresence mode="wait">
119
+ {sidebarOpen && (
120
+ <div className="w-72 border-r border-border shrink-0">
121
+ <ConversationSidebar
122
+ conversations={conversations}
123
+ activeConversationId={activeConversationId}
124
+ onSelectConversation={setActiveConversationId}
125
+ onNewConversation={createConversation}
126
+ onDeleteConversation={deleteConversation}
127
+ />
128
+ </div>
129
+ )}
130
+ </AnimatePresence>
131
+
132
+ {/* Main Chat Area */}
133
+ <div className="flex-1 flex flex-col min-w-0">
134
+ {/* Header */}
135
+ <header className="h-14 px-4 flex items-center justify-between border-b border-border bg-background/80 backdrop-blur-sm">
136
+ <div className="flex items-center gap-3">
137
+ <Button
138
+ variant="ghost"
139
+ size="icon"
140
+ onClick={() => setSidebarOpen(!sidebarOpen)}
141
+ className="text-muted-foreground hover:text-foreground"
142
+ >
143
+ {sidebarOpen ? (
144
+ <PanelLeftClose className="h-5 w-5" />
145
+ ) : (
146
+ <Menu className="h-5 w-5" />
147
+ )}
148
+ </Button>
149
+ <h1 className="font-semibold text-foreground">
150
+ {activeConversation?.title || "AI Assistant"}
151
+ </h1>
152
+ </div>
153
+
154
+ {isSharing && (
155
+ <div className="flex items-center gap-2 px-3 py-1.5 rounded-full bg-primary/10 border border-primary/20">
156
+ <div className="w-2 h-2 rounded-full bg-primary animate-pulse" />
157
+ <span className="text-xs font-medium text-primary">Screen Sharing Active</span>
158
+ </div>
159
+ )}
160
+ </header>
161
+
162
+ {/* Messages Area */}
163
+ <ScrollArea className="flex-1" ref={scrollRef}>
164
+ <div className="max-w-3xl mx-auto px-4">
165
+ {messages.length === 0 ? (
166
+ <WelcomeScreen onSuggestionClick={handleSendMessage} />
167
+ ) : (
168
+ <div className="py-4">
169
+ {messages.map((message) => (
170
+ <ChatMessage key={message.id} message={message} />
171
+ ))}
172
+ </div>
173
+ )}
174
+ </div>
175
+ </ScrollArea>
176
+
177
+ {/* Input Area */}
178
+ <ChatInput
179
+ onSendMessage={handleSendMessage}
180
+ isLoading={isLoading}
181
+ isListening={isListening}
182
+ isSpeaking={isSpeaking}
183
+ isSharing={isSharing}
184
+ voiceEnabled={voiceEnabled}
185
+ transcript={transcript}
186
+ onStartListening={startListening}
187
+ onStopListening={stopListening}
188
+ onToggleVoice={handleToggleVoice}
189
+ onToggleScreenShare={handleToggleScreenShare}
190
+ />
191
+ </div>
192
+
193
+ {/* Screen Share Preview */}
194
+ <AnimatePresence>
195
+ <ScreenSharePreview
196
+ stream={stream}
197
+ isSharing={isSharing}
198
+ onStop={stopSharing}
199
+ />
200
+ </AnimatePresence>
201
+ </div>
202
+ );
203
+ }
src/components/ChatMessage.tsx ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { motion } from "framer-motion";
2
+ import { User, Bot } from "lucide-react";
3
+ import { Message } from "@/types/chat";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ interface ChatMessageProps {
7
+ message: Message;
8
+ }
9
+
10
+ export function ChatMessage({ message }: ChatMessageProps) {
11
+ const isUser = message.role === "user";
12
+
13
+ return (
14
+ <motion.div
15
+ initial={{ opacity: 0, y: 10 }}
16
+ animate={{ opacity: 1, y: 0 }}
17
+ transition={{ duration: 0.2 }}
18
+ className={cn("flex gap-3 py-4", isUser ? "flex-row-reverse" : "")}
19
+ >
20
+ {/* Avatar */}
21
+ <div
22
+ className={cn(
23
+ "flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center",
24
+ isUser
25
+ ? "bg-primary text-primary-foreground"
26
+ : "bg-accent text-accent-foreground"
27
+ )}
28
+ >
29
+ {isUser ? (
30
+ <User className="h-4 w-4" />
31
+ ) : (
32
+ <Bot className="h-4 w-4" />
33
+ )}
34
+ </div>
35
+
36
+ {/* Message Bubble */}
37
+ <div
38
+ className={cn(
39
+ "max-w-[80%] rounded-2xl px-4 py-3",
40
+ isUser
41
+ ? "bg-primary text-primary-foreground rounded-tr-sm"
42
+ : "bg-card text-card-foreground rounded-tl-sm border border-border"
43
+ )}
44
+ >
45
+ <p className="text-sm whitespace-pre-wrap leading-relaxed">
46
+ {message.content}
47
+ {message.isStreaming && (
48
+ <span className="inline-block w-2 h-4 ml-1 bg-current animate-pulse rounded-sm" />
49
+ )}
50
+ </p>
51
+ </div>
52
+ </motion.div>
53
+ );
54
+ }
src/components/ConversationSidebar.tsx ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { motion } from "framer-motion";
2
+ import { Plus, MessageSquare, Trash2 } from "lucide-react";
3
+ import { Button } from "@/components/ui/button";
4
+ import { ScrollArea } from "@/components/ui/scroll-area";
5
+ import { Conversation } from "@/types/chat";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ interface ConversationSidebarProps {
9
+ conversations: Conversation[];
10
+ activeConversationId: string | null;
11
+ onSelectConversation: (id: string) => void;
12
+ onNewConversation: () => void;
13
+ onDeleteConversation: (id: string) => void;
14
+ }
15
+
16
+ export function ConversationSidebar({
17
+ conversations,
18
+ activeConversationId,
19
+ onSelectConversation,
20
+ onNewConversation,
21
+ onDeleteConversation,
22
+ }: ConversationSidebarProps) {
23
+ const groupedConversations = groupByDate(conversations);
24
+
25
+ return (
26
+ <div className="flex flex-col h-full bg-sidebar">
27
+ {/* Header */}
28
+ <div className="p-4 border-b border-sidebar-border">
29
+ <Button
30
+ onClick={onNewConversation}
31
+ className="w-full gap-2 bg-primary/10 hover:bg-primary/20 text-primary border-0"
32
+ variant="outline"
33
+ >
34
+ <Plus className="h-4 w-4" />
35
+ New chat
36
+ </Button>
37
+ </div>
38
+
39
+ {/* Conversation List */}
40
+ <ScrollArea className="flex-1 px-2">
41
+ <div className="py-2 space-y-4">
42
+ {Object.entries(groupedConversations).map(([group, items]) => (
43
+ <div key={group}>
44
+ <h3 className="px-3 py-2 text-xs font-medium text-muted-foreground uppercase tracking-wider">
45
+ {group}
46
+ </h3>
47
+ <div className="space-y-1">
48
+ {items.map((conversation) => (
49
+ <motion.div
50
+ key={conversation.id}
51
+ initial={{ opacity: 0, x: -10 }}
52
+ animate={{ opacity: 1, x: 0 }}
53
+ className="group relative"
54
+ >
55
+ <button
56
+ onClick={() => onSelectConversation(conversation.id)}
57
+ className={cn(
58
+ "w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-colors",
59
+ activeConversationId === conversation.id
60
+ ? "bg-sidebar-accent text-sidebar-accent-foreground"
61
+ : "text-sidebar-foreground hover:bg-sidebar-accent/50"
62
+ )}
63
+ >
64
+ <MessageSquare className="h-4 w-4 shrink-0 text-muted-foreground" />
65
+ <span className="truncate text-sm">
66
+ {conversation.title || "New conversation"}
67
+ </span>
68
+ </button>
69
+ <Button
70
+ size="icon"
71
+ variant="ghost"
72
+ className="absolute right-2 top-1/2 -translate-y-1/2 h-7 w-7 opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-destructive"
73
+ onClick={(e) => {
74
+ e.stopPropagation();
75
+ onDeleteConversation(conversation.id);
76
+ }}
77
+ >
78
+ <Trash2 className="h-3.5 w-3.5" />
79
+ </Button>
80
+ </motion.div>
81
+ ))}
82
+ </div>
83
+ </div>
84
+ ))}
85
+
86
+ {conversations.length === 0 && (
87
+ <div className="px-3 py-8 text-center text-muted-foreground text-sm">
88
+ No conversations yet
89
+ </div>
90
+ )}
91
+ </div>
92
+ </ScrollArea>
93
+ </div>
94
+ );
95
+ }
96
+
97
+ function groupByDate(conversations: Conversation[]): Record<string, Conversation[]> {
98
+ const today = new Date();
99
+ const yesterday = new Date(today);
100
+ yesterday.setDate(yesterday.getDate() - 1);
101
+
102
+ const groups: Record<string, Conversation[]> = {};
103
+
104
+ conversations.forEach((conversation) => {
105
+ const date = new Date(conversation.updatedAt);
106
+ let group: string;
107
+
108
+ if (isSameDay(date, today)) {
109
+ group = "Today";
110
+ } else if (isSameDay(date, yesterday)) {
111
+ group = "Yesterday";
112
+ } else {
113
+ group = "Older";
114
+ }
115
+
116
+ if (!groups[group]) {
117
+ groups[group] = [];
118
+ }
119
+ groups[group].push(conversation);
120
+ });
121
+
122
+ return groups;
123
+ }
124
+
125
+ function isSameDay(date1: Date, date2: Date): boolean {
126
+ return (
127
+ date1.getFullYear() === date2.getFullYear() &&
128
+ date1.getMonth() === date2.getMonth() &&
129
+ date1.getDate() === date2.getDate()
130
+ );
131
+ }
src/components/ScreenSharePreview.tsx ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useRef, useEffect } from "react";
2
+ import { motion } from "framer-motion";
3
+ import { X, Eye } from "lucide-react";
4
+ import { Button } from "@/components/ui/button";
5
+
6
+ interface ScreenSharePreviewProps {
7
+ stream: MediaStream | null;
8
+ isSharing: boolean;
9
+ onStop: () => void;
10
+ }
11
+
12
+ export function ScreenSharePreview({ stream, isSharing, onStop }: ScreenSharePreviewProps) {
13
+ const videoRef = useRef<HTMLVideoElement>(null);
14
+
15
+ useEffect(() => {
16
+ if (videoRef.current && stream) {
17
+ videoRef.current.srcObject = stream;
18
+ }
19
+ }, [stream]);
20
+
21
+ if (!isSharing || !stream) return null;
22
+
23
+ return (
24
+ <motion.div
25
+ initial={{ opacity: 0, scale: 0.9, y: 20 }}
26
+ animate={{ opacity: 1, scale: 1, y: 0 }}
27
+ exit={{ opacity: 0, scale: 0.9, y: 20 }}
28
+ className="fixed bottom-24 right-4 w-64 rounded-xl overflow-hidden shadow-2xl border border-border bg-card"
29
+ >
30
+ {/* Header */}
31
+ <div className="flex items-center justify-between px-3 py-2 bg-primary/10 border-b border-border">
32
+ <div className="flex items-center gap-2">
33
+ <Eye className="h-4 w-4 text-primary" />
34
+ <span className="text-xs font-medium text-foreground">Screen Sharing</span>
35
+ </div>
36
+ <Button
37
+ variant="ghost"
38
+ size="icon"
39
+ onClick={onStop}
40
+ className="h-6 w-6 rounded-full hover:bg-destructive/20 hover:text-destructive"
41
+ >
42
+ <X className="h-3.5 w-3.5" />
43
+ </Button>
44
+ </div>
45
+
46
+ {/* Video Preview */}
47
+ <div className="relative aspect-video bg-black">
48
+ <video
49
+ ref={videoRef}
50
+ autoPlay
51
+ muted
52
+ playsInline
53
+ className="w-full h-full object-contain"
54
+ />
55
+ <div className="absolute inset-0 ring-2 ring-primary/50 ring-inset rounded-b-xl pointer-events-none" />
56
+ </div>
57
+
58
+ {/* Status */}
59
+ <div className="px-3 py-2 flex items-center gap-2">
60
+ <div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
61
+ <span className="text-xs text-muted-foreground">AI can see your screen</span>
62
+ </div>
63
+ </motion.div>
64
+ );
65
+ }
src/components/WelcomeScreen.tsx ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useRef } from "react";
2
+ import { motion } from "framer-motion";
3
+ import { Bot } from "lucide-react";
4
+
5
+ interface WelcomeScreenProps {
6
+ onSuggestionClick: (suggestion: string) => void;
7
+ }
8
+
9
+ const suggestions = [
10
+ { text: "Write a first draft", description: "of an email, essay, or document" },
11
+ { text: "Get advice", description: "on a project, decision, or idea" },
12
+ { text: "Learn something new", description: "ask about any topic" },
13
+ { text: "Analyze my screen", description: "share your screen and ask questions" },
14
+ ];
15
+
16
+ export function WelcomeScreen({ onSuggestionClick }: WelcomeScreenProps) {
17
+ const getGreeting = () => {
18
+ const hour = new Date().getHours();
19
+ if (hour < 12) return "Good morning";
20
+ if (hour < 18) return "Good afternoon";
21
+ return "Good evening";
22
+ };
23
+
24
+ return (
25
+ <div className="flex flex-col items-center justify-center min-h-[60vh] px-4">
26
+ <motion.div
27
+ initial={{ scale: 0.8, opacity: 0 }}
28
+ animate={{ scale: 1, opacity: 1 }}
29
+ transition={{ duration: 0.5, type: "spring" }}
30
+ className="mb-8"
31
+ >
32
+ <div className="w-20 h-20 rounded-full bg-gradient-to-br from-primary to-blue-400 flex items-center justify-center glow">
33
+ <Bot className="h-10 w-10 text-primary-foreground" />
34
+ </div>
35
+ </motion.div>
36
+
37
+ <motion.h1
38
+ initial={{ y: 20, opacity: 0 }}
39
+ animate={{ y: 0, opacity: 1 }}
40
+ transition={{ delay: 0.1 }}
41
+ className="text-3xl font-semibold text-foreground mb-2"
42
+ >
43
+ {getGreeting()}
44
+ </motion.h1>
45
+
46
+ <motion.p
47
+ initial={{ y: 20, opacity: 0 }}
48
+ animate={{ y: 0, opacity: 1 }}
49
+ transition={{ delay: 0.2 }}
50
+ className="text-lg text-muted-foreground mb-10"
51
+ >
52
+ What can I help you with today?
53
+ </motion.p>
54
+
55
+ <motion.div
56
+ initial={{ y: 20, opacity: 0 }}
57
+ animate={{ y: 0, opacity: 1 }}
58
+ transition={{ delay: 0.3 }}
59
+ className="grid grid-cols-1 sm:grid-cols-2 gap-3 w-full max-w-2xl"
60
+ >
61
+ {suggestions.map((suggestion, index) => (
62
+ <motion.button
63
+ key={index}
64
+ initial={{ opacity: 0, y: 10 }}
65
+ animate={{ opacity: 1, y: 0 }}
66
+ transition={{ delay: 0.4 + index * 0.1 }}
67
+ onClick={() => onSuggestionClick(suggestion.text)}
68
+ className="group p-4 rounded-xl bg-card hover:bg-accent border border-border hover:border-primary/50 text-left transition-all duration-200"
69
+ >
70
+ <p className="font-medium text-foreground group-hover:text-primary transition-colors">
71
+ {suggestion.text}
72
+ </p>
73
+ <p className="text-sm text-muted-foreground">{suggestion.description}</p>
74
+ </motion.button>
75
+ ))}
76
+ </motion.div>
77
+ </div>
78
+ );
79
+ }
src/hooks/useChat.ts ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback } from "react";
2
+ import { Message, Conversation } from "@/types/chat";
3
+
4
+ const CHAT_URL = `${import.meta.env.VITE_SUPABASE_URL}/functions/v1/chat`;
5
+
6
+ export function useChat() {
7
+ const [conversations, setConversations] = useState<Conversation[]>([]);
8
+ const [activeConversationId, setActiveConversationId] = useState<string | null>(null);
9
+ const [isLoading, setIsLoading] = useState(false);
10
+
11
+ const activeConversation = conversations.find((c) => c.id === activeConversationId);
12
+ const messages = activeConversation?.messages || [];
13
+
14
+ const createConversation = useCallback(() => {
15
+ const newConversation: Conversation = {
16
+ id: crypto.randomUUID(),
17
+ title: "New conversation",
18
+ messages: [],
19
+ createdAt: new Date(),
20
+ updatedAt: new Date(),
21
+ };
22
+ setConversations((prev) => [newConversation, ...prev]);
23
+ setActiveConversationId(newConversation.id);
24
+ return newConversation.id;
25
+ }, []);
26
+
27
+ const updateConversationTitle = useCallback((id: string, title: string) => {
28
+ setConversations((prev) =>
29
+ prev.map((c) => (c.id === id ? { ...c, title } : c))
30
+ );
31
+ }, []);
32
+
33
+ const deleteConversation = useCallback((id: string) => {
34
+ setConversations((prev) => prev.filter((c) => c.id !== id));
35
+ if (activeConversationId === id) {
36
+ setActiveConversationId(null);
37
+ }
38
+ }, [activeConversationId]);
39
+
40
+ const sendMessage = useCallback(
41
+ async (content: string, screenContext?: string) => {
42
+ let conversationId = activeConversationId;
43
+
44
+ if (!conversationId) {
45
+ conversationId = createConversation();
46
+ }
47
+
48
+ const userMessage: Message = {
49
+ id: crypto.randomUUID(),
50
+ role: "user",
51
+ content,
52
+ timestamp: new Date(),
53
+ };
54
+
55
+ // Add user message
56
+ setConversations((prev) =>
57
+ prev.map((c) =>
58
+ c.id === conversationId
59
+ ? {
60
+ ...c,
61
+ messages: [...c.messages, userMessage],
62
+ updatedAt: new Date(),
63
+ title: c.messages.length === 0 ? content.slice(0, 50) : c.title,
64
+ }
65
+ : c
66
+ )
67
+ );
68
+
69
+ setIsLoading(true);
70
+
71
+ try {
72
+ const currentConversation = conversations.find((c) => c.id === conversationId);
73
+ const messageHistory = currentConversation?.messages || [];
74
+
75
+ const response = await fetch(CHAT_URL, {
76
+ method: "POST",
77
+ headers: {
78
+ "Content-Type": "application/json",
79
+ Authorization: `Bearer ${import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY}`,
80
+ },
81
+ body: JSON.stringify({
82
+ messages: [
83
+ ...messageHistory.map((m) => ({ role: m.role, content: m.content })),
84
+ { role: "user", content: screenContext ? `[Screen Context: ${screenContext}]\n\n${content}` : content },
85
+ ],
86
+ }),
87
+ });
88
+
89
+ if (!response.ok) {
90
+ throw new Error(`Failed to get response: ${response.status}`);
91
+ }
92
+
93
+ const reader = response.body?.getReader();
94
+ if (!reader) throw new Error("No response body");
95
+
96
+ const decoder = new TextDecoder();
97
+ let assistantContent = "";
98
+ const assistantId = crypto.randomUUID();
99
+
100
+ // Add empty assistant message
101
+ setConversations((prev) =>
102
+ prev.map((c) =>
103
+ c.id === conversationId
104
+ ? {
105
+ ...c,
106
+ messages: [
107
+ ...c.messages,
108
+ {
109
+ id: assistantId,
110
+ role: "assistant" as const,
111
+ content: "",
112
+ timestamp: new Date(),
113
+ isStreaming: true,
114
+ },
115
+ ],
116
+ }
117
+ : c
118
+ )
119
+ );
120
+
121
+ let textBuffer = "";
122
+
123
+ while (true) {
124
+ const { done, value } = await reader.read();
125
+ if (done) break;
126
+
127
+ textBuffer += decoder.decode(value, { stream: true });
128
+
129
+ let newlineIndex: number;
130
+ while ((newlineIndex = textBuffer.indexOf("\n")) !== -1) {
131
+ let line = textBuffer.slice(0, newlineIndex);
132
+ textBuffer = textBuffer.slice(newlineIndex + 1);
133
+
134
+ if (line.endsWith("\r")) line = line.slice(0, -1);
135
+ if (line.startsWith(":") || line.trim() === "") continue;
136
+ if (!line.startsWith("data: ")) continue;
137
+
138
+ const jsonStr = line.slice(6).trim();
139
+ if (jsonStr === "[DONE]") break;
140
+
141
+ try {
142
+ const parsed = JSON.parse(jsonStr);
143
+ const delta = parsed.choices?.[0]?.delta?.content;
144
+ if (delta) {
145
+ assistantContent += delta;
146
+ setConversations((prev) =>
147
+ prev.map((c) =>
148
+ c.id === conversationId
149
+ ? {
150
+ ...c,
151
+ messages: c.messages.map((m) =>
152
+ m.id === assistantId
153
+ ? { ...m, content: assistantContent }
154
+ : m
155
+ ),
156
+ }
157
+ : c
158
+ )
159
+ );
160
+ }
161
+ } catch {
162
+ textBuffer = line + "\n" + textBuffer;
163
+ break;
164
+ }
165
+ }
166
+ }
167
+
168
+ // Mark streaming complete
169
+ setConversations((prev) =>
170
+ prev.map((c) =>
171
+ c.id === conversationId
172
+ ? {
173
+ ...c,
174
+ messages: c.messages.map((m) =>
175
+ m.id === assistantId ? { ...m, isStreaming: false } : m
176
+ ),
177
+ updatedAt: new Date(),
178
+ }
179
+ : c
180
+ )
181
+ );
182
+ } catch (error) {
183
+ console.error("Chat error:", error);
184
+ const errorMessage: Message = {
185
+ id: crypto.randomUUID(),
186
+ role: "assistant",
187
+ content: "Sorry, I encountered an error. Please try again.",
188
+ timestamp: new Date(),
189
+ };
190
+
191
+ setConversations((prev) =>
192
+ prev.map((c) =>
193
+ c.id === conversationId
194
+ ? { ...c, messages: [...c.messages, errorMessage] }
195
+ : c
196
+ )
197
+ );
198
+ } finally {
199
+ setIsLoading(false);
200
+ }
201
+ },
202
+ [activeConversationId, conversations, createConversation]
203
+ );
204
+
205
+ return {
206
+ conversations,
207
+ activeConversation,
208
+ activeConversationId,
209
+ messages,
210
+ isLoading,
211
+ createConversation,
212
+ setActiveConversationId,
213
+ updateConversationTitle,
214
+ deleteConversation,
215
+ sendMessage,
216
+ };
217
+ }
src/hooks/useScreenShare.ts ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback, useRef } from "react";
2
+ import { ScreenShareState } from "@/types/chat";
3
+
4
+ export function useScreenShare() {
5
+ const [state, setState] = useState<ScreenShareState>({
6
+ isSharing: false,
7
+ stream: null,
8
+ error: null,
9
+ });
10
+
11
+ const videoRef = useRef<HTMLVideoElement | null>(null);
12
+ const canvasRef = useRef<HTMLCanvasElement | null>(null);
13
+
14
+ const startSharing = useCallback(async () => {
15
+ try {
16
+ const stream = await navigator.mediaDevices.getDisplayMedia({
17
+ video: {
18
+ displaySurface: "browser",
19
+ },
20
+ audio: false,
21
+ });
22
+
23
+ stream.getVideoTracks()[0].addEventListener("ended", () => {
24
+ setState({ isSharing: false, stream: null, error: null });
25
+ });
26
+
27
+ setState({ isSharing: true, stream, error: null });
28
+ return stream;
29
+ } catch (error) {
30
+ const message = error instanceof Error ? error.message : "Failed to start screen sharing";
31
+ setState({ isSharing: false, stream: null, error: message });
32
+ return null;
33
+ }
34
+ }, []);
35
+
36
+ const stopSharing = useCallback(() => {
37
+ if (state.stream) {
38
+ state.stream.getTracks().forEach((track) => track.stop());
39
+ }
40
+ setState({ isSharing: false, stream: null, error: null });
41
+ }, [state.stream]);
42
+
43
+ const captureFrame = useCallback((): string | null => {
44
+ if (!state.stream || !videoRef.current || !canvasRef.current) return null;
45
+
46
+ const video = videoRef.current;
47
+ const canvas = canvasRef.current;
48
+ const ctx = canvas.getContext("2d");
49
+
50
+ if (!ctx) return null;
51
+
52
+ canvas.width = video.videoWidth;
53
+ canvas.height = video.videoHeight;
54
+ ctx.drawImage(video, 0, 0);
55
+
56
+ return canvas.toDataURL("image/jpeg", 0.8);
57
+ }, [state.stream]);
58
+
59
+ const getScreenDescription = useCallback(async (): Promise<string | null> => {
60
+ const frame = captureFrame();
61
+ if (!frame) return null;
62
+
63
+ // Return base64 image for AI to analyze
64
+ return frame;
65
+ }, [captureFrame]);
66
+
67
+ return {
68
+ ...state,
69
+ videoRef,
70
+ canvasRef,
71
+ startSharing,
72
+ stopSharing,
73
+ captureFrame,
74
+ getScreenDescription,
75
+ };
76
+ }
src/hooks/useVoice.ts ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback, useRef, useEffect } from "react";
2
+ import { VoiceState } from "@/types/chat";
3
+
4
+ // Web Speech API types
5
+ interface SpeechRecognitionEvent extends Event {
6
+ results: SpeechRecognitionResultList;
7
+ }
8
+
9
+ interface SpeechRecognitionErrorEvent extends Event {
10
+ error: string;
11
+ }
12
+
13
+ interface SpeechRecognitionInstance extends EventTarget {
14
+ continuous: boolean;
15
+ interimResults: boolean;
16
+ lang: string;
17
+ start: () => void;
18
+ stop: () => void;
19
+ abort: () => void;
20
+ onresult: ((event: SpeechRecognitionEvent) => void) | null;
21
+ onerror: ((event: SpeechRecognitionErrorEvent) => void) | null;
22
+ onend: (() => void) | null;
23
+ }
24
+
25
+ declare global {
26
+ interface Window {
27
+ SpeechRecognition: new () => SpeechRecognitionInstance;
28
+ webkitSpeechRecognition: new () => SpeechRecognitionInstance;
29
+ }
30
+ }
31
+
32
+ export function useVoice() {
33
+ const [state, setState] = useState<VoiceState>({
34
+ isListening: false,
35
+ isSpeaking: false,
36
+ transcript: "",
37
+ error: null,
38
+ });
39
+
40
+ const recognitionRef = useRef<SpeechRecognitionInstance | null>(null);
41
+ const synthesisRef = useRef<SpeechSynthesisUtterance | null>(null);
42
+
43
+ // Initialize speech recognition
44
+ useEffect(() => {
45
+ if (typeof window !== "undefined") {
46
+ const SpeechRecognition =
47
+ window.SpeechRecognition || (window as any).webkitSpeechRecognition;
48
+
49
+ if (SpeechRecognition) {
50
+ const recognition = new SpeechRecognition();
51
+ recognition.continuous = false;
52
+ recognition.interimResults = true;
53
+ recognition.lang = "en-US";
54
+
55
+ recognition.onresult = (event) => {
56
+ const transcript = Array.from(event.results)
57
+ .map((result) => result[0].transcript)
58
+ .join("");
59
+
60
+ setState((prev) => ({ ...prev, transcript }));
61
+ };
62
+
63
+ recognition.onerror = (event) => {
64
+ setState((prev) => ({
65
+ ...prev,
66
+ isListening: false,
67
+ error: event.error,
68
+ }));
69
+ };
70
+
71
+ recognition.onend = () => {
72
+ setState((prev) => ({ ...prev, isListening: false }));
73
+ };
74
+
75
+ recognitionRef.current = recognition;
76
+ }
77
+ }
78
+
79
+ return () => {
80
+ if (recognitionRef.current) {
81
+ recognitionRef.current.abort();
82
+ }
83
+ };
84
+ }, []);
85
+
86
+ const startListening = useCallback(() => {
87
+ if (!recognitionRef.current) {
88
+ setState((prev) => ({
89
+ ...prev,
90
+ error: "Speech recognition not supported",
91
+ }));
92
+ return;
93
+ }
94
+
95
+ setState((prev) => ({
96
+ ...prev,
97
+ isListening: true,
98
+ transcript: "",
99
+ error: null,
100
+ }));
101
+
102
+ try {
103
+ recognitionRef.current.start();
104
+ } catch (error) {
105
+ console.error("Failed to start recognition:", error);
106
+ }
107
+ }, []);
108
+
109
+ const stopListening = useCallback(() => {
110
+ if (recognitionRef.current) {
111
+ recognitionRef.current.stop();
112
+ }
113
+ setState((prev) => ({ ...prev, isListening: false }));
114
+ }, []);
115
+
116
+ const speak = useCallback((text: string) => {
117
+ if (!window.speechSynthesis) {
118
+ setState((prev) => ({ ...prev, error: "Speech synthesis not supported" }));
119
+ return;
120
+ }
121
+
122
+ // Cancel any ongoing speech
123
+ window.speechSynthesis.cancel();
124
+
125
+ const utterance = new SpeechSynthesisUtterance(text);
126
+ utterance.rate = 1;
127
+ utterance.pitch = 1;
128
+ utterance.volume = 1;
129
+
130
+ utterance.onstart = () => {
131
+ setState((prev) => ({ ...prev, isSpeaking: true }));
132
+ };
133
+
134
+ utterance.onend = () => {
135
+ setState((prev) => ({ ...prev, isSpeaking: false }));
136
+ };
137
+
138
+ utterance.onerror = () => {
139
+ setState((prev) => ({ ...prev, isSpeaking: false }));
140
+ };
141
+
142
+ synthesisRef.current = utterance;
143
+ window.speechSynthesis.speak(utterance);
144
+ }, []);
145
+
146
+ const stopSpeaking = useCallback(() => {
147
+ window.speechSynthesis.cancel();
148
+ setState((prev) => ({ ...prev, isSpeaking: false }));
149
+ }, []);
150
+
151
+ return {
152
+ ...state,
153
+ startListening,
154
+ stopListening,
155
+ speak,
156
+ stopSpeaking,
157
+ };
158
+ }
src/index.css CHANGED
@@ -2,95 +2,96 @@
2
  @tailwind components;
3
  @tailwind utilities;
4
 
5
- /* Definition of the design system. All colors, gradients, fonts, etc should be defined here.
6
- All colors MUST be HSL.
7
- */
8
 
9
  @layer base {
10
  :root {
11
- --background: 0 0% 100%;
12
- --foreground: 222.2 84% 4.9%;
13
 
14
- --card: 0 0% 100%;
15
- --card-foreground: 222.2 84% 4.9%;
16
 
17
- --popover: 0 0% 100%;
18
- --popover-foreground: 222.2 84% 4.9%;
19
 
20
- --primary: 222.2 47.4% 11.2%;
21
- --primary-foreground: 210 40% 98%;
22
 
23
- --secondary: 210 40% 96.1%;
24
- --secondary-foreground: 222.2 47.4% 11.2%;
25
 
26
- --muted: 210 40% 96.1%;
27
- --muted-foreground: 215.4 16.3% 46.9%;
28
 
29
- --accent: 210 40% 96.1%;
30
- --accent-foreground: 222.2 47.4% 11.2%;
31
 
32
- --destructive: 0 84.2% 60.2%;
33
  --destructive-foreground: 210 40% 98%;
34
 
35
- --border: 214.3 31.8% 91.4%;
36
- --input: 214.3 31.8% 91.4%;
37
- --ring: 222.2 84% 4.9%;
38
-
39
- --radius: 0.5rem;
40
-
41
- --sidebar-background: 0 0% 98%;
42
-
43
- --sidebar-foreground: 240 5.3% 26.1%;
44
-
45
- --sidebar-primary: 240 5.9% 10%;
46
-
47
- --sidebar-primary-foreground: 0 0% 98%;
48
-
49
- --sidebar-accent: 240 4.8% 95.9%;
50
-
51
- --sidebar-accent-foreground: 240 5.9% 10%;
52
-
53
- --sidebar-border: 220 13% 91%;
54
-
55
- --sidebar-ring: 217.2 91.2% 59.8%;
 
 
56
  }
57
 
58
  .dark {
59
- --background: 222.2 84% 4.9%;
60
  --foreground: 210 40% 98%;
61
 
62
- --card: 222.2 84% 4.9%;
63
  --card-foreground: 210 40% 98%;
64
 
65
- --popover: 222.2 84% 4.9%;
66
  --popover-foreground: 210 40% 98%;
67
 
68
- --primary: 210 40% 98%;
69
- --primary-foreground: 222.2 47.4% 11.2%;
70
 
71
- --secondary: 217.2 32.6% 17.5%;
72
  --secondary-foreground: 210 40% 98%;
73
 
74
- --muted: 217.2 32.6% 17.5%;
75
- --muted-foreground: 215 20.2% 65.1%;
76
 
77
- --accent: 217.2 32.6% 17.5%;
78
  --accent-foreground: 210 40% 98%;
79
 
80
- --destructive: 0 62.8% 30.6%;
81
  --destructive-foreground: 210 40% 98%;
82
 
83
- --border: 217.2 32.6% 17.5%;
84
- --input: 217.2 32.6% 17.5%;
85
- --ring: 212.7 26.8% 83.9%;
86
- --sidebar-background: 240 5.9% 10%;
87
- --sidebar-foreground: 240 4.8% 95.9%;
88
- --sidebar-primary: 224.3 76.3% 48%;
89
- --sidebar-primary-foreground: 0 0% 100%;
90
- --sidebar-accent: 240 3.7% 15.9%;
91
- --sidebar-accent-foreground: 240 4.8% 95.9%;
92
- --sidebar-border: 240 3.7% 15.9%;
93
- --sidebar-ring: 217.2 91.2% 59.8%;
 
94
  }
95
  }
96
 
@@ -100,6 +101,79 @@ All colors MUST be HSL.
100
  }
101
 
102
  body {
103
- @apply bg-background text-foreground;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  }
 
2
  @tailwind components;
3
  @tailwind utilities;
4
 
5
+ /* AI Desktop Assistant - Dark Modern Theme (Copilot-inspired) */
 
 
6
 
7
  @layer base {
8
  :root {
9
+ --background: 222 47% 11%;
10
+ --foreground: 210 40% 98%;
11
 
12
+ --card: 222 47% 14%;
13
+ --card-foreground: 210 40% 98%;
14
 
15
+ --popover: 222 47% 12%;
16
+ --popover-foreground: 210 40% 98%;
17
 
18
+ --primary: 217 91% 60%;
19
+ --primary-foreground: 222 47% 11%;
20
 
21
+ --secondary: 217 33% 17%;
22
+ --secondary-foreground: 210 40% 98%;
23
 
24
+ --muted: 217 33% 17%;
25
+ --muted-foreground: 215 20% 65%;
26
 
27
+ --accent: 217 33% 22%;
28
+ --accent-foreground: 210 40% 98%;
29
 
30
+ --destructive: 0 84% 60%;
31
  --destructive-foreground: 210 40% 98%;
32
 
33
+ --border: 217 33% 20%;
34
+ --input: 217 33% 17%;
35
+ --ring: 217 91% 60%;
36
+
37
+ --radius: 0.75rem;
38
+
39
+ /* Custom tokens for AI assistant */
40
+ --glass: 222 47% 16%;
41
+ --glass-border: 217 33% 25%;
42
+ --glow: 217 91% 60%;
43
+ --user-bubble: 217 91% 50%;
44
+ --assistant-bubble: 222 47% 18%;
45
+ --sidebar-bg: 222 47% 8%;
46
+ --hover-accent: 217 33% 25%;
47
+
48
+ --sidebar-background: 222 47% 8%;
49
+ --sidebar-foreground: 215 20% 65%;
50
+ --sidebar-primary: 217 91% 60%;
51
+ --sidebar-primary-foreground: 222 47% 11%;
52
+ --sidebar-accent: 217 33% 17%;
53
+ --sidebar-accent-foreground: 210 40% 98%;
54
+ --sidebar-border: 217 33% 15%;
55
+ --sidebar-ring: 217 91% 60%;
56
  }
57
 
58
  .dark {
59
+ --background: 222 47% 11%;
60
  --foreground: 210 40% 98%;
61
 
62
+ --card: 222 47% 14%;
63
  --card-foreground: 210 40% 98%;
64
 
65
+ --popover: 222 47% 12%;
66
  --popover-foreground: 210 40% 98%;
67
 
68
+ --primary: 217 91% 60%;
69
+ --primary-foreground: 222 47% 11%;
70
 
71
+ --secondary: 217 33% 17%;
72
  --secondary-foreground: 210 40% 98%;
73
 
74
+ --muted: 217 33% 17%;
75
+ --muted-foreground: 215 20% 65%;
76
 
77
+ --accent: 217 33% 22%;
78
  --accent-foreground: 210 40% 98%;
79
 
80
+ --destructive: 0 62% 30%;
81
  --destructive-foreground: 210 40% 98%;
82
 
83
+ --border: 217 33% 20%;
84
+ --input: 217 33% 17%;
85
+ --ring: 217 91% 60%;
86
+
87
+ --sidebar-background: 222 47% 8%;
88
+ --sidebar-foreground: 215 20% 65%;
89
+ --sidebar-primary: 217 91% 60%;
90
+ --sidebar-primary-foreground: 222 47% 11%;
91
+ --sidebar-accent: 217 33% 17%;
92
+ --sidebar-accent-foreground: 210 40% 98%;
93
+ --sidebar-border: 217 33% 15%;
94
+ --sidebar-ring: 217 91% 60%;
95
  }
96
  }
97
 
 
101
  }
102
 
103
  body {
104
+ @apply bg-background text-foreground antialiased;
105
+ font-feature-settings: "rlig" 1, "calt" 1;
106
+ }
107
+ }
108
+
109
+ @layer utilities {
110
+ .glass {
111
+ @apply bg-card/80 backdrop-blur-xl border border-border/50;
112
+ }
113
+
114
+ .glass-hover {
115
+ @apply hover:bg-accent/50 transition-colors duration-200;
116
+ }
117
+
118
+ .glow {
119
+ box-shadow: 0 0 20px hsl(var(--primary) / 0.3);
120
  }
121
+
122
+ .glow-subtle {
123
+ box-shadow: 0 0 10px hsl(var(--primary) / 0.15);
124
+ }
125
+
126
+ .text-gradient {
127
+ @apply bg-gradient-to-r from-primary to-blue-400 bg-clip-text text-transparent;
128
+ }
129
+
130
+ .scrollbar-thin {
131
+ scrollbar-width: thin;
132
+ scrollbar-color: hsl(var(--muted)) transparent;
133
+ }
134
+
135
+ .scrollbar-thin::-webkit-scrollbar {
136
+ width: 6px;
137
+ height: 6px;
138
+ }
139
+
140
+ .scrollbar-thin::-webkit-scrollbar-track {
141
+ background: transparent;
142
+ }
143
+
144
+ .scrollbar-thin::-webkit-scrollbar-thumb {
145
+ background: hsl(var(--muted));
146
+ border-radius: 3px;
147
+ }
148
+
149
+ .scrollbar-thin::-webkit-scrollbar-thumb:hover {
150
+ background: hsl(var(--muted-foreground));
151
+ }
152
+ }
153
+
154
+ /* Animations */
155
+ @keyframes pulse-glow {
156
+ 0%, 100% {
157
+ box-shadow: 0 0 20px hsl(var(--primary) / 0.4);
158
+ }
159
+ 50% {
160
+ box-shadow: 0 0 40px hsl(var(--primary) / 0.6);
161
+ }
162
+ }
163
+
164
+ @keyframes voice-wave {
165
+ 0%, 100% {
166
+ transform: scaleY(0.5);
167
+ }
168
+ 50% {
169
+ transform: scaleY(1);
170
+ }
171
+ }
172
+
173
+ .animate-pulse-glow {
174
+ animation: pulse-glow 2s ease-in-out infinite;
175
+ }
176
+
177
+ .animate-voice-wave {
178
+ animation: voice-wave 0.5s ease-in-out infinite;
179
  }
src/integrations/supabase/client.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // This file is automatically generated. Do not edit it directly.
2
+ import { createClient } from '@supabase/supabase-js';
3
+ import type { Database } from './types';
4
+
5
+ const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL;
6
+ const SUPABASE_PUBLISHABLE_KEY = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY;
7
+
8
+ // Import the supabase client like this:
9
+ // import { supabase } from "@/integrations/supabase/client";
10
+
11
+ export const supabase = createClient<Database>(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, {
12
+ auth: {
13
+ storage: localStorage,
14
+ persistSession: true,
15
+ autoRefreshToken: true,
16
+ }
17
+ });
src/integrations/supabase/types.ts ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type Json =
2
+ | string
3
+ | number
4
+ | boolean
5
+ | null
6
+ | { [key: string]: Json | undefined }
7
+ | Json[]
8
+
9
+ export type Database = {
10
+ // Allows to automatically instantiate createClient with right options
11
+ // instead of createClient<Database, { PostgrestVersion: 'XX' }>(URL, KEY)
12
+ __InternalSupabase: {
13
+ PostgrestVersion: "14.1"
14
+ }
15
+ public: {
16
+ Tables: {
17
+ [_ in never]: never
18
+ }
19
+ Views: {
20
+ [_ in never]: never
21
+ }
22
+ Functions: {
23
+ [_ in never]: never
24
+ }
25
+ Enums: {
26
+ [_ in never]: never
27
+ }
28
+ CompositeTypes: {
29
+ [_ in never]: never
30
+ }
31
+ }
32
+ }
33
+
34
+ type DatabaseWithoutInternals = Omit<Database, "__InternalSupabase">
35
+
36
+ type DefaultSchema = DatabaseWithoutInternals[Extract<keyof Database, "public">]
37
+
38
+ export type Tables<
39
+ DefaultSchemaTableNameOrOptions extends
40
+ | keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
41
+ | { schema: keyof DatabaseWithoutInternals },
42
+ TableName extends DefaultSchemaTableNameOrOptions extends {
43
+ schema: keyof DatabaseWithoutInternals
44
+ }
45
+ ? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
46
+ DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
47
+ : never = never,
48
+ > = DefaultSchemaTableNameOrOptions extends {
49
+ schema: keyof DatabaseWithoutInternals
50
+ }
51
+ ? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
52
+ DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
53
+ Row: infer R
54
+ }
55
+ ? R
56
+ : never
57
+ : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] &
58
+ DefaultSchema["Views"])
59
+ ? (DefaultSchema["Tables"] &
60
+ DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
61
+ Row: infer R
62
+ }
63
+ ? R
64
+ : never
65
+ : never
66
+
67
+ export type TablesInsert<
68
+ DefaultSchemaTableNameOrOptions extends
69
+ | keyof DefaultSchema["Tables"]
70
+ | { schema: keyof DatabaseWithoutInternals },
71
+ TableName extends DefaultSchemaTableNameOrOptions extends {
72
+ schema: keyof DatabaseWithoutInternals
73
+ }
74
+ ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
75
+ : never = never,
76
+ > = DefaultSchemaTableNameOrOptions extends {
77
+ schema: keyof DatabaseWithoutInternals
78
+ }
79
+ ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
80
+ Insert: infer I
81
+ }
82
+ ? I
83
+ : never
84
+ : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
85
+ ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
86
+ Insert: infer I
87
+ }
88
+ ? I
89
+ : never
90
+ : never
91
+
92
+ export type TablesUpdate<
93
+ DefaultSchemaTableNameOrOptions extends
94
+ | keyof DefaultSchema["Tables"]
95
+ | { schema: keyof DatabaseWithoutInternals },
96
+ TableName extends DefaultSchemaTableNameOrOptions extends {
97
+ schema: keyof DatabaseWithoutInternals
98
+ }
99
+ ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
100
+ : never = never,
101
+ > = DefaultSchemaTableNameOrOptions extends {
102
+ schema: keyof DatabaseWithoutInternals
103
+ }
104
+ ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
105
+ Update: infer U
106
+ }
107
+ ? U
108
+ : never
109
+ : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
110
+ ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
111
+ Update: infer U
112
+ }
113
+ ? U
114
+ : never
115
+ : never
116
+
117
+ export type Enums<
118
+ DefaultSchemaEnumNameOrOptions extends
119
+ | keyof DefaultSchema["Enums"]
120
+ | { schema: keyof DatabaseWithoutInternals },
121
+ EnumName extends DefaultSchemaEnumNameOrOptions extends {
122
+ schema: keyof DatabaseWithoutInternals
123
+ }
124
+ ? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
125
+ : never = never,
126
+ > = DefaultSchemaEnumNameOrOptions extends {
127
+ schema: keyof DatabaseWithoutInternals
128
+ }
129
+ ? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
130
+ : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
131
+ ? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
132
+ : never
133
+
134
+ export type CompositeTypes<
135
+ PublicCompositeTypeNameOrOptions extends
136
+ | keyof DefaultSchema["CompositeTypes"]
137
+ | { schema: keyof DatabaseWithoutInternals },
138
+ CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
139
+ schema: keyof DatabaseWithoutInternals
140
+ }
141
+ ? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
142
+ : never = never,
143
+ > = PublicCompositeTypeNameOrOptions extends {
144
+ schema: keyof DatabaseWithoutInternals
145
+ }
146
+ ? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
147
+ : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
148
+ ? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
149
+ : never
150
+
151
+ export const Constants = {
152
+ public: {
153
+ Enums: {},
154
+ },
155
+ } as const
src/pages/Index.tsx CHANGED
@@ -1,14 +1,7 @@
1
- // Update this page (the content is just a fallback if you fail to update the page)
2
 
3
  const Index = () => {
4
- return (
5
- <div className="flex min-h-screen items-center justify-center bg-background">
6
- <div className="text-center">
7
- <h1 className="mb-4 text-4xl font-bold">Welcome to Your Blank App</h1>
8
- <p className="text-xl text-muted-foreground">Start building your amazing project here!</p>
9
- </div>
10
- </div>
11
- );
12
  };
13
 
14
  export default Index;
 
1
+ import { ChatInterface } from "@/components/ChatInterface";
2
 
3
  const Index = () => {
4
+ return <ChatInterface />;
 
 
 
 
 
 
 
5
  };
6
 
7
  export default Index;
src/types/chat.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface Message {
2
+ id: string;
3
+ role: "user" | "assistant";
4
+ content: string;
5
+ timestamp: Date;
6
+ isStreaming?: boolean;
7
+ }
8
+
9
+ export interface Conversation {
10
+ id: string;
11
+ title: string;
12
+ messages: Message[];
13
+ createdAt: Date;
14
+ updatedAt: Date;
15
+ }
16
+
17
+ export interface ScreenShareState {
18
+ isSharing: boolean;
19
+ stream: MediaStream | null;
20
+ error: string | null;
21
+ }
22
+
23
+ export interface VoiceState {
24
+ isListening: boolean;
25
+ isSpeaking: boolean;
26
+ transcript: string;
27
+ error: string | null;
28
+ }
supabase/config.toml ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ project_id = "rxrhjvwnfxtosbneybwa"
2
+
3
+ [functions.chat]
4
+ verify_jwt = false
supabase/functions/chat/index.ts ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
2
+
3
+ const corsHeaders = {
4
+ "Access-Control-Allow-Origin": "*",
5
+ "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
6
+ };
7
+
8
+ serve(async (req) => {
9
+ if (req.method === "OPTIONS") {
10
+ return new Response(null, { headers: corsHeaders });
11
+ }
12
+
13
+ try {
14
+ const { messages, screenContext } = await req.json();
15
+ const LOVABLE_API_KEY = Deno.env.get("LOVABLE_API_KEY");
16
+
17
+ if (!LOVABLE_API_KEY) {
18
+ throw new Error("LOVABLE_API_KEY is not configured");
19
+ }
20
+
21
+ const systemPrompt = `You are an intelligent AI desktop assistant with vision and voice capabilities. You are helpful, friendly, and professional.
22
+
23
+ Key capabilities:
24
+ - You can help with a wide range of tasks including writing, research, coding, and creative work
25
+ - When the user shares their screen, you can see what they're viewing and provide contextual assistance
26
+ - You can engage in natural voice conversations
27
+ - You're knowledgeable about many topics and can explain complex concepts clearly
28
+
29
+ Guidelines:
30
+ - Be concise but thorough in your responses
31
+ - When analyzing screen content, be specific about what you see and how you can help
32
+ - Maintain a warm, professional tone
33
+ - If you're unsure about something, acknowledge it honestly
34
+ - Format responses with markdown when appropriate for readability`;
35
+
36
+ const response = await fetch("https://ai.gateway.lovable.dev/v1/chat/completions", {
37
+ method: "POST",
38
+ headers: {
39
+ Authorization: `Bearer ${LOVABLE_API_KEY}`,
40
+ "Content-Type": "application/json",
41
+ },
42
+ body: JSON.stringify({
43
+ model: "google/gemini-3-flash-preview",
44
+ messages: [
45
+ { role: "system", content: systemPrompt },
46
+ ...messages,
47
+ ],
48
+ stream: true,
49
+ }),
50
+ });
51
+
52
+ if (!response.ok) {
53
+ if (response.status === 429) {
54
+ return new Response(
55
+ JSON.stringify({ error: "Rate limit exceeded. Please try again later." }),
56
+ { status: 429, headers: { ...corsHeaders, "Content-Type": "application/json" } }
57
+ );
58
+ }
59
+ if (response.status === 402) {
60
+ return new Response(
61
+ JSON.stringify({ error: "Usage limit reached. Please add credits to continue." }),
62
+ { status: 402, headers: { ...corsHeaders, "Content-Type": "application/json" } }
63
+ );
64
+ }
65
+ const errorText = await response.text();
66
+ console.error("AI gateway error:", response.status, errorText);
67
+ return new Response(
68
+ JSON.stringify({ error: "AI service error" }),
69
+ { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
70
+ );
71
+ }
72
+
73
+ return new Response(response.body, {
74
+ headers: { ...corsHeaders, "Content-Type": "text/event-stream" },
75
+ });
76
+ } catch (error) {
77
+ console.error("Chat function error:", error);
78
+ return new Response(
79
+ JSON.stringify({ error: error instanceof Error ? error.message : "Unknown error" }),
80
+ { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
81
+ );
82
+ }
83
+ });
tailwind.config.ts CHANGED
@@ -57,6 +57,8 @@ export default {
57
  border: "hsl(var(--sidebar-border))",
58
  ring: "hsl(var(--sidebar-ring))",
59
  },
 
 
60
  },
61
  borderRadius: {
62
  lg: "var(--radius)",
@@ -65,25 +67,32 @@ export default {
65
  },
66
  keyframes: {
67
  "accordion-down": {
68
- from: {
69
- height: "0",
70
- },
71
- to: {
72
- height: "var(--radix-accordion-content-height)",
73
- },
74
  },
75
  "accordion-up": {
76
- from: {
77
- height: "var(--radix-accordion-content-height)",
78
- },
79
- to: {
80
- height: "0",
81
- },
 
 
 
 
 
 
 
 
82
  },
83
  },
84
  animation: {
85
  "accordion-down": "accordion-down 0.2s ease-out",
86
  "accordion-up": "accordion-up 0.2s ease-out",
 
 
 
87
  },
88
  },
89
  },
 
57
  border: "hsl(var(--sidebar-border))",
58
  ring: "hsl(var(--sidebar-ring))",
59
  },
60
+ glass: "hsl(var(--glass))",
61
+ glow: "hsl(var(--glow))",
62
  },
63
  borderRadius: {
64
  lg: "var(--radius)",
 
67
  },
68
  keyframes: {
69
  "accordion-down": {
70
+ from: { height: "0" },
71
+ to: { height: "var(--radix-accordion-content-height)" },
 
 
 
 
72
  },
73
  "accordion-up": {
74
+ from: { height: "var(--radix-accordion-content-height)" },
75
+ to: { height: "0" },
76
+ },
77
+ "fade-in": {
78
+ "0%": { opacity: "0", transform: "translateY(10px)" },
79
+ "100%": { opacity: "1", transform: "translateY(0)" },
80
+ },
81
+ "slide-up": {
82
+ "0%": { opacity: "0", transform: "translateY(20px)" },
83
+ "100%": { opacity: "1", transform: "translateY(0)" },
84
+ },
85
+ "scale-in": {
86
+ "0%": { opacity: "0", transform: "scale(0.95)" },
87
+ "100%": { opacity: "1", transform: "scale(1)" },
88
  },
89
  },
90
  animation: {
91
  "accordion-down": "accordion-down 0.2s ease-out",
92
  "accordion-up": "accordion-up 0.2s ease-out",
93
+ "fade-in": "fade-in 0.3s ease-out",
94
+ "slide-up": "slide-up 0.4s ease-out",
95
+ "scale-in": "scale-in 0.2s ease-out",
96
  },
97
  },
98
  },
vite.config.ts CHANGED
@@ -2,6 +2,7 @@ import { defineConfig } from "vite";
2
  import react from "@vitejs/plugin-react-swc";
3
  import path from "path";
4
  import { componentTagger } from "lovable-tagger";
 
5
 
6
  // https://vitejs.dev/config/
7
  export default defineConfig(({ mode }) => ({
@@ -12,7 +13,62 @@ export default defineConfig(({ mode }) => ({
12
  overlay: false,
13
  },
14
  },
15
- plugins: [react(), mode === "development" && componentTagger()].filter(Boolean),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  resolve: {
17
  alias: {
18
  "@": path.resolve(__dirname, "./src"),
 
2
  import react from "@vitejs/plugin-react-swc";
3
  import path from "path";
4
  import { componentTagger } from "lovable-tagger";
5
+ import { VitePWA } from "vite-plugin-pwa";
6
 
7
  // https://vitejs.dev/config/
8
  export default defineConfig(({ mode }) => ({
 
13
  overlay: false,
14
  },
15
  },
16
+ plugins: [
17
+ react(),
18
+ mode === "development" && componentTagger(),
19
+ VitePWA({
20
+ registerType: "autoUpdate",
21
+ includeAssets: ["favicon.ico", "robots.txt"],
22
+ manifest: {
23
+ name: "AI Desktop Assistant",
24
+ short_name: "AI Assistant",
25
+ description: "Your intelligent desktop assistant with vision and voice capabilities",
26
+ theme_color: "#0f172a",
27
+ background_color: "#0f172a",
28
+ display: "standalone",
29
+ orientation: "portrait",
30
+ scope: "/",
31
+ start_url: "/",
32
+ icons: [
33
+ {
34
+ src: "/pwa-192x192.png",
35
+ sizes: "192x192",
36
+ type: "image/png",
37
+ },
38
+ {
39
+ src: "/pwa-512x512.png",
40
+ sizes: "512x512",
41
+ type: "image/png",
42
+ },
43
+ {
44
+ src: "/pwa-512x512.png",
45
+ sizes: "512x512",
46
+ type: "image/png",
47
+ purpose: "any maskable",
48
+ },
49
+ ],
50
+ },
51
+ workbox: {
52
+ globPatterns: ["**/*.{js,css,html,ico,png,svg,woff2}"],
53
+ runtimeCaching: [
54
+ {
55
+ urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
56
+ handler: "CacheFirst",
57
+ options: {
58
+ cacheName: "google-fonts-cache",
59
+ expiration: {
60
+ maxEntries: 10,
61
+ maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
62
+ },
63
+ cacheableResponse: {
64
+ statuses: [0, 200],
65
+ },
66
+ },
67
+ },
68
+ ],
69
+ },
70
+ }),
71
+ ].filter(Boolean),
72
  resolve: {
73
  alias: {
74
  "@": path.resolve(__dirname, "./src"),