mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-08 22:21:07 +00:00
migrate nova agent to claude agents sdk
This commit is contained in:
parent
489b5e23c0
commit
f03886fae1
38 changed files with 1107 additions and 290 deletions
186
apps/web/app/api/agent/chat/route.ts
Normal file
186
apps/web/app/api/agent/chat/route.ts
Normal 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
51
apps/web/app/api/agent/follow-ups/route.ts
Normal file
51
apps/web/app/api/agent/follow-ups/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
42
apps/web/app/api/agent/title/route.ts
Normal file
42
apps/web/app/api/agent/title/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(" ")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(" ")
|
||||
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
238
apps/web/hooks/use-claude-agent.ts
Normal file
238
apps/web/hooks/use-claude-agent.ts
Normal 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
164
apps/web/lib/agent/tools.ts
Normal 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.`
|
||||
75
apps/web/lib/agent/types.ts
Normal file
75
apps/web/lib/agent/types.ts
Normal 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"
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue