@realtimex/email-automator 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/server.ts +4 -8
- package/api/src/config/index.ts +6 -3
- package/bin/email-automator-setup.js +2 -3
- package/bin/email-automator.js +7 -11
- package/dist/api/server.js +109 -0
- package/dist/api/src/config/index.js +88 -0
- package/dist/api/src/middleware/auth.js +119 -0
- package/dist/api/src/middleware/errorHandler.js +78 -0
- package/dist/api/src/middleware/index.js +4 -0
- package/dist/api/src/middleware/rateLimit.js +57 -0
- package/dist/api/src/middleware/validation.js +111 -0
- package/dist/api/src/routes/actions.js +173 -0
- package/dist/api/src/routes/auth.js +106 -0
- package/dist/api/src/routes/emails.js +100 -0
- package/dist/api/src/routes/health.js +33 -0
- package/dist/api/src/routes/index.js +19 -0
- package/dist/api/src/routes/migrate.js +61 -0
- package/dist/api/src/routes/rules.js +104 -0
- package/dist/api/src/routes/settings.js +178 -0
- package/dist/api/src/routes/sync.js +118 -0
- package/dist/api/src/services/eventLogger.js +41 -0
- package/dist/api/src/services/gmail.js +350 -0
- package/dist/api/src/services/intelligence.js +243 -0
- package/dist/api/src/services/microsoft.js +256 -0
- package/dist/api/src/services/processor.js +503 -0
- package/dist/api/src/services/scheduler.js +210 -0
- package/dist/api/src/services/supabase.js +59 -0
- package/dist/api/src/utils/contentCleaner.js +94 -0
- package/dist/api/src/utils/crypto.js +68 -0
- package/dist/api/src/utils/logger.js +119 -0
- package/package.json +5 -5
- package/src/App.tsx +0 -622
- package/src/components/AccountSettings.tsx +0 -310
- package/src/components/AccountSettingsPage.tsx +0 -390
- package/src/components/Configuration.tsx +0 -1345
- package/src/components/Dashboard.tsx +0 -940
- package/src/components/ErrorBoundary.tsx +0 -71
- package/src/components/LiveTerminal.tsx +0 -308
- package/src/components/LoadingSpinner.tsx +0 -39
- package/src/components/Login.tsx +0 -371
- package/src/components/Logo.tsx +0 -57
- package/src/components/SetupWizard.tsx +0 -388
- package/src/components/Toast.tsx +0 -109
- package/src/components/migration/MigrationBanner.tsx +0 -97
- package/src/components/migration/MigrationModal.tsx +0 -458
- package/src/components/migration/MigrationPulseIndicator.tsx +0 -38
- package/src/components/mode-toggle.tsx +0 -24
- package/src/components/theme-provider.tsx +0 -72
- package/src/components/ui/alert.tsx +0 -66
- package/src/components/ui/button.tsx +0 -57
- package/src/components/ui/card.tsx +0 -75
- package/src/components/ui/dialog.tsx +0 -133
- package/src/components/ui/input.tsx +0 -22
- package/src/components/ui/label.tsx +0 -24
- package/src/components/ui/otp-input.tsx +0 -184
- package/src/context/AppContext.tsx +0 -422
- package/src/context/MigrationContext.tsx +0 -53
- package/src/context/TerminalContext.tsx +0 -31
- package/src/core/actions.ts +0 -76
- package/src/core/auth.ts +0 -108
- package/src/core/intelligence.ts +0 -76
- package/src/core/processor.ts +0 -112
- package/src/hooks/useRealtimeEmails.ts +0 -111
- package/src/index.css +0 -140
- package/src/lib/api-config.ts +0 -42
- package/src/lib/api-old.ts +0 -228
- package/src/lib/api.ts +0 -421
- package/src/lib/migration-check.ts +0 -264
- package/src/lib/sounds.ts +0 -120
- package/src/lib/supabase-config.ts +0 -117
- package/src/lib/supabase.ts +0 -28
- package/src/lib/types.ts +0 -166
- package/src/lib/utils.ts +0 -6
- package/src/main.tsx +0 -10
package/src/components/Login.tsx
DELETED
|
@@ -1,371 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
2
|
-
import { Mail, Loader2, LogIn, UserPlus, KeyRound, ArrowLeft } from 'lucide-react';
|
|
3
|
-
import { supabase } from '../lib/supabase';
|
|
4
|
-
import { Button } from './ui/button';
|
|
5
|
-
import { Input } from './ui/input';
|
|
6
|
-
import { OtpInput } from './ui/otp-input';
|
|
7
|
-
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './ui/card';
|
|
8
|
-
import { ModeToggle } from './mode-toggle';
|
|
9
|
-
import { toast } from './Toast';
|
|
10
|
-
import { Logo } from './Logo';
|
|
11
|
-
|
|
12
|
-
interface LoginProps {
|
|
13
|
-
onSuccess?: () => void;
|
|
14
|
-
onConfigure?: () => void;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function Login({ onSuccess, onConfigure }: LoginProps) {
|
|
18
|
-
const [email, setEmail] = useState('');
|
|
19
|
-
const [password, setPassword] = useState('');
|
|
20
|
-
const [firstName, setFirstName] = useState('');
|
|
21
|
-
const [lastName, setLastName] = useState('');
|
|
22
|
-
|
|
23
|
-
// UI State
|
|
24
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
25
|
-
const [isCheckingInit, setIsCheckingInit] = useState(true);
|
|
26
|
-
const [isInitialized, setIsInitialized] = useState(false);
|
|
27
|
-
const [error, setError] = useState('');
|
|
28
|
-
|
|
29
|
-
// Login Mode
|
|
30
|
-
const [loginMode, setLoginMode] = useState<'password' | 'otp'>('password');
|
|
31
|
-
const [otpStep, setOtpStep] = useState<'email' | 'verify'>('email');
|
|
32
|
-
const [otp, setOtp] = useState('');
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// Check initialization status on mount
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
checkInitialization();
|
|
38
|
-
}, []);
|
|
39
|
-
|
|
40
|
-
const checkInitialization = async () => {
|
|
41
|
-
try {
|
|
42
|
-
const { data, error } = await supabase.from('init_state').select('is_initialized');
|
|
43
|
-
if (error) {
|
|
44
|
-
// atomic-crm behavior: If check fails, assume initialized (Show Login)
|
|
45
|
-
// This covers cases where publishable keys block REST access but Auth works.
|
|
46
|
-
console.warn('[Login] Init check failed, defaulting to initialized (Login):', error);
|
|
47
|
-
// We do NOT show a blocking error, just log it.
|
|
48
|
-
// This allows the user to attempt login.
|
|
49
|
-
setIsInitialized(true);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// The view returns { is_initialized: 1 } if initialized, or 0/null if not
|
|
54
|
-
const initialized = data?.[0]?.is_initialized > 0;
|
|
55
|
-
setIsInitialized(initialized);
|
|
56
|
-
} catch (err: any) {
|
|
57
|
-
console.warn('[Login] Init check exception, defaulting to initialized:', err);
|
|
58
|
-
|
|
59
|
-
// On any exception (network, auth, etc), we default to showing the Login screen.
|
|
60
|
-
// This prevents getting stuck in Signup mode if the DB is actually initialized but unreachable.
|
|
61
|
-
// The "Update Connection" button is still available if they need to change config.
|
|
62
|
-
|
|
63
|
-
// Clear blocking error to allow UI to render Login form
|
|
64
|
-
if (err.message?.includes('Invalid API key') || err.status === 401) {
|
|
65
|
-
console.warn('[Login] API Key invalid for REST check (likely Publishable Key). defaulting to Setup Mode (isInitialized=false).');
|
|
66
|
-
setIsInitialized(false);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
setIsInitialized(true);
|
|
70
|
-
} finally {
|
|
71
|
-
setIsCheckingInit(false);
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
76
|
-
e.preventDefault();
|
|
77
|
-
setIsLoading(true);
|
|
78
|
-
setError('');
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
if (!isInitialized) {
|
|
82
|
-
// Admin Signup Flow
|
|
83
|
-
const { data, error } = await supabase.functions.invoke('setup', {
|
|
84
|
-
body: {
|
|
85
|
-
email,
|
|
86
|
-
password,
|
|
87
|
-
first_name: firstName,
|
|
88
|
-
last_name: lastName
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
if (error || !data) {
|
|
93
|
-
if (error?.message?.includes('First user already exists')) {
|
|
94
|
-
toast.info('System already initialized. Please log in.');
|
|
95
|
-
setIsInitialized(true);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
throw new Error(error?.message || 'Failed to create admin account');
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
toast.success('Admin account created! Signing you in...');
|
|
102
|
-
|
|
103
|
-
// Auto login after creation
|
|
104
|
-
const { error: signInError } = await supabase.auth.signInWithPassword({
|
|
105
|
-
email,
|
|
106
|
-
password,
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
if (signInError) throw signInError;
|
|
110
|
-
|
|
111
|
-
// Force re-check of initialization status
|
|
112
|
-
setIsInitialized(true);
|
|
113
|
-
onSuccess?.();
|
|
114
|
-
} else if (loginMode === 'password') {
|
|
115
|
-
// Regular Login Flow
|
|
116
|
-
const { error } = await supabase.auth.signInWithPassword({
|
|
117
|
-
email,
|
|
118
|
-
password,
|
|
119
|
-
});
|
|
120
|
-
if (error) throw error;
|
|
121
|
-
toast.success('Logged in successfully');
|
|
122
|
-
onSuccess?.();
|
|
123
|
-
} else {
|
|
124
|
-
// OTP Flow - Step 1: Send Code
|
|
125
|
-
if (otpStep === 'email') {
|
|
126
|
-
const { error } = await supabase.auth.signInWithOtp({
|
|
127
|
-
email,
|
|
128
|
-
options: { shouldCreateUser: false } // Only allow existing users to login this way
|
|
129
|
-
});
|
|
130
|
-
if (error) throw error;
|
|
131
|
-
setOtpStep('verify');
|
|
132
|
-
toast.success('Validation code sent to your email');
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
} catch (err: any) {
|
|
136
|
-
// Show full error message to user (e.g. "Invalid login credentials")
|
|
137
|
-
setError(err?.message || 'Authentication failed');
|
|
138
|
-
console.error('[Login] Error:', err);
|
|
139
|
-
} finally {
|
|
140
|
-
setIsLoading(false);
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const handleVerifyOtp = async () => {
|
|
145
|
-
setIsLoading(true);
|
|
146
|
-
setError('');
|
|
147
|
-
try {
|
|
148
|
-
const { data, error } = await supabase.auth.verifyOtp({
|
|
149
|
-
email,
|
|
150
|
-
token: otp,
|
|
151
|
-
type: 'magiclink'
|
|
152
|
-
});
|
|
153
|
-
if (error) throw error;
|
|
154
|
-
if (!data.session) throw new Error('Failed to create session');
|
|
155
|
-
|
|
156
|
-
toast.success('Logged in successfully');
|
|
157
|
-
onSuccess?.();
|
|
158
|
-
} catch (err: any) {
|
|
159
|
-
setError(err?.message || 'Invalid code');
|
|
160
|
-
} finally {
|
|
161
|
-
setIsLoading(false);
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
if (isCheckingInit) {
|
|
166
|
-
return (
|
|
167
|
-
<div className="min-h-screen bg-background flex items-center justify-center">
|
|
168
|
-
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
|
169
|
-
</div>
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return (
|
|
174
|
-
<div className="min-h-screen bg-background flex items-center justify-center p-8 relative">
|
|
175
|
-
<div className="absolute top-4 right-4">
|
|
176
|
-
<ModeToggle />
|
|
177
|
-
</div>
|
|
178
|
-
<Card className="w-full max-w-md shadow-2xl">
|
|
179
|
-
<CardHeader className="text-center">
|
|
180
|
-
<div className="mx-auto bg-primary/10 p-4 rounded-full w-fit mb-4">
|
|
181
|
-
<Logo className="w-12 h-12" />
|
|
182
|
-
</div>
|
|
183
|
-
<CardTitle className="text-2xl">
|
|
184
|
-
{!isInitialized ? 'Welcome to Email Automator' : 'Welcome Back'}
|
|
185
|
-
</CardTitle>
|
|
186
|
-
<CardDescription>
|
|
187
|
-
{!isInitialized
|
|
188
|
-
? 'Create the first admin account to get started'
|
|
189
|
-
: (loginMode === 'password'
|
|
190
|
-
? 'Sign in to access your email automation'
|
|
191
|
-
: (otpStep === 'email' ? 'Receive a login code via email' : `Enter code sent to ${email}`)
|
|
192
|
-
)
|
|
193
|
-
}
|
|
194
|
-
</CardDescription>
|
|
195
|
-
</CardHeader>
|
|
196
|
-
<form onSubmit={handleSubmit}>
|
|
197
|
-
<CardContent className="space-y-4">
|
|
198
|
-
{!isInitialized && (
|
|
199
|
-
<div className="grid grid-cols-2 gap-4">
|
|
200
|
-
<div className="space-y-2">
|
|
201
|
-
<label className="text-sm font-medium">First Name</label>
|
|
202
|
-
<Input
|
|
203
|
-
value={firstName}
|
|
204
|
-
onChange={(e) => setFirstName(e.target.value)}
|
|
205
|
-
required={!isInitialized}
|
|
206
|
-
placeholder="John"
|
|
207
|
-
/>
|
|
208
|
-
</div>
|
|
209
|
-
<div className="space-y-2">
|
|
210
|
-
<label className="text-sm font-medium">Last Name</label>
|
|
211
|
-
<Input
|
|
212
|
-
value={lastName}
|
|
213
|
-
onChange={(e) => setLastName(e.target.value)}
|
|
214
|
-
required={!isInitialized}
|
|
215
|
-
placeholder="Doe"
|
|
216
|
-
/>
|
|
217
|
-
</div>
|
|
218
|
-
</div>
|
|
219
|
-
)}
|
|
220
|
-
|
|
221
|
-
{loginMode === 'password' || otpStep === 'email' ? (
|
|
222
|
-
<div className="space-y-2">
|
|
223
|
-
<label className="text-sm font-medium">Email</label>
|
|
224
|
-
<Input
|
|
225
|
-
type="email"
|
|
226
|
-
placeholder="admin@example.com"
|
|
227
|
-
value={email}
|
|
228
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
229
|
-
required
|
|
230
|
-
autoComplete="email"
|
|
231
|
-
disabled={otpStep === 'verify'}
|
|
232
|
-
/>
|
|
233
|
-
</div>
|
|
234
|
-
) : null}
|
|
235
|
-
|
|
236
|
-
{loginMode === 'password' && (
|
|
237
|
-
<div className="space-y-2">
|
|
238
|
-
<label className="text-sm font-medium">Password</label>
|
|
239
|
-
<Input
|
|
240
|
-
type="password"
|
|
241
|
-
placeholder="••••••••"
|
|
242
|
-
value={password}
|
|
243
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
244
|
-
required
|
|
245
|
-
minLength={6}
|
|
246
|
-
autoComplete={!isInitialized ? 'new-password' : 'current-password'}
|
|
247
|
-
/>
|
|
248
|
-
</div>
|
|
249
|
-
)}
|
|
250
|
-
|
|
251
|
-
{loginMode === 'otp' && otpStep === 'verify' && (
|
|
252
|
-
<div className="space-y-4 py-2">
|
|
253
|
-
<div className="flex justify-center">
|
|
254
|
-
<OtpInput
|
|
255
|
-
value={otp}
|
|
256
|
-
onChange={setOtp}
|
|
257
|
-
length={6}
|
|
258
|
-
onComplete={() => { }}
|
|
259
|
-
/>
|
|
260
|
-
</div>
|
|
261
|
-
<Button
|
|
262
|
-
type="button"
|
|
263
|
-
variant="link"
|
|
264
|
-
className="w-full text-xs text-muted-foreground"
|
|
265
|
-
onClick={() => setOtpStep('email')}
|
|
266
|
-
>
|
|
267
|
-
Change email address
|
|
268
|
-
</Button>
|
|
269
|
-
</div>
|
|
270
|
-
)}
|
|
271
|
-
|
|
272
|
-
<div className="bg-destructive/10 text-destructive text-sm p-3 rounded-md flex flex-col gap-2">
|
|
273
|
-
<p>{error}</p>
|
|
274
|
-
{error.includes('configuration') || error.includes('Invalid API key') || error.includes('API key') && onConfigure && (
|
|
275
|
-
<Button
|
|
276
|
-
type="button"
|
|
277
|
-
variant="outline"
|
|
278
|
-
className="w-full border-destructive/50 hover:bg-destructive/20 text-destructive"
|
|
279
|
-
onClick={onConfigure}
|
|
280
|
-
>
|
|
281
|
-
Update Connection Settings
|
|
282
|
-
</Button>
|
|
283
|
-
)}
|
|
284
|
-
</div>
|
|
285
|
-
</CardContent>
|
|
286
|
-
|
|
287
|
-
<CardFooter className="flex flex-col gap-3">
|
|
288
|
-
{loginMode === 'otp' && otpStep === 'verify' ? (
|
|
289
|
-
<Button
|
|
290
|
-
type="button"
|
|
291
|
-
onClick={handleVerifyOtp}
|
|
292
|
-
disabled={isLoading || otp.length !== 6}
|
|
293
|
-
className="w-full"
|
|
294
|
-
>
|
|
295
|
-
{isLoading ? (
|
|
296
|
-
<>
|
|
297
|
-
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
298
|
-
Verifying...
|
|
299
|
-
</>
|
|
300
|
-
) : (
|
|
301
|
-
'Verify Code'
|
|
302
|
-
)}
|
|
303
|
-
</Button>
|
|
304
|
-
) : (
|
|
305
|
-
<Button
|
|
306
|
-
type="submit"
|
|
307
|
-
disabled={isLoading || !email || (loginMode === 'password' && !password)}
|
|
308
|
-
className="w-full"
|
|
309
|
-
>
|
|
310
|
-
{isLoading ? (
|
|
311
|
-
<>
|
|
312
|
-
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
313
|
-
{!isInitialized ? 'Creating Account...' : (loginMode === 'otp' ? 'Send Code' : 'Signing in...')}
|
|
314
|
-
</>
|
|
315
|
-
) : (
|
|
316
|
-
<>
|
|
317
|
-
{!isInitialized ? (
|
|
318
|
-
<UserPlus className="w-4 h-4 mr-2" />
|
|
319
|
-
) : (
|
|
320
|
-
loginMode === 'otp' ? <Mail className="w-4 h-4 mr-2" /> : <LogIn className="w-4 h-4 mr-2" />
|
|
321
|
-
)}
|
|
322
|
-
{!isInitialized ? 'Create Admin Account' : (loginMode === 'otp' ? 'Send Login Code' : 'Sign In')}
|
|
323
|
-
</>
|
|
324
|
-
)}
|
|
325
|
-
</Button>
|
|
326
|
-
)}
|
|
327
|
-
|
|
328
|
-
{isInitialized && (
|
|
329
|
-
<div className="flex flex-col gap-2 w-full">
|
|
330
|
-
{loginMode === 'password' ? (
|
|
331
|
-
<Button
|
|
332
|
-
type="button"
|
|
333
|
-
variant="ghost"
|
|
334
|
-
className="w-full text-sm font-normal text-muted-foreground hover:text-primary"
|
|
335
|
-
onClick={() => {
|
|
336
|
-
setLoginMode('otp');
|
|
337
|
-
setOtpStep('email');
|
|
338
|
-
setError('');
|
|
339
|
-
}}
|
|
340
|
-
>
|
|
341
|
-
<KeyRound className="w-4 h-4 mr-2" />
|
|
342
|
-
Sign in with Code
|
|
343
|
-
</Button>
|
|
344
|
-
) : (
|
|
345
|
-
<Button
|
|
346
|
-
type="button"
|
|
347
|
-
variant="ghost"
|
|
348
|
-
className="w-full text-sm font-normal text-muted-foreground hover:text-primary"
|
|
349
|
-
onClick={() => {
|
|
350
|
-
setLoginMode('password');
|
|
351
|
-
setError('');
|
|
352
|
-
}}
|
|
353
|
-
>
|
|
354
|
-
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
355
|
-
Sign in with Password
|
|
356
|
-
</Button>
|
|
357
|
-
)}
|
|
358
|
-
</div>
|
|
359
|
-
)}
|
|
360
|
-
|
|
361
|
-
{isInitialized && (
|
|
362
|
-
<p className="text-xs text-center text-muted-foreground mt-2">
|
|
363
|
-
New users must be invited by an admin.
|
|
364
|
-
</p>
|
|
365
|
-
)}
|
|
366
|
-
</CardFooter>
|
|
367
|
-
</form>
|
|
368
|
-
</Card>
|
|
369
|
-
</div>
|
|
370
|
-
);
|
|
371
|
-
}
|
package/src/components/Logo.tsx
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { cn } from '../lib/utils';
|
|
2
|
-
|
|
3
|
-
interface LogoProps {
|
|
4
|
-
className?: string;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function Logo({ className }: LogoProps) {
|
|
8
|
-
return (
|
|
9
|
-
<svg
|
|
10
|
-
width="512"
|
|
11
|
-
height="512"
|
|
12
|
-
viewBox="0 0 512 512"
|
|
13
|
-
fill="none"
|
|
14
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
15
|
-
className={cn("w-9 h-9", className)}
|
|
16
|
-
>
|
|
17
|
-
{/* Main Envelope Shape */}
|
|
18
|
-
<path
|
|
19
|
-
d="M112 112H400C426.51 112 448 133.49 448 160V352C448 378.51 426.51 400 400 400H112C85.4903 400 64 378.51 64 352V160C64 133.49 85.4903 112 112 112Z"
|
|
20
|
-
className="stroke-foreground"
|
|
21
|
-
strokeWidth="32"
|
|
22
|
-
strokeLinecap="round"
|
|
23
|
-
strokeLinejoin="round"
|
|
24
|
-
/>
|
|
25
|
-
|
|
26
|
-
{/* The Flap (Open) */}
|
|
27
|
-
<path
|
|
28
|
-
d="M64 160 L200 270"
|
|
29
|
-
className="stroke-foreground"
|
|
30
|
-
strokeWidth="32"
|
|
31
|
-
strokeLinecap="round"
|
|
32
|
-
strokeLinejoin="round"
|
|
33
|
-
/>
|
|
34
|
-
<path
|
|
35
|
-
d="M448 160 L312 270"
|
|
36
|
-
className="stroke-foreground"
|
|
37
|
-
strokeWidth="32"
|
|
38
|
-
strokeLinecap="round"
|
|
39
|
-
strokeLinejoin="round"
|
|
40
|
-
/>
|
|
41
|
-
|
|
42
|
-
{/* The AI Spark (Purple) */}
|
|
43
|
-
<path
|
|
44
|
-
d="M256 128 C256 128 276 170 306 178 C276 186 256 228 256 228 C256 228 236 186 206 178 C236 170 256 128 256 128 Z"
|
|
45
|
-
fill="#9333ea"
|
|
46
|
-
>
|
|
47
|
-
<animateTransform
|
|
48
|
-
attributeName="transform"
|
|
49
|
-
type="translate"
|
|
50
|
-
values="0 0; 0 -10; 0 0"
|
|
51
|
-
dur="3s"
|
|
52
|
-
repeatCount="indefinite"
|
|
53
|
-
/>
|
|
54
|
-
</path>
|
|
55
|
-
</svg>
|
|
56
|
-
);
|
|
57
|
-
}
|