diff options
38 files changed, 1108 insertions, 291 deletions
diff --git a/apps/web/app/api/agent/chat/route.ts b/apps/web/app/api/agent/chat/route.ts new file mode 100644 index 00000000..110c4ef3 --- /dev/null +++ b/apps/web/app/api/agent/chat/route.ts @@ -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 } + ) + } +} diff --git a/apps/web/app/api/agent/follow-ups/route.ts b/apps/web/app/api/agent/follow-ups/route.ts new file mode 100644 index 00000000..6653226a --- /dev/null +++ b/apps/web/app/api/agent/follow-ups/route.ts @@ -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 }) + } +} diff --git a/apps/web/app/api/agent/title/route.ts b/apps/web/app/api/agent/title/route.ts new file mode 100644 index 00000000..5626387c --- /dev/null +++ b/apps/web/app/api/agent/title/route.ts @@ -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 }) + } +} diff --git a/apps/web/components/new/chat/index.tsx b/apps/web/components/new/chat/index.tsx index 51bbe455..dc0c49f6 100644 --- a/apps/web/components/new/chat/index.tsx +++ b/apps/web/components/new/chat/index.tsx @@ -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) diff --git a/apps/web/components/new/chat/input/chain-of-thought.tsx b/apps/web/components/new/chat/input/chain-of-thought.tsx index b1923146..d78ad272 100644 --- a/apps/web/components/new/chat/input/chain-of-thought.tsx +++ b/apps/web/components/new/chat/input/chain-of-thought.tsx @@ -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) { diff --git a/apps/web/components/new/chat/message/agent-message.tsx b/apps/web/components/new/chat/message/agent-message.tsx index f4528ef5..705785af 100644 --- a/apps/web/components/new/chat/message/agent-message.tsx +++ b/apps/web/components/new/chat/message/agent-message.tsx @@ -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(" ") diff --git a/apps/web/components/new/chat/message/related-memories.tsx b/apps/web/components/new/chat/message/related-memories.tsx index ad83d03e..2dbfcc6f 100644 --- a/apps/web/components/new/chat/message/related-memories.tsx +++ b/apps/web/components/new/chat/message/related-memories.tsx @@ -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 } diff --git a/apps/web/components/new/chat/message/user-message.tsx b/apps/web/components/new/chat/message/user-message.tsx index 8e5d8c5b..3507fc40 100644 --- a/apps/web/components/new/chat/message/user-message.tsx +++ b/apps/web/components/new/chat/message/user-message.tsx @@ -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(" ") diff --git a/apps/web/components/new/onboarding/setup/chat-sidebar.tsx b/apps/web/components/new/onboarding/setup/chat-sidebar.tsx index 47af432d..20828d86 100644 --- a/apps/web/components/new/onboarding/setup/chat-sidebar.tsx +++ b/apps/web/components/new/onboarding/setup/chat-sidebar.tsx @@ -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(() => { diff --git a/apps/web/components/views/chat/chat-messages.tsx b/apps/web/components/views/chat/chat-messages.tsx index 304db7aa..5069231d 100644 --- a/apps/web/components/views/chat/chat-messages.tsx +++ b/apps/web/components/views/chat/chat-messages.tsx @@ -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 - - 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) - } + 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): 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 ( diff --git a/apps/web/hooks/use-claude-agent.ts b/apps/web/hooks/use-claude-agent.ts new file mode 100644 index 00000000..1a0c8ef5 --- /dev/null +++ b/apps/web/hooks/use-claude-agent.ts @@ -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" + } +} diff --git a/apps/web/lib/agent/tools.ts b/apps/web/lib/agent/tools.ts new file mode 100644 index 00000000..23986a7e --- /dev/null +++ b/apps/web/lib/agent/tools.ts @@ -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.` diff --git a/apps/web/lib/agent/types.ts b/apps/web/lib/agent/types.ts new file mode 100644 index 00000000..9a85026d --- /dev/null +++ b/apps/web/lib/agent/types.ts @@ -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" diff --git a/apps/web/package.json b/apps/web/package.json index aa37a4bd..624d68f7 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -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", diff --git a/packages/ui/components/badge.tsx b/packages/ui/components/badge.tsx index c5a03c29..c3244a04 100644 --- a/packages/ui/components/badge.tsx +++ b/packages/ui/components/badge.tsx @@ -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 diff --git a/packages/ui/components/breadcrumb.tsx b/packages/ui/components/breadcrumb.tsx index 1582ccdb..940f030d 100644 --- a/packages/ui/components/breadcrumb.tsx +++ b/packages/ui/components/breadcrumb.tsx @@ -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 diff --git a/packages/ui/components/button.tsx b/packages/ui/components/button.tsx index ae77aa5d..6784d8c3 100644 --- a/packages/ui/components/button.tsx +++ b/packages/ui/components/button.tsx @@ -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 diff --git a/packages/ui/components/sidebar.tsx b/packages/ui/components/sidebar.tsx index a2406d18..cb322745 100644 --- a/packages/ui/components/sidebar.tsx +++ b/packages/ui/components/sidebar.tsx @@ -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 diff --git a/packages/ui/text/heading/heading-h1-bold.tsx b/packages/ui/text/heading/heading-h1-bold.tsx index b76f3b9b..bacf9bf5 100644 --- a/packages/ui/text/heading/heading-h1-bold.tsx +++ b/packages/ui/text/heading/heading-h1-bold.tsx @@ -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, diff --git a/packages/ui/text/heading/heading-h1-medium.tsx b/packages/ui/text/heading/heading-h1-medium.tsx index 5724e1f1..9a75d7c7 100644 --- a/packages/ui/text/heading/heading-h1-medium.tsx +++ b/packages/ui/text/heading/heading-h1-medium.tsx @@ -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, diff --git a/packages/ui/text/heading/heading-h2-bold.tsx b/packages/ui/text/heading/heading-h2-bold.tsx index 6711de50..91e01d34 100644 --- a/packages/ui/text/heading/heading-h2-bold.tsx +++ b/packages/ui/text/heading/heading-h2-bold.tsx @@ -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, diff --git a/packages/ui/text/heading/heading-h2-medium.tsx b/packages/ui/text/heading/heading-h2-medium.tsx index afac0a42..8316511a 100644 --- a/packages/ui/text/heading/heading-h2-medium.tsx +++ b/packages/ui/text/heading/heading-h2-medium.tsx @@ -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, diff --git a/packages/ui/text/heading/heading-h3-bold.tsx b/packages/ui/text/heading/heading-h3-bold.tsx index be15a33c..7a9b72a8 100644 --- a/packages/ui/text/heading/heading-h3-bold.tsx +++ b/packages/ui/text/heading/heading-h3-bold.tsx @@ -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, diff --git a/packages/ui/text/heading/heading-h3-medium.tsx b/packages/ui/text/heading/heading-h3-medium.tsx index cdaa24a2..be9c44be 100644 --- a/packages/ui/text/heading/heading-h3-medium.tsx +++ b/packages/ui/text/heading/heading-h3-medium.tsx @@ -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, diff --git a/packages/ui/text/heading/heading-h4-bold.tsx b/packages/ui/text/heading/heading-h4-bold.tsx index 5e99c031..e3596710 100644 --- a/packages/ui/text/heading/heading-h4-bold.tsx +++ b/packages/ui/text/heading/heading-h4-bold.tsx @@ -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, diff --git a/packages/ui/text/heading/heading-h4-medium.tsx b/packages/ui/text/heading/heading-h4-medium.tsx index 1a536508..d87b2c05 100644 --- a/packages/ui/text/heading/heading-h4-medium.tsx +++ b/packages/ui/text/heading/heading-h4-medium.tsx @@ -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, diff --git a/packages/ui/text/label/label-1-medium.tsx b/packages/ui/text/label/label-1-medium.tsx index e599f3e7..0a9782a2 100644 --- a/packages/ui/text/label/label-1-medium.tsx +++ b/packages/ui/text/label/label-1-medium.tsx @@ -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} />; } diff --git a/packages/ui/text/label/label-1-regular.tsx b/packages/ui/text/label/label-1-regular.tsx index ad9ea319..6bfe79cc 100644 --- a/packages/ui/text/label/label-1-regular.tsx +++ b/packages/ui/text/label/label-1-regular.tsx @@ -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} />; } diff --git a/packages/ui/text/label/label-2-medium.tsx b/packages/ui/text/label/label-2-medium.tsx index 89aa2f2d..6fbfadbc 100644 --- a/packages/ui/text/label/label-2-medium.tsx +++ b/packages/ui/text/label/label-2-medium.tsx @@ -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} />; } diff --git a/packages/ui/text/label/label-2-regular.tsx b/packages/ui/text/label/label-2-regular.tsx index 951dc5bf..26ac60ea 100644 --- a/packages/ui/text/label/label-2-regular.tsx +++ b/packages/ui/text/label/label-2-regular.tsx @@ -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} />; } diff --git a/packages/ui/text/label/label-3-medium.tsx b/packages/ui/text/label/label-3-medium.tsx index 5308452e..e4baabfd 100644 --- a/packages/ui/text/label/label-3-medium.tsx +++ b/packages/ui/text/label/label-3-medium.tsx @@ -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} />; } diff --git a/packages/ui/text/label/label-3-regular.tsx b/packages/ui/text/label/label-3-regular.tsx index 9ca0d65e..092721d0 100644 --- a/packages/ui/text/label/label-3-regular.tsx +++ b/packages/ui/text/label/label-3-regular.tsx @@ -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} />; } diff --git a/packages/ui/text/title/title-1-bold.tsx b/packages/ui/text/title/title-1-bold.tsx index a87e637b..dfcb7756 100644 --- a/packages/ui/text/title/title-1-bold.tsx +++ b/packages/ui/text/title/title-1-bold.tsx @@ -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, diff --git a/packages/ui/text/title/title-1-medium.tsx b/packages/ui/text/title/title-1-medium.tsx index 2ac13520..4face639 100644 --- a/packages/ui/text/title/title-1-medium.tsx +++ b/packages/ui/text/title/title-1-medium.tsx @@ -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, diff --git a/packages/ui/text/title/title-2-bold.tsx b/packages/ui/text/title/title-2-bold.tsx index 38bbe34e..745f0da4 100644 --- a/packages/ui/text/title/title-2-bold.tsx +++ b/packages/ui/text/title/title-2-bold.tsx @@ -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, diff --git a/packages/ui/text/title/title-2-medium.tsx b/packages/ui/text/title/title-2-medium.tsx index c5a5deae..a3b6b473 100644 --- a/packages/ui/text/title/title-2-medium.tsx +++ b/packages/ui/text/title/title-2-medium.tsx @@ -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, diff --git a/packages/ui/text/title/title-3-bold.tsx b/packages/ui/text/title/title-3-bold.tsx index cf9ab777..d3fb0207 100644 --- a/packages/ui/text/title/title-3-bold.tsx +++ b/packages/ui/text/title/title-3-bold.tsx @@ -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, diff --git a/packages/ui/text/title/title-3-medium.tsx b/packages/ui/text/title/title-3-medium.tsx index f862e618..b71d65f1 100644 --- a/packages/ui/text/title/title-3-medium.tsx +++ b/packages/ui/text/title/title-3-medium.tsx @@ -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, |