diff --git a/apps/web/.env.example b/apps/web/.env.example index abd39cef9..ea6cacb9c 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1,5 +1,3 @@ NEXT_PUBLIC_BACKEND_URL=https://api.supermemory.ai NEXT_PUBLIC_POSTHOG_KEY= -EXA_API_KEY= -XAI_API_KEY= NEXT_PUBLIC_AGENTID_AUTH_ENABLED= diff --git a/apps/web/app/api/onboarding/extract-content/route.ts b/apps/web/app/api/onboarding/extract-content/route.ts deleted file mode 100644 index 56a18d5bb..000000000 --- a/apps/web/app/api/onboarding/extract-content/route.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { hasVerifiedSession } from "@/lib/verify-session" - -export interface ExaContentResult { - url: string - text: string - title: string - author?: string -} - -interface ExaApiResponse { - results: ExaContentResult[] -} - -const exaApiKey = process.env.EXA_API_KEY -if (!exaApiKey) { - console.error( - "EXA_API_KEY is not configured; /api/onboarding/extract-content will return 503", - ) -} - -function parseHttpUrl(value: string): URL | null { - try { - const url = new URL(value) - return url.protocol === "http:" || url.protocol === "https:" ? url : null - } catch { - return null - } -} - -export async function POST(request: Request) { - try { - if (!(await hasVerifiedSession(request))) { - return Response.json({ error: "Unauthorized" }, { status: 401 }) - } - - if (!exaApiKey) { - return Response.json( - { error: "Content extraction is unavailable" }, - { status: 503 }, - ) - } - - 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 }, - ) - } - - const MAX_URLS = 10 - if (urls.length > MAX_URLS) { - return Response.json( - { error: `Invalid input: at most ${MAX_URLS} urls per request` }, - { status: 400 }, - ) - } - - const invalid = Response.json( - { - error: - "Invalid input: all urls must be http(s) strings of at most 2048 characters", - }, - { status: 400 }, - ) - - const normalizedUrls: string[] = [] - const seen = new Set() - for (const url of urls) { - if (typeof url !== "string" || !url.trim() || url.length > 2048) { - return invalid - } - const parsed = parseHttpUrl(url.trim()) - if (!parsed) { - return invalid - } - if (seen.has(parsed.href)) continue - seen.add(parsed.href) - normalizedUrls.push(parsed.href) - } - - const response = await fetch("https://api.exa.ai/contents", { - method: "POST", - headers: { - "x-api-key": exaApiKey, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - urls: normalizedUrls, - 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 deleted file mode 100644 index e2b7b1294..000000000 --- a/apps/web/app/api/onboarding/research/route.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { xai } from "@ai-sdk/xai" -import { generateText } from "ai" -import { hasVerifiedSession } from "@/lib/verify-session" - -interface ResearchRequest { - xUrl: string - name?: string - email?: string -} - -const ALLOWED_X_HOSTS: ReadonlySet = new Set([ - "x.com", - "www.x.com", - "twitter.com", - "www.twitter.com", - "mobile.twitter.com", -]) - -const X_URL_FALLBACK_REGEX = - /^(?:https?:\/\/)?(?:x\.com|www\.x\.com|twitter\.com|www\.twitter\.com|mobile\.twitter\.com)\/([^/\s?#]+)/i - -// Each value occupies one prompt line, so collapse whitespace or it can forge extra lines. -function sanitizeContextField(value: unknown): string { - return typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "" -} - -function isXHost(hostname: string): boolean { - return ALLOWED_X_HOSTS.has(hostname.toLowerCase()) -} - -function extractHandle(input: string): string { - const trimmed = input.trim() - if (!trimmed) return "" - - let handle = trimmed.replace(/^@+/, "") - const lower = handle.toLowerCase() - - if (lower.includes("x.com") || lower.includes("twitter.com")) { - try { - const parsed = new URL( - handle.startsWith("http://") || handle.startsWith("https://") - ? handle - : `https://${handle}`, - ) - handle = isXHost(parsed.hostname) - ? (parsed.pathname.split("/").filter(Boolean)[0] ?? "") - : "" - } catch { - handle = handle.match(X_URL_FALLBACK_REGEX)?.[1] ?? "" - } - } - - return handle.replace(/^@+/, "").split(/[/?#]/)[0]?.toLowerCase() ?? "" -} - -function finalPrompt(handle: string, userContext: string) { - return `You are researching a user based on their X/Twitter profile to help personalize their experience. - -X Handle: @${handle}${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 { - if (!(await hasVerifiedSession(req))) { - return Response.json({ error: "Unauthorized" }, { status: 401 }) - } - - const { xUrl, name, email }: ResearchRequest = await req.json() - - if (!xUrl?.trim()) { - return Response.json( - { error: "X/Twitter URL or handle is required" }, - { status: 400 }, - ) - } - - if ( - (name !== undefined && (typeof name !== "string" || name.length > 200)) || - (email !== undefined && (typeof email !== "string" || email.length > 320)) - ) { - return Response.json( - { error: "Invalid input: name/email too long" }, - { status: 400 }, - ) - } - - const handle = extractHandle(xUrl) - - if (!/^[A-Za-z0-9_]{1,15}$/.test(handle)) { - return Response.json( - { error: "Could not parse a valid X/Twitter handle from the input" }, - { status: 400 }, - ) - } - - const safeName = sanitizeContextField(name) - const safeEmail = sanitizeContextField(email) - const contextParts: string[] = [] - if (safeName) contextParts.push(`Name: ${safeName}`) - if (safeEmail) contextParts.push(`Email: ${safeEmail}`) - const userContext = - contextParts.length > 0 - ? `\n\nAdditional context about the user:\n${contextParts.join("\n")}` - : "" - - const { text } = await generateText({ - model: xai.responses("grok-4-fast"), - prompt: finalPrompt(handle, userContext), - abortSignal: AbortSignal.timeout(60_000), - tools: { - web_search: xai.tools.webSearch(), - x_search: xai.tools.xSearch({ - allowedXHandles: [handle], - }), - }, - }) - - return Response.json({ text }) - } catch (error) { - console.error("Research API error:", error) - return Response.json({ error: "Internal server error" }, { status: 500 }) - } -} diff --git a/apps/web/components/fullscreen-note-modal.tsx b/apps/web/components/fullscreen-note-modal.tsx index 634987929..276576608 100644 --- a/apps/web/components/fullscreen-note-modal.tsx +++ b/apps/web/components/fullscreen-note-modal.tsx @@ -61,7 +61,6 @@ export function FullscreenNoteModal({ const handleContentChange = useCallback( (newContent: string) => { - console.log("handleContentChange", newContent) setContent(newContent) setDraft(newContent) }, diff --git a/apps/web/package.json b/apps/web/package.json index 63c4acee4..dcb65c472 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -23,7 +23,6 @@ "dependencies": { "@ai-sdk/google": "^3.0.64", "@ai-sdk/react": "^3.0.170", - "@ai-sdk/xai": "^3.0.83", "@better-fetch/fetch": "^1.1.18", "@cloudflare/ai-chat": "^0.0.7", "@dnd-kit/core": "^6.3.1", diff --git a/bun.lock b/bun.lock index f7c898691..89abf2e73 100644 --- a/bun.lock +++ b/bun.lock @@ -148,7 +148,6 @@ "dependencies": { "@ai-sdk/google": "^3.0.64", "@ai-sdk/react": "^3.0.170", - "@ai-sdk/xai": "^3.0.83", "@better-fetch/fetch": "^1.1.18", "@cloudflare/ai-chat": "^0.0.7", "@dnd-kit/core": "^6.3.1", @@ -470,7 +469,7 @@ "@ai-sdk/vercel": ["@ai-sdk/vercel@2.0.39", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8eu3ljJpkCTP4ppcyYB+NcBrkcBoSOFthCSgk5VnjaxnDaOJFaxnPwfddM7wx3RwMk2CiK1O61Px/LlqNc7QkQ=="], - "@ai-sdk/xai": ["@ai-sdk/xai@3.0.83", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SuQz68BZGeuZjrSUJAzku97IlhdiNJJBsvG/Tvm3K2tuxkBS7TJq0fH4/AzAM7w2H2jxVUgboP7kRR6IfpRxcg=="], + "@ai-sdk/xai": ["@ai-sdk/xai@3.0.67", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.35", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-KQQIDc91dUA5IGFMnXBuvPBeraYNTdpDC1qUS+JG8vE+/299//5sZFafI1kKYUu3f3p7LaZrKXYgZ1Ni7QIRbw=="], "@aihubmix/ai-sdk-provider": ["@aihubmix/ai-sdk-provider@1.0.3", "", { "dependencies": { "@ai-sdk/anthropic": "^3.0.0", "@ai-sdk/google": "^3.0.0", "@ai-sdk/openai": "^3.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "zod": "3.25.76" } }, "sha512-6YSu/3rLkPlY7fqSHTwq6LMiK+0BLVZsbsi/28w+og2MrpUYPNaBhkj7F80K79NE28+HFOqIJROFgAUFmaoh6w=="], @@ -5118,11 +5117,11 @@ "@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - "@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], + "@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-g3wA57IAQFb+3j4YuFndgkUdXyRETZVvbfAWM+UX7bZSxA3xjes0v3XKgIdKdekPtDGsh4ZX2byHD0gJIMPfiA=="], "@ai-sdk/xai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], - "@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.23", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg=="], + "@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], "@aihubmix/ai-sdk-provider/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.58", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/53SACgmVukO4bkms4dpxpRlYhW8Ct6QZRe6sj1Pi5H00hYhxIrqfiLbZBGxkdRvjsBQeP/4TVGsXgH5rQeb8Q=="], @@ -5578,8 +5577,6 @@ "@voltagent/core/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - "@voltagent/core/@ai-sdk/xai": ["@ai-sdk/xai@3.0.67", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.35", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-KQQIDc91dUA5IGFMnXBuvPBeraYNTdpDC1qUS+JG8vE+/299//5sZFafI1kKYUu3f3p7LaZrKXYgZ1Ni7QIRbw=="], - "@voltagent/core/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="], "@voltagent/core/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.204.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DqxY8yoAaiBPivoJD4UtgrMS8gEmzZ5lnaxzPojzLVHBGqPxgWm4zcuvcUHZiqQ6kRX2Klel2r9y8cA2HAtqpw=="], @@ -6632,10 +6629,6 @@ "@voltagent/core/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@voltagent/core/@ai-sdk/xai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], - - "@voltagent/core/@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="], - "@voltagent/core/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], "@voltagent/core/@modelcontextprotocol/sdk/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], @@ -7288,8 +7281,6 @@ "@voltagent/core/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@voltagent/core/@ai-sdk/xai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@voltagent/core/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "@voltagent/core/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],