aboutsummaryrefslogtreecommitdiff
path: root/apps/web/app/api/onboarding
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/app/api/onboarding')
-rw-r--r--apps/web/app/api/onboarding/extract-content/route.ts61
-rw-r--r--apps/web/app/api/onboarding/research/route.ts81
2 files changed, 142 insertions, 0 deletions
diff --git a/apps/web/app/api/onboarding/extract-content/route.ts b/apps/web/app/api/onboarding/extract-content/route.ts
new file mode 100644
index 00000000..6cdb40d5
--- /dev/null
+++ b/apps/web/app/api/onboarding/extract-content/route.ts
@@ -0,0 +1,61 @@
+export interface ExaContentResult {
+ url: string
+ text: string
+ title: string
+ author?: string
+}
+
+interface ExaApiResponse {
+ results: ExaContentResult[]
+}
+
+export async function POST(request: Request) {
+ try {
+ const { urls } = await request.json()
+
+ if (!Array.isArray(urls) || urls.length === 0) {
+ return Response.json(
+ { error: "Invalid input: urls must be a non-empty array" },
+ { status: 400 },
+ )
+ }
+
+ if (!urls.every((url) => typeof url === "string" && url.trim())) {
+ return Response.json(
+ { error: "Invalid input: all urls must be non-empty strings" },
+ { status: 400 },
+ )
+ }
+
+ const response = await fetch("https://api.exa.ai/contents", {
+ method: "POST",
+ headers: {
+ "x-api-key": process.env.EXA_API_KEY ?? "",
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ urls,
+ text: true,
+ livecrawl: "fallback",
+ }),
+ })
+
+ if (!response.ok) {
+ console.error(
+ "Exa API request failed:",
+ response.status,
+ response.statusText,
+ )
+ return Response.json(
+ { error: "Failed to fetch content from Exa API" },
+ { status: 500 },
+ )
+ }
+
+ const data: ExaApiResponse = await response.json()
+ return Response.json({ results: data.results })
+ } catch (error) {
+ console.error("Exa API request error:", error)
+ return Response.json({ error: "Internal server error" }, { status: 500 })
+ }
+}
diff --git a/apps/web/app/api/onboarding/research/route.ts b/apps/web/app/api/onboarding/research/route.ts
new file mode 100644
index 00000000..d0d6eded
--- /dev/null
+++ b/apps/web/app/api/onboarding/research/route.ts
@@ -0,0 +1,81 @@
+import { xai } from "@ai-sdk/xai"
+import { generateText } from "ai"
+
+interface ResearchRequest {
+ xUrl: string
+ name?: string
+ email?: string
+}
+
+// prompt to get user context from X/Twitter profile
+function finalPrompt(xUrl: string, userContext: string) {
+ return `You are researching a user based on their X/Twitter profile to help personalize their experience.
+
+X/Twitter Profile URL: ${xUrl}${userContext}
+
+Please analyze this X/Twitter profile and provide a comprehensive but concise summary of the user. Include:
+- Professional background and current role (if available)
+- Key interests and topics they engage with
+- Notable projects, achievements, or affiliations
+- Their expertise areas
+- Any other relevant information that helps understand who they are
+
+Format the response as clear, readable paragraphs. Focus on factual information from their profile. If certain information is not available, skip that section rather than speculating.`
+}
+
+export async function POST(req: Request) {
+ try {
+ const { xUrl, name, email }: ResearchRequest = await req.json()
+
+ if (!xUrl?.trim()) {
+ return Response.json(
+ { error: "X/Twitter URL is required" },
+ { status: 400 },
+ )
+ }
+
+ const lowerUrl = xUrl.toLowerCase()
+ if (!lowerUrl.includes("x.com") && !lowerUrl.includes("twitter.com")) {
+ return Response.json(
+ { error: "URL must be an X/Twitter profile link" },
+ { status: 400 },
+ )
+ }
+
+ const contextParts: string[] = []
+ if (name) contextParts.push(`Name: ${name}`)
+ if (email) contextParts.push(`Email: ${email}`)
+ const userContext =
+ contextParts.length > 0
+ ? `\n\nAdditional context about the user:\n${contextParts.join("\n")}`
+ : ""
+
+ const { text } = await generateText({
+ model: xai("grok-4-1-fast-reasoning"),
+ prompt: finalPrompt(xUrl, userContext),
+ providerOptions: {
+ xai: {
+ searchParameters: {
+ mode: "on",
+ sources: [
+ {
+ type: "web",
+ safeSearch: true,
+ },
+ {
+ type: "x",
+ includedXHandles: [lowerUrl.replace("https://x.com/", "").replace("https://twitter.com/", "")],
+ postFavoriteCount: 10,
+ },
+ ],
+ },
+ },
+ },
+ })
+
+ return Response.json({ text })
+ } catch (error) {
+ console.error("Research API error:", error)
+ return Response.json({ error: "Internal server error" }, { status: 500 })
+ }
+}