-
Notifications
You must be signed in to change notification settings - Fork 98
Expand file tree
/
Copy pathpage.js
More file actions
406 lines (387 loc) · 16.9 KB
/
page.js
File metadata and controls
406 lines (387 loc) · 16.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
"use client"
import React from "react"
import { useState, useEffect } from "react" // Importing necessary React hooks
import { useRouter } from "next/navigation" // Hook for client-side navigation in Next.js
import Disclaimer from "@components/Disclaimer" // Component to display a disclaimer for data usage
import AppCard from "@components/AppCard" // Reusable component to display app integration cards
import AnimatedLogo from "@components/AnimatedLogo" // Component for displaying an animated logo
import { Tooltip } from "@node_modules/react-tooltip/dist/react-tooltip" // Component for creating tooltips
import ProIcon from "@components/ProIcon" // Component to indicate Pro features
import ShiningButton from "@components/ShiningButton" // Custom button component with a shining effect
import { IconQuestionMark } from "@node_modules/@tabler/icons-react/dist/esm/tabler-icons-react" // Icon for help/question mark
import { WavyBackground } from "@components/WavyBackground" // Component for a wavy background effect
import AnimatedBeam from "@components/AnimatedBeam" // Component for an animated beam effect during loading
import toast from "react-hot-toast" // Library for displaying toast notifications
/**
* AppIntegration Component - Handles the app integration page where users can connect social media profiles.
*
* This component allows users to connect their LinkedIn, Reddit, and Twitter accounts to enhance their profile.
* It includes features for scraping profile data, displaying disclaimers, and building a user profile based on connected apps.
*
* @returns {React.ReactNode} - The AppIntegration component UI.
*/
const AppIntegration = () => {
const router = useRouter() // Hook to get the router object for navigation
const [showDisclaimer, setShowDisclaimer] = useState(false) // State to control the visibility of the disclaimer modal - showDisclaimer: boolean
const [linkedInProfileUrl, setLinkedInProfileUrl] = useState("") // State to store LinkedIn profile URL input - linkedInProfileUrl: string
const [redditProfileUrl, setRedditProfileUrl] = useState("") // State to store Reddit profile URL input - redditProfileUrl: string
const [twitterProfileUrl, setTwitterProfileUrl] = useState("") // State to store Twitter profile URL input - twitterProfileUrl: string
const [isBuildingProfile, setIsBuildingProfile] = useState(false) // State to indicate if the profile building process is active - isBuildingProfile: boolean
const [selectedApp, setSelectedApp] = useState("") // State to store the name of the app currently being connected - selectedApp: string ("LinkedIn" | "Reddit" | "Twitter" | "")
const [isConnecting, setIsConnecting] = useState(false) // State to indicate if the connection process is active - isConnecting: boolean
const [connectedApps, setConnectedApps] = useState({
// State to track which apps are currently connected - connectedApps: { LinkedIn: boolean, Reddit: boolean, Twitter: boolean }
LinkedIn: false,
Reddit: false,
Twitter: false
})
const [pricingPlan, setPricingPlan] = useState("free") // State to store the user's pricing plan, defaults to "free" - pricingPlan: string ("free" | "pro")
/**
* useEffect hook to fetch the user's pricing plan on component mount.
* Calls the electron backend to retrieve the pricing plan and updates the state.
*/
useEffect(() => {
/**
* Fetches the pricing plan from the backend using electron invoke.
* Sets the pricingPlan state with the fetched plan or defaults to "free" on error or if no plan is returned.
*/
const fetchPricingPlan = async () => {
try {
const pricing =
await window.electron?.invoke("fetch-pricing-plan") // Invoke electron backend to get pricing plan
setPricingPlan(pricing || "free") // Set state with fetched pricing or default to 'free'
} catch (error) {
toast.error(`Error fetching pricing plan: ${error}`) // Show error toast if fetching fails
}
}
fetchPricingPlan() // Call fetchPricingPlan on component mount
}, [])
/**
* Handles setting or adding data to the database.
*
* Fetches existing data from the database and either sets new data for a key if it doesn't exist or is empty,
* or adds data under an existing key if it already contains data.
*
* @async
* @function handleSetOrAddData
* @param {string} key - The database key to set or add data to.
* @param {any} data - The data to be set or added.
* @throws {Error} - Throws an error if fetching existing user data fails.
* @returns {Promise<void>}
*/
const handleSetOrAddData = async (key, data) => {
try {
const response = await window.electron?.invoke("get-db-data") // Invoke electron to get data from database
if (response.status !== 200) {
throw new Error("Error fetching existing user data") // Throw error if response status is not 200
}
const existingData = response.data || {} // Get existing data from response, default to empty object if no data
// Check if data for the key is empty or doesn't exist
if (
!existingData[key] ||
Object.keys(existingData[key]).length === 0 ||
existingData[key].length === 0
) {
await window.electron?.invoke("set-db-data", {
// Invoke electron to set data in database
data: { [key]: data } // Data to set for the given key
})
} else {
await window.electron?.invoke("add-db-data", {
// Invoke electron to add data to existing data in database
data: { [key]: data } // Data to add for the given key
})
}
} catch (error) {
toast.error(`Error handling data for key ${key}: ${error}`) // Show error toast if any error occurs
}
}
/**
* Handles the acceptance of the disclaimer and proceeds with profile scraping.
*
* This function is called when the user accepts the disclaimer in the Disclaimer component.
* It sets the `isConnecting` state to true, initiates profile scraping based on the `selectedApp`,
* and updates the `connectedApps` state upon successful scraping.
*
* @async
* @function handleDisclaimerAccept
* @returns {Promise<void>}
*/
const handleDisclaimerAccept = async () => {
setShowDisclaimer(false) // Hide the disclaimer modal
setIsConnecting(true) // Set connecting state to true to indicate connection process is starting
try {
// Handle LinkedIn profile scraping
if (selectedApp === "LinkedIn" && linkedInProfileUrl) {
let response = await window.electron?.invoke(
"scrape-linkedin",
{
// Invoke electron to scrape LinkedIn profile
linkedInProfileUrl // Pass LinkedIn profile URL to backend
}
)
if (response.status === 200) {
await handleSetOrAddData(
"linkedInProfile",
response.profile
) // Store scraped LinkedIn profile data in database
setConnectedApps((prev) => ({ ...prev, LinkedIn: true })) // Update connectedApps state to mark LinkedIn as connected
}
// Handle Reddit profile scraping
} else if (selectedApp === "Reddit" && redditProfileUrl) {
let response = await window.electron?.invoke("scrape-reddit", {
// Invoke electron to scrape Reddit profile
redditProfileUrl // Pass Reddit profile URL to backend
})
if (response.status === 200) {
await handleSetOrAddData("redditProfile", response.topics) // Store scraped Reddit topics data in database
setConnectedApps((prev) => ({ ...prev, Reddit: true })) // Update connectedApps state to mark Reddit as connected
}
// Handle Twitter profile scraping
} else if (selectedApp === "Twitter" && twitterProfileUrl) {
let response = await window.electron?.invoke("scrape-twitter", {
// Invoke electron to scrape Twitter profile
twitterProfileUrl // Pass Twitter profile URL to backend
})
if (response.status === 200) {
await handleSetOrAddData("twitterProfile", response.topics) // Store scraped Twitter topics data in database
setConnectedApps((prev) => ({ ...prev, Twitter: true })) // Update connectedApps state to mark Twitter as connected
}
}
} catch (error) {
toast.error(`Error processing ${selectedApp} profile: ${error}`) // Show error toast if any error occurs during scraping
} finally {
setIsConnecting(false) // Set connecting state to false regardless of success or failure
}
}
/**
* Handles the decline action from the disclaimer component.
* Simply hides the disclaimer modal by setting `showDisclaimer` state to false.
*
* @function handleDisclaimerDecline
* @returns {void}
*/
const handleDisclaimerDecline = () => {
setShowDisclaimer(false) // Hide the disclaimer modal
}
/**
* Handles the action to build the user profile after app integrations.
*
* This function sets `isBuildingProfile` to true to display a loading animation,
* invokes the electron backend to create a document and graph based on user data,
* and then marks the first run as completed in the database.
* Finally, it redirects the user to the chat page.
*
* @async
* @function handleBuildProfile
* @returns {Promise<void>}
*/
const handleBuildProfile = async () => {
setIsBuildingProfile(true) // Set building profile state to true to show loading animation
try {
const response = await window.electron?.invoke(
"create-document-and-graph"
) // Invoke electron to create document and graph in backend
if (response.status !== 200) {
throw new Error("Error creating graph") // Throw error if response status is not 200
}
await window.electron?.invoke("set-db-data", {
// Invoke electron to set data in database
data: { firstRunCompleted: true } // Mark first run as completed in database
})
} catch (error) {
toast.error("Error building profile") // Show error toast if any error occurs during profile building
} finally {
router.push("/chat") // Redirect user to the chat page after profile building is complete or failed
}
}
/**
* Conditional rendering for the profile building animation screen.
*
* If `isBuildingProfile` is true, this screen with animated logo and "Building profile" message is displayed.
*
* @returns {React.ReactNode | null} - Returns the animated beam component with loading animation if `isBuildingProfile` is true, otherwise null.
*/
if (isBuildingProfile) {
return (
<AnimatedBeam>
{" "}
{/* AnimatedBeam component for loading effect */}
<div className="flex flex-col items-center justify-center min-h-screen">
<div className="flex flex-col items-center justify-center h-full backdrop-blur-xs">
<AnimatedLogo /> {/* Animated logo component */}
<div className="flex items-center justify-center gap-3">
<h1 className="text-white font-Poppins text-4xl mb-6 mt-8">
Building profile
</h1>{" "}
{/* Text indicating profile building */}
<div className="flex space-x-1 mt-4">
<div className="dot dot1" />{" "}
{/* Animated dots for loading indicator */}
<div className="dot dot2" />
<div className="dot dot3" />
</div>
</div>
</div>
</div>
</AnimatedBeam>
)
}
/**
* Main return statement for the AppIntegration component, rendering the app integration UI.
*
* Includes app cards for LinkedIn, Reddit, and Twitter, a "Build Profile" button,
* a tooltip for integrations info, and conditional rendering for the Disclaimer component.
*
* @returns {React.ReactNode} - The main UI for the AppIntegration component.
*/
return (
<>
<WavyBackground className="max-w-screen overflow-hidden">
{" "}
{/* Wavy background for the entire page */}
<div className="flex flex-col items-center justify-between py-20 min-h-screen bg-transparent relative">
<h1 className="text-white text-5xl font-Poppins">
Connect Your Apps
</h1>{" "}
{/* Title of the page */}
<div className="flex items-center justify-center h-full w-1/2 space-x-8">
{" "}
{/* Container for app cards */}
<AppCard
logo="/images/linkedin-logo.png" // LinkedIn logo image path
name="LinkedIn" // App name - LinkedIn
description="Connect your LinkedIn account to add your professional information to Sentient's context." // Description for LinkedIn card
onClick={() => {
// OnClick handler for LinkedIn card
if (!isConnecting && !connectedApps.LinkedIn) {
// Check if not already connecting and LinkedIn not already connected
setShowDisclaimer(true) // Show disclaimer modal
setSelectedApp("LinkedIn") // Set selected app to LinkedIn
}
}}
action={
connectedApps.LinkedIn ? "Connected" : "connect"
} // Action text based on connection status
loading={selectedApp === "LinkedIn" && isConnecting} // Show loading state when connecting LinkedIn
disabled={connectedApps.LinkedIn || isConnecting} // Disable card if already connected or currently connecting
/>
<AppCard
logo="/images/reddit-logo.png" // Reddit logo image path
name="Reddit" // App name - Reddit
description="Connect your Reddit account to let Sentient analyze your subreddit activity and identify topics of interest." // Description for Reddit card
onClick={() => {
// OnClick handler for Reddit card
if (
!isConnecting &&
!connectedApps.Reddit &&
pricingPlan !== "free"
) {
// Check if not connecting, not connected, and not free plan
setShowDisclaimer(true) // Show disclaimer modal
setSelectedApp("Reddit") // Set selected app to Reddit
}
}}
action={
connectedApps.Reddit ? (
"Connected"
) : pricingPlan === "free" ? (
<ProIcon />
) : (
"connect"
)
} // Action text, shows ProIcon for free plan
loading={selectedApp === "Reddit" && isConnecting} // Show loading state when connecting Reddit
disabled={
connectedApps.Reddit ||
isConnecting ||
pricingPlan === "free"
} // Disable if connected, connecting, or free plan
/>
<AppCard
logo="/images/twitter-logo.png" // Twitter logo image path
name="Twitter" // App name - Twitter
description="Connect your Twitter account to let Sentient analyze your tweets and identify topics of interest." // Description for Twitter card
onClick={() => {
// OnClick handler for Twitter card
if (
!isConnecting &&
!connectedApps.Twitter &&
pricingPlan !== "free"
) {
// Check if not connecting, not connected, and not free plan
setShowDisclaimer(true) // Show disclaimer modal
setSelectedApp("Twitter") // Set selected app to Twitter
}
}}
action={
connectedApps.Twitter ? (
"Connected"
) : pricingPlan === "free" ? (
<ProIcon />
) : (
"connect"
)
} // Action text, shows ProIcon for free plan
loading={selectedApp === "Twitter" && isConnecting} // Loading state when connecting Twitter
disabled={
connectedApps.Twitter ||
isConnecting ||
pricingPlan === "free"
} // Disable if connected, connecting, or free plan
/>
</div>
<ShiningButton // Shining button component for "Build Profile" action
className="font-Poppins font-semibold" // Styling classes
onClick={handleBuildProfile} // OnClick handler to build profile
disabled={isConnecting} // Disable button while connecting
>
Build Profile
</ShiningButton>
<div
data-tooltip-id="integrations" // Tooltip ID for integrations info
data-tooltip-content="You can always connect or disconnect the apps from the Settings page later. All data stays local." // Tooltip content
className="absolute top-4 right-4" // Positioning classes
>
<button className="text-gray-300 hover-button p-2 rounded-[50%] text-sm cursor-default">
<IconQuestionMark />
</button>{" "}
{/* Question mark icon button for tooltip */}
</div>
<Tooltip
id="integrations"
place="right"
type="dark"
effect="float"
/>{" "}
{/* Tooltip component for integrations info */}
</div>
{showDisclaimer && ( // Conditional rendering for Disclaimer component
<Disclaimer
appName={selectedApp} // App name to display in disclaimer
profileUrl={
// Profile URL prop based on selected app
selectedApp === "LinkedIn"
? linkedInProfileUrl
: selectedApp === "Reddit"
? redditProfileUrl
: twitterProfileUrl
}
setProfileUrl={
// Set profile URL function based on selected app
selectedApp === "LinkedIn"
? setLinkedInProfileUrl
: selectedApp === "Reddit"
? setRedditProfileUrl
: setTwitterProfileUrl
}
onAccept={handleDisclaimerAccept} // Handler for disclaimer accept action
onDecline={handleDisclaimerDecline} // Handler for disclaimer decline action
action="connect" // Action type for disclaimer - "connect"
/>
)}
</WavyBackground>
</>
)
}
export default AppIntegration