migrate nova agent to claude agents sdk

This commit is contained in:
Shoubhit Dash 2026-01-29 22:58:07 +05:30
parent 489b5e23c0
commit f03886fae1
38 changed files with 1107 additions and 290 deletions

View file

@ -0,0 +1,186 @@
import { query } from "@anthropic-ai/claude-agent-sdk"
import { cookies } from "next/headers"
import { createSupermemoryMcpServer, SUPERMEMORY_SYSTEM_PROMPT } from "@/lib/agent/tools"
import type { ChatRequest, AgentMessagePart } from "@/lib/agent/types"
export const runtime = "nodejs"
export async function POST(req: Request) {
try {
const body: ChatRequest = await req.json()
const { messages, metadata } = body
const cookieStore = await cookies()
const cookieHeader = cookieStore
.getAll()
.map((c) => `${c.name}=${c.value}`)
.join("; ")
const supermemoryServer = createSupermemoryMcpServer({
cookies: cookieHeader,
projectId: metadata.projectId,
})
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
try {
const conversationText = messages
.map((msg) => `${msg.role === "user" ? "User" : "Assistant"}: ${msg.content}`)
.join("\n\n")
const lastUserMessage = messages.filter((m) => m.role === "user").pop()
const promptText = lastUserMessage?.content ?? conversationText
const result = query({
prompt: promptText,
options: {
systemPrompt: `${SUPERMEMORY_SYSTEM_PROMPT}\n\nConversation history:\n${conversationText}`,
model: "claude-sonnet-4-20250514",
mcpServers: {
supermemory: supermemoryServer,
},
allowedTools: [
"mcp__supermemory__searchMemories",
"mcp__supermemory__addMemory",
],
permissionMode: "bypassPermissions",
allowDangerouslySkipPermissions: true,
includePartialMessages: true,
},
})
for await (const message of result) {
const parts: AgentMessagePart[] = []
const msgType = message.type
if (msgType === "assistant") {
const assistantMsg = message as {
type: "assistant"
message: {
id?: string
content: string | Array<{ type: string; text?: string }>
}
}
const textContent =
typeof assistantMsg.message.content === "string"
? assistantMsg.message.content
: Array.isArray(assistantMsg.message.content)
? assistantMsg.message.content
.filter((c) => c.type === "text" && typeof c.text === "string")
.map((c) => c.text as string)
.join("")
: ""
if (textContent) {
parts.push({ type: "text", text: textContent })
}
const event = {
type: "assistant",
id: assistantMsg.message.id ?? crypto.randomUUID(),
role: "assistant",
parts,
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`))
}
if (msgType === "result") {
const resultMsg = message as {
type: "result"
subtype?: string
tool_name?: string
tool_input?: Record<string, unknown>
tool_result?: {
content?: Array<{ type: string; text?: string }>
}
}
if (resultMsg.subtype === "tool_use" && resultMsg.tool_name) {
const toolName = resultMsg.tool_name
if (toolName === "mcp__supermemory__searchMemories") {
parts.push({
type: "tool-searchMemories",
state: "input-available",
input: resultMsg.tool_input as { query: string; limit?: number },
})
} else if (toolName === "mcp__supermemory__addMemory") {
parts.push({
type: "tool-addMemory",
state: "input-available",
input: resultMsg.tool_input as { content: string; title?: string },
})
}
if (parts.length > 0) {
const event = { type: "tool_use", parts }
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`))
}
}
if (resultMsg.subtype === "tool_result" && resultMsg.tool_result?.content) {
try {
const resultText = resultMsg.tool_result.content
.filter((c) => c.type === "text")
.map((c) => c.text)
.join("")
if (resultText) {
const parsed = JSON.parse(resultText)
if ("count" in parsed && "results" in parsed) {
parts.push({
type: "tool-searchMemories",
state: "output-available",
output: parsed,
})
} else if ("status" in parsed) {
parts.push({
type: "tool-addMemory",
state: "output-available",
output: parsed,
})
}
}
} catch {
// Ignore parse errors
}
if (parts.length > 0) {
const event = { type: "tool_result", parts }
controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`))
}
}
}
}
controller.enqueue(encoder.encode("data: [DONE]\n\n"))
controller.close()
} catch (error) {
console.error("Agent stream error:", error)
const errorEvent = {
type: "error",
error: error instanceof Error ? error.message : "Unknown error",
}
controller.enqueue(encoder.encode(`data: ${JSON.stringify(errorEvent)}\n\n`))
controller.close()
}
},
})
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
})
} catch (error) {
console.error("Agent route error:", error)
return Response.json(
{ error: error instanceof Error ? error.message : "Internal server error" },
{ status: 500 }
)
}
}

View file

@ -0,0 +1,51 @@
import { unstable_v2_prompt } from "@anthropic-ai/claude-agent-sdk"
import type { FollowUpRequest } from "@/lib/agent/types"
export const runtime = "nodejs"
export async function POST(req: Request) {
try {
const body: FollowUpRequest = await req.json()
const { messages } = body
const conversationContext = messages
.slice(-5)
.map((msg) => `${msg.role}: ${msg.content}`)
.join("\n\n")
const prompt = `Based on this conversation, generate exactly 3 brief follow-up questions that the user might want to ask next. Return ONLY a JSON array of strings, no other text.
Conversation:
${conversationContext}
Return format: ["question 1", "question 2", "question 3"]`
const response = await unstable_v2_prompt(prompt, {
model: "claude-sonnet-4-20250514",
})
let questions: string[] = []
let resultText = ""
if (response.type === "result" && "result" in response) {
resultText = (response as { result?: string }).result ?? ""
}
try {
const jsonMatch = resultText.match(/\[[\s\S]*\]/)
if (jsonMatch) {
questions = JSON.parse(jsonMatch[0])
}
} catch {
const lines = resultText.split("\n").filter((line: string) => line.trim())
questions = lines.slice(0, 3).map((line: string) =>
line.replace(/^[\d\.\-\*\s]+/, "").replace(/["\[\]]/g, "").trim()
)
}
return Response.json({ questions: questions.slice(0, 3) })
} catch (error) {
console.error("Follow-ups route error:", error)
return Response.json({ questions: [] }, { status: 200 })
}
}

View file

@ -0,0 +1,42 @@
import { unstable_v2_prompt } from "@anthropic-ai/claude-agent-sdk"
import type { TitleRequest } from "@/lib/agent/types"
export const runtime = "nodejs"
export async function POST(req: Request) {
try {
const body: TitleRequest = await req.json()
const { messages } = body
if (messages.length === 0) {
return Response.json({ title: "New Chat" })
}
const conversationContext = messages
.slice(0, 4)
.map((msg) => `${msg.role}: ${msg.content.slice(0, 500)}`)
.join("\n\n")
const prompt = `Generate a short, descriptive title (3-6 words) for this conversation. Return ONLY the title text, no quotes or punctuation at the end.
Conversation:
${conversationContext}
Title:`
const response = await unstable_v2_prompt(prompt, {
model: "claude-sonnet-4-20250514",
})
let resultText = "New Chat"
if (response.type === "result" && "result" in response) {
resultText = (response as { result?: string }).result ?? "New Chat"
}
const title = resultText.trim().replace(/^["']|["']$/g, "").slice(0, 100)
return Response.json({ title })
} catch (error) {
console.error("Title route error:", error)
return Response.json({ title: "New Chat" }, { status: 200 })
}
}

View file

@ -1,11 +1,13 @@
"use client"
import { useState, useEffect, useCallback, useRef } from "react"
import type { UIMessage } from "@ai-sdk/react"
import { motion, AnimatePresence } from "motion/react"
import { useChat } from "@ai-sdk/react"
import { DefaultChatTransport } from "ai"
import NovaOrb from "@/components/nova/nova-orb"
import {
useClaudeAgent,
generateFollowUpQuestions,
} from "@/hooks/use-claude-agent"
import type { AgentMessage as AgentMessageType } from "@/lib/agent/types"
import { Button } from "@ui/components/button"
import {
Dialog,
@ -32,9 +34,9 @@ import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import ChatInput from "./input"
import ChatModelSelector from "./model-selector"
import type { ModelId } from "@/lib/models"
import { GradientLogo, LogoBgGradient } from "@ui/assets/Logo"
import { useProject } from "@/stores"
import type { ModelId } from "@/lib/models"
import { SuperLoader } from "../../superloader"
import { UserMessage } from "./message/user-message"
import { AgentMessage } from "./message/agent-message"
@ -108,7 +110,7 @@ export function ChatSidebar({
}) {
const isMobile = useIsMobile()
const [input, setInput] = useState("")
const [selectedModel, setSelectedModel] = useState<ModelId>("gemini-2.5-pro")
const [selectedModel, setSelectedModel] = useState<ModelId>("claude-sonnet-4.5")
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null)
const [hoveredMessageId, setHoveredMessageId] = useState<string | null>(null)
const [messageFeedback, setMessageFeedback] = useState<
@ -138,7 +140,7 @@ export function ChatSidebar({
const [currentChatId, setCurrentChatId] = useState<string>(() => generateId())
const [pendingThreadLoad, setPendingThreadLoad] = useState<{
id: string
messages: UIMessage[]
messages: AgentMessageType[]
} | null>(null)
// Adjust chat height based on scroll position (desktop only)
@ -159,24 +161,15 @@ export function ChatSidebar({
return () => window.removeEventListener("scroll", handleWindowScroll)
}, [isMobile])
const { messages, sendMessage, status, setMessages, stop } = useChat({
const { messages, sendMessage, status, setMessages, stop } = useClaudeAgent({
id: currentChatId ?? undefined,
transport: new DefaultChatTransport({
api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/v2`,
credentials: "include",
body: {
metadata: {
projectId: selectedProject,
model: selectedModel,
chatId: currentChatId,
},
},
}),
metadata: {
projectId: selectedProject,
chatId: currentChatId,
},
onFinish: async (result) => {
if (result.message.role !== "assistant") return
// Mark this message as needing follow-up generation
// We'll generate it after the message is fully in the messages array
if (result.message.id) {
pendingFollowUpGenerations.current.add(result.message.id)
}
@ -229,38 +222,20 @@ export function ChatSidebar({
}))
try {
// Get recent messages for context
const recentMessages = messages.slice(-5).map((msg) => ({
role: msg.role,
content: msg.parts
.filter((p) => p.type === "text")
.filter((p): p is { type: "text"; text: string } => p.type === "text")
.map((p) => p.text)
.join(" "),
}))
const response = await fetch(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/follow-ups`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({
messages: recentMessages,
assistantResponse: assistantText,
}),
},
)
if (response.ok) {
const data = await response.json()
if (data.questions && Array.isArray(data.questions)) {
setFollowUpQuestions((prev) => ({
...prev,
[message.id]: data.questions,
}))
}
const questions = await generateFollowUpQuestions(recentMessages)
if (questions.length > 0) {
setFollowUpQuestions((prev) => ({
...prev,
[message.id]: questions,
}))
}
} catch (error) {
console.error("Failed to generate follow-up questions:", error)
@ -402,21 +377,29 @@ export function ChatSidebar({
)
if (response.ok) {
const data = await response.json()
const uiMessages = data.messages.map(
const agentMessages: AgentMessageType[] = data.messages.map(
(m: {
id: string
role: string
parts: unknown
parts: Array<{ type: string; text?: string }>
createdAt: string
}) => ({
id: m.id,
role: m.role,
parts: m.parts || [],
createdAt: new Date(m.createdAt),
}),
}) => {
const textParts = (m.parts || []).filter(
(p): p is { type: "text"; text: string } =>
p.type === "text" && typeof p.text === "string"
)
const content = textParts.map((p) => p.text).join(" ")
return {
id: m.id,
role: m.role as "user" | "assistant",
content,
parts: m.parts || [],
createdAt: new Date(m.createdAt),
}
}
)
setCurrentChatId(threadId)
setPendingThreadLoad({ id: threadId, messages: uiMessages })
setPendingThreadLoad({ id: threadId, messages: agentMessages })
analytics.chatThreadLoaded({ thread_id: threadId })
setIsHistoryOpen(false)
setConfirmingDeleteId(null)

View file

@ -1,16 +1,8 @@
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 "@/lib/fonts"
interface MemoryResult {
documentId?: string
title?: string
content?: string
url?: string
score?: number
}
import type { AgentMessage, MemoryResult } from "@/lib/agent/types"
interface ReasoningStep {
type: string
@ -18,13 +10,13 @@ interface ReasoningStep {
message: string
}
export function ChainOfThought({ messages }: { messages: UIMessage[] }) {
export function ChainOfThought({ messages }: { messages: AgentMessage[] }) {
const { user } = useAuth()
// Group messages into user-assistant pairs
const messagePairs: Array<{
userMessage: UIMessage
agentMessage?: UIMessage
userMessage: AgentMessage
agentMessage?: AgentMessage
}> = []
for (let i = 0; i < messages.length; i++) {
@ -46,9 +38,10 @@ export function ChainOfThought({ messages }: { messages: UIMessage[] }) {
<div className="absolute left-[11px] top-0 bottom-0 w-px bg-[#151F31] self-stretch mb-0 -z-10" />
{messagePairs.map((pair, pairIdx) => {
const userMessageText =
pair.userMessage.parts.find((part) => part.type === "text")?.text ??
""
const textPart = pair.userMessage.parts.find(
(part): part is { type: "text"; text: string } => part.type === "text"
)
const userMessageText = textPart?.text ?? ""
const reasoningSteps: ReasoningStep[] = []
if (pair.agentMessage) {

View file

@ -1,13 +1,13 @@
"use client"
import type { UIMessage } from "@ai-sdk/react"
import { Streamdown } from "streamdown"
import { RelatedMemories } from "./related-memories"
import { MessageActions } from "./message-actions"
import { FollowUpQuestions } from "./follow-up-questions"
import type { AgentMessage as AgentMessageType } from "@/lib/agent/types"
interface AgentMessageProps {
message: UIMessage
message: AgentMessageType
index: number
messagesLength: number
hoveredMessageId: string | null
@ -43,7 +43,7 @@ export function AgentMessage({
index === messagesLength - 1 && message.role === "assistant"
const isHovered = hoveredMessageId === message.id
const messageText = message.parts
.filter((part) => part.type === "text")
.filter((part): part is { type: "text"; text: string } => part.type === "text")
.map((part) => part.text)
.join(" ")

View file

@ -1,18 +1,10 @@
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import type { UIMessage } from "@ai-sdk/react"
import { dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
interface MemoryResult {
documentId?: string
title?: string
content?: string
url?: string
score?: number
}
import type { AgentMessage, MemoryResult } from "@/lib/agent/types"
interface RelatedMemoriesProps {
message: UIMessage
message: AgentMessage
expandedMemories: string | null
onToggle: (messageId: string) => void
}

View file

@ -1,10 +1,10 @@
"use client"
import { Copy, Check } from "lucide-react"
import type { UIMessage } from "@ai-sdk/react"
import type { AgentMessage } from "@/lib/agent/types"
interface UserMessageProps {
message: UIMessage
message: AgentMessage
copiedMessageId: string | null
onCopy: (messageId: string, text: string) => void
}
@ -15,7 +15,7 @@ export function UserMessage({
onCopy,
}: UserMessageProps) {
const text = message.parts
.filter((part) => part.type === "text")
.filter((part): part is { type: "text"; text: string } => part.type === "text")
.map((part) => part.text)
.join(" ")

View file

@ -2,8 +2,7 @@
import { useState, useEffect, useCallback, useRef } from "react"
import { motion, AnimatePresence } from "motion/react"
import { useChat } from "@ai-sdk/react"
import { DefaultChatTransport } from "ai"
import { useClaudeAgent } from "@/hooks/use-claude-agent"
import NovaOrb from "@/components/nova/nova-orb"
import { Button } from "@ui/components/button"
import {
@ -80,17 +79,10 @@ export function ChatSidebar({ formData }: ChatSidebarProps) {
messages: chatMessages,
sendMessage,
status,
} = useChat({
transport: new DefaultChatTransport({
api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/v2`,
credentials: "include",
body: {
metadata: {
projectId: selectedProject,
model: "gemini-2.5-pro",
},
},
}),
} = useClaudeAgent({
metadata: {
projectId: selectedProject,
},
})
const buildOnboardingContext = useCallback(() => {

View file

@ -1,9 +1,12 @@
"use client"
import { useChat, useCompletion, type UIMessage } from "@ai-sdk/react"
import { cn } from "@lib/utils"
import { Button } from "@ui/components/button"
import { DefaultChatTransport } from "ai"
import {
useClaudeAgent,
generateChatTitle,
} from "@/hooks/use-claude-agent"
import type { AgentMessage, AgentMessagePart } from "@/lib/agent/types"
import {
ArrowUp,
Check,
@ -20,9 +23,8 @@ import { Streamdown } from "streamdown"
import { TextShimmer } from "@/components/text-shimmer"
import { usePersistentChat, useProject } from "@/stores"
import { useGraphHighlights } from "@/stores/highlights"
import { ModelIcon } from "@/lib/models"
import { ModelIcon, type ModelId } from "@/lib/models"
import { Spinner } from "../../spinner"
import { areUIMessageArraysEqual } from "@/stores/chat"
interface MemoryResult {
documentId?: string
@ -59,6 +61,18 @@ interface ChatMessage {
parts: MessagePart[]
}
function areMessagesEqual(a: AgentMessage[], b: AgentMessage[]): boolean {
if (a === b) return true
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) {
const msgA = a[i]
const msgB = b[i]
if (!msgA || !msgB) return false
if (msgA.id !== msgB.id) return false
}
return true
}
function ExpandableMemories({ foundCount, results }: ExpandableMemoriesProps) {
const [isExpanded, setIsExpanded] = useState(false)
@ -243,50 +257,58 @@ export function ChatMessages() {
const storageKey = `chat-model-${currentChatId}`
const [input, setInput] = useState("")
const [selectedModel, setSelectedModel] = useState<
"gpt-5" | "claude-sonnet-4.5" | "gemini-2.5-pro"
>("gemini-2.5-pro")
const [selectedModel, setSelectedModel] = useState<ModelId>("claude-sonnet-4.5")
const activeChatIdRef = useRef<string | null>(null)
const shouldGenerateTitleRef = useRef<boolean>(false)
const hasRunInitialMessageRef = useRef<boolean>(false)
const lastSavedMessagesRef = useRef<UIMessage[] | null>(null)
const lastSavedMessagesRef = useRef<AgentMessage[] | null>(null)
const lastSavedActiveIdRef = useRef<string | null>(null)
const lastLoadedChatIdRef = useRef<string | null>(null)
const lastLoadedMessagesRef = useRef<UIMessage[] | null>(null)
const lastLoadedMessagesRef = useRef<AgentMessage[] | null>(null)
const { setDocumentIds } = useGraphHighlights()
const { messages, sendMessage, status, stop, setMessages, id, regenerate } =
useChat({
id: currentChatId ?? undefined,
transport: new DefaultChatTransport({
api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat`,
credentials: "include",
body: {
metadata: {
projectId: selectedProject,
model: selectedModel,
chatId: currentChatId,
},
},
}),
onFinish: (result) => {
const activeId = activeChatIdRef.current
if (!activeId) return
if (result.message.role !== "assistant") return
const { messages, sendMessage, status, stop, setMessages } = useClaudeAgent({
id: currentChatId ?? undefined,
metadata: {
projectId: selectedProject,
chatId: currentChatId ?? undefined,
},
onFinish: async (result) => {
const activeId = activeChatIdRef.current
if (!activeId) return
if (result.message.role !== "assistant") return
if (shouldGenerateTitleRef.current) {
const textPart = result.message.parts.find(
(p: { type?: string; text?: string }) => p?.type === "text",
) as { text?: string } | undefined
const text = textPart?.text?.trim()
if (text) {
shouldGenerateTitleRef.current = false
complete(text)
}
if (shouldGenerateTitleRef.current) {
const textPart = result.message.parts.find(
(p): p is { type: "text"; text: string } => p?.type === "text"
)
const text = textPart?.text?.trim()
if (text) {
shouldGenerateTitleRef.current = false
const allMessages = [...messages, result.message]
const recentMessages = allMessages.slice(-4).map((msg) => ({
role: msg.role,
content: msg.parts
.filter((p): p is { type: "text"; text: string } => p.type === "text")
.map((p) => p.text)
.join(" "),
}))
const title = await generateChatTitle(recentMessages)
setConversationTitle(activeId, title)
}
},
})
}
},
})
const id = currentChatId
const regenerate = useCallback(
(_options: { messageId: string }) => {
toast.info("Regenerate is not yet supported with Claude Agent SDK")
},
[]
)
useEffect(() => {
lastLoadedMessagesRef.current = messages
@ -350,9 +372,10 @@ export function ChatMessages() {
if (msgs && msgs.length > 0) {
const currentMessages = lastLoadedMessagesRef.current
if (!currentMessages || !areUIMessageArraysEqual(currentMessages, msgs)) {
lastLoadedMessagesRef.current = msgs
setMessages(msgs)
const agentMsgs = msgs as unknown as AgentMessage[]
if (!currentMessages || !areMessagesEqual(currentMessages, agentMsgs)) {
lastLoadedMessagesRef.current = agentMsgs
setMessages(agentMsgs)
}
} else if (!currentChatId) {
if (
@ -380,24 +403,15 @@ export function ChatMessages() {
}
const lastSaved = lastSavedMessagesRef.current
if (lastSaved && areUIMessageArraysEqual(lastSaved, messages)) {
if (lastSaved && areMessagesEqual(lastSaved, messages)) {
return
}
lastSavedMessagesRef.current = messages
setConversation(activeId, messages)
// Cast to unknown first to satisfy persistent chat store which uses UIMessage type
setConversation(activeId, messages as unknown as import("@ai-sdk/react").UIMessage[])
}, [messages, currentChatId, id, setConversation])
const { complete } = useCompletion({
api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/title`,
credentials: "include",
onFinish: (_, completion) => {
const activeId = activeChatIdRef.current
if (!completion || !activeId) return
setConversationTitle(activeId, completion.trim())
},
})
// Update graph highlights from the most recent tool-searchMemories output
useEffect(() => {
try {
@ -550,10 +564,8 @@ export function ChatMessages() {
"count" in output
? Number(output.count) || 0
: 0
// @ts-expect-error
const results = Array.isArray(output?.results)
? // @ts-expect-error
output.results
? output.results
: []
return (

View file

@ -0,0 +1,238 @@
"use client"
import { useState, useCallback, useRef } from "react"
import type {
AgentMessage,
AgentMessagePart,
ClaudeAgentStatus,
ChatMetadata,
} from "@/lib/agent/types"
import { generateId } from "@lib/generate-id"
interface UseClaudeAgentOptions {
id?: string
metadata: ChatMetadata
onFinish?: (result: { message: AgentMessage }) => void
onError?: (error: Error) => void
}
interface UseClaudeAgentReturn {
messages: AgentMessage[]
sendMessage: (options: { text: string }) => void
status: ClaudeAgentStatus
setMessages: (messages: AgentMessage[]) => void
stop: () => void
}
export function useClaudeAgent(options: UseClaudeAgentOptions): UseClaudeAgentReturn {
const { metadata, onFinish, onError } = options
const [messages, setMessages] = useState<AgentMessage[]>([])
const [status, setStatus] = useState<ClaudeAgentStatus>("idle")
const abortControllerRef = useRef<AbortController | null>(null)
const currentAssistantMessageRef = useRef<AgentMessage | null>(null)
const sendMessage = useCallback(
async (messageOptions: { text: string }) => {
const { text } = messageOptions
const userMessage: AgentMessage = {
id: generateId(),
role: "user",
content: text,
parts: [{ type: "text", text }],
createdAt: new Date(),
}
setMessages((prev) => [...prev, userMessage])
setStatus("submitted")
abortControllerRef.current = new AbortController()
try {
const allMessages = [...messages, userMessage]
const requestMessages = allMessages.map((msg) => ({
role: msg.role,
content: msg.content,
}))
const response = await fetch("/api/agent/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
messages: requestMessages,
metadata,
}),
signal: abortControllerRef.current.signal,
})
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`)
}
const reader = response.body?.getReader()
if (!reader) {
throw new Error("No response body")
}
const decoder = new TextDecoder()
let buffer = ""
currentAssistantMessageRef.current = {
id: generateId(),
role: "assistant",
content: "",
parts: [],
createdAt: new Date(),
}
setMessages((prev) => [...prev, currentAssistantMessageRef.current!])
setStatus("streaming")
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split("\n")
buffer = lines.pop() ?? ""
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6)
if (data === "[DONE]") {
setStatus("idle")
if (currentAssistantMessageRef.current && onFinish) {
onFinish({ message: currentAssistantMessageRef.current })
}
continue
}
try {
const event = JSON.parse(data)
handleStreamEvent(event)
} catch {
// Ignore JSON parse errors
}
}
}
}
} catch (error) {
if ((error as Error).name === "AbortError") {
setStatus("idle")
return
}
console.error("Claude agent error:", error)
setStatus("error")
onError?.(error instanceof Error ? error : new Error(String(error)))
}
},
[messages, metadata, onFinish, onError]
)
const handleStreamEvent = useCallback(
(event: { type: string; parts?: AgentMessagePart[]; id?: string; error?: string }) => {
if (!currentAssistantMessageRef.current) return
if (event.type === "error") {
setStatus("error")
onError?.(new Error(event.error ?? "Unknown error"))
return
}
if (event.type === "assistant" && event.parts) {
const textParts = event.parts.filter(
(p): p is { type: "text"; text: string } => p.type === "text"
)
if (textParts.length > 0) {
const newText = textParts.map((p) => p.text).join("")
currentAssistantMessageRef.current.content += newText
currentAssistantMessageRef.current.parts = [
...currentAssistantMessageRef.current.parts.filter((p) => p.type !== "text"),
{ type: "text", text: currentAssistantMessageRef.current.content },
]
}
}
if ((event.type === "tool_use" || event.type === "tool_result") && event.parts) {
currentAssistantMessageRef.current.parts = [
...currentAssistantMessageRef.current.parts,
...event.parts,
]
}
setMessages((prev) => {
const newMessages = [...prev]
const lastIndex = newMessages.length - 1
if (lastIndex >= 0 && newMessages[lastIndex]?.role === "assistant") {
newMessages[lastIndex] = { ...currentAssistantMessageRef.current! }
}
return newMessages
})
},
[onError]
)
const stop = useCallback(() => {
abortControllerRef.current?.abort()
setStatus("idle")
}, [])
return {
messages,
sendMessage,
status,
setMessages,
stop,
}
}
export async function generateFollowUpQuestions(
messages: Array<{ role: string; content: string }>
): Promise<string[]> {
try {
const response = await fetch("/api/agent/follow-ups", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ messages }),
})
if (!response.ok) {
return []
}
const data = await response.json()
return data.questions ?? []
} catch {
return []
}
}
export async function generateChatTitle(
messages: Array<{ role: string; content: string }>
): Promise<string> {
try {
const response = await fetch("/api/agent/title", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ messages }),
})
if (!response.ok) {
return "New Chat"
}
const data = await response.json()
return data.title ?? "New Chat"
} catch {
return "New Chat"
}
}

164
apps/web/lib/agent/tools.ts Normal file
View file

@ -0,0 +1,164 @@
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk"
import { z } from "zod"
import type { SearchMemoriesOutput, AddMemoryOutput } from "./types"
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
interface ToolContext {
cookies: string
projectId: string
}
export function createSupermemoryMcpServer(context: ToolContext) {
const searchMemoriesTool = tool(
"searchMemories",
"Search through user's memories/documents to find relevant information. Use this when you need context about something the user has saved.",
{
query: z.string().describe("The search query to find relevant memories"),
limit: z.number().min(1).max(20).optional().describe("Maximum number of results to return (default: 5)"),
containerTags: z.array(z.string()).optional().describe("Filter by container tags"),
},
async (args): Promise<{ content: Array<{ type: "text"; text: string }> }> => {
try {
const response = await fetch(`${BACKEND_URL}/v3/search`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Cookie: context.cookies,
"X-Project-Id": context.projectId,
},
body: JSON.stringify({
q: args.query,
limit: args.limit ?? 5,
containerTags: args.containerTags,
}),
})
if (!response.ok) {
throw new Error(`Search failed: ${response.statusText}`)
}
const data = await response.json()
const results = data.results ?? data.documents ?? []
const output: SearchMemoriesOutput = {
count: results.length,
results: results.map((r: {
id?: string
documentId?: string
title?: string
content?: string
chunk?: string
url?: string
score?: number
}) => ({
documentId: r.id ?? r.documentId ?? "",
title: r.title,
content: r.content ?? r.chunk,
url: r.url,
score: r.score,
})),
}
return {
content: [
{
type: "text",
text: JSON.stringify(output),
},
],
}
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify({
count: 0,
results: [],
error: error instanceof Error ? error.message : "Search failed",
}),
},
],
}
}
}
)
const addMemoryTool = tool(
"addMemory",
"Add a new memory/document for the user. Use this when the user wants to save information for later.",
{
content: z.string().describe("The content to save as a memory"),
title: z.string().optional().describe("Optional title for the memory"),
url: z.string().optional().describe("Optional URL associated with the content"),
containerTags: z.array(z.string()).optional().describe("Tags to organize the memory"),
},
async (args): Promise<{ content: Array<{ type: "text"; text: string }> }> => {
try {
const response = await fetch(`${BACKEND_URL}/v3/documents`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Cookie: context.cookies,
"X-Project-Id": context.projectId,
},
body: JSON.stringify({
content: args.content,
title: args.title,
url: args.url,
containerTags: args.containerTags,
}),
})
if (!response.ok) {
throw new Error(`Add memory failed: ${response.statusText}`)
}
const data = await response.json()
const output: AddMemoryOutput = {
id: data.id ?? data.documentId ?? "",
status: "success",
message: "Memory added successfully",
}
return {
content: [
{
type: "text",
text: JSON.stringify(output),
},
],
}
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify({
id: "",
status: "error",
message: error instanceof Error ? error.message : "Failed to add memory",
}),
},
],
}
}
}
)
return createSdkMcpServer({
name: "supermemory",
version: "1.0.0",
tools: [searchMemoriesTool, addMemoryTool],
})
}
export const SUPERMEMORY_SYSTEM_PROMPT = `You are Nova, a helpful AI assistant for Supermemory. You help users search and manage their memories - content they've saved from the web, notes, documents, and other information.
When a user asks a question:
1. First search their memories to find relevant context using the searchMemories tool
2. Use that context to provide helpful, accurate answers
3. If the user wants to save something, use the addMemory tool
Be conversational, helpful, and concise. Reference specific memories when relevant.`

View file

@ -0,0 +1,75 @@
export interface AgentMessage {
id: string
role: "user" | "assistant"
content: string
parts: AgentMessagePart[]
createdAt?: Date
}
export type AgentMessagePart =
| { type: "text"; text: string }
| {
type: "tool-searchMemories"
state: "input-available" | "input-streaming" | "output-available" | "output-error"
input?: SearchMemoriesInput
output?: SearchMemoriesOutput
}
| {
type: "tool-addMemory"
state: "input-available" | "input-streaming" | "output-available" | "output-error"
input?: AddMemoryInput
output?: AddMemoryOutput
}
export interface SearchMemoriesInput {
query: string
limit?: number
containerTags?: string[]
}
export interface SearchMemoriesOutput {
count: number
results: MemoryResult[]
}
export interface AddMemoryInput {
content: string
title?: string
url?: string
containerTags?: string[]
}
export interface AddMemoryOutput {
id: string
status: "success" | "error"
message?: string
}
export interface MemoryResult {
documentId: string
title?: string
content?: string
url?: string
score?: number
}
export interface ChatMetadata {
projectId: string
model?: string
chatId?: string
}
export interface ChatRequest {
messages: Array<{ role: "user" | "assistant"; content: string }>
metadata: ChatMetadata
}
export interface FollowUpRequest {
messages: Array<{ role: "user" | "assistant"; content: string }>
}
export interface TitleRequest {
messages: Array<{ role: "user" | "assistant"; content: string }>
}
export type ClaudeAgentStatus = "idle" | "submitted" | "streaming" | "error"

View file

@ -18,6 +18,7 @@
"@ai-sdk/google": "^3.0.9",
"@ai-sdk/react": "^3.0.39",
"@ai-sdk/xai": "^3.0.23",
"@anthropic-ai/claude-agent-sdk": "^0.2.12",
"@better-fetch/fetch": "^1.1.18",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",

View file

@ -31,7 +31,7 @@ function Badge({
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span";
const Comp = (asChild ? Slot : "span") as React.ElementType;
return (
<Comp

View file

@ -37,7 +37,7 @@ function BreadcrumbLink({
}: React.ComponentProps<"a"> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "a";
const Comp = (asChild ? Slot : "a") as React.ElementType;
return (
<Comp

View file

@ -54,7 +54,7 @@ function Button({
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";
const Comp = (asChild ? Slot : "button") as React.ElementType;
return (
<Comp

View file

@ -397,7 +397,7 @@ function SidebarGroupLabel({
asChild = false,
...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div";
const Comp = (asChild ? Slot : "div") as React.ElementType;
return (
<Comp
@ -418,7 +418,7 @@ function SidebarGroupAction({
asChild = false,
...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button";
const Comp = (asChild ? Slot : "button") as React.ElementType;
return (
<Comp
@ -507,7 +507,7 @@ function SidebarMenuButton({
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot : "button";
const Comp = (asChild ? Slot : "button") as React.ElementType;
const { isMobile, state } = useSidebar();
const button = (
@ -553,7 +553,7 @@ function SidebarMenuAction({
asChild?: boolean;
showOnHover?: boolean;
}) {
const Comp = asChild ? Slot : "button";
const Comp = (asChild ? Slot : "button") as React.ElementType;
return (
<Comp
@ -676,7 +676,7 @@ function SidebarMenuSubButton({
size?: "sm" | "md";
isActive?: boolean;
}) {
const Comp = asChild ? Slot : "a";
const Comp = (asChild ? Slot : "a") as React.ElementType;
return (
<Comp

View file

@ -1,14 +1,19 @@
import { cn } from "@lib/utils";
import { Root } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot";
import type * as React from "react";
const SlotComp = Slot as React.ComponentType<React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>;
export function HeadingH1Bold({
className,
asChild,
...props
}: React.ComponentProps<"h1"> & { asChild?: boolean }) {
const Comp = asChild ? Root : "h1";
}: Omit<React.ComponentProps<"h1">, "ref"> & { asChild?: boolean }) {
if (asChild) {
return <SlotComp className={cn("text-sm sm:text-base md:text-lg lg:text-xl font-bold leading-[32px] tracking-[-0.4px]", className)} {...props} />;
}
return (
<Comp
<h1
className={cn(
"text-sm sm:text-base md:text-lg lg:text-xl font-bold leading-[32px] tracking-[-0.4px]",
className,

View file

@ -1,14 +1,19 @@
import { cn } from "@lib/utils";
import { Root } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot";
import type * as React from "react";
const SlotComp = Slot as React.ComponentType<React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>;
export function HeadingH1Medium({
className,
asChild,
...props
}: React.ComponentProps<"h1"> & { asChild?: boolean }) {
const Comp = asChild ? Root : "h1";
}: Omit<React.ComponentProps<"h1">, "ref"> & { asChild?: boolean }) {
if (asChild) {
return <SlotComp className={cn("text-sm sm:text-base md:text-lg lg:text-xl font-medium leading-[32px] tracking-[-0.4px]", className)} {...props} />;
}
return (
<Comp
<h1
className={cn(
"text-sm sm:text-base md:text-lg lg:text-xl font-medium leading-[32px] tracking-[-0.4px]",
className,

View file

@ -1,14 +1,19 @@
import { cn } from "@lib/utils";
import { Root } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot";
import type * as React from "react";
const SlotComp = Slot as React.ComponentType<React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>;
export function HeadingH2Bold({
className,
asChild,
...props
}: React.ComponentProps<"h2"> & { asChild?: boolean }) {
const Comp = asChild ? Root : "h2";
}: Omit<React.ComponentProps<"h2">, "ref"> & { asChild?: boolean }) {
if (asChild) {
return <SlotComp className={cn("text-xs sm:text-sm md:text-base lg:text-lg font-bold leading-[30px] tracking-[-0.4px]", className)} {...props} />;
}
return (
<Comp
<h2
className={cn(
"text-xs sm:text-sm md:text-base lg:text-lg font-bold leading-[30px] tracking-[-0.4px]",
className,

View file

@ -1,14 +1,19 @@
import { cn } from "@lib/utils";
import { Root } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot";
import type * as React from "react";
const SlotComp = Slot as React.ComponentType<React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>;
export function HeadingH2Medium({
className,
asChild,
...props
}: React.ComponentProps<"h2"> & { asChild?: boolean }) {
const Comp = asChild ? Root : "h2";
}: Omit<React.ComponentProps<"h2">, "ref"> & { asChild?: boolean }) {
if (asChild) {
return <SlotComp className={cn("text-xs sm:text-sm md:text-base lg:text-lg font-medium leading-[30px] tracking-[-0.4px]", className)} {...props} />;
}
return (
<Comp
<h2
className={cn(
"text-xs sm:text-sm md:text-base lg:text-lg font-medium leading-[30px] tracking-[-0.4px]",
className,

View file

@ -1,14 +1,19 @@
import { cn } from "@lib/utils";
import { Root } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot";
import type * as React from "react";
const SlotComp = Slot as React.ComponentType<React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>;
export function HeadingH3Bold({
className,
asChild,
...props
}: React.ComponentProps<"h3"> & { asChild?: boolean }) {
const Comp = asChild ? Root : "h3";
}: Omit<React.ComponentProps<"h3">, "ref"> & { asChild?: boolean }) {
if (asChild) {
return <SlotComp className={cn("text-[0.625rem] sm:text-xs md:text-sm lg:text-base font-bold leading-[28px] tracking-[-0.4px]", className)} {...props} />;
}
return (
<Comp
<h3
className={cn(
"text-[0.625rem] sm:text-xs md:text-sm lg:text-base font-bold leading-[28px] tracking-[-0.4px]",
className,

View file

@ -1,14 +1,19 @@
import { cn } from "@lib/utils";
import { Root } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot";
import type * as React from "react";
const SlotComp = Slot as React.ComponentType<React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>;
export function HeadingH3Medium({
className,
asChild,
...props
}: React.ComponentProps<"h3"> & { asChild?: boolean }) {
const Comp = asChild ? Root : "h3";
}: Omit<React.ComponentProps<"h3">, "ref"> & { asChild?: boolean }) {
if (asChild) {
return <SlotComp className={cn("text-[0.625rem] sm:text-xs md:text-sm lg:text-base font-medium leading-[28px] tracking-[-0.4px]", className)} {...props} />;
}
return (
<Comp
<h3
className={cn(
"text-[0.625rem] sm:text-xs md:text-sm lg:text-base font-medium leading-[28px] tracking-[-0.4px]",
className,

View file

@ -1,14 +1,19 @@
import { cn } from "@lib/utils";
import { Root } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot";
import type * as React from "react";
const SlotComp = Slot as React.ComponentType<React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>;
export function HeadingH4Bold({
className,
asChild,
...props
}: React.ComponentProps<"h4"> & { asChild?: boolean }) {
const Comp = asChild ? Root : "h4";
}: Omit<React.ComponentProps<"h4">, "ref"> & { asChild?: boolean }) {
if (asChild) {
return <SlotComp className={cn("text-[0.5rem] sm:text-[0.625rem] md:text-xs lg:text-sm font-bold leading-[24px] tracking-[-0.4px]", className)} {...props} />;
}
return (
<Comp
<h4
className={cn(
"text-[0.5rem] sm:text-[0.625rem] md:text-xs lg:text-sm font-bold leading-[24px] tracking-[-0.4px]",
className,

View file

@ -1,14 +1,19 @@
import { cn } from "@lib/utils";
import { Root } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot";
import type * as React from "react";
const SlotComp = Slot as React.ComponentType<React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>;
export function HeadingH4Medium({
className,
asChild,
...props
}: React.ComponentProps<"h4"> & { asChild?: boolean }) {
const Comp = asChild ? Root : "h4";
}: Omit<React.ComponentProps<"h4">, "ref"> & { asChild?: boolean }) {
if (asChild) {
return <SlotComp className={cn("text-[0.5rem] sm:text-[0.625rem] md:text-xs lg:text-sm font-medium leading-[24px] tracking-[-0.4px]", className)} {...props} />;
}
return (
<Comp
<h4
className={cn(
"text-[0.5rem] sm:text-[0.625rem] md:text-xs lg:text-sm font-medium leading-[24px] tracking-[-0.4px]",
className,

View file

@ -1,19 +1,15 @@
import { cn } from "@lib/utils";
import { Root } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot";
const SlotComp = Slot as React.ComponentType<React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>;
export function Label1Medium({
className,
asChild,
...props
}: React.ComponentProps<"p"> & { asChild?: boolean }) {
const Comp = asChild ? Root : "p";
return (
<Comp
className={cn(
"text-[0.875rem] md:text-[1rem] font-medium leading-[1.5rem] tracking-[-0.4px]",
className,
)}
{...props}
/>
);
}: Omit<React.ComponentProps<"p">, "ref"> & { asChild?: boolean }) {
if (asChild) {
return <SlotComp className={cn("text-[0.875rem] md:text-[1rem] font-medium leading-[1.5rem] tracking-[-0.4px]", className)} {...props} />;
}
return <p className={cn("text-[0.875rem] md:text-[1rem] font-medium leading-[1.5rem] tracking-[-0.4px]", className)} {...props} />;
}

View file

@ -1,19 +1,15 @@
import { cn } from "@lib/utils";
import { Root } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot";
const SlotComp = Slot as React.ComponentType<React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>;
export function Label1Regular({
className,
asChild,
...props
}: React.ComponentProps<"p"> & { asChild?: boolean }) {
const Comp = asChild ? Root : "p";
return (
<Comp
className={cn(
"text-[0.875rem] md:text-[1rem] font-normal leading-[1.5rem] tracking-[-0.4px]",
className,
)}
{...props}
/>
);
}: Omit<React.ComponentProps<"p">, "ref"> & { asChild?: boolean }) {
if (asChild) {
return <SlotComp className={cn("text-[0.875rem] md:text-[1rem] font-normal leading-[1.5rem] tracking-[-0.4px]", className)} {...props} />;
}
return <p className={cn("text-[0.875rem] md:text-[1rem] font-normal leading-[1.5rem] tracking-[-0.4px]", className)} {...props} />;
}

View file

@ -1,19 +1,15 @@
import { cn } from "@lib/utils";
import { Root } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot";
const SlotComp = Slot as React.ComponentType<React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>;
export function Label2Medium({
className,
asChild,
...props
}: React.ComponentProps<"p"> & { asChild?: boolean }) {
const Comp = asChild ? Root : "p";
return (
<Comp
className={cn(
"text-[0.25rem] sm:text-[0.375rem] md:text-[0.5rem] lg:text-[0.625rem] font-medium leading-[18px] tracking-[-0.4px] text-muted-foreground",
className,
)}
{...props}
/>
);
}: Omit<React.ComponentProps<"p">, "ref"> & { asChild?: boolean }) {
if (asChild) {
return <SlotComp className={cn("text-[0.25rem] sm:text-[0.375rem] md:text-[0.5rem] lg:text-[0.625rem] font-medium leading-[18px] tracking-[-0.4px] text-muted-foreground", className)} {...props} />;
}
return <p className={cn("text-[0.25rem] sm:text-[0.375rem] md:text-[0.5rem] lg:text-[0.625rem] font-medium leading-[18px] tracking-[-0.4px] text-muted-foreground", className)} {...props} />;
}

View file

@ -1,19 +1,15 @@
import { cn } from "@lib/utils";
import { Root } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot";
const SlotComp = Slot as React.ComponentType<React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>;
export function Label2Regular({
className,
asChild,
...props
}: React.ComponentProps<"p"> & { asChild?: boolean }) {
const Comp = asChild ? Root : "p";
return (
<Comp
className={cn(
"text-[0.25rem] sm:text-[0.375rem] md:text-[0.5rem] lg:text-[0.625rem] font-normal leading-[18px] tracking-[-0.4px] text-muted-foreground",
className,
)}
{...props}
/>
);
}: Omit<React.ComponentProps<"p">, "ref"> & { asChild?: boolean }) {
if (asChild) {
return <SlotComp className={cn("text-[0.25rem] sm:text-[0.375rem] md:text-[0.5rem] lg:text-[0.625rem] font-normal leading-[18px] tracking-[-0.4px] text-muted-foreground", className)} {...props} />;
}
return <p className={cn("text-[0.25rem] sm:text-[0.375rem] md:text-[0.5rem] lg:text-[0.625rem] font-normal leading-[18px] tracking-[-0.4px] text-muted-foreground", className)} {...props} />;
}

View file

@ -1,19 +1,15 @@
import { cn } from "@lib/utils";
import { Root } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot";
const SlotComp = Slot as React.ComponentType<React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>;
export function Label3Medium({
className,
asChild,
...props
}: React.ComponentProps<"p"> & { asChild?: boolean }) {
const Comp = asChild ? Root : "p";
return (
<Comp
className={cn(
"text-[0.125rem] sm:text-[0.25rem] md:text-[0.375rem] lg:text-[0.5rem] font-medium leading-[16px] tracking-[-0.2px] text-muted-foreground",
className,
)}
{...props}
/>
);
}: Omit<React.ComponentProps<"p">, "ref"> & { asChild?: boolean }) {
if (asChild) {
return <SlotComp className={cn("text-[0.125rem] sm:text-[0.25rem] md:text-[0.375rem] lg:text-[0.5rem] font-medium leading-[16px] tracking-[-0.2px] text-muted-foreground", className)} {...props} />;
}
return <p className={cn("text-[0.125rem] sm:text-[0.25rem] md:text-[0.375rem] lg:text-[0.5rem] font-medium leading-[16px] tracking-[-0.2px] text-muted-foreground", className)} {...props} />;
}

View file

@ -1,19 +1,15 @@
import { cn } from "@lib/utils";
import { Root } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot";
const SlotComp = Slot as React.ComponentType<React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>;
export function Label3Regular({
className,
asChild,
...props
}: React.ComponentProps<"p"> & { asChild?: boolean }) {
const Comp = asChild ? Root : "p";
return (
<Comp
className={cn(
"text-[0.125rem] sm:text-[0.25rem] md:text-[0.375rem] lg:text-[0.5rem] font-normal leading-[16px] tracking-[-0.2px] text-muted-foreground",
className,
)}
{...props}
/>
);
}: Omit<React.ComponentProps<"p">, "ref"> & { asChild?: boolean }) {
if (asChild) {
return <SlotComp className={cn("text-[0.125rem] sm:text-[0.25rem] md:text-[0.375rem] lg:text-[0.5rem] font-normal leading-[16px] tracking-[-0.2px] text-muted-foreground", className)} {...props} />;
}
return <p className={cn("text-[0.125rem] sm:text-[0.25rem] md:text-[0.375rem] lg:text-[0.5rem] font-normal leading-[16px] tracking-[-0.2px] text-muted-foreground", className)} {...props} />;
}

View file

@ -1,14 +1,26 @@
import { cn } from "@lib/utils";
import { Root } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot";
const SlotComp = Slot as React.ComponentType<React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>;
export function Title1Bold({
className,
asChild,
...props
}: React.ComponentProps<"h1"> & { asChild?: boolean }) {
const Comp = asChild ? Root : "h1";
}: Omit<React.ComponentProps<"h1">, "ref"> & { asChild?: boolean }) {
if (asChild) {
return (
<SlotComp
className={cn(
"text-xl sm:text-2xl md:text-3xl lg:text-4xl font-bold leading-[70px] tracking-[-0.8px]",
className,
)}
{...props}
/>
);
}
return (
<Comp
<h1
className={cn(
"text-xl sm:text-2xl md:text-3xl lg:text-4xl font-bold leading-[70px] tracking-[-0.8px]",
className,

View file

@ -1,14 +1,26 @@
import { cn } from "@lib/utils";
import { Root } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot";
const SlotComp = Slot as React.ComponentType<React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>;
export function Title1Medium({
className,
asChild,
...props
}: React.ComponentProps<"h1"> & { asChild?: boolean }) {
const Comp = asChild ? Root : "h1";
}: Omit<React.ComponentProps<"h1">, "ref"> & { asChild?: boolean }) {
if (asChild) {
return (
<SlotComp
className={cn(
"text-xl sm:text-2xl md:text-3xl lg:text-4xl font-medium leading-[70px] tracking-[-0.8px]",
className,
)}
{...props}
/>
);
}
return (
<Comp
<h1
className={cn(
"text-xl sm:text-2xl md:text-3xl lg:text-4xl font-medium leading-[70px] tracking-[-0.8px]",
className,

View file

@ -1,14 +1,26 @@
import { cn } from "@lib/utils";
import { Root } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot";
const SlotComp = Slot as React.ComponentType<React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>;
export function Title2Bold({
className,
asChild,
...props
}: React.ComponentProps<"h2"> & { asChild?: boolean }) {
const Comp = asChild ? Root : "h2";
}: Omit<React.ComponentProps<"h2">, "ref"> & { asChild?: boolean }) {
if (asChild) {
return (
<SlotComp
className={cn(
"text-lg sm:text-xl md:text-2xl lg:text-3xl font-bold leading-[48px] tracking-[-0.4px]",
className,
)}
{...props}
/>
);
}
return (
<Comp
<h2
className={cn(
"text-lg sm:text-xl md:text-2xl lg:text-3xl font-bold leading-[48px] tracking-[-0.4px]",
className,

View file

@ -1,14 +1,26 @@
import { cn } from "@lib/utils";
import { Root } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot";
const SlotComp = Slot as React.ComponentType<React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>;
export function Title2Medium({
className,
asChild,
...props
}: React.ComponentProps<"h2"> & { asChild?: boolean }) {
const Comp = asChild ? Root : "h2";
}: Omit<React.ComponentProps<"h2">, "ref"> & { asChild?: boolean }) {
if (asChild) {
return (
<SlotComp
className={cn(
"text-lg sm:text-xl md:text-2xl lg:text-3xl font-medium leading-[32px] md:leading-[48px] tracking-[-0.4px]",
className,
)}
{...props}
/>
);
}
return (
<Comp
<h2
className={cn(
"text-lg sm:text-xl md:text-2xl lg:text-3xl font-medium leading-[32px] md:leading-[48px] tracking-[-0.4px]",
className,

View file

@ -1,14 +1,26 @@
import { cn } from "@lib/utils";
import { Root } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot";
const SlotComp = Slot as React.ComponentType<React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>;
export function Title3Bold({
className,
asChild,
...props
}: React.ComponentProps<"h3"> & { asChild?: boolean }) {
const Comp = asChild ? Root : "h3";
}: Omit<React.ComponentProps<"h3">, "ref"> & { asChild?: boolean }) {
if (asChild) {
return (
<SlotComp
className={cn(
"text-base sm:text-lg md:text-xl lg:text-2xl font-bold leading-[40px] tracking-[-0.4px]",
className,
)}
{...props}
/>
);
}
return (
<Comp
<h3
className={cn(
"text-base sm:text-lg md:text-xl lg:text-2xl font-bold leading-[40px] tracking-[-0.4px]",
className,

View file

@ -1,14 +1,26 @@
import { cn } from "@lib/utils";
import { Root } from "@radix-ui/react-slot";
import { Slot } from "@radix-ui/react-slot";
const SlotComp = Slot as React.ComponentType<React.HTMLAttributes<HTMLElement> & { children?: React.ReactNode }>;
export function Title3Medium({
className,
asChild,
...props
}: React.ComponentProps<"h3"> & { asChild?: boolean }) {
const Comp = asChild ? Root : "h3";
}: Omit<React.ComponentProps<"h3">, "ref"> & { asChild?: boolean }) {
if (asChild) {
return (
<SlotComp
className={cn(
"text-base sm:text-lg md:text-xl lg:text-2xl font-medium leading-[40px] tracking-[-0.4px]",
className,
)}
{...props}
/>
);
}
return (
<Comp
<h3
className={cn(
"text-base sm:text-lg md:text-xl lg:text-2xl font-medium leading-[40px] tracking-[-0.4px]",
className,