onairos 2.1.12 → 2.2.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.
@@ -1,66 +1,143 @@
1
- import React, { useState } from 'react';
1
+ import React, { useState, useEffect } from 'react';
2
+
3
+ // Platform connectors with correct icons
4
+ const platformConnectors = [
5
+ { name: 'YouTube', icon: '📺', color: 'bg-red-500', connector: 'youtube' },
6
+ { name: 'LinkedIn', icon: '💼', color: 'bg-blue-700', connector: 'linkedin' },
7
+ { name: 'Reddit', icon: '🔥', color: 'bg-orange-500', connector: 'reddit' },
8
+ { name: 'Pinterest', icon: '📌', color: 'bg-red-600', connector: 'pinterest' },
9
+ { name: 'Instagram', icon: '📷', color: 'bg-pink-500', connector: 'instagram' },
10
+ { name: 'GitHub', icon: '⚡', color: 'bg-gray-800', connector: 'github' },
11
+ { name: 'Facebook', icon: '👥', color: 'bg-blue-600', connector: 'facebook' },
12
+ { name: 'Gmail', icon: '✉️', color: 'bg-red-400', connector: 'gmail' }
13
+ ];
2
14
 
3
15
  const dataTypes = [
4
16
  {
5
17
  id: 'basic',
6
18
  name: 'Basic Info',
7
- description: 'Essential profile information and account details',
19
+ description: 'Essential profile information, account details, and basic demographics',
8
20
  icon: '👤',
9
- required: false
21
+ required: true, // Auto-selected and non-deselectable
22
+ tooltip: 'Includes name, email, basic profile information. This data is essential for personalization and is always included.',
23
+ privacyLink: 'https://onairos.uk/privacy#basic-info'
10
24
  },
11
25
  {
12
26
  id: 'personality',
13
27
  name: 'Personality',
14
- description: 'Personality traits, behavioral patterns and insights',
28
+ description: 'Personality traits, behavioral patterns and psychological insights',
15
29
  icon: '🧠',
16
- required: false
30
+ required: false,
31
+ tooltip: 'AI-analyzed personality traits based on your social media activity and interactions. Used to improve content recommendations.',
32
+ privacyLink: 'https://onairos.uk/privacy#personality-data'
17
33
  },
18
34
  {
19
35
  id: 'preferences',
20
36
  name: 'Preferences',
21
- description: 'User preferences, settings and choices',
37
+ description: 'User preferences, interests, settings and personal choices',
22
38
  icon: '⚙️',
23
- required: false
39
+ required: false,
40
+ tooltip: 'Your likes, interests, content preferences, and behavioral patterns. Helps create a more personalized experience.',
41
+ privacyLink: 'https://onairos.uk/privacy#preferences-data'
24
42
  }
25
43
  ];
26
44
 
45
+ // Tooltip component
46
+ const Tooltip = ({ children, content, privacyLink }) => {
47
+ const [showTooltip, setShowTooltip] = useState(false);
48
+
49
+ return (
50
+ <div className="relative inline-block">
51
+ <span
52
+ onMouseEnter={() => setShowTooltip(true)}
53
+ onMouseLeave={() => setShowTooltip(false)}
54
+ className="border-b border-dotted border-gray-400 cursor-help"
55
+ >
56
+ {children}
57
+ </span>
58
+ {showTooltip && (
59
+ <div className="absolute z-50 w-64 p-3 mt-2 text-sm bg-white border border-gray-200 rounded-lg shadow-lg left-0">
60
+ <p className="mb-2 text-gray-700">{content}</p>
61
+ <a
62
+ href={privacyLink}
63
+ target="_blank"
64
+ rel="noopener noreferrer"
65
+ className="text-blue-600 hover:text-blue-800 text-xs font-medium"
66
+ >
67
+ Learn more about privacy →
68
+ </a>
69
+ </div>
70
+ )}
71
+ </div>
72
+ );
73
+ };
74
+
27
75
  export default function DataRequest({
28
76
  onComplete,
29
77
  userEmail,
30
78
  appName = 'App',
31
79
  autoFetch = false,
32
- testMode = false
80
+ testMode = false,
81
+ connectedAccounts = {} // Connected platforms from onboarding
33
82
  }) {
34
83
  const [selectedData, setSelectedData] = useState({
35
- basic: false, // User can choose
84
+ basic: true, // Auto-selected and required
36
85
  personality: false,
37
86
  preferences: false
38
87
  });
88
+
89
+ const [connectorStates, setConnectorStates] = useState({});
39
90
  const [isSubmitting, setIsSubmitting] = useState(false);
40
91
  const [isLoadingApi, setIsLoadingApi] = useState(false);
41
92
  const [apiResponse, setApiResponse] = useState(null);
42
93
  const [apiError, setApiError] = useState(null);
43
94
 
95
+ // Initialize connector states based on connected accounts
96
+ useEffect(() => {
97
+ const initialStates = {};
98
+ platformConnectors.forEach(platform => {
99
+ initialStates[platform.name] = {
100
+ connected: connectedAccounts[platform.name] || false,
101
+ selected: false
102
+ };
103
+ });
104
+ setConnectorStates(initialStates);
105
+ }, [connectedAccounts]);
106
+
44
107
  const handleDataToggle = (dataId) => {
108
+ // Don't allow toggling required items
109
+ const dataType = dataTypes.find(dt => dt.id === dataId);
110
+ if (dataType?.required) return;
111
+
45
112
  setSelectedData(prev => ({
46
113
  ...prev,
47
114
  [dataId]: !prev[dataId]
48
115
  }));
49
116
  };
50
117
 
118
+ const handleConnectorToggle = (platformName) => {
119
+ setConnectorStates(prev => ({
120
+ ...prev,
121
+ [platformName]: {
122
+ ...prev[platformName],
123
+ selected: !prev[platformName]?.selected
124
+ }
125
+ }));
126
+ };
127
+
51
128
  const handleRowClick = (dataId) => {
52
- // Make the entire row clickable for better UX
129
+ const dataType = dataTypes.find(dt => dt.id === dataId);
130
+ if (dataType?.required) return; // Don't allow clicking required items
53
131
  handleDataToggle(dataId);
54
132
  };
55
133
 
56
134
  const generateUserHash = (email) => {
57
- // Simple hash function for user identification
58
135
  let hash = 0;
59
136
  const str = email + Date.now().toString();
60
137
  for (let i = 0; i < str.length; i++) {
61
138
  const char = str.charCodeAt(i);
62
139
  hash = ((hash << 5) - hash) + char;
63
- hash = hash & hash; // Convert to 32-bit integer
140
+ hash = hash & hash;
64
141
  }
65
142
  return `user_${Math.abs(hash).toString(36)}`;
66
143
  };
@@ -70,7 +147,6 @@ export default function DataRequest({
70
147
  setApiError(null);
71
148
 
72
149
  try {
73
- // Create a unique user hash for this request
74
150
  const userHash = generateUserHash(userEmail);
75
151
 
76
152
  // Get selected data types
@@ -78,16 +154,19 @@ export default function DataRequest({
78
154
  .filter(([key, value]) => value)
79
155
  .map(([key]) => key);
80
156
 
81
- // Map frontend data types to backend confirmation types
157
+ // Get selected connectors
158
+ const selectedConnectors = Object.entries(connectorStates)
159
+ .filter(([platform, state]) => state.selected && state.connected)
160
+ .map(([platform]) => platform);
161
+
82
162
  const mapDataTypesToConfirmations = (approvedData) => {
83
163
  const confirmations = [];
84
164
  const currentDate = new Date().toISOString();
85
165
 
86
- // Map frontend types to backend types according to API expectations
87
166
  const dataTypeMapping = {
88
- 'basic': 'Medium', // Basic info -> Medium data
89
- 'personality': 'Large', // Personality -> Large analysis
90
- 'preferences': 'Traits' // Preferences -> Traits data
167
+ 'basic': 'Medium',
168
+ 'personality': 'Large',
169
+ 'preferences': 'Traits'
91
170
  };
92
171
 
93
172
  approvedData.forEach(dataType => {
@@ -102,46 +181,43 @@ export default function DataRequest({
102
181
  return confirmations;
103
182
  };
104
183
 
105
- // Determine API endpoint based on test mode
106
184
  const apiEndpoint = testMode
107
185
  ? 'https://api2.onairos.uk/inferenceTest'
108
186
  : 'https://api2.onairos.uk/getAPIurlMobile';
109
187
 
110
- // Prepare the base result
111
188
  const baseResult = {
112
189
  userHash,
113
190
  appName,
114
191
  approvedData,
192
+ selectedConnectors,
115
193
  apiUrl: apiEndpoint,
116
194
  testMode,
117
195
  timestamp: new Date().toISOString()
118
196
  };
119
197
 
120
198
  if (autoFetch) {
121
- // Auto mode true: make API request and return results
122
199
  try {
123
200
  const confirmations = mapDataTypesToConfirmations(approvedData);
124
201
 
125
- // Format request according to backend expectations
126
202
  const requestBody = testMode ? {
127
- // Test mode: simple format for testing
128
203
  approvedData,
204
+ selectedConnectors,
129
205
  userEmail,
130
206
  appName,
131
207
  testMode,
132
208
  timestamp: new Date().toISOString()
133
209
  } : {
134
- // Live mode: proper Info format for backend
135
210
  Info: {
136
211
  storage: "local",
137
212
  appId: appName,
138
213
  confirmations: confirmations,
139
- EncryptedUserPin: "pending_pin_integration", // TODO: Get from user PIN setup
214
+ connectors: selectedConnectors,
215
+ EncryptedUserPin: "pending_pin_integration",
140
216
  account: userEmail,
141
217
  proofMode: false,
142
218
  Domain: window.location.hostname,
143
- web3Type: "standard", // or "Othent" if using Othent
144
- OthentSub: null // Only if using Othent authentication
219
+ web3Type: "standard",
220
+ OthentSub: null
145
221
  }
146
222
  };
147
223
 
@@ -149,52 +225,55 @@ export default function DataRequest({
149
225
  method: 'POST',
150
226
  headers: {
151
227
  'Content-Type': 'application/json',
228
+ 'x-api-key': 'onairos_web_sdk_live_key_2024'
152
229
  },
153
230
  body: JSON.stringify(requestBody)
154
231
  });
155
232
 
156
233
  if (!response.ok) {
157
- throw new Error(`API call failed with status: ${response.status}`);
234
+ throw new Error(`API request failed: ${response.status}`);
158
235
  }
159
236
 
160
- const apiData = await response.json();
161
-
162
- // Format response according to test mode requirements
163
- let formattedData = apiData;
164
- if (testMode && apiData) {
165
- formattedData = {
166
- InferenceResult: {
167
- output: apiData.croppedInference || apiData.output || apiData.inference,
168
- traits: apiData.traitResult || apiData.traits || apiData.personalityData
169
- }
170
- };
171
- }
172
-
173
- setApiResponse(formattedData);
174
- return {
237
+ const data = await response.json();
238
+ setApiResponse(data);
239
+
240
+ const result = {
175
241
  ...baseResult,
176
- apiResponse: formattedData,
242
+ apiResponse: data,
177
243
  success: true
178
244
  };
245
+
246
+ setTimeout(() => {
247
+ onComplete(result);
248
+ }, 1500);
249
+
250
+ return result;
251
+
179
252
  } catch (error) {
180
- setApiError(error.message);
181
- return {
253
+ console.error('API request failed:', error);
254
+ setApiError(`API request failed: ${error.message}`);
255
+
256
+ const result = {
182
257
  ...baseResult,
183
- apiError: error.message,
258
+ error: error.message,
184
259
  success: false
185
260
  };
261
+
262
+ setTimeout(() => {
263
+ onComplete(result);
264
+ }, 2000);
265
+
266
+ return result;
186
267
  }
187
268
  } else {
188
- // Auto mode false (default): return API endpoint URL for manual calling
189
- return {
190
- ...baseResult,
191
- success: true,
192
- message: 'Data request approved. Use the provided API URL to fetch user data.'
193
- };
269
+ onComplete(baseResult);
270
+ return baseResult;
194
271
  }
272
+
195
273
  } catch (error) {
196
- setApiError(`Failed to process request: ${error.message}`);
197
- return null;
274
+ console.error('Data processing failed:', error);
275
+ setApiError(`Processing failed: ${error.message}`);
276
+ throw error;
198
277
  } finally {
199
278
  setIsLoadingApi(false);
200
279
  }
@@ -218,6 +297,8 @@ export default function DataRequest({
218
297
  };
219
298
 
220
299
  const selectedCount = Object.values(selectedData).filter(Boolean).length;
300
+ const selectedConnectorCount = Object.values(connectorStates).filter(state => state.selected).length;
301
+ const connectedCount = Object.values(connectorStates).filter(state => state.connected).length;
221
302
 
222
303
  return (
223
304
  <div className="w-full max-w-md mx-auto bg-white rounded-lg shadow-xl overflow-hidden" style={{ maxHeight: '90vh', height: 'auto' }}>
@@ -225,7 +306,7 @@ export default function DataRequest({
225
306
  <div className="text-center mb-4 sm:mb-6">
226
307
  <h2 className="text-lg sm:text-xl font-bold text-gray-900 mb-2">Data Request</h2>
227
308
  <p className="text-gray-600 text-xs sm:text-sm">
228
- Select the data types you'd like to share with <span className="font-medium">{appName}</span>
309
+ Select the data types and connections to share with <span className="font-medium">{appName}</span>
229
310
  </p>
230
311
  </div>
231
312
 
@@ -237,65 +318,125 @@ export default function DataRequest({
237
318
  </div>
238
319
 
239
320
  {/* Data Types Selection */}
240
- <div className="space-y-2 sm:space-y-3 mb-4 sm:mb-6">
241
- {dataTypes.map((dataType) => {
242
- const isSelected = selectedData[dataType.id] || false;
243
- const isRequired = dataType.required;
244
-
245
- return (
246
- <div
247
- key={dataType.id}
248
- className={`flex items-center justify-between p-3 sm:p-4 border rounded-lg transition-colors ${
249
- isRequired
250
- ? 'bg-blue-50 border-blue-200'
251
- : 'hover:bg-gray-50 cursor-pointer'
252
- }`}
253
- onClick={() => handleRowClick(dataType.id)}
254
- >
255
- <div className="flex items-center space-x-3">
256
- <div className="text-xl sm:text-2xl">
257
- {dataType.icon}
258
- </div>
259
- <div>
260
- <h3 className="font-medium text-gray-900 text-sm sm:text-base">
261
- {dataType.name}
262
- {isRequired && <span className="text-blue-600 ml-1">*</span>}
263
- </h3>
264
- <p className="text-xs sm:text-sm text-gray-500">{dataType.description}</p>
321
+ <div className="mb-6">
322
+ <h3 className="text-md font-semibold text-gray-900 mb-3">Data Types</h3>
323
+ <div className="space-y-2 sm:space-y-3">
324
+ {dataTypes.map((dataType) => {
325
+ const isSelected = selectedData[dataType.id] || false;
326
+ const isRequired = dataType.required;
327
+
328
+ return (
329
+ <div
330
+ key={dataType.id}
331
+ className={`flex items-center justify-between p-3 sm:p-4 border rounded-lg transition-colors ${
332
+ isRequired
333
+ ? 'bg-gray-100 border-gray-300 cursor-not-allowed'
334
+ : 'hover:bg-gray-50 cursor-pointer border-gray-200'
335
+ }`}
336
+ onClick={() => handleRowClick(dataType.id)}
337
+ >
338
+ <div className="flex items-center space-x-3">
339
+ <div className="text-xl sm:text-2xl">
340
+ {dataType.icon}
341
+ </div>
342
+ <div>
343
+ <h4 className="font-medium text-gray-900 text-sm sm:text-base">
344
+ <Tooltip content={dataType.tooltip} privacyLink={dataType.privacyLink}>
345
+ {dataType.name}
346
+ </Tooltip>
347
+ {isRequired && <span className="text-gray-500 ml-1 text-xs">(Required)</span>}
348
+ </h4>
349
+ <p className="text-xs sm:text-sm text-gray-500">{dataType.description}</p>
350
+ </div>
265
351
  </div>
352
+
353
+ {/* Toggle Switch or Required Badge */}
354
+ {isRequired ? (
355
+ <div className="px-2 py-1 bg-gray-400 text-white text-xs rounded-full">
356
+ Required
357
+ </div>
358
+ ) : (
359
+ <button
360
+ onClick={(e) => {
361
+ e.stopPropagation();
362
+ handleDataToggle(dataType.id);
363
+ }}
364
+ className={`relative inline-flex h-5 sm:h-6 w-9 sm:w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${
365
+ isSelected ? 'bg-blue-600' : 'bg-gray-200'
366
+ }`}
367
+ >
368
+ <span
369
+ className={`inline-block h-3 sm:h-4 w-3 sm:w-4 transform rounded-full bg-white transition-transform ${
370
+ isSelected ? 'translate-x-5 sm:translate-x-6' : 'translate-x-1'
371
+ }`}
372
+ />
373
+ </button>
374
+ )}
266
375
  </div>
267
-
268
- {/* Toggle Switch or Required Badge */}
269
- {isRequired ? (
270
- <div className="px-2 py-1 bg-blue-600 text-white text-xs rounded-full">
271
- Required
376
+ );
377
+ })}
378
+ </div>
379
+ </div>
380
+
381
+ {/* Platform Connectors */}
382
+ <div className="mb-6">
383
+ <h3 className="text-md font-semibold text-gray-900 mb-3">Data Connectors</h3>
384
+ <p className="text-xs text-gray-600 mb-3">Select connected platforms to include their data</p>
385
+
386
+ <div className="grid grid-cols-2 gap-2">
387
+ {platformConnectors.map((platform) => {
388
+ const connectorState = connectorStates[platform.name];
389
+ const isConnected = connectorState?.connected || false;
390
+ const isSelected = connectorState?.selected || false;
391
+
392
+ return (
393
+ <div
394
+ key={platform.name}
395
+ className={`p-2 border rounded-lg transition-all duration-200 cursor-pointer ${
396
+ !isConnected ? 'opacity-50 cursor-not-allowed border-gray-200 bg-gray-50' :
397
+ isSelected ? 'border-green-400 bg-green-50' :
398
+ 'border-gray-200 bg-white hover:border-gray-300'
399
+ }`}
400
+ onClick={() => isConnected && handleConnectorToggle(platform.name)}
401
+ >
402
+ <div className={`w-6 h-6 rounded ${platform.color} flex items-center justify-center text-white text-sm mb-2 mx-auto relative`}>
403
+ {platform.icon}
404
+
405
+ {/* Connection Status Indicator */}
406
+ {isConnected && isSelected && (
407
+ <div className="absolute -top-1 -right-1 w-3 h-3 bg-green-500 rounded-full flex items-center justify-center">
408
+ <svg className="w-2 h-2 text-white" fill="currentColor" viewBox="0 0 20 20">
409
+ <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
410
+ </svg>
411
+ </div>
412
+ )}
272
413
  </div>
273
- ) : (
274
- <button
275
- onClick={(e) => {
276
- e.stopPropagation();
277
- handleDataToggle(dataType.id);
278
- }}
279
- className={`relative inline-flex h-5 sm:h-6 w-9 sm:w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${
280
- isSelected ? 'bg-blue-600' : 'bg-gray-200'
281
- }`}
282
- >
283
- <span
284
- className={`inline-block h-3 sm:h-4 w-3 sm:w-4 transform rounded-full bg-white transition-transform ${
285
- isSelected ? 'translate-x-5 sm:translate-x-6' : 'translate-x-1'
286
- }`}
287
- />
288
- </button>
289
- )}
290
- </div>
291
- );
292
- })}
414
+
415
+ <div className="text-center">
416
+ <h4 className="font-medium text-gray-900 text-xs">{platform.name}</h4>
417
+ <p className={`text-xs ${
418
+ isConnected ? (isSelected ? 'text-green-600' : 'text-blue-600') : 'text-gray-400'
419
+ }`}>
420
+ {!isConnected ? 'Not connected' :
421
+ isSelected ? 'Selected' : 'Tap to select'}
422
+ </p>
423
+ </div>
424
+ </div>
425
+ );
426
+ })}
427
+ </div>
428
+
429
+ {connectedCount === 0 && (
430
+ <p className="text-xs text-yellow-600 mt-2 text-center">
431
+ ⚠️ No platforms connected. Connect platforms in onboarding to select data sources.
432
+ </p>
433
+ )}
293
434
  </div>
294
435
 
295
436
  {/* Selection Summary */}
296
437
  <div className="mb-3 sm:mb-4 p-2 sm:p-3 bg-green-50 border border-green-200 rounded-lg">
297
438
  <p className="text-green-800 text-xs sm:text-sm">
298
- ✅ {selectedCount} data type{selectedCount > 1 ? 's' : ''} selected for sharing
439
+ ✅ {selectedCount} data type{selectedCount > 1 ? 's' : ''} and {selectedConnectorCount} connector{selectedConnectorCount > 1 ? 's' : ''} selected
299
440
  </p>
300
441
  </div>
301
442
 
@@ -323,12 +464,12 @@ export default function DataRequest({
323
464
  : 'bg-gray-300 text-gray-500 cursor-not-allowed'
324
465
  }`}
325
466
  >
326
- {isSubmitting ? 'Processing...' : `Share ${selectedCount} data type${selectedCount > 1 ? 's' : ''}`}
467
+ {isSubmitting ? 'Processing...' : `Share Selected Data`}
327
468
  </button>
328
469
 
329
470
  <button
330
471
  type="button"
331
- onClick={() => onComplete({ selectedData: {}, cancelled: true })}
472
+ onClick={() => onComplete({ selectedData: {}, selectedConnectors: [], cancelled: true })}
332
473
  className="w-full py-2 text-gray-500 hover:text-gray-700 text-xs sm:text-sm"
333
474
  >
334
475
  Cancel
@@ -25,7 +25,7 @@ export function OnairosButton({
25
25
  }) {
26
26
 
27
27
  const [showOverlay, setShowOverlay] = useState(false);
28
- const [currentFlow, setCurrentFlow] = useState('email'); // 'email' | 'onboarding' | 'pin' | 'training' | 'dataRequest'
28
+ const [currentFlow, setCurrentFlow] = useState('email'); // 'email' | 'onboarding' | 'pin' | 'dataRequest' (training is within onboarding)
29
29
  const [userData, setUserData] = useState(null);
30
30
  const [error, setError] = useState(null);
31
31
 
@@ -100,17 +100,11 @@ export function OnairosButton({
100
100
 
101
101
  // Flow decision logic
102
102
  if (isNewUser) {
103
- console.log('🚀 New user detected → Starting onboarding flow');
103
+ console.log('🚀 New user detected → Starting onboarding flow (includes training)');
104
104
  setCurrentFlow('onboarding');
105
105
  } else {
106
- console.log('👋 Returning user detected → Skipping onboarding');
107
- if (enableTraining) {
108
- console.log('📚 Training enabled → Going to training flow');
109
- setCurrentFlow('training');
110
- } else {
111
- console.log('📋 Training disabled → Going to data request flow');
112
- setCurrentFlow('dataRequest');
113
- }
106
+ console.log('👋 Existing user detected → Going directly to data request');
107
+ setCurrentFlow('dataRequest');
114
108
  }
115
109
  };
116
110
 
@@ -230,6 +224,7 @@ export function OnairosButton({
230
224
  autoFetch={autoFetch}
231
225
  testMode={testMode}
232
226
  appIcon={appIcon}
227
+ connectedAccounts={userData?.connectedAccounts || {}}
233
228
  />
234
229
  );
235
230