crewos 0.1.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.
Files changed (64) hide show
  1. package/app/.env.example +1 -0
  2. package/app/index.html +50 -0
  3. package/app/package.json +25 -0
  4. package/app/public/favicon.svg +1 -0
  5. package/app/public/images/cursor-ide-guiiding.png +0 -0
  6. package/app/public/images/gpt.jpg +0 -0
  7. package/app/src/app.jsx +22 -0
  8. package/app/src/components/ConfirmModal.jsx +50 -0
  9. package/app/src/components/Icons.jsx +377 -0
  10. package/app/src/components/RedirectRoute.jsx +14 -0
  11. package/app/src/components/SplashScreen.jsx +15 -0
  12. package/app/src/hooks/useAuth.js +28 -0
  13. package/app/src/index.css +268 -0
  14. package/app/src/main.jsx +5 -0
  15. package/app/src/navigations/AuthRoutes.jsx +15 -0
  16. package/app/src/navigations/MainRoutes.jsx +15 -0
  17. package/app/src/navigations/OnboardingRoutes.jsx +15 -0
  18. package/app/src/navigations/index.jsx +37 -0
  19. package/app/src/pages/Home/index.jsx +2095 -0
  20. package/app/src/pages/Login/index.jsx +118 -0
  21. package/app/src/pages/Onboarding/index.jsx +550 -0
  22. package/app/src/services/api.js +46 -0
  23. package/app/src/services/auth.service.js +3 -0
  24. package/app/src/services/config.service.js +13 -0
  25. package/app/src/services/member.service.js +7 -0
  26. package/app/src/services/onboarding.service.js +17 -0
  27. package/app/src/services/role.service.js +6 -0
  28. package/app/src/services/task.service.js +22 -0
  29. package/app/src/stores/auth.store.js +7 -0
  30. package/app/src/utils/environments.js +5 -0
  31. package/app/vite.config.js +10 -0
  32. package/app/yarn.lock +1337 -0
  33. package/backend/package-lock.json +918 -0
  34. package/backend/package.json +18 -0
  35. package/backend/src/configs/db.config.js +40 -0
  36. package/backend/src/controllers/auth.controller.js +19 -0
  37. package/backend/src/controllers/config.controller.js +23 -0
  38. package/backend/src/controllers/member.controller.js +30 -0
  39. package/backend/src/controllers/models.controller.js +25 -0
  40. package/backend/src/controllers/onboarding.controller.js +49 -0
  41. package/backend/src/controllers/role.controller.js +17 -0
  42. package/backend/src/controllers/task.controller.js +63 -0
  43. package/backend/src/index.js +36 -0
  44. package/backend/src/middlewares/onboarding.guard.js +14 -0
  45. package/backend/src/routes/auth.route.js +8 -0
  46. package/backend/src/routes/config.route.js +11 -0
  47. package/backend/src/routes/index.js +22 -0
  48. package/backend/src/routes/member.route.js +11 -0
  49. package/backend/src/routes/models.route.js +8 -0
  50. package/backend/src/routes/onboarding.route.js +13 -0
  51. package/backend/src/routes/role.route.js +9 -0
  52. package/backend/src/routes/task.route.js +20 -0
  53. package/backend/src/services/auth.service.js +14 -0
  54. package/backend/src/services/config.service.js +176 -0
  55. package/backend/src/services/data/roles.json +474 -0
  56. package/backend/src/services/member.service.js +77 -0
  57. package/backend/src/services/onboarding.service.js +328 -0
  58. package/backend/src/services/role.service.js +23 -0
  59. package/backend/src/services/task.service.js +665 -0
  60. package/backend/src/utils/catcher.js +9 -0
  61. package/backend/src/utils/sanitize.js +13 -0
  62. package/backend/yarn.lock +513 -0
  63. package/bin/crewos.js +307 -0
  64. package/package.json +11 -0
