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.
- package/app/.env.example +1 -0
- package/app/index.html +50 -0
- package/app/package.json +25 -0
- package/app/public/favicon.svg +1 -0
- package/app/public/images/cursor-ide-guiiding.png +0 -0
- package/app/public/images/gpt.jpg +0 -0
- package/app/src/app.jsx +22 -0
- package/app/src/components/ConfirmModal.jsx +50 -0
- package/app/src/components/Icons.jsx +377 -0
- package/app/src/components/RedirectRoute.jsx +14 -0
- package/app/src/components/SplashScreen.jsx +15 -0
- package/app/src/hooks/useAuth.js +28 -0
- package/app/src/index.css +268 -0
- package/app/src/main.jsx +5 -0
- package/app/src/navigations/AuthRoutes.jsx +15 -0
- package/app/src/navigations/MainRoutes.jsx +15 -0
- package/app/src/navigations/OnboardingRoutes.jsx +15 -0
- package/app/src/navigations/index.jsx +37 -0
- package/app/src/pages/Home/index.jsx +2095 -0
- package/app/src/pages/Login/index.jsx +118 -0
- package/app/src/pages/Onboarding/index.jsx +550 -0
- package/app/src/services/api.js +46 -0
- package/app/src/services/auth.service.js +3 -0
- package/app/src/services/config.service.js +13 -0
- package/app/src/services/member.service.js +7 -0
- package/app/src/services/onboarding.service.js +17 -0
- package/app/src/services/role.service.js +6 -0
- package/app/src/services/task.service.js +22 -0
- package/app/src/stores/auth.store.js +7 -0
- package/app/src/utils/environments.js +5 -0
- package/app/vite.config.js +10 -0
- package/app/yarn.lock +1337 -0
- package/backend/package-lock.json +918 -0
- package/backend/package.json +18 -0
- package/backend/src/configs/db.config.js +40 -0
- package/backend/src/controllers/auth.controller.js +19 -0
- package/backend/src/controllers/config.controller.js +23 -0
- package/backend/src/controllers/member.controller.js +30 -0
- package/backend/src/controllers/models.controller.js +25 -0
- package/backend/src/controllers/onboarding.controller.js +49 -0
- package/backend/src/controllers/role.controller.js +17 -0
- package/backend/src/controllers/task.controller.js +63 -0
- package/backend/src/index.js +36 -0
- package/backend/src/middlewares/onboarding.guard.js +14 -0
- package/backend/src/routes/auth.route.js +8 -0
- package/backend/src/routes/config.route.js +11 -0
- package/backend/src/routes/index.js +22 -0
- package/backend/src/routes/member.route.js +11 -0
- package/backend/src/routes/models.route.js +8 -0
- package/backend/src/routes/onboarding.route.js +13 -0
- package/backend/src/routes/role.route.js +9 -0
- package/backend/src/routes/task.route.js +20 -0
- package/backend/src/services/auth.service.js +14 -0
- package/backend/src/services/config.service.js +176 -0
- package/backend/src/services/data/roles.json +474 -0
- package/backend/src/services/member.service.js +77 -0
- package/backend/src/services/onboarding.service.js +328 -0
- package/backend/src/services/role.service.js +23 -0
- package/backend/src/services/task.service.js +665 -0
- package/backend/src/utils/catcher.js +9 -0
- package/backend/src/utils/sanitize.js +13 -0
- package/backend/yarn.lock +513 -0
- package/bin/crewos.js +307 -0
- 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;
|