feat: deep-research on user profile and tiptap integration (#672)

deep-research on user profile

add novel integration

tiptap 3.x integration
This commit is contained in:
MaheshtheDev 2026-01-15 21:53:53 +00:00
parent 641db19e35
commit 59c294b299
59 changed files with 1805 additions and 365 deletions

View file

@ -3,21 +3,9 @@ name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
@ -38,17 +26,53 @@ jobs:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
use_sticky_comment: true
prompt: |
Please review this pull request and provide feedback on:
- Code quality and best practices
- Potential bugs or issues
- Performance considerations
- Security concerns
- Test coverage
You are reviewing a PR for supermemory - a Turbo monorepo with multiple apps and packages.
Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
## Repository Structure Context
**Apps (apps/):**
- `web` - Next.js web application (no tests)
- `mcp` - Model Context Protocol server on Cloudflare Workers (no tests)
- `browser-extension` - WXT-based browser extension (no tests)
- `raycast-extension` - Raycast app extension (no tests)
- `docs` - Mintlify documentation site
**Published Packages (packages/) - with tests:**
- `tools` - AI SDK memory tools (Vitest)
- `ai-sdk` - supermemory AI SDK wrapper (Vitest)
- `openai-sdk-python` - Python OpenAI integration (pytest)
- `pipecat-sdk-python` - Python Pipecat integration (pytest)
**Internal Packages (packages/) - no tests:**
- `ui` - Shared React/Radix UI components
- `lib` - Shared utilities
- `hooks` - Custom React hooks
- `memory-graph` - D3-based graph visualization
- `validation` - Zod schemas
## Review Instructions
1. First, run `gh pr diff` to see what files changed
2. Based on the changes, review for:
- **Code quality**: Follow Biome linting rules (double quotes, tabs, no default exports)
- **Type safety**: Ensure proper TypeScript usage
- **Security**: Check for injection vulnerabilities, credential exposure, unsafe patterns
- **Performance**: Look for unnecessary re-renders (React), N+1 queries, memory leaks
3. **Test coverage** - ONLY review if changes touch these packages:
- `packages/tools/**` or `packages/ai-sdk/**` → Check for Vitest tests
- `packages/openai-sdk-python/**` or `packages/pipecat-sdk-python/**` → Check for pytest tests
- Skip test coverage review for apps and other packages (they have no test setup)
4. **Package-specific concerns:**
- Published packages (tools, ai-sdk, memory-graph, *-python): Check for breaking API changes
- UI package: Check accessibility (a11y) and component API consistency
- Web app: Check for proper data fetching patterns (TanStack Query), Zustand state management
- MCP server: Check Hono routing and Cloudflare Workers compatibility
5. Be concise and actionable. Focus on issues that matter, not style nitpicks (Biome handles that).
Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'

View file

@ -1,2 +1,4 @@
NEXT_PUBLIC_BACKEND_URL=https://api.supermemory.ai
NEXT_PUBLIC_POSTHOG_KEY=
NEXT_PUBLIC_POSTHOG_KEY=
EXA_API_KEY=
XAI_API_KEY=

View file

@ -14,7 +14,7 @@ import { InitialHeader } from "@/components/initial-header"
import { useRouter, useSearchParams } from "next/navigation"
import { useState, useEffect } from "react"
import { motion } from "motion/react"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { Logo } from "@ui/assets/Logo"
@ -30,7 +30,11 @@ function AnimatedGradientBackground() {
}}
transition={{
y: { duration: 0.75, ease: "easeOut" },
opacity: { duration: 8, repeat: Number.POSITIVE_INFINITY, ease: "easeInOut" },
opacity: {
duration: 8,
repeat: Number.POSITIVE_INFINITY,
ease: "easeInOut",
},
}}
/>
<motion.div
@ -42,7 +46,11 @@ function AnimatedGradientBackground() {
}}
transition={{
y: { duration: 0.75, ease: "easeOut" },
opacity: { duration: 8, repeat: Number.POSITIVE_INFINITY, ease: "easeInOut" },
opacity: {
duration: 8,
repeat: Number.POSITIVE_INFINITY,
ease: "easeInOut",
},
}}
/>
<motion.div

View file

@ -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 })
}
}

View file

@ -6,18 +6,18 @@ import { useState, useEffect } from "react"
import { useAuth } from "@lib/auth-context"
import { cn } from "@lib/utils"
import { InputStep } from "./welcome/input-step"
import { GreetingStep } from "./welcome/greeting-step"
import { WelcomeStep } from "./welcome/welcome-step"
import { ContinueStep } from "./welcome/continue-step"
import { FeaturesStep } from "./welcome/features-step"
import { MemoriesStep } from "./welcome/memories-step"
import { RelatableQuestion } from "./setup/relatable-question"
import { IntegrationsStep } from "./setup/integrations-step"
import { InputStep } from "../../../components/new/onboarding/welcome/input-step"
import { GreetingStep } from "../../../components/new/onboarding/welcome/greeting-step"
import { WelcomeStep } from "../../../components/new/onboarding/welcome/welcome-step"
import { ContinueStep } from "../../../components/new/onboarding/welcome/continue-step"
import { FeaturesStep } from "../../../components/new/onboarding/welcome/features-step"
import { ProfileStep } from "../../../components/new/onboarding/welcome/profile-step"
import { RelatableQuestion } from "../../../components/new/onboarding/setup/relatable-question"
import { IntegrationsStep } from "../../../components/new/onboarding/setup/integrations-step"
import { InitialHeader } from "@/components/initial-header"
import { SetupHeader } from "./setup/header"
import { ChatSidebar } from "./setup/chat-sidebar"
import { SetupHeader } from "../../../components/new/onboarding/setup/header"
import { ChatSidebar } from "../../../components/new/onboarding/setup/chat-sidebar"
import { Logo } from "@ui/assets/Logo"
import NovaOrb from "@/components/nova/nova-orb"
import { AnimatedGradientBackground } from "@/components/new/animated-gradient-background"
@ -152,7 +152,7 @@ export default function OnboardingPage() {
case "features":
return <FeaturesStep key="features" />
case "memories":
return <MemoriesStep key="memories" onSubmit={setMemoryFormData} />
return <ProfileStep key="profile" onSubmit={setMemoryFormData} />
default:
return null
}

View file

@ -6,7 +6,7 @@ import { motion } from "motion/react"
import NovaOrb from "@/components/nova/nova-orb"
import { useState, useEffect, useRef } from "react"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import Account from "@/components/new/settings/account"
import Integrations from "@/components/new/settings/integrations"
import ConnectionsMCP from "@/components/new/settings/connections-mcp"
@ -175,7 +175,11 @@ export default function SettingsPage() {
return (
<div className="h-screen flex flex-col overflow-hidden">
<header className="flex justify-between items-center px-6 py-3 shrink-0">
<button type="button" onClick={() => router.push("/new")} className="cursor-pointer">
<button
type="button"
onClick={() => router.push("/new")}
className="cursor-pointer"
>
<Logo className="h-7" />
</button>
<div className="flex items-center gap-2">

View file

@ -10,7 +10,7 @@ import { Check, Loader, Trash2, Zap } from "lucide-react"
import { useEffect, useState } from "react"
import { toast } from "sonner"
import type { z } from "zod"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { Button } from "@ui/components/button"

View file

@ -2,7 +2,7 @@
import { useState, useEffect } from "react"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import { FileIcon } from "lucide-react"
import { useHotkeys } from "react-hotkeys-hook"
@ -19,7 +19,12 @@ interface FileContentProps {
isOpen?: boolean
}
export function FileContent({ onSubmit, onDataChange, isSubmitting, isOpen }: FileContentProps) {
export function FileContent({
onSubmit,
onDataChange,
isSubmitting,
isOpen,
}: FileContentProps) {
const [isDragging, setIsDragging] = useState(false)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [title, setTitle] = useState("")
@ -33,8 +38,16 @@ export function FileContent({ onSubmit, onDataChange, isSubmitting, isOpen }: Fi
}
}
const updateData = (newFile: File | null, newTitle: string, newDescription: string) => {
onDataChange?.({ file: newFile, title: newTitle, description: newDescription })
const updateData = (
newFile: File | null,
newTitle: string,
newDescription: string,
) => {
onDataChange?.({
file: newFile,
title: newTitle,
description: newDescription,
})
}
const handleFileChange = (file: File | null) => {

View file

@ -3,7 +3,7 @@
import { useState, useEffect, useMemo, useCallback } from "react"
import { Dialog, DialogContent, DialogTitle } from "@repo/ui/components/dialog"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import {
FileTextIcon,
GlobeIcon,

View file

@ -3,7 +3,7 @@
import { useState, useEffect } from "react"
import { cn } from "@lib/utils"
import { Button } from "@ui/components/button"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import { useHotkeys } from "react-hotkeys-hook"
import { Image as ImageIcon, Loader2 } from "lucide-react"
import { toast } from "sonner"

View file

@ -1,7 +1,7 @@
"use client"
import { useState, useEffect } from "react"
import { useHotkeys } from "react-hotkeys-hook"
import { TextEditor } from "../text-editor"
interface NoteContentProps {
onSubmit?: (content: string) => void
@ -31,11 +31,6 @@ export function NoteContent({
onContentChange?.(newContent)
}
useHotkeys("mod+enter", handleSubmit, {
enabled: isOpen && canSubmit,
enableOnFormTags: ["TEXTAREA"],
})
// Reset content when modal closes
useEffect(() => {
if (!isOpen) {
@ -45,12 +40,12 @@ export function NoteContent({
}, [isOpen, onContentChange])
return (
<textarea
value={content}
onChange={(e) => handleContentChange(e.target.value)}
placeholder="Write your note here..."
disabled={isSubmitting}
className="w-full h-full p-4 mb-4! rounded-[14px] bg-[#14161A] shadow-inside-out resize-none disabled:opacity-50 outline-none"
/>
<div className="p-4 overflow-y-auto flex-1 w-full h-full mb-4! bg-[#14161A] shadow-inside-out rounded-[14px]">
<TextEditor
content={undefined}
onContentChange={handleContentChange}
onSubmit={handleSubmit}
/>
</div>
)
}

View file

@ -14,7 +14,7 @@ import {
SquarePenIcon,
} from "lucide-react"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import ChatInput from "./input"
import ChatModelSelector from "./model-selector"
import { GradientLogo, LogoBgGradient } from "@ui/assets/Logo"
@ -110,7 +110,6 @@ export function ChatSidebar({
},
},
}),
maxSteps: 10,
onFinish: async (result) => {
if (result.message.role !== "assistant") return
@ -285,14 +284,20 @@ export function ChatSidebar({
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const activeElement = document.activeElement as HTMLElement | null
const isInEditableContext =
activeElement?.tagName === "INPUT" ||
activeElement?.tagName === "TEXTAREA" ||
activeElement?.isContentEditable ||
activeElement?.closest('[contenteditable="true"]')
if (
e.key.toLowerCase() === "t" &&
!e.metaKey &&
!e.ctrlKey &&
!e.altKey &&
isChatOpen &&
document.activeElement?.tagName !== "INPUT" &&
document.activeElement?.tagName !== "TEXTAREA"
!isInEditableContext
) {
e.preventDefault()
handleNewChat()

View file

@ -2,7 +2,7 @@ import { useAuth } from "@lib/auth-context"
import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar"
import type { UIMessage } from "@ai-sdk/react"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
interface MemoryResult {
documentId?: string

View file

@ -3,7 +3,7 @@
import { ChevronUpIcon } from "lucide-react"
import NovaOrb from "@/components/nova/nova-orb"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import { useRef, useState } from "react"
import { motion } from "motion/react"
import { SendButton, StopButton } from "./actions"

View file

@ -1,6 +1,6 @@
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import type { UIMessage } from "@ai-sdk/react"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
interface MemoryResult {

View file

@ -3,7 +3,7 @@
import { useState } from "react"
import { cn } from "@lib/utils"
import { Button } from "@ui/components/button"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import { ChevronDownIcon } from "lucide-react"
import { models, type ModelId, modelNames } from "@/lib/models"

View file

@ -3,7 +3,7 @@
import { useState } from "react"
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { PDF } from "@ui/assets/icons"
import { FileText, Image, Video } from "lucide-react"

View file

@ -2,7 +2,7 @@
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>

View file

@ -2,7 +2,7 @@
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { ClaudeDesktopIcon, MCPIcon } from "@ui/assets/icons"
@ -13,11 +13,16 @@ export function McpPreview({ document }: { document: DocumentWithMemories }) {
return (
<div className="bg-[#0B1017] p-3 rounded-[18px] space-y-2">
<div className="flex items-center justify-between gap-1">
<p className={cn(dmSansClassName(), "text-[12px] font-semibold flex items-center gap-1")}>
<ClaudeDesktopIcon className="size-3" />
<p
className={cn(
dmSansClassName(),
"text-[12px] font-semibold flex items-center gap-1",
)}
>
<ClaudeDesktopIcon className="size-3" />
Claude Desktop
</p>
<MCPIcon className="size-6" />
<MCPIcon className="size-6" />
</div>
<div className="space-y-[6px]">
{document.title && (

View file

@ -2,12 +2,54 @@
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { useMemo } from "react"
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
type DocumentWithMemories = DocumentsResponse["documents"][0]
type TipTapNode = {
type: string
text?: string
content?: TipTapNode[]
attrs?: Record<string, unknown>
}
function extractTextFromTipTapContent(content: string): string {
try {
const json = JSON.parse(content) as TipTapNode
return extractTextFromNode(json)
} catch {
return content
}
}
function extractTextFromNode(node: TipTapNode): string {
if (node.type === "text" && node.text) {
return node.text
}
if (!node.content) {
return ""
}
const texts: string[] = []
for (const child of node.content) {
const text = extractTextFromNode(child)
if (text) {
texts.push(text)
}
}
const blockTypes = ["paragraph", "heading", "listItem", "blockquote", "codeBlock"]
if (blockTypes.includes(node.type)) {
return `${texts.join("")}\n`
}
return texts.join("")
}
function NoteIcon() {
return (
<svg
@ -79,6 +121,11 @@ function NoteIcon() {
}
export function NotePreview({ document }: { document: DocumentWithMemories }) {
const previewText = useMemo(() => {
if (!document.content) return ""
return extractTextFromTipTapContent(document.content).trim()
}, [document.content])
return (
<div className="bg-[#0B1017] p-3 rounded-[18px] space-y-2">
<div className="flex items-center gap-1">
@ -98,9 +145,9 @@ export function NotePreview({ document }: { document: DocumentWithMemories }) {
{document.title}
</p>
)}
{document.content && (
{previewText && (
<p className="text-[10px] text-[#737373] line-clamp-4">
{document.content}
{previewText}
</p>
)}
</div>

View file

@ -11,7 +11,7 @@ import {
TweetSkeleton,
} from "react-tweet"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
export function TweetPreview({
data,

View file

@ -3,7 +3,7 @@
import { useState } from "react"
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>

View file

@ -2,7 +2,7 @@
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { extractYouTubeVideoId } from "../utils"

View file

@ -80,7 +80,7 @@ const PDFIcon = ({ className }: { className: string }) => {
width="8.25216"
height="10.2522"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend
@ -158,7 +158,7 @@ const TextDocumentIcon = ({ className }: { className: string }) => {
width="18.0253"
height="13.8376"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
colorInterpolationFilters="sRGB"
>
<feFlood floodOpacity="0" result="BackgroundImageFix" />
<feBlend

View file

@ -2,19 +2,24 @@
import { Dialog, DialogContent, DialogTitle } from "@repo/ui/components/dialog"
import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
import { ArrowUpRightIcon, XIcon } from "lucide-react"
import { ArrowUpRightIcon, XIcon, Loader2 } from "lucide-react"
import type { z } from "zod"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { cn } from "@lib/utils"
import dynamic from "next/dynamic"
import { Title } from "./title"
import { Summary as DocumentSummary } from "./summary"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import { GraphListMemories, type MemoryEntry } from "./graph-list-memories"
import { YoutubeVideo } from "./content/yt-video"
import { TweetContent } from "./content/tweet"
import { isTwitterUrl } from "@/utils/url-helpers"
import { isTwitterUrl } from "@/lib/url-helpers"
import { NotionDoc } from "./content/notion-doc"
import { TextEditor } from "../text-editor"
import { useState, useEffect, useCallback, useMemo } from "react"
import { motion, AnimatePresence } from "motion/react"
import { Button } from "@repo/ui/components/button"
import { useDocumentMutations } from "@/hooks/use-document-mutations"
// Dynamically importing to prevent DOMMatrix error
const PdfViewer = dynamic(
@ -43,7 +48,49 @@ export function DocumentModal({
isOpen,
onClose,
}: DocumentModalProps) {
console.log(_document)
const { updateMutation } = useDocumentMutations()
const { initialEditorContent, initialEditorString } = useMemo(() => {
const content = _document?.content as string | null | undefined
return {
initialEditorContent: content ?? undefined,
initialEditorString: content ?? "",
}
}, [_document?.content])
const [draftContentString, setDraftContentString] =
useState(initialEditorString)
const [editorResetNonce, setEditorResetNonce] = useState(0)
const [lastSavedContent, setLastSavedContent] = useState<string | null>(null)
const resetEditor = useCallback(() => {
setDraftContentString(initialEditorString)
setEditorResetNonce((n) => n + 1)
setLastSavedContent(null)
}, [initialEditorString])
useEffect(() => {
setDraftContentString(initialEditorString)
setEditorResetNonce((n) => n + 1)
setLastSavedContent(null)
}, [initialEditorString])
useEffect(() => {
if (!isOpen) resetEditor()
}, [isOpen, resetEditor])
const hasUnsavedChanges =
draftContentString !== initialEditorString &&
draftContentString !== lastSavedContent
const handleSave = useCallback(() => {
if (!_document?.id) return
updateMutation.mutate(
{ documentId: _document.id, content: draftContentString },
{ onSuccess: (_data, variables) => setLastSavedContent(variables.content) },
)
}, [_document?.id, draftContentString, updateMutation])
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent
@ -79,7 +126,7 @@ export function DocumentModal({
</a>
)}
<DialogPrimitive.Close
className="bg-[#0D121A] w-7 h-7 flex items-center justify-center focus:ring-ring rounded-full transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]"
className="bg-[#0D121A] w-7 h-7 flex items-center justify-center focus:ring-ring rounded-full transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none cursor-pointer [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]"
data-slot="dialog-close"
>
<XIcon stroke="#737373" />
@ -91,7 +138,7 @@ export function DocumentModal({
<div
id="document-preview"
className={cn(
"bg-[#14161A] rounded-[14px] overflow-hidden flex flex-col shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)]",
"bg-[#14161A] rounded-[14px] overflow-hidden flex flex-col shadow-[inset_0_2px_4px_rgba(0,0,0,0.3),inset_0_1px_2px_rgba(0,0,0,0.1)] relative",
)}
>
{(_document?.type === "tweet" ||
@ -104,9 +151,66 @@ export function DocumentModal({
/>
)}
{_document?.type === "text" && (
<div className="p-4 overflow-y-auto flex-1">
{_document.content}
</div>
<>
<div className="p-4 overflow-y-auto flex-1 scrollbar-thin">
<TextEditor
key={`${_document.id}-${editorResetNonce}`}
content={initialEditorContent}
onContentChange={setDraftContentString}
onSubmit={handleSave}
/>
</div>
<AnimatePresence>
{hasUnsavedChanges && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
transition={{ duration: 0.2 }}
className="absolute bottom-4 left-1/2 -translate-x-1/2 flex items-center gap-3 px-4 py-2 bg-[#1B1F24] rounded-full shadow-[0_4px_20px_rgba(0,0,0,0.4),inset_1px_1px_1px_rgba(255,255,255,0.1)]"
>
<span className="text-sm text-[#737373]">
Unsaved changes
</span>
<Button
variant="ghost"
size="sm"
onClick={resetEditor}
disabled={updateMutation.isPending}
className="text-[#737373]/80 hover:text-white rounded-full px-3"
>
Cancel
</Button>
<Button
variant="insideOut"
size="sm"
onClick={handleSave}
disabled={updateMutation.isPending}
className="hover:text-white rounded-full px-4"
>
{updateMutation.isPending ? (
<>
<Loader2 className="size-4 animate-spin mr-1" />
Saving...
</>
) : (
<>
Save
<span
className={cn(
"bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm px-1 py-0.5 text-[10px] flex items-center justify-center",
dmSansClassName(),
)}
>
⌘+Enter
</span>
</>
)}
</Button>
</motion.div>
)}
</AnimatePresence>
</>
)}
{_document?.type === "pdf" && <PdfViewer url={_document.url} />}
{_document?.type === "notion_doc" && (

View file

@ -1,5 +1,5 @@
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import type { DocumentTypeEnum } from "@repo/validation/schemas"
import type { z } from "zod"
import { getDocumentIcon } from "@/components/new/document-modal/document-icon"

View file

@ -18,7 +18,7 @@ import {
} from "lucide-react"
import { Button } from "@ui/components/button"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import { Tabs, TabsList, TabsTrigger } from "@ui/components/tabs"
import { useProjectName } from "@/hooks/use-project-name"
import {
@ -259,10 +259,12 @@ export function Header({ onAddMemory, onOpenMCP }: HeaderProps) {
<Settings className="h-4 w-4" />
Settings
</DropdownMenuItem>
<DropdownMenuItem onClick={() => {
authClient.signOut()
router.push("/login/new")
}}>
<DropdownMenuItem
onClick={() => {
authClient.signOut()
router.push("/login/new")
}}
>
<LogOut className="h-4 w-4" />
Logout
</DropdownMenuItem>

View file

@ -1,4 +1,4 @@
import { dmSans125ClassName, dmSansClassName } from "@/utils/fonts"
import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts"
import { Dialog, DialogContent, DialogFooter } from "@repo/ui/components/dialog"
import { cn } from "@lib/utils"
import * as DialogPrimitive from "@radix-ui/react-dialog"

View file

@ -14,7 +14,7 @@ import Image from "next/image"
import { toast } from "sonner"
import { analytics } from "@/lib/analytics"
import { cn } from "@lib/utils"
import { dmMonoClassName, dmSansClassName } from "@/utils/fonts"
import { dmMonoClassName, dmSansClassName } from "@/lib/fonts"
import { SyncLogoIcon } from "@ui/assets/icons"
const clients = {
@ -78,7 +78,14 @@ export function MCPSteps({ variant = "full" }: MCPStepsProps) {
>
<div
className="absolute left-4 top-0 w-px bg-[#1E293B] z-10"
style={{ height: activeStep === 3 ? isEmbedded ? "100%" : "calc(100% - 4rem)" : "100%" }}
style={{
height:
activeStep === 3
? isEmbedded
? "100%"
: "calc(100% - 4rem)"
: "100%",
}}
/>
<div className="flex items-start space-x-4 z-20">
<button

View file

@ -6,11 +6,8 @@ import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
import { useInfiniteQuery } from "@tanstack/react-query"
import { useCallback, memo, useMemo, useState, useRef } from "react"
import type { z } from "zod"
import {
Masonry,
useInfiniteLoader,
} from "masonic"
import { dmSansClassName } from "@/utils/fonts"
import { Masonry, useInfiniteLoader } from "masonic"
import { dmSansClassName } from "@/lib/fonts"
import { SuperLoader } from "@/components/superloader"
import { cn } from "@lib/utils"
import { Button } from "@ui/components/button"
@ -151,9 +148,7 @@ export function MemoriesGrid({ isChatOpen }: { isChatOpen: boolean }) {
}
return (
<div
className="h-full"
>
<div className="h-full">
<Button
className={cn(
dmSansClassName(),

View file

@ -5,10 +5,11 @@ import { motion, AnimatePresence } from "motion/react"
import NovaOrb from "@/components/nova/nova-orb"
import { Button } from "@ui/components/button"
import { PanelRightCloseIcon, SendIcon } from "lucide-react"
import { collectValidUrls } from "@/utils/url-helpers"
import { collectValidUrls } from "@/lib/url-helpers"
import { $fetch } from "@lib/api"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import { useAuth } from "@lib/auth-context"
interface ChatSidebarProps {
formData: {
@ -20,6 +21,7 @@ interface ChatSidebarProps {
}
export function ChatSidebar({ formData }: ChatSidebarProps) {
const { user } = useAuth()
const [message, setMessage] = useState("")
const [isChatOpen, setIsChatOpen] = useState(true)
const [messages, setMessages] = useState<
@ -127,9 +129,60 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
useEffect(() => {
if (!formData) return
const urls = collectValidUrls(formData.linkedin, formData.otherLinks)
const formDataMessages: typeof messages = []
console.log("urls", urls)
if (formData.twitter) {
formDataMessages.push({
message: formData.twitter,
url: formData.twitter,
title: "X/Twitter",
description: formData.twitter,
type: "formData" as const,
})
}
if (formData.linkedin) {
formDataMessages.push({
message: formData.linkedin,
url: formData.linkedin,
title: "LinkedIn",
description: formData.linkedin,
type: "formData" as const,
})
}
if (formData.otherLinks.length > 0) {
formData.otherLinks.forEach((link) => {
formDataMessages.push({
message: link,
url: link,
title: "Link",
description: link,
type: "formData" as const,
})
})
}
if (formData.description?.trim()) {
formDataMessages.push({
message: formData.description,
title: "Likes",
description: formData.description,
type: "formData" as const,
})
}
setMessages(formDataMessages)
const hasContent =
formData.twitter ||
formData.linkedin ||
formData.otherLinks.length > 0 ||
formData.description?.trim()
if (!hasContent) return
const urls = collectValidUrls(formData.linkedin, formData.otherLinks)
const processContent = async () => {
setIsLoading(true)
@ -137,17 +190,73 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
try {
const documentIds: string[] = []
// Step 1: Fetch content from Exa if URLs exist
if (formData.description?.trim()) {
try {
const descDocResponse = await $fetch("@post/documents", {
body: {
content: formData.description,
containerTags: ["sm_project_default"],
metadata: {
sm_source: "consumer",
description_source: "user_input",
},
},
})
if (descDocResponse.data?.id) {
documentIds.push(descDocResponse.data.id)
}
} catch (error) {
console.warn("Error creating description document:", error)
}
}
if (formData.twitter) {
try {
const researchResponse = await fetch("/api/onboarding/research", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
xUrl: formData.twitter,
name: user?.name,
email: user?.email,
}),
})
if (researchResponse.ok) {
const { text } = await researchResponse.json()
if (text?.trim()) {
const xDocResponse = await $fetch("@post/documents", {
body: {
content: text,
containerTags: ["sm_project_default"],
metadata: {
sm_source: "consumer",
onboarding_source: "x_research",
x_url: formData.twitter,
},
},
})
if (xDocResponse.data?.id) {
documentIds.push(xDocResponse.data.id)
}
}
}
} catch (error) {
console.warn("Error fetching X research:", error)
}
}
if (urls.length > 0) {
const response = await fetch("/api/exa/fetch-content", {
const response = await fetch("/api/onboarding/extract-content", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ urls }),
})
const { results } = await response.json()
console.log("results", results)
// Create documents from Exa results
for (const result of results) {
try {
const docResponse = await $fetch("@post/documents", {
@ -171,95 +280,17 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
}
}
// Step 2: Create document from description if it exists
if (formData.description?.trim()) {
try {
const descDocResponse = await $fetch("@post/documents", {
body: {
content: formData.description,
containerTags: ["sm_project_default"],
metadata: {
sm_source: "consumer",
description_source: "user_input",
},
},
})
if (descDocResponse.data?.id) {
documentIds.push(descDocResponse.data.id)
}
} catch (error) {
console.warn("Error creating description document:", error)
}
}
// Step 3: Poll for memories or show form data
if (documentIds.length > 0) {
await pollForMemories(documentIds)
} else {
// No documents created, show form data or waiting
const formDataMessages = []
if (formData.twitter) {
formDataMessages.push({
message: `Twitter: ${formData.twitter}`,
url: formData.twitter,
title: "Twitter Profile",
description: `Twitter: ${formData.twitter}`,
type: "formData" as const,
})
}
if (formData.linkedin) {
formDataMessages.push({
message: `LinkedIn: ${formData.linkedin}`,
url: formData.linkedin,
title: "LinkedIn Profile",
description: `LinkedIn: ${formData.linkedin}`,
type: "formData" as const,
})
}
if (formData.otherLinks.length > 0) {
formData.otherLinks.forEach((link) => {
formDataMessages.push({
message: `Link: ${link}`,
url: link,
title: "Other Link",
description: `Link: ${link}`,
type: "formData" as const,
})
})
}
const waitingMessage = {
message: "Waiting for your input",
url: "",
title: "",
description: "Waiting for your input",
type: "waiting" as const,
}
setMessages([...formDataMessages, waitingMessage])
}
} catch (error) {
console.warn("Error processing content:", error)
const waitingMessage = {
message: "Waiting for your input",
url: "",
title: "",
description: "Waiting for your input",
type: "waiting" as const,
}
setMessages([waitingMessage])
}
setIsLoading(false)
}
processContent()
}, [formData, pollForMemories])
}, [formData, pollForMemories, user])
return (
<AnimatePresence mode="wait">
@ -330,6 +361,39 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
)}
<div className="w-px flex-1 bg-[#293952]/40" />
</div>
{msg.type === "formData" && (
<div className="bg-[#293952]/40 rounded-lg p-2 px-3 space-y-1 flex-1">
{msg.title && (
<h3
className="text-sm font-medium"
style={{
background:
"linear-gradient(90deg, #369BFD 0%, #36FDFD 30%, #36FDB5 100%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
}}
>
{msg.title}
</h3>
)}
{msg.url && (
<a
href={msg.url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-400 hover:underline break-all block"
>
{msg.url}
</a>
)}
{msg.title === "Likes" && msg.description && (
<p className="text-xs text-white/70 mt-1">
{msg.description}
</p>
)}
</div>
)}
{msg.type === "memory" && (
<div className="space-y-2 w-full max-h-60 overflow-y-auto scrollbar-thin">
{msg.memories?.map((memory) => (
@ -383,7 +447,7 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
{isLoading && (
<div className="flex items-center gap-2 text-foreground/50">
<NovaOrb size={28} className="blur-none!" />
<span className="text-sm">Fetching your memories...</span>
<span className="text-sm">Extracting memories...</span>
</div>
)}
</div>

View file

@ -3,10 +3,10 @@
import { useState } from "react"
import { Button } from "@ui/components/button"
import { MCPDetailView } from "@/components/new/mcp-modal/mcp-detail-view"
import { XBookmarksDetailView } from "@/components/x-bookmarks-detail-view"
import { XBookmarksDetailView } from "@/components/new/onboarding/x-bookmarks-detail-view"
import { useRouter } from "next/navigation"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import { useOnboardingStorage } from "@hooks/use-onboarding-storage"
const integrationCards = [
@ -164,7 +164,9 @@ export function IntegrationsStep() {
<Button
variant="link"
className="text-white hover:text-gray-300 hover:no-underline cursor-pointer"
onClick={() => router.push("/new/onboarding?flow=setup&step=relatable")}
onClick={() =>
router.push("/new/onboarding?flow=setup&step=relatable")
}
>
← Back
</Button>

View file

@ -5,7 +5,7 @@ import { motion, AnimatePresence } from "motion/react"
import { Button } from "@ui/components/button"
import { useRouter } from "next/navigation"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
const relatableOptions = [
{

View file

@ -1,4 +1,4 @@
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { Button } from "@ui/components/button"
import { motion } from "motion/react"

View file

@ -2,7 +2,7 @@ import { motion } from "motion/react"
import { Button } from "@ui/components/button"
import { useRouter } from "next/navigation"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
export function FeaturesStep() {
const router = useRouter()

View file

@ -3,9 +3,15 @@ import { Button } from "@ui/components/button"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import {
parseXHandle,
parseLinkedInHandle,
toXProfileUrl,
toLinkedInProfileUrl,
} from "@/lib/url-helpers"
interface MemoriesStepProps {
interface ProfileStepProps {
onSubmit: (data: {
twitter: string
linkedin: string
@ -19,7 +25,7 @@ type ValidationError = {
linkedin: string | null
}
export function MemoriesStep({ onSubmit }: MemoriesStepProps) {
export function ProfileStep({ onSubmit }: ProfileStepProps) {
const router = useRouter()
const [otherLinks, setOtherLinks] = useState([""])
const [twitterHandle, setTwitterHandle] = useState("")
@ -43,63 +49,43 @@ export function MemoriesStep({ onSubmit }: MemoriesStepProps) {
setOtherLinks(updated)
}
const validateTwitterLink = (value: string): string | null => {
if (!value.trim()) return null
const validateTwitterHandle = (handle: string): string | null => {
if (!handle.trim()) return null
const normalized = value.trim().toLowerCase()
const isXDomain =
normalized.includes("x.com") || normalized.includes("twitter.com")
if (!isXDomain) {
return "share your X profile link"
// Basic validation: handle should be alphanumeric, underscore, or hyphen
// X/Twitter handles can contain letters, numbers, and underscores, max 15 chars
const handlePattern = /^[a-zA-Z0-9_]{1,15}$/
if (!handlePattern.test(handle.trim())) {
return "Enter your handle or profile link"
}
// Check if it's a profile link (not a status/tweet link)
const profilePattern =
/^(https?:\/\/)?(www\.)?(x\.com|twitter\.com)\/[^\/]+$/
const statusPattern = /\/status\//i
if (statusPattern.test(normalized) || !profilePattern.test(normalized)) {
return "share your X profile link"
}
// Note: 404 validation would require a backend API endpoint
// Format validation is handled above
return null
}
const validateLinkedInLink = (value: string): string | null => {
if (!value.trim()) return null
const validateLinkedInHandle = (handle: string): string | null => {
if (!handle.trim()) return null
const normalized = value.trim().toLowerCase()
const isLinkedInDomain = normalized.includes("linkedin.com")
if (!isLinkedInDomain) {
return "share your Linkedin profile link"
// Basic validation: LinkedIn handles are typically alphanumeric with hyphens
// They can be quite long, so we'll be lenient
const handlePattern = /^[a-zA-Z0-9-]+$/
if (!handlePattern.test(handle.trim())) {
return "Enter your handle or profile link"
}
// Check if it's a profile link (should have /in/ or /pub/)
const profilePattern =
/^(https?:\/\/)?(www\.)?linkedin\.com\/(in|pub)\/[^\/]+/
if (!profilePattern.test(normalized)) {
return "share your Linkedin profile link"
}
// Note: 404 validation would require a backend API endpoint
// Format validation is handled above
return null
}
const handleTwitterChange = (value: string) => {
setTwitterHandle(value)
const error = validateTwitterLink(value)
const parsedHandle = parseXHandle(value)
setTwitterHandle(parsedHandle)
const error = validateTwitterHandle(parsedHandle)
setErrors((prev) => ({ ...prev, twitter: error }))
}
const handleLinkedInChange = (value: string) => {
setLinkedinProfile(value)
const error = validateLinkedInLink(value)
const parsedHandle = parseLinkedInHandle(value)
setLinkedinProfile(parsedHandle)
const error = validateLinkedInHandle(parsedHandle)
setErrors((prev) => ({ ...prev, linkedin: error }))
}
@ -123,24 +109,33 @@ export function MemoriesStep({ onSubmit }: MemoriesStepProps) {
X/Twitter
</label>
<div className="relative flex items-center">
<input
id="twitter-handle"
type="text"
placeholder="x.com/yourhandle"
value={twitterHandle}
onChange={(e) => handleTwitterChange(e.target.value)}
onBlur={() => {
if (twitterHandle.trim()) {
const error = validateTwitterLink(twitterHandle)
setErrors((prev) => ({ ...prev, twitter: error }))
}
}}
className={`w-full px-4 py-2 bg-[#070E1B] border rounded-xl text-white placeholder-onboarding focus:outline-none transition-colors h-[40px] ${
<div
className={`flex items-center border rounded-xl overflow-hidden h-[40px] w-full ${
errors.twitter
? "border-[#52596633] bg-[#290F0A]"
: "border-onboarding/20"
}`}
/>
>
<div className="px-3 py-2 bg-[#070E1B] text-white/60 text-sm border-r border-onboarding/20 whitespace-nowrap">
x.com/
</div>
<input
id="twitter-handle"
type="text"
placeholder="handle"
value={twitterHandle}
onChange={(e) => handleTwitterChange(e.target.value)}
onBlur={() => {
if (twitterHandle.trim()) {
const error = validateTwitterHandle(twitterHandle)
setErrors((prev) => ({ ...prev, twitter: error }))
}
}}
className={`flex-1 px-4 py-2 bg-[#070E1B] text-white placeholder-onboarding focus:outline-none transition-colors ${
errors.twitter ? "bg-[#290F0A]" : ""
}`}
/>
</div>
{errors.twitter && (
<div className="absolute left-full ml-3">
<div
@ -165,24 +160,33 @@ export function MemoriesStep({ onSubmit }: MemoriesStepProps) {
LinkedIn
</label>
<div className="relative flex items-center">
<input
id="linkedin-profile"
type="text"
placeholder="linkedin.com/in/yourname"
value={linkedinProfile}
onChange={(e) => handleLinkedInChange(e.target.value)}
onBlur={() => {
if (linkedinProfile.trim()) {
const error = validateLinkedInLink(linkedinProfile)
setErrors((prev) => ({ ...prev, linkedin: error }))
}
}}
className={`w-full px-4 py-2 bg-[#070E1B] border rounded-xl text-white placeholder-onboarding focus:outline-none transition-colors h-[40px] ${
<div
className={`flex items-center border rounded-xl overflow-hidden h-[40px] w-full ${
errors.linkedin
? "border-[#52596633] bg-[#290F0A]"
: "border-onboarding/20"
}`}
/>
>
<div className="px-3 py-2 bg-[#070E1B] text-white/60 text-sm border-r border-onboarding/20 whitespace-nowrap w-[140px]">
linkedin.com/in/
</div>
<input
id="linkedin-profile"
type="text"
placeholder="username"
value={linkedinProfile}
onChange={(e) => handleLinkedInChange(e.target.value)}
onBlur={() => {
if (linkedinProfile.trim()) {
const error = validateLinkedInHandle(linkedinProfile)
setErrors((prev) => ({ ...prev, linkedin: error }))
}
}}
className={`flex-1 px-4 py-2 bg-[#070E1B] text-white placeholder-onboarding focus:outline-none transition-colors ${
errors.linkedin ? "bg-[#290F0A]" : ""
}`}
/>
</div>
{errors.linkedin && (
<div className="absolute left-full ml-3">
<div
@ -282,8 +286,8 @@ export function MemoriesStep({ onSubmit }: MemoriesStepProps) {
}}
onClick={() => {
const formData = {
twitter: twitterHandle,
linkedin: linkedinProfile,
twitter: toXProfileUrl(twitterHandle),
linkedin: toLinkedInProfileUrl(linkedinProfile),
description: description,
otherLinks: otherLinks.filter((l) => l.trim()),
}

View file

@ -2,7 +2,7 @@
import { Button } from "@ui/components/button"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/utils/fonts"
import { dmSansClassName } from "@/lib/fonts"
import Image from "next/image"
interface XBookmarksDetailViewProps {
@ -27,9 +27,7 @@ const steps = [
},
]
export function XBookmarksDetailView({
onBack,
}: XBookmarksDetailViewProps) {
export function XBookmarksDetailView({ onBack }: XBookmarksDetailViewProps) {
const handleInstall = () => {
window.open(
"https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnangednailhoegogi",

View file

@ -1,6 +1,6 @@
"use client"
import { dmSans125ClassName } from "@/utils/fonts"
import { dmSans125ClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { useAuth } from "@lib/auth-context"
import { Avatar, AvatarFallback, AvatarImage } from "@ui/components/avatar"

View file

@ -1,6 +1,6 @@
"use client"
import { dmSans125ClassName } from "@/utils/fonts"
import { dmSans125ClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { $fetch } from "@lib/api"
import { fetchSubscriptionStatus } from "@lib/queries"

View file

@ -1,6 +1,6 @@
"use client"
import { dmSans125ClassName } from "@/utils/fonts"
import { dmSans125ClassName } from "@/lib/fonts"
import { analytics } from "@/lib/analytics"
import { cn } from "@lib/utils"
import { authClient } from "@lib/auth"

View file

@ -1,6 +1,6 @@
"use client"
import { dmSans125ClassName } from "@/utils/fonts"
import { dmSans125ClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
import { ArrowUpRight } from "lucide-react"

View file

@ -0,0 +1,92 @@
import StarterKit from "@tiptap/starter-kit"
import Placeholder from "@tiptap/extension-placeholder"
import Link from "@tiptap/extension-link"
import Image from "@tiptap/extension-image"
import TaskList from "@tiptap/extension-task-list"
import TaskItem from "@tiptap/extension-task-item"
import { cx } from "class-variance-authority"
const placeholder = Placeholder.configure({
placeholder: 'Write, paste anything or type "/" for commands...',
})
const taskList = TaskList.configure({
HTMLAttributes: {
class: cx("not-prose pl-2"),
},
})
const taskItem = TaskItem.configure({
HTMLAttributes: {
class: cx("flex items-start my-4"),
},
nested: true,
})
const link = Link.configure({
HTMLAttributes: {
class: cx(
"text-muted-foreground underline underline-offset-[3px] hover:text-primary transition-colors cursor-pointer",
),
},
openOnClick: false,
})
const image = Image.configure({
HTMLAttributes: {
class: cx("rounded-lg border border-muted"),
},
})
const starterKit = StarterKit.configure({
bulletList: {
HTMLAttributes: {
class: cx("list-disc list-outside leading-3 -mt-2"),
},
},
orderedList: {
HTMLAttributes: {
class: cx("list-decimal list-outside leading-3 -mt-2"),
},
},
listItem: {
HTMLAttributes: {
class: cx("leading-normal -mb-2"),
},
},
blockquote: {
HTMLAttributes: {
class: cx("border-l-4 border-primary"),
},
},
codeBlock: {
HTMLAttributes: {
class: cx("rounded-sm bg-muted border p-5 font-mono font-medium"),
},
},
code: {
HTMLAttributes: {
class: cx("rounded-md bg-muted px-1.5 py-1 font-mono font-medium"),
spellcheck: "false",
},
},
horizontalRule: {
HTMLAttributes: {
class: cx("mt-4 mb-6 border-t border-muted-foreground"),
},
},
dropcursor: {
color: "#DBEAFE",
width: 4,
},
gapcursor: false,
})
export const defaultExtensions = [
starterKit,
placeholder,
link,
image,
taskList,
taskItem,
]

View file

@ -0,0 +1,174 @@
"use client"
import { useEditor, EditorContent } from "@tiptap/react"
import { BubbleMenu } from "@tiptap/react/menus"
import type { Editor } from "@tiptap/core"
import { Markdown } from "@tiptap/markdown"
import { useRef, useEffect, useCallback } from "react"
import { defaultExtensions } from "./extensions"
import { slashCommand } from "./suggestions"
import { Bold, Italic, Code } from "lucide-react"
import { useDebouncedCallback } from "use-debounce"
import { cn } from "@lib/utils"
const extensions = [...defaultExtensions, slashCommand, Markdown]
export function TextEditor({
content: initialContent,
onContentChange,
onSubmit,
}: {
content: string | undefined
onContentChange: (content: string) => void
onSubmit: () => void
}) {
const containerRef = useRef<HTMLDivElement>(null)
const editorRef = useRef<Editor | null>(null)
const onSubmitRef = useRef(onSubmit)
const hasUserEditedRef = useRef(false)
useEffect(() => {
onSubmitRef.current = onSubmit
}, [onSubmit])
const debouncedUpdates = useDebouncedCallback((editor: Editor) => {
if (!hasUserEditedRef.current) return
const json = editor.getJSON()
const markdown = editor.storage.markdown?.manager?.serialize(json) ?? ""
onContentChange?.(markdown)
}, 500)
const editor = useEditor({
extensions,
content: initialContent,
contentType: "markdown",
immediatelyRender: true,
onCreate: ({ editor }) => {
editorRef.current = editor
},
onUpdate: ({ editor }) => {
editorRef.current = editor
debouncedUpdates(editor)
},
editorProps: {
handleKeyDown: (_view, event) => {
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
event.preventDefault()
onSubmitRef.current?.()
return true
}
hasUserEditedRef.current = true
return false
},
handleTextInput: () => {
hasUserEditedRef.current = true
return false
},
handlePaste: () => {
hasUserEditedRef.current = true
return false
},
handleDrop: () => {
hasUserEditedRef.current = true
return false
},
},
})
useEffect(() => {
if (editor && initialContent) {
hasUserEditedRef.current = false
editor.commands.setContent(initialContent, { contentType: "markdown" })
}
}, [editor, initialContent])
const handleClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
const target = e.target as HTMLElement
if (target.closest(".ProseMirror")) {
return
}
if (target.closest("button, a")) {
return
}
const proseMirror = containerRef.current?.querySelector(
".ProseMirror",
) as HTMLElement
if (proseMirror && editorRef.current) {
setTimeout(() => {
proseMirror.focus()
editorRef.current?.commands.focus("end")
}, 0)
}
},
[],
)
useEffect(() => {
return () => {
editor?.destroy()
}
}, [editor])
return (
<>
{/* biome-ignore lint/a11y/useSemanticElements: div is needed as container for editor, cannot use button */}
{/* biome-ignore lint/a11y/useKeyWithClickEvents: we need to use a div to get the focus on the editor */}
<div
role="button"
tabIndex={0}
ref={containerRef}
onClick={handleClick}
className="w-full h-full outline-none prose prose-invert max-w-none [&_.ProseMirror]:outline-none [&_.ProseMirror]:focus:outline-none [&_.ProseMirror-focused]:outline-none text-editor-prose cursor-text"
>
<EditorContent editor={editor} />
</div>
{editor && (
<BubbleMenu
editor={editor}
options={{ placement: "bottom-start", offset: 8 }}
>
<div className="flex items-center gap-1 rounded-[8px] bg-[#1b1f24] p-2 shadow-[0px_4px_20px_0px_rgba(0,0,0,0.25),inset_1px_1px_1px_0px_rgba(255,255,255,0.1)]">
<button
type="button"
onClick={() =>
editor.chain().focus().toggleBold().run()
}
className={cn(
"flex items-center justify-center rounded-[4px] p-1.5 hover:bg-[#2e353d] cursor-pointer text-[#fafafa]",
editor.isActive("bold") && "bg-[#2e353d]",
)}
>
<Bold size={16} />
</button>
<button
type="button"
onClick={() =>
editor.chain().focus().toggleItalic().run()
}
className={cn(
"flex items-center justify-center rounded-[4px] p-1.5 hover:bg-[#2e353d] cursor-pointer text-[#fafafa]",
editor.isActive("italic") && "bg-[#2e353d]",
)}
>
<Italic size={16} />
</button>
<button
type="button"
onClick={() =>
editor.chain().focus().toggleCode().run()
}
className={cn(
"flex items-center justify-center rounded-[4px] p-1.5 hover:bg-[#2e353d] cursor-pointer text-[#fafafa]",
editor.isActive("code") && "bg-[#2e353d]",
)}
>
<Code size={16} />
</button>
</div>
</BubbleMenu>
)}
</>
)
}

View file

@ -0,0 +1,308 @@
"use client"
import { Extension, type Editor, type Range } from "@tiptap/core"
import Suggestion, { type SuggestionOptions } from "@tiptap/suggestion"
import { useEffect, useLayoutEffect, useState, useRef } from "react"
import { createPortal } from "react-dom"
import { createRoot, type Root } from "react-dom/client"
import {
useFloating,
offset,
flip,
shift,
autoUpdate,
} from "@floating-ui/react"
import { cn } from "@lib/utils"
export interface SuggestionItem {
title: string
description: string
searchTerms?: string[]
icon: React.ReactNode
command: (props: { editor: Editor; range: Range }) => void
}
interface CommandListProps {
items: SuggestionItem[]
command: (item: SuggestionItem) => void
selectedIndex: number
}
function CommandList({ items, command, selectedIndex }: CommandListProps) {
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const selectedElement = containerRef.current?.querySelector(
`[data-index="${selectedIndex}"]`,
)
selectedElement?.scrollIntoView({ block: "nearest" })
}, [selectedIndex])
if (items.length === 0) {
return (
<div className="z-50 h-auto max-h-[330px] overflow-y-auto rounded-[8px] bg-[#1b1f24] p-2 shadow-[0px_4px_20px_0px_rgba(0,0,0,0.25),inset_1px_1px_1px_0px_rgba(255,255,255,0.1)]">
<div className="px-2 text-muted-foreground">No results</div>
</div>
)
}
return (
<div
ref={containerRef}
className="z-50 h-auto max-h-[330px] overflow-y-auto rounded-[8px] bg-[#1b1f24] p-2 shadow-[0px_4px_20px_0px_rgba(0,0,0,0.25),inset_1px_1px_1px_0px_rgba(255,255,255,0.1)]"
>
{items.map((item, index) => (
<button
type="button"
key={item.title}
data-index={index}
onClick={() => command(item)}
className={cn(
"flex w-full items-center gap-2 rounded-[4px] px-3 py-2 text-left hover:bg-[#2e353d]",
index === selectedIndex && "bg-[#2e353d]",
)}
>
<div className="flex size-[20px] shrink-0 items-center justify-center text-[#fafafa]">
{item.icon}
</div>
<p className="font-medium text-[16px] leading-[1.35] tracking-[-0.16px] text-[#fafafa]">
{item.title}
</p>
</button>
))}
</div>
)
}
interface CommandMenuProps {
items: SuggestionItem[]
command: (item: SuggestionItem) => void
clientRect: (() => DOMRect | null) | null
selectedIndex: number
}
function CommandMenu({
items,
command,
clientRect,
selectedIndex,
}: CommandMenuProps) {
const [mounted, setMounted] = useState(false)
const { refs, floatingStyles } = useFloating({
placement: "bottom-start",
middleware: [offset(8), flip(), shift()],
whileElementsMounted: autoUpdate,
})
useLayoutEffect(() => {
setMounted(true)
}, [])
useEffect(() => {
const rect = clientRect?.()
if (rect) {
refs.setReference({
getBoundingClientRect: () => rect,
})
}
}, [clientRect, refs])
if (!mounted) return null
return createPortal(
<div ref={refs.setFloating} style={floatingStyles} className="z-50">
<CommandList
items={items}
command={command}
selectedIndex={selectedIndex}
/>
</div>,
document.body,
)
}
export function createSlashCommand(items: SuggestionItem[]) {
let component: {
updateProps: (props: CommandMenuProps) => void
destroy: () => void
element: HTMLElement
} | null = null
let root: Root | null = null
let selectedIndex = 0
let currentItems: SuggestionItem[] = []
const renderMenu = (props: {
items: SuggestionItem[]
command: (item: SuggestionItem) => void
clientRect: (() => DOMRect | null) | null
}) => {
root?.render(
<CommandMenu
items={props.items}
command={props.command}
clientRect={props.clientRect}
selectedIndex={selectedIndex}
/>,
)
}
const suggestion: Omit<SuggestionOptions<SuggestionItem>, "editor"> = {
char: "/",
items: ({ query }) => {
return items.filter(
(item) =>
item.title.toLowerCase().includes(query.toLowerCase()) ||
item.searchTerms?.some((term) =>
term.toLowerCase().includes(query.toLowerCase()),
),
)
},
command: ({ editor, range, props }) => {
props.command({ editor, range })
},
render: () => {
let currentCommand: ((item: SuggestionItem) => void) | null = null
let currentClientRect: (() => DOMRect | null) | null = null
return {
onStart: (props) => {
selectedIndex = 0
currentItems = props.items as SuggestionItem[]
currentCommand = props.command as (
item: SuggestionItem,
) => void
currentClientRect = props.clientRect ?? null
const element = document.createElement("div")
document.body.appendChild(element)
root = createRoot(element)
if (currentCommand) {
renderMenu({
items: currentItems,
command: currentCommand,
clientRect: currentClientRect,
})
}
component = {
element,
updateProps: (newProps: CommandMenuProps) => {
root?.render(
<CommandMenu
items={newProps.items}
command={newProps.command}
clientRect={newProps.clientRect}
selectedIndex={newProps.selectedIndex}
/>,
)
},
destroy: () => {
root?.unmount()
element.remove()
root = null
},
}
},
onUpdate: (props) => {
currentItems = props.items as SuggestionItem[]
currentCommand = props.command as (
item: SuggestionItem,
) => void
currentClientRect = props.clientRect ?? null
if (selectedIndex >= currentItems.length) {
selectedIndex = Math.max(0, currentItems.length - 1)
}
if (currentCommand) {
component?.updateProps({
items: currentItems,
command: currentCommand,
clientRect: currentClientRect,
selectedIndex,
})
}
},
onKeyDown: (props) => {
const { event } = props
if (event.key === "Escape") {
component?.destroy()
component = null
return true
}
if (event.key === "ArrowUp") {
selectedIndex =
selectedIndex <= 0
? currentItems.length - 1
: selectedIndex - 1
if (currentCommand) {
component?.updateProps({
items: currentItems,
command: currentCommand,
clientRect: currentClientRect,
selectedIndex,
})
}
return true
}
if (event.key === "ArrowDown") {
selectedIndex =
selectedIndex >= currentItems.length - 1
? 0
: selectedIndex + 1
if (currentCommand) {
component?.updateProps({
items: currentItems,
command: currentCommand,
clientRect: currentClientRect,
selectedIndex,
})
}
return true
}
if (event.key === "Enter") {
const item = currentItems[selectedIndex]
if (item && currentCommand) {
currentCommand(item)
}
return true
}
return false
},
onExit: () => {
component?.destroy()
component = null
},
}
},
}
return Extension.create({
name: "slashCommand",
addOptions() {
return {
suggestion,
}
},
addProseMirrorPlugins() {
return [
Suggestion({
editor: this.editor,
...this.options.suggestion,
}),
]
},
})
}

View file

@ -0,0 +1,103 @@
import {
Heading1,
Heading2,
Heading3,
List,
ListOrdered,
TextQuote,
Text,
} from "lucide-react"
import { createSlashCommand, type SuggestionItem } from "./slash-command"
export const suggestionItems: SuggestionItem[] = [
{
title: "Heading 1",
description: "Big section heading.",
searchTerms: ["title", "big", "large"],
icon: <Heading1 size={20} />,
command: ({ editor, range }) => {
editor
.chain()
.focus()
.deleteRange(range)
.setNode("heading", { level: 1 })
.run()
},
},
{
title: "Heading 2",
description: "Medium section heading.",
searchTerms: ["subtitle", "medium"],
icon: <Heading2 size={20} />,
command: ({ editor, range }) => {
editor
.chain()
.focus()
.deleteRange(range)
.setNode("heading", { level: 2 })
.run()
},
},
{
title: "Heading 3",
description: "Small section heading.",
searchTerms: ["subtitle", "small"],
icon: <Heading3 size={20} />,
command: ({ editor, range }) => {
editor
.chain()
.focus()
.deleteRange(range)
.setNode("heading", { level: 3 })
.run()
},
},
{
title: "Text",
description: "Just start typing with plain text.",
searchTerms: ["p", "paragraph"],
icon: <Text size={18} />,
command: ({ editor, range }) => {
editor
.chain()
.focus()
.deleteRange(range)
.toggleNode("paragraph", "paragraph")
.run()
},
},
{
title: "Bullet List",
description: "Create a simple bullet list.",
searchTerms: ["unordered", "point"],
icon: <List size={20} />,
command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).toggleBulletList().run()
},
},
{
title: "Numbered List",
description: "Create a list with numbering.",
searchTerms: ["ordered"],
icon: <ListOrdered size={20} />,
command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).toggleOrderedList().run()
},
},
{
title: "Block Quote",
description: "Capture a quote.",
searchTerms: ["blockquote"],
icon: <TextQuote size={20} />,
command: ({ editor, range }) =>
editor
.chain()
.focus()
.deleteRange(range)
.toggleNode("paragraph", "paragraph")
.toggleBlockquote()
.run(),
},
]
export const slashCommand = createSlashCommand(suggestionItems)

View file

@ -88,3 +88,35 @@
inset 0 2px 4px rgba(0, 0, 0, 0.3),
inset 0 1px 2px rgba(0, 0, 0, 0.1);
}
/* Disable ProseMirror focus styles */
.ProseMirror:focus,
.ProseMirror-focused,
.ProseMirror:focus-visible {
outline: none;
box-shadow: none;
border: none;
}
/* Override prose paragraph margins for text editor */
.text-editor-prose.prose :where(p):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
margin-top: 0;
margin-bottom: 0;
}
/* Style placeholder for text editor */
.text-editor-prose .ProseMirror p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
color: #525966;
pointer-events: none;
height: 0;
}
.text-editor-prose .ProseMirror .is-empty::before {
content: attr(data-placeholder);
float: left;
color: #525966;
pointer-events: none;
height: 0;
}

View file

@ -10,10 +10,10 @@ interface DocumentsQueryData {
}
interface UseDocumentMutationsOptions {
onClose: () => void
onClose?: () => void
}
export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions) {
export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions = {}) {
const queryClient = useQueryClient()
const noteMutation = useMutation({
@ -101,7 +101,7 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions) {
queryClient.invalidateQueries({
queryKey: ["documents-with-memories", variables.project],
})
onClose()
onClose?.()
},
})
@ -184,7 +184,7 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions) {
queryClient.invalidateQueries({
queryKey: ["documents-with-memories", variables.project],
})
onClose()
onClose?.()
},
})
@ -301,7 +301,43 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions) {
queryClient.invalidateQueries({
queryKey: ["documents-with-memories", variables.project],
})
onClose()
onClose?.()
},
})
const updateMutation = useMutation({
mutationFn: async ({
documentId,
content,
}: {
documentId: string
content: string
}) => {
const response = await $fetch(`@patch/documents/${documentId}`, {
body: {
content,
metadata: {
sm_source: "consumer",
},
},
})
if (response.error) {
throw new Error(response.error?.message || "Failed to save document")
}
return response.data
},
onSuccess: () => {
toast.success("Document saved successfully!")
queryClient.invalidateQueries({
queryKey: ["documents-with-memories"],
})
},
onError: (error) => {
toast.error("Failed to save document", {
description: error instanceof Error ? error.message : "Unknown error",
})
},
})
@ -309,5 +345,6 @@ export function useDocumentMutations({ onClose }: UseDocumentMutationsOptions) {
noteMutation,
linkMutation,
fileMutation,
updateMutation,
}
}

190
apps/web/lib/url-helpers.ts Normal file
View file

@ -0,0 +1,190 @@
/**
* Validates if a string is a valid URL.
*/
export const isValidUrl = (url: string): boolean => {
try {
new URL(url)
return true
} catch {
return false
}
}
/**
* Normalizes a URL by adding https:// prefix if missing.
*/
export const normalizeUrl = (url: string): string => {
if (!url.trim()) return ""
if (url.startsWith("http://") || url.startsWith("https://")) {
return url
}
return `https://${url}`
}
/**
* Checks if a URL is a Twitter/X URL.
*/
export const isTwitterUrl = (url: string): boolean => {
const normalizedUrl = url.toLowerCase()
return (
normalizedUrl.includes("twitter.com") || normalizedUrl.includes("x.com")
)
}
/**
* Checks if a URL is a LinkedIn profile URL (not a company page).
*/
export const isLinkedInProfileUrl = (url: string): boolean => {
const normalizedUrl = url.toLowerCase()
return (
normalizedUrl.includes("linkedin.com/in/") &&
!normalizedUrl.includes("linkedin.com/company/")
)
}
/**
* Collects and validates URLs from LinkedIn profile and other links, excluding Twitter.
*/
export const collectValidUrls = (
linkedinProfile: string,
otherLinks: string[],
): string[] => {
const urls: string[] = []
if (linkedinProfile.trim()) {
const normalizedLinkedIn = normalizeUrl(linkedinProfile.trim())
if (
isValidUrl(normalizedLinkedIn) &&
isLinkedInProfileUrl(normalizedLinkedIn)
) {
urls.push(normalizedLinkedIn)
}
}
otherLinks
.filter((link) => link.trim())
.forEach((link) => {
const normalizedLink = normalizeUrl(link.trim())
if (isValidUrl(normalizedLink) && !isTwitterUrl(normalizedLink)) {
urls.push(normalizedLink)
}
})
return urls
}
/**
* Extracts X/Twitter handle from various input formats (URLs, handles with @, etc.).
*/
export function parseXHandle(input: string): string {
if (!input.trim()) return ""
let value = input.trim()
if (value.startsWith("@")) {
value = value.slice(1)
}
const lowerValue = value.toLowerCase()
if (lowerValue.includes("x.com") || lowerValue.includes("twitter.com")) {
try {
let url: URL
if (value.startsWith("http://") || value.startsWith("https://")) {
url = new URL(value)
} else {
url = new URL(`https://${value}`)
}
const pathSegments = url.pathname.split("/").filter(Boolean)
if (pathSegments.length > 0) {
const firstSegment = pathSegments[0]
if (firstSegment && firstSegment !== "status" && firstSegment !== "i") {
return firstSegment
}
}
} catch {
const match = value.match(/(?:x\.com|twitter\.com)\/([^/\s?#]+)/i)
const handle = match?.[1]
if (handle && handle !== "status") {
return handle
}
}
}
if (
value.includes("/") &&
!lowerValue.includes("x.com") &&
!lowerValue.includes("twitter.com")
) {
const parts = value.split("/").filter(Boolean)
const firstPart = parts[0]
if (firstPart) {
return firstPart
}
}
return value
}
/**
* Extracts LinkedIn handle from various input formats (URLs, handles with @, etc.).
*/
export function parseLinkedInHandle(input: string): string {
if (!input.trim()) return ""
let value = input.trim()
if (value.startsWith("@")) {
value = value.slice(1)
}
const lowerValue = value.toLowerCase()
if (lowerValue.includes("linkedin.com")) {
try {
let url: URL
if (value.startsWith("http://") || value.startsWith("https://")) {
url = new URL(value)
} else {
url = new URL(`https://${value}`)
}
const pathMatch = url.pathname.match(/\/(in|pub)\/([^/\s?#]+)/i)
const handle = pathMatch?.[2]
if (handle) {
return handle
}
} catch {
const match = value.match(/linkedin\.com\/(?:in|pub)\/([^/\s?#]+)/i)
const handle = match?.[1]
if (handle) {
return handle
}
}
}
if (value.includes("/in/") || value.includes("/pub/")) {
const match = value.match(/\/(?:in|pub)\/([^/\s?#]+)/i)
const handle = match?.[1]
if (handle) {
return handle
}
}
return value
}
/**
* Converts X/Twitter handle to full profile URL.
*/
export function toXProfileUrl(handle: string): string {
if (!handle.trim()) return ""
return `https://x.com/${handle.trim()}`
}
/**
* Converts LinkedIn handle to full profile URL.
*/
export function toLinkedInProfileUrl(handle: string): string {
if (!handle.trim()) return ""
return `https://linkedin.com/in/${handle.trim()}`
}

View file

@ -2,6 +2,19 @@ import { withSentryConfig } from "@sentry/nextjs"
import type { NextConfig } from "next"
const nextConfig: NextConfig = {
transpilePackages: [
"@tiptap/core",
"@tiptap/react",
"@tiptap/pm",
"@tiptap/starter-kit",
"@tiptap/extension-placeholder",
"@tiptap/extension-link",
"@tiptap/extension-image",
"@tiptap/extension-task-list",
"@tiptap/extension-task-item",
"@tiptap/suggestion",
"@tiptap/markdown",
],
experimental: {
viewTransition: true,
turbopackFileSystemCacheForDev: true,

View file

@ -15,13 +15,15 @@
"postdeploy": "bun run sentry:sourcemaps"
},
"dependencies": {
"@ai-sdk/google": "^2.0.0-beta.13",
"@ai-sdk/react": "2.0.0-beta.24",
"@ai-sdk/google": "^3.0.9",
"@ai-sdk/react": "^3.0.39",
"@ai-sdk/xai": "^3.0.23",
"@better-fetch/fetch": "^1.1.18",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@floating-ui/react": "^0.27.0",
"@opennextjs/cloudflare": "^1.12.0",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.1.14",
@ -48,8 +50,19 @@
"@tanstack/react-query-devtools": "^5.84.2",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.12",
"@tiptap/core": "^3.15.3",
"@tiptap/extension-image": "^3.15.3",
"@tiptap/extension-link": "^3.15.3",
"@tiptap/extension-placeholder": "^3.15.3",
"@tiptap/extension-task-item": "^3.15.3",
"@tiptap/extension-task-list": "^3.15.3",
"@tiptap/markdown": "^3.15.3",
"@tiptap/pm": "^3.15.3",
"@tiptap/react": "^3.15.3",
"@tiptap/starter-kit": "^3.15.3",
"@tiptap/suggestion": "^3.15.3",
"@types/dompurify": "^3.2.0",
"ai": "5.0.0-beta.24",
"ai": "^6.0.35",
"autumn-js": "0.0.116",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@ -85,6 +98,7 @@
"streamdown": "^1.1.6",
"tailwind-merge": "^3.3.1",
"tw-animate-css": "^1.3.4",
"use-debounce": "^10.1.0",
"vaul": "^1.1.2",
"zustand": "^5.0.7"
},

View file

@ -1,59 +0,0 @@
export const isValidUrl = (url: string): boolean => {
try {
new URL(url)
return true
} catch {
return false
}
}
export const normalizeUrl = (url: string): string => {
if (!url.trim()) return ""
if (url.startsWith("http://") || url.startsWith("https://")) {
return url
}
return `https://${url}`
}
export const isTwitterUrl = (url: string): boolean => {
const normalizedUrl = url.toLowerCase()
return (
normalizedUrl.includes("twitter.com") || normalizedUrl.includes("x.com")
)
}
export const isLinkedInProfileUrl = (url: string): boolean => {
const normalizedUrl = url.toLowerCase()
return (
normalizedUrl.includes("linkedin.com/in/") &&
!normalizedUrl.includes("linkedin.com/company/")
)
}
export const collectValidUrls = (
linkedinProfile: string,
otherLinks: string[],
): string[] => {
const urls: string[] = []
if (linkedinProfile.trim()) {
const normalizedLinkedIn = normalizeUrl(linkedinProfile.trim())
if (
isValidUrl(normalizedLinkedIn) &&
isLinkedInProfileUrl(normalizedLinkedIn)
) {
urls.push(normalizedLinkedIn)
}
}
otherLinks
.filter((link) => link.trim())
.forEach((link) => {
const normalizedLink = normalizeUrl(link.trim())
if (isValidUrl(normalizedLink) && !isTwitterUrl(normalizedLink)) {
urls.push(normalizedLink)
}
})
return urls
}

208
bun.lock
View file

@ -138,13 +138,15 @@
"name": "@repo/web",
"version": "0.1.0",
"dependencies": {
"@ai-sdk/google": "^2.0.0-beta.13",
"@ai-sdk/react": "2.0.0-beta.24",
"@ai-sdk/google": "^3.0.9",
"@ai-sdk/react": "^3.0.39",
"@ai-sdk/xai": "^3.0.23",
"@better-fetch/fetch": "^1.1.18",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@floating-ui/react": "^0.27.0",
"@opennextjs/cloudflare": "^1.12.0",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.1.14",
@ -171,8 +173,19 @@
"@tanstack/react-query-devtools": "^5.84.2",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.12",
"@tiptap/core": "^3.15.3",
"@tiptap/extension-image": "^3.15.3",
"@tiptap/extension-link": "^3.15.3",
"@tiptap/extension-placeholder": "^3.15.3",
"@tiptap/extension-task-item": "^3.15.3",
"@tiptap/extension-task-list": "^3.15.3",
"@tiptap/markdown": "^3.15.3",
"@tiptap/pm": "^3.15.3",
"@tiptap/react": "^3.15.3",
"@tiptap/starter-kit": "^3.15.3",
"@tiptap/suggestion": "^3.15.3",
"@types/dompurify": "^3.2.0",
"ai": "5.0.0-beta.24",
"ai": "^6.0.35",
"autumn-js": "0.0.116",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@ -208,6 +221,7 @@
"streamdown": "^1.1.6",
"tailwind-merge": "^3.3.1",
"tw-animate-css": "^1.3.4",
"use-debounce": "^10.1.0",
"vaul": "^1.1.2",
"zustand": "^5.0.7",
},
@ -372,10 +386,12 @@
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
"@ai-sdk/react": ["@ai-sdk/react@2.0.0-beta.24", "", { "dependencies": { "@ai-sdk/provider-utils": "3.0.0-beta.5", "ai": "5.0.0-beta.24", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.25.76 || ^4" }, "optionalPeers": ["zod"] }, "sha512-Hfgb+z7MteHMaSdQ3G8hhTDridmUk2fkuRa17OaFlsfySYaFQd/exC0EBJlT+05ljcJPInyurdrxbgZLtsoPpg=="],
"@ai-sdk/react": ["@ai-sdk/react@3.0.39", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.7", "ai": "6.0.37", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-Q/39hAazwxItCbqEDWC4pa3+HXLVvTzeu/zu7ghANqRNCxzmqBb/GYEIU/wiYNRS05hP6R1l9bA9S4U6sx0iTA=="],
"@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.2.11", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w=="],
"@ai-sdk/xai": ["@ai-sdk/xai@3.0.23", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.10", "@ai-sdk/provider": "3.0.3", "@ai-sdk/provider-utils": "4.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-sWWJK0lGNFWfGTzgFkgTRZUeXycaXY3QufMfPwev2uRYvRNF6QFGZn2Ut3PM70OVl8NP6DHMnYl0loM4VyC7ug=="],
"@aklinker1/rollup-plugin-visualizer": ["@aklinker1/rollup-plugin-visualizer@5.12.0", "", { "dependencies": { "open": "^8.4.0", "picomatch": "^2.3.1", "source-map": "^0.7.4", "yargs": "^17.5.1" }, "peerDependencies": { "rollup": "2.x || 3.x || 4.x" }, "optionalPeers": ["rollup"], "bin": { "rollup-plugin-visualizer": "dist/bin/cli.js" } }, "sha512-X24LvEGw6UFmy0lpGJDmXsMyBD58XmX1bbwsaMLhNoM+UMQfQ3b2RtC+nz4b/NoRK5r6QJSKJHBNVeUdwqybaQ=="],
"@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.3", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-jsElTJ0sQ4wHRz+C45tfect76BwbTbgkgKByOzpCN9xG61N5V6u/glvg1CsNJhq2xJIFpKHSwG3D2wPPuEYOrQ=="],
@ -782,6 +798,8 @@
"@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
"@floating-ui/react": ["@floating-ui/react@0.27.16", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.6", "@floating-ui/utils": "^0.2.10", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g=="],
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
@ -1306,6 +1324,8 @@
"@react-router/serve": ["@react-router/serve@7.12.0", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0", "@react-router/express": "7.12.0", "@react-router/node": "7.12.0", "compression": "^1.8.1", "express": "^4.19.2", "get-port": "5.1.1", "morgan": "^1.10.1", "source-map-support": "^0.5.21" }, "peerDependencies": { "react-router": "7.12.0" }, "bin": { "react-router-serve": "bin.js" } }, "sha512-j1ltgU7s3wAwOosZ5oxgHSsmVyK706gY/yIs8qVmC239wQ3zr3eqaXk3TVVLMeRy+eDgPNmgc6oNJv2o328VgA=="],
"@remirror/core-constants": ["@remirror/core-constants@3.0.0", "", {}, "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg=="],
"@remix-run/node-fetch-server": ["@remix-run/node-fetch-server@0.9.0", "", {}, "sha512-SoLMv7dbH+njWzXnOY6fI08dFMI5+/dQ+vY3n8RnnbdG7MdJEgiP28Xj/xWlnRnED/aB6SFw56Zop+LbmaaKqA=="],
"@repo/docs": ["@repo/docs@workspace:apps/docs"],
@ -1698,6 +1718,74 @@
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.18", "", {}, "sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg=="],
"@tiptap/core": ["@tiptap/core@3.15.3", "", { "peerDependencies": { "@tiptap/pm": "^3.15.3" } }, "sha512-bmXydIHfm2rEtGju39FiQNfzkFx9CDvJe+xem1dgEZ2P6Dj7nQX9LnA1ZscW7TuzbBRkL5p3dwuBIi3f62A66A=="],
"@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@3.15.3", "", { "peerDependencies": { "@tiptap/core": "^3.15.3" } }, "sha512-13x5UsQXtttFpoS/n1q173OeurNxppsdWgP3JfsshzyxIghhC141uL3H6SGYQLPU31AizgDs2OEzt6cSUevaZg=="],
"@tiptap/extension-bold": ["@tiptap/extension-bold@3.15.3", "", { "peerDependencies": { "@tiptap/core": "^3.15.3" } }, "sha512-I8JYbkkUTNUXbHd/wCse2bR0QhQtJD7+0/lgrKOmGfv5ioLxcki079Nzuqqay3PjgYoJLIJQvm3RAGxT+4X91w=="],
"@tiptap/extension-bubble-menu": ["@tiptap/extension-bubble-menu@3.15.3", "", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "@tiptap/core": "^3.15.3", "@tiptap/pm": "^3.15.3" } }, "sha512-e88DG1bTy6hKxrt7iPVQhJnH5/EOrnKpIyp09dfRDgWrrW88fE0Qjys7a/eT8W+sXyXM3z10Ye7zpERWsrLZDg=="],
"@tiptap/extension-bullet-list": ["@tiptap/extension-bullet-list@3.15.3", "", { "peerDependencies": { "@tiptap/extension-list": "^3.15.3" } }, "sha512-MGwEkNT7ltst6XaWf0ObNgpKQ4PvuuV3igkBrdYnQS+qaAx9IF4isygVPqUc9DvjYC306jpyKsNqNrENIXcosA=="],
"@tiptap/extension-code": ["@tiptap/extension-code@3.15.3", "", { "peerDependencies": { "@tiptap/core": "^3.15.3" } }, "sha512-x6LFt3Og6MFINYpsMzrJnz7vaT9Yk1t4oXkbJsJRSavdIWBEBcoRudKZ4sSe/AnsYlRJs8FY2uR76mt9e+7xAQ=="],
"@tiptap/extension-code-block": ["@tiptap/extension-code-block@3.15.3", "", { "peerDependencies": { "@tiptap/core": "^3.15.3", "@tiptap/pm": "^3.15.3" } }, "sha512-q1UB9icNfdJppTqMIUWfoRKkx5SSdWIpwZoL2NeOI5Ah3E20/dQKVttIgLhsE521chyvxCYCRaHD5tMNGKfhyw=="],
"@tiptap/extension-document": ["@tiptap/extension-document@3.15.3", "", { "peerDependencies": { "@tiptap/core": "^3.15.3" } }, "sha512-AC72nI2gnogBuETCKbZekn+h6t5FGGcZG2abPGKbz/x9rwpb6qV2hcbAQ30t6M7H6cTOh2/Ut8bEV2MtMB15sw=="],
"@tiptap/extension-dropcursor": ["@tiptap/extension-dropcursor@3.15.3", "", { "peerDependencies": { "@tiptap/extensions": "^3.15.3" } }, "sha512-jGI5XZpdo8GSYQFj7HY15/oEwC2m2TqZz0/Fln5qIhY32XlZhWrsMuMI6WbUJrTH16es7xO6jmRlDsc6g+vJWg=="],
"@tiptap/extension-floating-menu": ["@tiptap/extension-floating-menu@3.15.3", "", { "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "^3.15.3", "@tiptap/pm": "^3.15.3" } }, "sha512-+3DVBleKKffadEJEdLYxmYAJOjHjLSqtiSFUE3RABT4V2ka1ODy2NIpyKX0o1SvQ5N1jViYT9Q+yUbNa6zCcDw=="],
"@tiptap/extension-gapcursor": ["@tiptap/extension-gapcursor@3.15.3", "", { "peerDependencies": { "@tiptap/extensions": "^3.15.3" } }, "sha512-Kaw0sNzP0bQI/xEAMSfIpja6xhsu9WqqAK/puzOIS1RKWO47Wps/tzqdSJ9gfslPIb5uY5mKCfy8UR8Xgiia8w=="],
"@tiptap/extension-hard-break": ["@tiptap/extension-hard-break@3.15.3", "", { "peerDependencies": { "@tiptap/core": "^3.15.3" } }, "sha512-8HjxmeRbBiXW+7JKemAJtZtHlmXQ9iji398CPQ0yYde68WbIvUhHXjmbJE5pxFvvQTJ/zJv1aISeEOZN2bKBaw=="],
"@tiptap/extension-heading": ["@tiptap/extension-heading@3.15.3", "", { "peerDependencies": { "@tiptap/core": "^3.15.3" } }, "sha512-G1GG6iN1YXPS+75arDpo+bYRzhr3dNDw99c7D7na3aDawa9Qp7sZ/bVrzFUUcVEce0cD6h83yY7AooBxEc67hA=="],
"@tiptap/extension-horizontal-rule": ["@tiptap/extension-horizontal-rule@3.15.3", "", { "peerDependencies": { "@tiptap/core": "^3.15.3", "@tiptap/pm": "^3.15.3" } }, "sha512-FYkN7L6JsfwwNEntmLklCVKvgL0B0N47OXMacRk6kYKQmVQ4Nvc7q/VJLpD9sk4wh4KT1aiCBfhKEBTu5pv1fg=="],
"@tiptap/extension-image": ["@tiptap/extension-image@3.15.3", "", { "peerDependencies": { "@tiptap/core": "^3.15.3" } }, "sha512-Tjq9BHlC/0bGR9/uySA0tv6I1Ua1Q5t5P/mdbWyZi4JdUpKHRfgenzfXF5DYnklJ01QJ7uOPSp9sAGgPzBixtQ=="],
"@tiptap/extension-italic": ["@tiptap/extension-italic@3.15.3", "", { "peerDependencies": { "@tiptap/core": "^3.15.3" } }, "sha512-6XeuPjcWy7OBxpkgOV7bD6PATO5jhIxc8SEK4m8xn8nelGTBIbHGqK37evRv+QkC7E0MUryLtzwnmmiaxcKL0Q=="],
"@tiptap/extension-link": ["@tiptap/extension-link@3.15.3", "", { "dependencies": { "linkifyjs": "^4.3.2" }, "peerDependencies": { "@tiptap/core": "^3.15.3", "@tiptap/pm": "^3.15.3" } }, "sha512-PdDXyBF9Wco9U1x6e+b7tKBWG+kqBDXDmaYXHkFm/gYuQCQafVJ5mdrDdKgkHDWVnJzMWZXBcZjT9r57qtlLWg=="],
"@tiptap/extension-list": ["@tiptap/extension-list@3.15.3", "", { "peerDependencies": { "@tiptap/core": "^3.15.3", "@tiptap/pm": "^3.15.3" } }, "sha512-n7y/MF9lAM5qlpuH5IR4/uq+kJPEJpe9NrEiH+NmkO/5KJ6cXzpJ6F4U17sMLf2SNCq+TWN9QK8QzoKxIn50VQ=="],
"@tiptap/extension-list-item": ["@tiptap/extension-list-item@3.15.3", "", { "peerDependencies": { "@tiptap/extension-list": "^3.15.3" } }, "sha512-CCxL5ek1p0lO5e8aqhnPzIySldXRSigBFk2fP9OLgdl5qKFLs2MGc19jFlx5+/kjXnEsdQTFbGY1Sizzt0TVDw=="],
"@tiptap/extension-list-keymap": ["@tiptap/extension-list-keymap@3.15.3", "", { "peerDependencies": { "@tiptap/extension-list": "^3.15.3" } }, "sha512-UxqnTEEAKrL+wFQeSyC9z0mgyUUVRS2WTcVFoLZCE6/Xus9F53S4bl7VKFadjmqI4GpDk5Oe2IOUc72o129jWg=="],
"@tiptap/extension-ordered-list": ["@tiptap/extension-ordered-list@3.15.3", "", { "peerDependencies": { "@tiptap/extension-list": "^3.15.3" } }, "sha512-/8uhw528Iy0c9wF6tHCiIn0ToM0Ml6Ll2c/3iPRnKr4IjXwx2Lr994stUFihb+oqGZwV1J8CPcZJ4Ufpdqi4Dw=="],
"@tiptap/extension-paragraph": ["@tiptap/extension-paragraph@3.15.3", "", { "peerDependencies": { "@tiptap/core": "^3.15.3" } }, "sha512-lc0Qu/1AgzcEfS67NJMj5tSHHhH6NtA6uUpvppEKGsvJwgE2wKG1onE4isrVXmcGRdxSMiCtyTDemPNMu6/ozQ=="],
"@tiptap/extension-placeholder": ["@tiptap/extension-placeholder@3.15.3", "", { "peerDependencies": { "@tiptap/extensions": "^3.15.3" } }, "sha512-XcHHnojT186hKIoOgcPBesXk89+caNGVUdMtc171Vcr/5s0dpnr4q5LfE+YRC+S85CpCxCRRnh84Ou+XRtOqrw=="],
"@tiptap/extension-strike": ["@tiptap/extension-strike@3.15.3", "", { "peerDependencies": { "@tiptap/core": "^3.15.3" } }, "sha512-Y1P3eGNY7RxQs2BcR6NfLo9VfEOplXXHAqkOM88oowWWOE7dMNeFFZM9H8HNxoQgXJ7H0aWW9B7ZTWM9hWli2Q=="],
"@tiptap/extension-task-item": ["@tiptap/extension-task-item@3.15.3", "", { "peerDependencies": { "@tiptap/extension-list": "^3.15.3" } }, "sha512-bkrmouc1rE5n9ONw2G7+zCGfBRoF2HJWq8REThPMzg/6+L5GJJ5YTN4UmncaP48U9jHX8xeihjgg9Ypenjl4lw=="],
"@tiptap/extension-task-list": ["@tiptap/extension-task-list@3.15.3", "", { "peerDependencies": { "@tiptap/extension-list": "^3.15.3" } }, "sha512-nh8iBk1LHVIoqxphLoqZlLAN9fF2i9ZeK+2TjGSS35lfh7sYzRoSjNW0E81Uy48YuCzM1NQYghYR5Qfc7vm4jA=="],
"@tiptap/extension-text": ["@tiptap/extension-text@3.15.3", "", { "peerDependencies": { "@tiptap/core": "^3.15.3" } }, "sha512-MhkBz8ZvrqOKtKNp+ZWISKkLUlTrDR7tbKZc2OnNcUTttL9dz0HwT+cg91GGz19fuo7ttDcfsPV6eVmflvGToA=="],
"@tiptap/extension-underline": ["@tiptap/extension-underline@3.15.3", "", { "peerDependencies": { "@tiptap/core": "^3.15.3" } }, "sha512-r/IwcNN0W366jGu4Y0n2MiFq9jGa4aopOwtfWO4d+J0DyeS2m7Go3+KwoUqi0wQTiVU74yfi4DF6eRsMQ9/iHQ=="],
"@tiptap/extensions": ["@tiptap/extensions@3.15.3", "", { "peerDependencies": { "@tiptap/core": "^3.15.3", "@tiptap/pm": "^3.15.3" } }, "sha512-ycx/BgxR4rc9tf3ZyTdI98Z19yKLFfqM3UN+v42ChuIwkzyr9zyp7kG8dB9xN2lNqrD+5y/HyJobz/VJ7T90gA=="],
"@tiptap/markdown": ["@tiptap/markdown@3.15.3", "", { "dependencies": { "marked": "^15.0.12" }, "peerDependencies": { "@tiptap/core": "^3.15.3", "@tiptap/pm": "^3.15.3" } }, "sha512-JjjZ/X7H2+/Jeapk8GurbncJVyG9ai5YD/eJLBKDyWqsoQwsrHbDlYBx26q9J5VwWXzxe9G6fmHNlAfw0Pokow=="],
"@tiptap/pm": ["@tiptap/pm@3.15.3", "", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", "prosemirror-markdown": "^1.13.1", "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.24.1", "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.5.0", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.38.1" } }, "sha512-Zm1BaU1TwFi3CQiisxjgnzzIus+q40bBKWLqXf6WEaus8Z6+vo1MT2pU52dBCMIRaW9XNDq3E5cmGtMc1AlveA=="],
"@tiptap/react": ["@tiptap/react@3.15.3", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "fast-equals": "^5.3.3", "use-sync-external-store": "^1.4.0" }, "optionalDependencies": { "@tiptap/extension-bubble-menu": "^3.15.3", "@tiptap/extension-floating-menu": "^3.15.3" }, "peerDependencies": { "@tiptap/core": "^3.15.3", "@tiptap/pm": "^3.15.3", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-XvouB+Hrqw8yFmZLPEh+HWlMeRSjZfHSfWfWuw5d8LSwnxnPeu3Bg/rjHrRrdwb+7FumtzOnNWMorpb/PSOttQ=="],
"@tiptap/starter-kit": ["@tiptap/starter-kit@3.15.3", "", { "dependencies": { "@tiptap/core": "^3.15.3", "@tiptap/extension-blockquote": "^3.15.3", "@tiptap/extension-bold": "^3.15.3", "@tiptap/extension-bullet-list": "^3.15.3", "@tiptap/extension-code": "^3.15.3", "@tiptap/extension-code-block": "^3.15.3", "@tiptap/extension-document": "^3.15.3", "@tiptap/extension-dropcursor": "^3.15.3", "@tiptap/extension-gapcursor": "^3.15.3", "@tiptap/extension-hard-break": "^3.15.3", "@tiptap/extension-heading": "^3.15.3", "@tiptap/extension-horizontal-rule": "^3.15.3", "@tiptap/extension-italic": "^3.15.3", "@tiptap/extension-link": "^3.15.3", "@tiptap/extension-list": "^3.15.3", "@tiptap/extension-list-item": "^3.15.3", "@tiptap/extension-list-keymap": "^3.15.3", "@tiptap/extension-ordered-list": "^3.15.3", "@tiptap/extension-paragraph": "^3.15.3", "@tiptap/extension-strike": "^3.15.3", "@tiptap/extension-text": "^3.15.3", "@tiptap/extension-underline": "^3.15.3", "@tiptap/extensions": "^3.15.3", "@tiptap/pm": "^3.15.3" } }, "sha512-ia+eQr9Mt1ln2UO+kK4kFTJOrZK4GhvZXFjpCCYuHtco3rhr2fZAIxEEY4cl/vo5VO5WWyPqxhkFeLcoWmNjSw=="],
"@tiptap/suggestion": ["@tiptap/suggestion@3.15.3", "", { "peerDependencies": { "@tiptap/core": "^3.15.3", "@tiptap/pm": "^3.15.3" } }, "sha512-+CbaHhPfKUe+fNpUIQaOPhh6xI+xL5jbK1zw++U+CZIRrVAAmHRhO+D0O2HdiE1RK7596y8bRqMiB2CRHF7emA=="],
"@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
@ -1836,10 +1924,16 @@
"@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="],
"@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="],
"@types/lodash": ["@types/lodash@4.17.23", "", {}, "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA=="],
"@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="],
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
"@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="],
"@types/minimatch": ["@types/minimatch@3.0.5", "", {}, "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ=="],
@ -1874,6 +1968,8 @@
"@types/urijs": ["@types/urijs@1.19.26", "", {}, "sha512-wkXrVzX5yoqLnndOwFsieJA7oKM8cNkOKJtf/3vVGSUFkWDKZvFHpIl9Pvqb/T9UsawBBFMTTD8xu7sK5MWuvg=="],
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.53.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/type-utils": "8.53.0", "@typescript-eslint/utils": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.53.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg=="],
@ -2336,6 +2432,8 @@
"cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="],
"crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
"cron-schedule": ["cron-schedule@6.0.0", "", {}, "sha512-BoZaseYGXOo5j5HUwTaegIog3JJbuH4BbrY9A1ArLjXpy+RWb3mV28F/9Gv1dDA7E2L8kngWva4NWisnLTyfgQ=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
@ -3266,6 +3364,10 @@
"linkedom": ["linkedom@0.18.12", "", { "dependencies": { "css-select": "^5.1.0", "cssom": "^0.5.0", "html-escaper": "^3.0.3", "htmlparser2": "^10.0.0", "uhyphen": "^0.2.0" }, "peerDependencies": { "canvas": ">= 2" }, "optionalPeers": ["canvas"] }, "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q=="],
"linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="],
"linkifyjs": ["linkifyjs@4.3.2", "", {}, "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA=="],
"listr2": ["listr2@8.3.3", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ=="],
"loader-runner": ["loader-runner@4.3.1", "", {}, "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q=="],
@ -3316,9 +3418,11 @@
"markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
"markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="],
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
"marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
"marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="],
"marky": ["marky@1.3.0", "", {}, "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="],
@ -3362,6 +3466,8 @@
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
"mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
"media-query-parser": ["media-query-parser@2.0.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" } }, "sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w=="],
"media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
@ -3614,6 +3720,8 @@
"ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="],
"orderedmap": ["orderedmap@2.1.1", "", {}, "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="],
"os-shim": ["os-shim@0.1.3", "", {}, "sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A=="],
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
@ -3792,6 +3900,42 @@
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
"prosemirror-changeset": ["prosemirror-changeset@2.3.1", "", { "dependencies": { "prosemirror-transform": "^1.0.0" } }, "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ=="],
"prosemirror-collab": ["prosemirror-collab@1.3.1", "", { "dependencies": { "prosemirror-state": "^1.0.0" } }, "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ=="],
"prosemirror-commands": ["prosemirror-commands@1.7.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.10.2" } }, "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w=="],
"prosemirror-dropcursor": ["prosemirror-dropcursor@1.8.2", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0", "prosemirror-view": "^1.1.0" } }, "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw=="],
"prosemirror-gapcursor": ["prosemirror-gapcursor@1.4.0", "", { "dependencies": { "prosemirror-keymap": "^1.0.0", "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-view": "^1.0.0" } }, "sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ=="],
"prosemirror-history": ["prosemirror-history@1.5.0", "", { "dependencies": { "prosemirror-state": "^1.2.2", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.31.0", "rope-sequence": "^1.3.0" } }, "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg=="],
"prosemirror-inputrules": ["prosemirror-inputrules@1.5.1", "", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.0.0" } }, "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw=="],
"prosemirror-keymap": ["prosemirror-keymap@1.2.3", "", { "dependencies": { "prosemirror-state": "^1.0.0", "w3c-keyname": "^2.2.0" } }, "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw=="],
"prosemirror-markdown": ["prosemirror-markdown@1.13.2", "", { "dependencies": { "@types/markdown-it": "^14.0.0", "markdown-it": "^14.0.0", "prosemirror-model": "^1.25.0" } }, "sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g=="],
"prosemirror-menu": ["prosemirror-menu@1.2.5", "", { "dependencies": { "crelt": "^1.0.0", "prosemirror-commands": "^1.0.0", "prosemirror-history": "^1.0.0", "prosemirror-state": "^1.0.0" } }, "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ=="],
"prosemirror-model": ["prosemirror-model@1.25.4", "", { "dependencies": { "orderedmap": "^2.0.0" } }, "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA=="],
"prosemirror-schema-basic": ["prosemirror-schema-basic@1.2.4", "", { "dependencies": { "prosemirror-model": "^1.25.0" } }, "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ=="],
"prosemirror-schema-list": ["prosemirror-schema-list@1.5.1", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.7.3" } }, "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q=="],
"prosemirror-state": ["prosemirror-state@1.4.4", "", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.27.0" } }, "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw=="],
"prosemirror-tables": ["prosemirror-tables@1.8.5", "", { "dependencies": { "prosemirror-keymap": "^1.2.3", "prosemirror-model": "^1.25.4", "prosemirror-state": "^1.4.4", "prosemirror-transform": "^1.10.5", "prosemirror-view": "^1.41.4" } }, "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw=="],
"prosemirror-trailing-node": ["prosemirror-trailing-node@3.0.0", "", { "dependencies": { "@remirror/core-constants": "3.0.0", "escape-string-regexp": "^4.0.0" }, "peerDependencies": { "prosemirror-model": "^1.22.1", "prosemirror-state": "^1.4.2", "prosemirror-view": "^1.33.8" } }, "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ=="],
"prosemirror-transform": ["prosemirror-transform@1.10.5", "", { "dependencies": { "prosemirror-model": "^1.21.0" } }, "sha512-RPDQCxIDhIBb1o36xxwsaeAvivO8VLJcgBtzmOwQ64bMtsVFh5SSuJ6dWSxO1UsHTiTXPCgQm3PDJt7p6IOLbw=="],
"prosemirror-view": ["prosemirror-view@1.41.5", "", { "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA=="],
"proto-list": ["proto-list@1.2.4", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="],
"protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
@ -3810,6 +3954,8 @@
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="],
"pupa": ["pupa@3.3.0", "", { "dependencies": { "escape-goat": "^4.0.0" } }, "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA=="],
"puppeteer": ["puppeteer@22.14.0", "", { "dependencies": { "@puppeteer/browsers": "2.3.0", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1312386", "puppeteer-core": "22.14.0" }, "bin": { "puppeteer": "lib/esm/puppeteer/node/cli.js" } }, "sha512-MGTR6/pM8zmWbTdazb6FKnwIihzsSEXBPH49mFFU96DNZpQOevCAZMnjBZGlZRGRzRK6aADCavR6SQtrbv5dQw=="],
@ -3998,6 +4144,8 @@
"rollup": ["rollup@4.55.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.1", "@rollup/rollup-android-arm64": "4.55.1", "@rollup/rollup-darwin-arm64": "4.55.1", "@rollup/rollup-darwin-x64": "4.55.1", "@rollup/rollup-freebsd-arm64": "4.55.1", "@rollup/rollup-freebsd-x64": "4.55.1", "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", "@rollup/rollup-linux-arm-musleabihf": "4.55.1", "@rollup/rollup-linux-arm64-gnu": "4.55.1", "@rollup/rollup-linux-arm64-musl": "4.55.1", "@rollup/rollup-linux-loong64-gnu": "4.55.1", "@rollup/rollup-linux-loong64-musl": "4.55.1", "@rollup/rollup-linux-ppc64-gnu": "4.55.1", "@rollup/rollup-linux-ppc64-musl": "4.55.1", "@rollup/rollup-linux-riscv64-gnu": "4.55.1", "@rollup/rollup-linux-riscv64-musl": "4.55.1", "@rollup/rollup-linux-s390x-gnu": "4.55.1", "@rollup/rollup-linux-x64-gnu": "4.55.1", "@rollup/rollup-linux-x64-musl": "4.55.1", "@rollup/rollup-openbsd-x64": "4.55.1", "@rollup/rollup-openharmony-arm64": "4.55.1", "@rollup/rollup-win32-arm64-msvc": "4.55.1", "@rollup/rollup-win32-ia32-msvc": "4.55.1", "@rollup/rollup-win32-x64-gnu": "4.55.1", "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A=="],
"rope-sequence": ["rope-sequence@1.3.4", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="],
"rou3": ["rou3@0.6.3", "", {}, "sha512-1HSG1ENTj7Kkm5muMnXuzzfdDOf7CFnbSYFA+H3Fp/rB9lOCxCPgy1jlZxTKyFoC5jJay8Mmc+VbPLYRjzYLrA=="],
"roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
@ -4218,6 +4366,8 @@
"swr": ["swr@2.3.8", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w=="],
"tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
@ -4346,6 +4496,8 @@
"typescript-eslint": ["typescript-eslint@8.53.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.53.0", "@typescript-eslint/parser": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/utils": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw=="],
"uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
"ufo": ["ufo@1.6.2", "", {}, "sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q=="],
"uhyphen": ["uhyphen@0.2.0", "", {}, "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA=="],
@ -4420,6 +4572,8 @@
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
"use-debounce": ["use-debounce@10.1.0", "", { "peerDependencies": { "react": "*" } }, "sha512-lu87Za35V3n/MyMoEpD5zJv0k7hCn0p+V/fK2kWD+3k2u3kOCwO593UArbczg1fhfs2rqPEnHpULJ3KmGdDzvg=="],
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
@ -4466,6 +4620,8 @@
"vscode-uri": ["vscode-uri@3.0.8", "", {}, "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw=="],
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
"warning": ["warning@4.0.3", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w=="],
"watchpack": ["watchpack@2.4.4", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA=="],
@ -4598,12 +4754,18 @@
"@ai-sdk/provider-utils/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"@ai-sdk/react/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0-beta.5", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-4Dv/wiGZrvO6fI7P0yMLa4XZru0XW8LPibTObbkHBdweLUVGIze7aCfxxQeY44Uqcbl/h6/yBTkx2XmPtwf/Ow=="],
"@ai-sdk/react/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.7", "", { "dependencies": { "@ai-sdk/provider": "3.0.3", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ItzTdBxRLieGz1GHPwl9X3+HKfwTfFd9MdIa91aXRnOjUVRw68ENjAGKm3FcXGsBLkXDLaFWgjbTVdXe2igs2w=="],
"@ai-sdk/react/ai": ["ai@5.0.0-beta.24", "", { "dependencies": { "@ai-sdk/gateway": "1.0.0-beta.10", "@ai-sdk/provider": "2.0.0-beta.1", "@ai-sdk/provider-utils": "3.0.0-beta.5", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" }, "bin": { "ai": "dist/bin/ai.min.js" } }, "sha512-glQIA+PGEP+UEPB+thdqNZi9Ot4Yjiqsl071S1KPaRTGHmBIg/c8OYb2mXCRM+3cNCFGVnCTudZoYUVNwBpFxg=="],
"@ai-sdk/react/ai": ["ai@6.0.37", "", { "dependencies": { "@ai-sdk/gateway": "3.0.15", "@ai-sdk/provider": "3.0.3", "@ai-sdk/provider-utils": "4.0.7", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-GUMBYx0TXKxXRcWy6DY3ryHJZ7cATxc99WPT7WCD8hGCYkdhFVItKPTtINeTlK+FlUomDqzjPGtiDcVftcw//w=="],
"@ai-sdk/ui-utils/@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
"@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.10", "", { "dependencies": { "@ai-sdk/provider": "3.0.3", "@ai-sdk/provider-utils": "4.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-dM+CXjW3+QAZVl2NKhch9T5gCa8p7CMJR16KtaRS9HzY8H/ASD8hiAssnDGz00XyOKYpDkd9Mj2mwK25u5LxnQ=="],
"@ai-sdk/xai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qGPYdoAuECaUXPrrz0BPX1SacZQuJ6zky0aakxpW89QW1hrY0eF4gcFm/3L9Pk8C5Fwe+RvBf2z7ZjDhaPjnlg=="],
"@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.6", "", { "dependencies": { "@ai-sdk/provider": "3.0.3", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-o/SP1GQOrpXAzHjMosPHI0Pu+YkwxIMndSjSLrEXtcVixdrjqrGaA9I7xJcWf+XpRFJ9byPHrKYnprwS+36gMg=="],
"@aklinker1/rollup-plugin-visualizer/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
"@aklinker1/rollup-plugin-visualizer/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
@ -5040,9 +5202,9 @@
"@repo/docs/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"@repo/web/@ai-sdk/google": ["@ai-sdk/google@2.0.52", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-2XUnGi3f7TV4ujoAhA+Fg3idUoG/+Y2xjCRg70a1/m0DH1KSQqYaCboJ1C19y6ZHGdf5KNT20eJdswP6TvrY2g=="],
"@repo/web/@ai-sdk/google": ["@ai-sdk/google@3.0.9", "", { "dependencies": { "@ai-sdk/provider": "3.0.3", "@ai-sdk/provider-utils": "4.0.7" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-whRdK0gCZL92UbEHdmQ6edm3cp5ZZSqwE79AIvTEaJR+BeaMUJchTxz/I5w9l3EHGOiDxrKYssklqO3z45KGXg=="],
"@repo/web/ai": ["ai@5.0.0-beta.24", "", { "dependencies": { "@ai-sdk/gateway": "1.0.0-beta.10", "@ai-sdk/provider": "2.0.0-beta.1", "@ai-sdk/provider-utils": "3.0.0-beta.5", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" }, "bin": { "ai": "dist/bin/ai.min.js" } }, "sha512-glQIA+PGEP+UEPB+thdqNZi9Ot4Yjiqsl071S1KPaRTGHmBIg/c8OYb2mXCRM+3cNCFGVnCTudZoYUVNwBpFxg=="],
"@repo/web/ai": ["ai@6.0.35", "", { "dependencies": { "@ai-sdk/gateway": "3.0.14", "@ai-sdk/provider": "3.0.3", "@ai-sdk/provider-utils": "4.0.6", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MxgtU6CjnegH1rhRfomM0gptKxP6r+9sxbLvYq36C1l85+o0LacqbXLdNVYzqab+lHN4q7ZP3QS8wZq4YkZahA=="],
"@repo/web/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
@ -5354,6 +5516,8 @@
"memory-graph-playground/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
"mermaid/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
@ -5474,6 +5638,8 @@
"streamdown/lucide-react": ["lucide-react@0.542.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw=="],
"streamdown/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@ -5564,11 +5730,15 @@
"wxt/vite-node": ["vite-node@5.2.0", "", { "dependencies": { "cac": "^6.7.14", "es-module-lexer": "^1.7.0", "obug": "^2.0.0", "pathe": "^2.0.3", "vite": "^7.2.2" }, "bin": { "vite-node": "dist/cli.mjs" } }, "sha512-7UT39YxUukIA97zWPXUGb0SGSiLexEGlavMwU3HDE6+d/HJhKLjLqu4eX2qv6SQiocdhKLRcusroDwXHQ6CnRQ=="],
"@ai-sdk/react/@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0-beta.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Z8SPncMtS3RsoXITmT7NVwrAq6M44dmw0DoUOYJqNNtCu8iMWuxB8Nxsoqpa0uEEy9R1V1ZThJAXTYgjTUxl3w=="],
"@ai-sdk/react/@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@3.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qGPYdoAuECaUXPrrz0BPX1SacZQuJ6zky0aakxpW89QW1hrY0eF4gcFm/3L9Pk8C5Fwe+RvBf2z7ZjDhaPjnlg=="],
"@ai-sdk/react/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.0-beta.10", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.1", "@ai-sdk/provider-utils": "3.0.0-beta.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-v+LXXm8INLYAdxHnNMVAJ/B7k+Nejn5dCQMg/F8SRetB5dEQ4sbfimE+b6rawILJznnsy2fugUO1oFFXlUS5Yg=="],
"@ai-sdk/react/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@ai-sdk/react/ai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0-beta.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Z8SPncMtS3RsoXITmT7NVwrAq6M44dmw0DoUOYJqNNtCu8iMWuxB8Nxsoqpa0uEEy9R1V1ZThJAXTYgjTUxl3w=="],
"@ai-sdk/react/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.15", "", { "dependencies": { "@ai-sdk/provider": "3.0.3", "@ai-sdk/provider-utils": "4.0.7", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OsWcXMfkF9U38YhU7926rYt4IAtJuZlnM1e8STN8hHb4qbiTaORBSXcFaJuOEZ1kYOf3DqqP7CxbmM543ePzIg=="],
"@ai-sdk/react/ai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qGPYdoAuECaUXPrrz0BPX1SacZQuJ6zky0aakxpW89QW1hrY0eF4gcFm/3L9Pk8C5Fwe+RvBf2z7ZjDhaPjnlg=="],
"@ai-sdk/xai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@aklinker1/rollup-plugin-visualizer/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
@ -6076,13 +6246,15 @@
"@raycast/api/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
"@repo/web/@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.20", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-iXHVe0apM2zUEzauqJwqmpC37A5rihrStAih5Ks+JE32iTe4LZ58y17UGBjpQQTCRw9YxMeo2UFLxLpBluyvLQ=="],
"@repo/web/@ai-sdk/google/@ai-sdk/provider": ["@ai-sdk/provider@3.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qGPYdoAuECaUXPrrz0BPX1SacZQuJ6zky0aakxpW89QW1hrY0eF4gcFm/3L9Pk8C5Fwe+RvBf2z7ZjDhaPjnlg=="],
"@repo/web/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.0-beta.10", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.1", "@ai-sdk/provider-utils": "3.0.0-beta.5" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-v+LXXm8INLYAdxHnNMVAJ/B7k+Nejn5dCQMg/F8SRetB5dEQ4sbfimE+b6rawILJznnsy2fugUO1oFFXlUS5Yg=="],
"@repo/web/@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.7", "", { "dependencies": { "@ai-sdk/provider": "3.0.3", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ItzTdBxRLieGz1GHPwl9X3+HKfwTfFd9MdIa91aXRnOjUVRw68ENjAGKm3FcXGsBLkXDLaFWgjbTVdXe2igs2w=="],
"@repo/web/ai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0-beta.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Z8SPncMtS3RsoXITmT7NVwrAq6M44dmw0DoUOYJqNNtCu8iMWuxB8Nxsoqpa0uEEy9R1V1ZThJAXTYgjTUxl3w=="],
"@repo/web/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.14", "", { "dependencies": { "@ai-sdk/provider": "3.0.3", "@ai-sdk/provider-utils": "4.0.6", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-udVpkDaQ00jMcBvtGGvmkEBU31XidsHB4E8HIF9l7/H7lyjOS/EtXzN2adoupDg5j1/VjjSI3Ny5P1zHUvLyMA=="],
"@repo/web/ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0-beta.5", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-4Dv/wiGZrvO6fI7P0yMLa4XZru0XW8LPibTObbkHBdweLUVGIze7aCfxxQeY44Uqcbl/h6/yBTkx2XmPtwf/Ow=="],
"@repo/web/ai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qGPYdoAuECaUXPrrz0BPX1SacZQuJ6zky0aakxpW89QW1hrY0eF4gcFm/3L9Pk8C5Fwe+RvBf2z7ZjDhaPjnlg=="],
"@repo/web/ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.6", "", { "dependencies": { "@ai-sdk/provider": "3.0.3", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-o/SP1GQOrpXAzHjMosPHI0Pu+YkwxIMndSjSLrEXtcVixdrjqrGaA9I7xJcWf+XpRFJ9byPHrKYnprwS+36gMg=="],
"@stoplight/better-ajv-errors/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
@ -6608,6 +6780,10 @@
"@puppeteer/browsers/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@repo/web/@ai-sdk/google/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@repo/web/ai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"agents/@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"agents/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],

View file

@ -27,7 +27,7 @@ const buttonVariants = cva(
"rounded-xl !px-6 !py-3 bg-black border border-[#161F2C] hover:bg-[#161F2C] !h-[40px] cursor-pointer text-white",
linkPreview:
"rounded-xl !px-3 !py-1 bg-black border border-[#161F2C] hover:bg-[#161F2C] cursor-pointer text-white border border-[#161F2C]",
insideOut: "shadow-inside-out rounded-full bg-[#0D121A]",
insideOut: "shadow-inside-out rounded-full bg-[#0D121A] cursor-pointer",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",