@@ -0,0 +1,118 @@
1
+ import { useState } from 'preact/hooks';
2
+
3
+ import { IconKey, IconLogo } from '../../components/Icons';
4
+ import { auth as authStore } from '../../stores/auth.store';
5
+ import { getMe } from '../../services/auth.service';
6
+
7
+ const Login = () => {
8
+ const [password, setPassword] = useState('');
9
+ const [loading, setLoading] = useState(false);
10
+
11
+ const handleSubmit = async (e) => {
12
+ e.preventDefault();
13
+
14
+ if (!password.trim()) return;
15
+
16
+ setLoading(true);
17
+
18
+ localStorage.setItem('crewos_token', password.trim());
19
+
20
+ try {
21
+ const res = await getMe();
22
+ authStore.value = {
23
+ ...authStore.value,
24
+ user: { authenticated: true },
25
+ onboardingCompleted: res.data?.onboardingCompleted || false,
26
+ };
27
+ } catch {
28
+ localStorage.removeItem('crewos_token');
29
+ setLoading(false);
30
+ }
31
+ };
32
+
33
+ return (
34
+ <div class="ui-page">
35
+ <div
36
+ class="ui-card"
37
+ style={{ width: 380, maxWidth: '90vw', padding: 'var(--space-8)' }}
38
+ >
39
+ <div style={{ textAlign: 'center', marginBottom: 'var(--space-8)' }}>
40
+ <IconLogo
41
+ class="mx-auto"
42
+ style={{ width: 36, height: 36, marginBottom: 'var(--space-4)' }}
43
+ />
44
+ <h1
45
+ style={{
46
+ fontSize: '1.5rem',
47
+ fontWeight: 700,
48
+ margin: 0,
49
+ color: 'var(--color-text)',
50
+ }}
51
+ >
52
+ crewOS
53
+ </h1>
54
+ <p
55
+ style={{
56
+ fontSize: '0.85rem',
57
+ color: 'var(--color-muted)',
58
+ marginTop: 'var(--space-2)',
59
+ }}
60
+ >
61
+ Enter your app password to continue
62
+ </p>
63
+ </div>
64
+
65
+ <form onSubmit={handleSubmit}>
66
+ <label
67
+ class="ui-label"
68
+ style={{ display: 'block', marginBottom: 'var(--space-2)' }}
69
+ >
70
+ Password
71
+ </label>
72
+ <div style={{ position: 'relative', marginBottom: 'var(--space-6)' }}>
73
+ <IconKey
74
+ style={{
75
+ position: 'absolute',
76
+ left: 'var(--space-4)',
77
+ top: '50%',
78
+ transform: 'translateY(-50%)',
79
+ width: 18,
80
+ height: 18,
81
+ color: 'var(--color-muted)',
82
+ }}
83
+ />
84
+ <input
85
+ type="password"
86
+ value={password}
87
+ onInput={(e) => setPassword(e.target.value)}
88
+ placeholder="Enter your password"
89
+ autoFocus
90
+ style={{
91
+ width: '100%',
92
+ padding: 'var(--space-3) var(--space-4) var(--space-3) 2.6rem',
93
+ background: 'var(--color-surface-soft)',
94
+ border: '1px solid var(--color-border)',
95
+ borderRadius: 'var(--radius-md)',
96
+ color: 'var(--color-text)',
97
+ fontSize: '0.95rem',
98
+ outline: 'none',
99
+ transition: 'border-color 180ms ease',
100
+ }}
101
+ />
102
+ </div>
103
+
104
+ <button
105
+ type="submit"
106
+ class="ui-button-primary"
107
+ disabled={loading || !password.trim()}
108
+ style={{ width: '100%', justifyContent: 'center' }}
109
+ >
110
+ {loading ? 'Authenticating...' : 'Sign In'}
111
+ </button>
112
+ </form>
113
+ </div>
114
+ </div>
115
+ );
116
+ };
117
+
118
+ export default Login;
@@ -0,0 +1,550 @@
1
+ import { useState, useEffect, useRef } from 'preact/hooks';
2
+ import { toast } from 'sonner';
3
+
4
+ import {
5
+ getOnboardingStatus,
6
+ validateCredentials,
7
+ saveCredentials,
8
+ startAnalysis,
9
+ getAnalysisProgress,
10
+ completeOnboarding,
11
+ } from '../../services/onboarding.service';
12
+ import { auth as authStore } from '../../stores/auth.store';
13
+ import { IconLogo, IconEye } from '../../components/Icons';
14
+
15
+ const Spinner = () => (
16
+ <div class="h-4 w-4 border-2 border-zinc-600 border-t-emerald-400 animate-spin flex-shrink-0" />
17
+ );
18
+
19
+ const Onboarding = () => {
20
+ const [step, setStep] = useState(1);
21
+ const [baseURL, setBaseURL] = useState('');
22
+ const [apiKey, setApiKey] = useState('');
23
+ const [showApiKey, setShowApiKey] = useState(false);
24
+ const [validating, setValidating] = useState(false);
25
+ const [validationError, setValidationError] = useState('');
26
+ const [saving, setSaving] = useState(false);
27
+ const [analyzing, setAnalyzing] = useState(false);
28
+ const [analysisDone, setAnalysisDone] = useState(false);
29
+ const [progressLines, setProgressLines] = useState([]);
30
+ const [completing, setCompleting] = useState(false);
31
+ const [loading, setLoading] = useState(true);
32
+
33
+ const progressRef = useRef(null);
34
+ const pollRef = useRef(null);
35
+
36
+ useEffect(() => {
37
+ const init = async () => {
38
+ try {
39
+ const res = await getOnboardingStatus();
40
+ const s = res.data;
41
+
42
+ if (s.hasCredentials) {
43
+ setBaseURL(s.baseURL || '');
44
+ setApiKey(s.apiKey || '');
45
+ setStep(s.hasKnowledge ? 3 : 2);
46
+ if (s.isAnalyzing) {
47
+ setAnalyzing(true);
48
+ startPolling();
49
+ }
50
+ if (s.hasKnowledge) {
51
+ setAnalysisDone(true);
52
+ }
53
+ } else {
54
+ setStep(1);
55
+ }
56
+ } catch {
57
+ // keep step 1
58
+ } finally {
59
+ setLoading(false);
60
+ }
61
+ };
62
+ init();
63
+ return () => {
64
+ if (pollRef.current) clearInterval(pollRef.current);
65
+ };
66
+ }, []);
67
+
68
+ const startPolling = () => {
69
+ if (pollRef.current) clearInterval(pollRef.current);
70
+ pollRef.current = setInterval(async () => {
71
+ try {
72
+ const res = await getAnalysisProgress();
73
+ setProgressLines(res.data.lines || []);
74
+ if (!res.data.isAnalyzing) {
75
+ clearInterval(pollRef.current);
76
+ pollRef.current = null;
77
+ setAnalyzing(false);
78
+ setAnalysisDone(true);
79
+ }
80
+ } catch {
81
+ // ignore
82
+ }
83
+ }, 1000);
84
+ };
85
+
86
+ useEffect(() => {
87
+ if (progressRef.current) {
88
+ progressRef.current.scrollTop = progressRef.current.scrollHeight;
89
+ }
90
+ }, [progressLines]);
91
+
92
+ const handleValidate = async () => {
93
+ if (!baseURL.trim() || !apiKey.trim()) return;
94
+
95
+ setValidating(true);
96
+ setValidationError('');
97
+
98
+ try {
99
+ await validateCredentials(baseURL.trim(), apiKey.trim());
100
+ setValidating(false);
101
+ toast.success('Connection is valid');
102
+ } catch (err) {
103
+ setValidationError(err.message);
104
+ setValidating(false);
105
+ }
106
+ };
107
+
108
+ const handleSaveAndNext = async () => {
109
+ if (!baseURL.trim() || !apiKey.trim()) return;
110
+
111
+ setSaving(true);
112
+ try {
113
+ await saveCredentials(baseURL.trim(), apiKey.trim());
114
+ setStep(2);
115
+ } catch (err) {
116
+ toast.error(err.message);
117
+ } finally {
118
+ setSaving(false);
119
+ }
120
+ };
121
+
122
+ const handleStartAnalysis = async () => {
123
+ setAnalyzing(true);
124
+ setAnalysisDone(false);
125
+ setProgressLines([]);
126
+
127
+ try {
128
+ await startAnalysis();
129
+ startPolling();
130
+ } catch (err) {
131
+ setAnalyzing(false);
132
+ toast.error(err.message);
133
+ }
134
+ };
135
+
136
+ const handleComplete = async () => {
137
+ setCompleting(true);
138
+ try {
139
+ await completeOnboarding();
140
+ authStore.value = {
141
+ ...authStore.value,
142
+ onboardingCompleted: true,
143
+ };
144
+ } catch (err) {
145
+ toast.error(err.message);
146
+ } finally {
147
+ setCompleting(false);
148
+ }
149
+ };
150
+
151
+ if (loading) {
152
+ return (
153
+ <div class="ui-page">
154
+ <div class="ui-card flex items-center gap-4 p-8">
155
+ <div class="h-10 w-10 rounded-full border-4 border-zinc-800 border-t-emerald-400 animate-spin" />
156
+ <div>
157
+ <p class="text-sm font-semibold text-zinc-100">crewOS</p>
158
+ <p class="text-xs text-zinc-500">Loading setup...</p>
159
+ </div>
160
+ </div>
161
+ </div>
162
+ );
163
+ }
164
+
165
+ const inputCls =
166
+ 'w-full px-3 py-2 bg-zinc-800 border border-zinc-700 text-zinc-100 text-sm outline-none focus:border-emerald-400/50 transition-colors';
167
+
168
+ const btnPrimary =
169
+ 'inline-flex items-center gap-2 px-4 py-2 bg-emerald-500 hover:bg-emerald-400 text-black font-semibold text-sm uppercase tracking-[0.05em] transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer';
170
+
171
+ const btnSecondary =
172
+ 'inline-flex items-center gap-2 px-4 py-2 border border-zinc-700 text-zinc-300 hover:border-zinc-500 font-semibold text-sm uppercase tracking-[0.05em] transition-colors disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer';
173
+
174
+ const stepDots = [1, 2, 3].map((s) => (
175
+ <div
176
+ class={`w-3 h-3 border-2 ${
177
+ s <= step
178
+ ? 'bg-emerald-400 border-emerald-400'
179
+ : 'bg-transparent border-zinc-600'
180
+ }`}
181
+ />
182
+ ));
183
+
184
+ return (
185
+ <div class="ui-page">
186
+ <div
187
+ class="ui-card"
188
+ style={{
189
+ width: 520,
190
+ maxWidth: '92vw',
191
+ padding: 'var(--space-8)',
192
+ maxHeight: '90vh',
193
+ overflow: 'hidden',
194
+ display: 'flex',
195
+ flexDirection: 'column',
196
+ }}
197
+ >
198
+ {/* header */}
199
+ <div style={{ textAlign: 'center', marginBottom: 'var(--space-6)' }}>
200
+ <IconLogo
201
+ class="mx-auto"
202
+ style={{ width: 32, height: 32, marginBottom: 'var(--space-3)' }}
203
+ />
204
+ <h1
205
+ style={{
206
+ fontSize: '1.25rem',
207
+ fontWeight: 700,
208
+ margin: 0,
209
+ color: 'var(--color-text)',
210
+ }}
211
+ >
212
+ crewOS Setup
213
+ </h1>
214
+ <p
215
+ style={{
216
+ fontSize: '0.8rem',
217
+ color: 'var(--color-muted)',
218
+ marginTop: 'var(--space-2)',
219
+ }}
220
+ >
221
+ Complete these steps to start using crewOS
222
+ </p>
223
+ </div>
224
+
225
+ {/* steps indicator */}
226
+ <div
227
+ style={{
228
+ display: 'flex',
229
+ alignItems: 'center',
230
+ justifyContent: 'center',
231
+ gap: 'var(--space-4)',
232
+ marginBottom: 'var(--space-6)',
233
+ }}
234
+ >
235
+ {stepDots.reduce((acc, dot, i) => {
236
+ acc.push(dot);
237
+ if (i < 2) {
238
+ acc.push(
239
+ <div
240
+ class="h-px flex-1 max-w-8"
241
+ style={{
242
+ background:
243
+ step > i + 1
244
+ ? 'var(--color-primary)'
245
+ : 'var(--color-border)',
246
+ }}
247
+ />,
248
+ );
249
+ }
250
+ return acc;
251
+ }, [])}
252
+ </div>
253
+
254
+ <div class="flex-1 overflow-y-auto">
255
+ {/* Step 1: Credentials */}
256
+ {step === 1 && (
257
+ <div>
258
+ <h2
259
+ style={{
260
+ fontSize: '0.9rem',
261
+ fontWeight: 600,
262
+ color: 'var(--color-text)',
263
+ marginBottom: 'var(--space-1)',
264
+ }}
265
+ >
266
+ Configure LLM Provider
267
+ </h2>
268
+ <p
269
+ style={{
270
+ fontSize: '0.75rem',
271
+ color: 'var(--color-muted)',
272
+ marginBottom: 'var(--space-5)',
273
+ }}
274
+ >
275
+ Enter your LLM provider's API endpoint and key
276
+ </p>
277
+
278
+ <div style={{ marginBottom: 'var(--space-4)' }}>
279
+ <label
280
+ style={{
281
+ display: 'block',
282
+ fontSize: '0.68rem',
283
+ fontWeight: 600,
284
+ color: 'var(--color-muted)',
285
+ marginBottom: 'var(--space-2)',
286
+ textTransform: 'uppercase',
287
+ letterSpacing: '0.06em',
288
+ }}
289
+ >
290
+ Base URL
291
+ </label>
292
+ <input
293
+ type="text"
294
+ value={baseURL}
295
+ onInput={(e) => {
296
+ setBaseURL(e.target.value);
297
+ setValidationError('');
298
+ }}
299
+ placeholder="https://api.openai.com/v1"
300
+ class={inputCls}
301
+ />
302
+ </div>
303
+
304
+ <div style={{ marginBottom: 'var(--space-2)' }}>
305
+ <label
306
+ style={{
307
+ display: 'block',
308
+ fontSize: '0.68rem',
309
+ fontWeight: 600,
310
+ color: 'var(--color-muted)',
311
+ marginBottom: 'var(--space-2)',
312
+ textTransform: 'uppercase',
313
+ letterSpacing: '0.06em',
314
+ }}
315
+ >
316
+ API Key
317
+ </label>
318
+ <div style={{ position: 'relative' }}>
319
+ <input
320
+ type={showApiKey ? 'text' : 'password'}
321
+ value={apiKey}
322
+ onInput={(e) => {
323
+ setApiKey(e.target.value);
324
+ setValidationError('');
325
+ }}
326
+ placeholder="sk-..."
327
+ class={inputCls}
328
+ style={{ paddingRight: '2.4rem' }}
329
+ />
330
+ <button
331
+ type="button"
332
+ onClick={() => setShowApiKey(!showApiKey)}
333
+ class="absolute right-1.5 top-1/2 -translate-y-1/2 w-[26px] h-[26px] bg-transparent text-zinc-500 flex items-center justify-center cursor-pointer hover:text-zinc-300 transition-colors"
334
+ >
335
+ <IconEye off={!showApiKey} />
336
+ </button>
337
+ </div>
338
+ </div>
339
+
340
+ {validationError && (
341
+ <div
342
+ style={{
343
+ fontSize: '0.75rem',
344
+ color: '#f87171',
345
+ marginBottom: 'var(--space-4)',
346
+ padding: 'var(--space-3)',
347
+ background: 'rgba(248,113,113,0.08)',
348
+ border: '1px solid rgba(248,113,113,0.2)',
349
+ }}
350
+ >
351
+ {validationError}
352
+ </div>
353
+ )}
354
+
355
+ <div
356
+ style={{
357
+ display: 'flex',
358
+ gap: 'var(--space-3)',
359
+ justifyContent: 'flex-end',
360
+ marginTop: 'var(--space-6)',
361
+ }}
362
+ >
363
+ <button
364
+ type="button"
365
+ onClick={handleValidate}
366
+ disabled={validating || !baseURL.trim() || !apiKey.trim()}
367
+ class={btnSecondary}
368
+ >
369
+ {validating && <Spinner />}
370
+ Test Connection
371
+ </button>
372
+ <button
373
+ type="button"
374
+ onClick={handleSaveAndNext}
375
+ disabled={saving || !baseURL.trim() || !apiKey.trim()}
376
+ class={btnPrimary}
377
+ >
378
+ {saving ? 'Saving...' : 'Save & Continue'}
379
+ </button>
380
+ </div>
381
+ </div>
382
+ )}
383
+
384
+ {/* Step 2: Project Analysis */}
385
+ {step === 2 && (
386
+ <div>
387
+ <h2
388
+ style={{
389
+ fontSize: '0.9rem',
390
+ fontWeight: 600,
391
+ color: 'var(--color-text)',
392
+ marginBottom: 'var(--space-1)',
393
+ }}
394
+ >
395
+ Analyze Project
396
+ </h2>
397
+ <p
398
+ style={{
399
+ fontSize: '0.75rem',
400
+ color: 'var(--color-muted)',
401
+ marginBottom: 'var(--space-5)',
402
+ }}
403
+ >
404
+ crewOS will scan your project to learn its structure,
405
+ conventions, and design. This knowledge is injected into every
406
+ task for better results.
407
+ </p>
408
+
409
+ {!analyzing && !analysisDone && (
410
+ <div
411
+ style={{ textAlign: 'center', padding: 'var(--space-6) 0' }}
412
+ >
413
+ <p
414
+ style={{
415
+ fontSize: '0.8rem',
416
+ color: 'var(--color-muted)',
417
+ marginBottom: 'var(--space-4)',
418
+ }}
419
+ >
420
+ Ready to analyze your project. This may take 1–3 minutes.
421
+ </p>
422
+ <div
423
+ style={{
424
+ display: 'flex',
425
+ gap: 'var(--space-3)',
426
+ justifyContent: 'center',
427
+ }}
428
+ >
429
+ <button
430
+ type="button"
431
+ onClick={handleStartAnalysis}
432
+ class={btnPrimary}
433
+ >
434
+ Start Analysis
435
+ </button>
436
+ </div>
437
+ </div>
438
+ )}
439
+
440
+ {analyzing && (
441
+ <div>
442
+ <div
443
+ style={{
444
+ display: 'flex',
445
+ alignItems: 'center',
446
+ gap: 'var(--space-2)',
447
+ marginBottom: 'var(--space-3)',
448
+ }}
449
+ >
450
+ <Spinner />
451
+ <span
452
+ style={{
453
+ fontSize: '0.78rem',
454
+ color: 'var(--color-primary)',
455
+ }}
456
+ >
457
+ Analyzing project...
458
+ </span>
459
+ </div>
460
+ <div
461
+ ref={progressRef}
462
+ style={{
463
+ background: '#000',
464
+ border: '1px solid var(--color-border)',
465
+ padding: 'var(--space-3)',
466
+ maxHeight: 240,
467
+ overflowY: 'auto',
468
+ fontFamily: 'monospace',
469
+ fontSize: '0.72rem',
470
+ lineHeight: 1.6,
471
+ color: 'var(--color-muted)',
472
+ }}
473
+ >
474
+ {progressLines.length === 0 ? (
475
+ <span class="animate-pulse">Waiting for output...</span>
476
+ ) : (
477
+ progressLines.map((line, i) => (
478
+ <div key={i} class="whitespace-pre-wrap break-all">
479
+ {line}
480
+ </div>
481
+ ))
482
+ )}
483
+ </div>
484
+ </div>
485
+ )}
486
+
487
+ {analysisDone && !analyzing && (
488
+ <div
489
+ style={{
490
+ textAlign: 'center',
491
+ padding: 'var(--space-6) 0',
492
+ color: 'var(--color-primary)',
493
+ }}
494
+ >
495
+ <p style={{ marginBottom: 'var(--space-4)' }}>
496
+ Analysis complete. Project knowledge has been saved.
497
+ </p>
498
+ <button
499
+ type="button"
500
+ onClick={() => setStep(3)}
501
+ class={btnPrimary}
502
+ >
503
+ Continue
504
+ </button>
505
+ </div>
506
+ )}
507
+ </div>
508
+ )}
509
+
510
+ {/* Step 3: Complete */}
511
+ {step === 3 && (
512
+ <div style={{ textAlign: 'center', padding: 'var(--space-6) 0' }}>
513
+ <h2
514
+ style={{
515
+ fontSize: '0.9rem',
516
+ fontWeight: 600,
517
+ color: 'var(--color-text)',
518
+ marginBottom: 'var(--space-3)',
519
+ }}
520
+ >
521
+ Ready to Go
522
+ </h2>
523
+ <p
524
+ style={{
525
+ fontSize: '0.78rem',
526
+ color: 'var(--color-muted)',
527
+ marginBottom: 'var(--space-5)',
528
+ }}
529
+ >
530
+ {analysisDone
531
+ ? 'Your project has been analyzed and crewOS is ready.'
532
+ : 'Project analysis must be completed to continue.'}
533
+ </p>
534
+ <button
535
+ type="button"
536
+ onClick={handleComplete}
537
+ disabled={completing}
538
+ class={btnPrimary}
539
+ >
540
+ {completing ? 'Finishing...' : 'Go to Dashboard'}
541
+ </button>
542
+ </div>
543
+ )}
544
+ </div>
545
+ </div>
546
+ </div>
547
+ );
548
+ };
549
+
550
+ export default Onboarding;
@@ -0,0 +1,46 @@
1
+ import axios from 'axios';
2
+
3
+ import environments from '../utils/environments';
4
+
5
+ const { BACKEND_URL } = environments;
6
+
7
+ const getErrorMessage = (err) => {
8
+ const responseData = err?.response?.data;
9
+ if (typeof responseData === 'string') return responseData;
10
+ if (responseData?.message) return responseData.message;
11
+ return err?.message || 'Something went wrong';
12
+ };
13
+
14
+ const api = axios.create({
15
+ baseURL: BACKEND_URL,
16
+ headers: {
17
+ 'Content-Type': 'application/json',
18
+ },
19
+ });
20
+
21
+ api.interceptors.request.use(
22
+ async (config) => {
23
+ const token = await localStorage.getItem('crewos_token');
24
+
25
+ if (token) {
26
+ config.headers.Authorization = `Bearer ${token}`;
27
+ }
28
+
29
+ return config;
30
+ },
31
+ (error) => {
32
+ return Promise.reject(error);
33
+ },
34
+ );
35
+
36
+ api.interceptors.response.use(
37
+ (response) => {
38
+ return response;
39
+ },
40
+ (err) => {
41
+ const message = getErrorMessage(err);
42
+ throw new Error(message);
43
+ },
44
+ );
45
+
46
+ export default api;