diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 04679e6..53a3154 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -1,8 +1,4 @@ -import { - streamText, - convertToModelMessages, - stepCountIs, -} from 'ai'; +import { streamText, convertToModelMessages, stepCountIs } from 'ai'; import { withSupermemory } from '@supermemory/tools/ai-sdk'; import { auth, type UserType } from '@/app/(auth)/auth'; import { type RequestHints, systemPrompt } from '@/lib/ai/prompts'; @@ -38,7 +34,8 @@ export async function POST(request: Request) { } try { - const { id, message, selectedChatModel, selectedVisibilityType } = requestBody; + const { id, message, selectedChatModel, selectedVisibilityType } = + requestBody; const session = await auth(); if (!session?.user) { @@ -74,9 +71,10 @@ export async function POST(request: Request) { // Convert DB messages to AI SDK v5 format (using parts array) const formattedPreviousMessages = previousMessages.map((dbMsg: any) => { // Ensure parts array exists, or create one from content if needed - const parts = Array.isArray(dbMsg.parts) && dbMsg.parts.length > 0 - ? dbMsg.parts - : [{ type: 'text', text: dbMsg.content || '' }]; + const parts = + Array.isArray(dbMsg.parts) && dbMsg.parts.length > 0 + ? dbMsg.parts + : [{ type: 'text', text: dbMsg.content || '' }]; return { id: dbMsg.id, @@ -95,10 +93,7 @@ export async function POST(request: Request) { }; // Append current message to previous messages - const messages = [ - ...formattedPreviousMessages, - formattedCurrentMessage, - ]; + const messages = [...formattedPreviousMessages, formattedCurrentMessage]; const { longitude, latitude, city, country } = geolocation(request); const requestHints: RequestHints = { @@ -115,7 +110,8 @@ export async function POST(request: Request) { id: message.id, role: 'user', parts: message.parts, - attachments: message.parts?.filter((part: any) => part.type === 'file') ?? [], + attachments: + message.parts?.filter((part: any) => part.type === 'file') ?? [], createdAt: new Date(), }, ], @@ -127,13 +123,19 @@ export async function POST(request: Request) { // Get the API key for supermemory tools const supermemoryApiKey = process.env.SUPERMEMORY_API_KEY; if (!supermemoryApiKey) { - return new ChatSDKError('bad_request:api', 'SUPERMEMORY_API_KEY is not configured').toResponse(); + return new ChatSDKError( + 'bad_request:api', + 'SUPERMEMORY_API_KEY is not configured', + ).toResponse(); } // Get the API key for Exa tools const exaApiKey = process.env.EXA_API_KEY; if (!exaApiKey) { - return new ChatSDKError('bad_request:api', 'EXA_API_KEY is not configured').toResponse(); + return new ChatSDKError( + 'bad_request:api', + 'EXA_API_KEY is not configured', + ).toResponse(); } // Always use user ID as container tag @@ -145,25 +147,30 @@ export async function POST(request: Request) { const webSearchTool = createWebSearchTool(exaApiKey); // Wrap the language model with supermemory - const baseModel = myProvider(session.user.id).languageModel(selectedChatModel); + const baseModel = myProvider(session.user.id).languageModel( + selectedChatModel, + ); const modelWithMemory = withSupermemory(baseModel, containerTag, { conversationId: id, - mode: "full", + mode: 'full', verbose: true, - addMemory: "always" + addMemory: 'always', }); const toolsConfig = { searchMemories: memoryTools.searchMemories, webSearch: webSearchTool, }; - + // Log what messages we're sending to AI SDK const convertedMessages = convertToModelMessages(messages as any); convertedMessages.forEach((msg, idx) => { console.log(`[Chat API] Message ${idx}:`, { role: msg.role, - content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content) + content: + typeof msg.content === 'string' + ? msg.content + : JSON.stringify(msg.content), }); }); @@ -178,20 +185,25 @@ export async function POST(request: Request) { if (session.user?.id) { try { // Check if the response contains split delimiter - const splitMessages = text.split('').map(t => t.trim()).filter(t => t.length > 0); - + const splitMessages = text + .split('') + .map((t) => t.trim()) + .filter((t) => t.length > 0); + // If there are multiple messages, save them separately with small time delays if (splitMessages.length > 1) { - const messagesToSave = splitMessages.map((messageText, index) => ({ - id: generateUUID(), - chatId: id, - role: 'assistant' as const, - parts: [{ type: 'text' as const, text: messageText }], - attachments: [], - // Add small time increments to ensure correct ordering - createdAt: new Date(Date.now() + index * 100), - })); - + const messagesToSave = splitMessages.map( + (messageText, index) => ({ + id: generateUUID(), + chatId: id, + role: 'assistant' as const, + parts: [{ type: 'text' as const, text: messageText }], + attachments: [], + // Add small time increments to ensure correct ordering + createdAt: new Date(Date.now() + index * 100), + }), + ); + await saveMessages({ messages: messagesToSave }); } else { // Single message, save as before @@ -217,7 +229,7 @@ export async function POST(request: Request) { experimental_telemetry: { isEnabled: isProductionEnvironment, functionId: 'stream-text', - } + }, }); return result.toUIMessageStreamResponse(); @@ -254,4 +266,4 @@ export async function DELETE(request: Request) { const deletedChat = await deleteChatById({ id }); return Response.json(deletedChat, { status: 200 }); -} \ No newline at end of file +} diff --git a/app/globals.css b/app/globals.css index a48575e..f049cd1 100644 --- a/app/globals.css +++ b/app/globals.css @@ -159,6 +159,22 @@ } } + /* Staggered message animation - slide from below */ + @keyframes slideInFromBottom { + 0% { + opacity: 0; + transform: translateY(30px); + } + 100% { + opacity: 1; + transform: translateY(0); + } + } + + .animate-message-in { + animation: slideInFromBottom 0.4s cubic-bezier(0.34, 0.69, 0.1, 1) both; + } + /* Global caret styling */ * { caret-color: #0A7CFF; diff --git a/bun.lock b/bun.lock index 82e65b3..efcb4cc 100644 --- a/bun.lock +++ b/bun.lock @@ -16,6 +16,7 @@ "@codemirror/view": "^6.36.7", "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", + "@hookform/resolvers": "^5.2.2", "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.200.0", "@radix-ui/react-alert-dialog": "^1.1.13", @@ -24,12 +25,12 @@ "@radix-ui/react-dialog": "^1.1.13", "@radix-ui/react-dropdown-menu": "^2.1.14", "@radix-ui/react-icons": "^1.3.2", - "@radix-ui/react-label": "^2.1.6", + "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.2.4", "@radix-ui/react-separator": "^1.1.6", - "@radix-ui/react-slot": "^1.2.2", + "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tooltip": "^1.2.6", "@radix-ui/react-visually-hidden": "^1.2.2", @@ -79,6 +80,7 @@ "react": "19.1.0", "react-data-grid": "7.0.0-beta.52", "react-dom": "19.1.0", + "react-hook-form": "^7.65.0", "react-markdown": "^10.1.0", "react-resizable-panels": "^3.0.1", "react-swipeable": "^7.0.2", @@ -94,7 +96,7 @@ "tailwindcss-animate": "^1.0.7", "usehooks-ts": "^3.1.1", "vaul": "^1.1.2", - "zod": "4", + "zod": "^4.1.12", }, "devDependencies": { "@biomejs/biome": "1.9.4", @@ -355,6 +357,8 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="], + "@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="], + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], @@ -537,7 +541,7 @@ "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], - "@radix-ui/react-label": ["@radix-ui/react-label@2.1.6", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-S/hv1mTlgcPX2gCTJrWuTjSXf7ER3Zf7zWGtOprxhIIY93Qin3n5VgNA0Ez9AgrK/lEtlYgzLd4f5x6AVar4Yw=="], + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="], "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="], @@ -559,7 +563,7 @@ "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.6", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Izof3lPpbCfTM7WDta+LRkz31jem890VjEvpVRoWQNKpDUMMVffuyq854XPGP1KYGWWmjmYvHvPFeocWhFCy1w=="], - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="], @@ -705,6 +709,8 @@ "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], + "@supermemory/tools": ["@supermemory/tools@1.2.17", "", { "dependencies": { "@ai-sdk/anthropic": "^2.0.25", "@ai-sdk/openai": "^2.0.23", "@ai-sdk/provider": "^2.0.0", "ai": "^5.0.29", "openai": "^4.104.0", "supermemory": "^3.0.0-alpha.26", "zod": "^4.1.5" } }, "sha512-cdAZbA0meTy9lMe5vx18XGtMEU3J9J9s8mwmkq+/aiwGRU3ToKsI+QcN23LrYtvf8XUNLrwsX5jtTe0IY7CtWg=="], "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], @@ -1789,6 +1795,8 @@ "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], + "react-hook-form": ["react-hook-form@7.65.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw=="], + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], @@ -2149,16 +2157,24 @@ "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="], + "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ=="], + "@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], "@radix-ui/react-avatar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ=="], + "@radix-ui/react-context-menu/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], "@radix-ui/react-context-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ=="], + "@radix-ui/react-dropdown-menu/@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.6", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.9", "@radix-ui/react-focus-guards": "1.1.2", "@radix-ui/react-focus-scope": "1.1.6", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.6", "@radix-ui/react-portal": "1.1.8", "@radix-ui/react-presence": "1.1.4", "@radix-ui/react-primitive": "2.1.2", "@radix-ui/react-roving-focus": "1.1.9", "@radix-ui/react-slot": "1.2.2", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0zSiBAIFq9GSKoSH5PdEaQeRB3RnEGxC+H2P0egtnKoKKLNBH8VBHyVO6/jskhjAezhOIplyRUj7U2lds9A+Yg=="], + "@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-menu/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], "@radix-ui/react-menu/@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], @@ -2175,8 +2191,6 @@ "@radix-ui/react-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@radix-ui/react-popover/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], "@radix-ui/react-popover/@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], @@ -2191,10 +2205,10 @@ "@radix-ui/react-popover/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@radix-ui/react-popper/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ=="], + "@radix-ui/react-roving-focus/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], "@radix-ui/react-roving-focus/@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], @@ -2209,12 +2223,16 @@ "@radix-ui/react-select/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.6", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.6", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.2", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7iqXaOWIjDBfIG7aq8CUEeCSsQMLFdn7VEE8TaFz704DtEzpPHR7w/uuzRflvKgltqSAImgcmxQ7fFX3X7wasg=="], + "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ=="], + "@radix-ui/react-switch/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], "@radix-ui/react-switch/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], "@radix-ui/react-tooltip/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.6", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.6", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.2", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7iqXaOWIjDBfIG7aq8CUEeCSsQMLFdn7VEE8TaFz704DtEzpPHR7w/uuzRflvKgltqSAImgcmxQ7fFX3X7wasg=="], + "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ=="], + "@supermemory/tools/ai": ["ai@5.0.72", "", { "dependencies": { "@ai-sdk/gateway": "1.0.40", "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.12", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LB4APrlESLGHG/5x+VVdl0yYPpHPHpnGd5Gwl7AWVL+n7T0GYsNos/S/6dZ5CZzxLnPPEBkRgvJC4rupeZqyNg=="], "@supermemory/tools/supermemory": ["supermemory@3.4.0", "", {}, "sha512-acomTikn701JDhGnK7jGlsE/hTa+nYha1/Q4+4C8oB80BOIiQuyNufM3wXQ45z1YIqhL5m+JkgdTDGZH5zCIVw=="], @@ -2395,28 +2413,14 @@ "@opentelemetry/sdk-logs/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.33.0", "", {}, "sha512-TIpZvE8fiEILFfTlfPnltpBaD3d9/+uQHVCyC3vfdh6WfCXKhNFzoP5RyDDIndfvZC5GrA4pyEDNyjPloJud+w=="], - "@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-avatar/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-context-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@radix-ui/react-dropdown-menu/@radix-ui/react-menu/@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.6", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.6", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.2", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7iqXaOWIjDBfIG7aq8CUEeCSsQMLFdn7VEE8TaFz704DtEzpPHR7w/uuzRflvKgltqSAImgcmxQ7fFX3X7wasg=="], "@radix-ui/react-dropdown-menu/@radix-ui/react-menu/@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.9", "", { "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-collection": "1.1.6", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.2", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ=="], - "@radix-ui/react-popper/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-roving-focus/@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-scroll-area/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-dropdown-menu/@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ=="], "@radix-ui/react-select/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.6", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-2JMfHJf/eVnwq+2dewT3C0acmCWD3XiVA1Da+jTDqo342UlU13WvXtqHhG+yJw5JeQmu4ue2eMy6gcEArLBlcw=="], - "@radix-ui/react-switch/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@radix-ui/react-tooltip/@radix-ui/react-popper/@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.6", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-2JMfHJf/eVnwq+2dewT3C0acmCWD3XiVA1Da+jTDqo342UlU13WvXtqHhG+yJw5JeQmu4ue2eMy6gcEArLBlcw=="], "@typescript-eslint/parser/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], diff --git a/components/app.tsx b/components/app.tsx index 2618117..ef3a6fc 100644 --- a/components/app.tsx +++ b/components/app.tsx @@ -753,14 +753,16 @@ export default function App() { // Method to handle reaction const handleReaction = useCallback( - (messageId: string, reaction: Reaction) => { + (messageId: string, reaction: Reaction, splitIndex?: number) => { setConversations((prevConversations) => { return prevConversations.map((conversation) => { const messages = conversation.messages.map((message) => { if (message.id === messageId) { - // Check if this exact reaction already exists + // Check if this exact reaction already exists (including splitIndex) const existingReaction = message.reactions?.find( - (r) => r.sender === reaction.sender && r.type === reaction.type, + (r) => r.sender === reaction.sender && + r.type === reaction.type && + r.splitIndex === splitIndex, ); if (existingReaction) { @@ -772,15 +774,16 @@ export default function App() { (r) => !( r.sender === reaction.sender && - r.type === reaction.type + r.type === reaction.type && + r.splitIndex === splitIndex ), ) || [], }; } else { - // Remove any other reaction from this sender and add the new one + // Remove any other reaction from this sender for this split and add the new one const otherReactions = message.reactions?.filter( - (r) => r.sender !== reaction.sender, + (r) => !(r.sender === reaction.sender && r.splitIndex === splitIndex), ) || []; return { ...message, diff --git a/components/chat-area.tsx b/components/chat-area.tsx index 19dea32..d044cfb 100644 --- a/components/chat-area.tsx +++ b/components/chat-area.tsx @@ -16,6 +16,7 @@ import { MessageInput } from './message-input'; import { ConversationHeader } from './conversation-header'; import { Markdown } from './markdown'; import { TypingBubble } from './typing-bubble'; +import { MessageForm } from './message-form'; interface ChatAreaProps { isNewChat: boolean; @@ -29,7 +30,11 @@ interface ChatAreaProps { conversationId?: string, attachments?: Attachment[], ) => void; - onReaction?: (messageId: string, reaction: Reaction) => void; + onReaction?: ( + messageId: string, + reaction: Reaction, + splitIndex?: number, + ) => void; typingStatus: { conversationId: string; recipient: string } | null; conversationId: string | null; onUpdateConversationName?: (name: string) => void; @@ -63,6 +68,7 @@ export function ChatArea({ userId, }: ChatAreaProps) { const [inputValue, setInputValue] = useState(messageDraft); + const [submittedForm, setSubmittedForm] = useState(false); const messagesEndRef = useRef(null); const fileInputRef = useRef(null); const { theme, systemTheme } = useTheme(); @@ -140,10 +146,21 @@ export function ChatArea({ }; // Check if a specific reaction type is already active for the current user - const isReactionActive = (message: any, type: ReactionType) => { + const isReactionActive = ( + message: any, + type: ReactionType, + splitIndex?: number, + hasSplits?: boolean, + ) => { return ( message.reactions?.some( - (r: Reaction) => r.type === type && r.sender === 'me', + (r: Reaction) => + r.type === type && + r.sender === 'me' && + // Check splitIndex if message has splits + (!hasSplits || + r.splitIndex === splitIndex || + (r.splitIndex === undefined && splitIndex === 0)), ) ?? false ); }; @@ -163,6 +180,16 @@ export function ChatArea({ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [activeConversation?.messages]); + useEffect(() => { + if ( + localStorage.getItem('submittedForm') === `submitted-${conversationId}` + ) { + setSubmittedForm(true); + } else { + setSubmittedForm(false); + } + }, [conversationId]); + const handleInputChange = (value: string) => { setInputValue(value); if (conversationId) { @@ -242,6 +269,16 @@ export function ChatArea({ return []; } + // Calculate if this is a recent message for animation + const totalMessages = activeConversation.messages.length; + const recentThreshold = 5; // Show animation for last 5 messages + const isRecentMessage = index >= totalMessages - recentThreshold; + + // Calculate animation delay based on position in recent messages + const recentMessageIndex = isRecentMessage + ? index - (totalMessages - recentThreshold) + : -1; + return contentParts.map((content, splitIndex) => { const prevMessage = index > 0 ? activeConversation.messages[index - 1] : null; @@ -259,6 +296,18 @@ export function ChatArea({ splitIndex === contentParts.length - 1 && (!nextMessage || nextMessage.sender !== message.sender); + const isNewMessage = + new Date().getTime() - new Date(message.timestamp).getTime() < + 2000; + + // Calculate staggered animation delay - messages appear one by one + const baseDelay = 150; // Base delay between messages in ms + const splitDelay = 75; // Additional delay for split parts + const animationDelay = + isRecentMessage && isNewMessage + ? `${recentMessageIndex * baseDelay + splitIndex * Math.random() * splitDelay}ms` + : '0ms'; + return (
- - -
- - - - {/* Reaction menu */} - - {/* Reaction buttons */} - {Object.entries(menuReactionIcons).map( - ([type, icon]) => ( - + + + {/* Reaction menu */} + + {/* Reaction buttons */} + {Object.entries(menuReactionIcons).map( + ([type, icon]) => ( + - ), + ? 'bg-[#0A7CFF] text-white scale-110' + : '', + )} + > + 1, + ) + ? icon + .replace('-gray', '-white') + .replace('-dark', '-white') + : icon + } + width={16} + height={16} + alt={`${type} reaction`} + style={ + type === 'emphasize' + ? { transform: 'scale(0.75)' } + : type === 'question' + ? { transform: 'scale(0.6)' } + : undefined + } + /> + + ), + )} + + + ) : ( + /* When there's a form and it hasn't been submitted, render without popover */ +
+
+ {content} +
+ {/* Render form if it exists and hasn't been submitted */} + {message.form && !submittedForm && ( + { + // Send the form submission directly without updating input state + onSendMessage( + `Form Submission:\nName: ${data.name}\nEmail: ${data.email}`, + conversationId || undefined, + ); + setSubmittedForm(true); + localStorage.setItem( + `submittedForm`, + `submitted-${conversationId}`, + ); + }} + /> )} - - +
+ )} {/* Display existing reactions */} {message.reactions && message.reactions.length > 0 && ( @@ -428,6 +564,14 @@ export function ChatArea({ )} > {[...message.reactions] + .filter( + (r) => + // Only show reactions for this split part + contentParts.length <= 1 || + r.splitIndex === splitIndex || + (r.splitIndex === undefined && + splitIndex === 0), // Backward compatibility + ) .sort( (a, b) => new Date(a.timestamp).getTime() - @@ -502,7 +646,7 @@ export function ChatArea({
{/* Input - iMessage style with TipTap - hide for read-only chats */} - {!isReadOnly && ( + {!isReadOnly && submittedForm && (
void; +} + +export const MessageForm = memo(function MessageForm({ messageId, onSubmit }: MessageFormProps) { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: '', + email: '', + }, + }); + + return ( +
+ + ( + + Full Name + + + + + + )} + /> + ( + + Email + + + + + + )} + /> + + + + ); +}); \ No newline at end of file diff --git a/components/message-input.tsx b/components/message-input.tsx index a9b8373..bcb1e2f 100644 --- a/components/message-input.tsx +++ b/components/message-input.tsx @@ -1,4 +1,4 @@ -import type { Recipient, Attachment } from "../types"; +import type { Recipient, Attachment } from '../types'; import { useState, useRef, @@ -7,18 +7,18 @@ import { useImperativeHandle, useCallback, type ChangeEvent, -} from "react"; -import data from "@emoji-mart/data"; -import Picker from "@emoji-mart/react"; -import { Icons } from "./icons"; -import { useTheme } from "next-themes"; -import { useEditor, EditorContent } from "@tiptap/react"; -import StarterKit from "@tiptap/starter-kit"; -import Mention from "@tiptap/extension-mention"; -import type { SuggestionProps } from "@tiptap/suggestion"; -import Placeholder from "@tiptap/extension-placeholder"; -import { soundEffects } from "@/lib/sound-effects"; -import Image from "next/image"; +} from 'react'; +import data from '@emoji-mart/data'; +import Picker from '@emoji-mart/react'; +import { Icons } from './icons'; +import { useTheme } from 'next-themes'; +import { useEditor, EditorContent } from '@tiptap/react'; +import StarterKit from '@tiptap/starter-kit'; +import Mention from '@tiptap/extension-mention'; +import type { SuggestionProps } from '@tiptap/suggestion'; +import Placeholder from '@tiptap/extension-placeholder'; +import { soundEffects } from '@/lib/sound-effects'; +import Image from 'next/image'; interface MessageInputProps { message: string; @@ -39,7 +39,7 @@ export type MessageInputHandle = { // Forward ref component to expose focus method to parent export const MessageInput = forwardRef< MessageInputHandle, - Omit + Omit >(function MessageInput( { message, @@ -51,7 +51,7 @@ export const MessageInput = forwardRef< conversationId, isNewChat = false, }, - ref + ref, ) { const [showEmojiPicker, setShowEmojiPicker] = useState(false); const [attachments, setAttachments] = useState([]); @@ -67,34 +67,34 @@ export const MessageInput = forwardRef< extensions: [ StarterKit, Placeholder.configure({ - placeholder: disabled ? "This chat is read-only" : "Type a message...", + placeholder: disabled ? 'This chat is read-only' : 'Type a message...', }), Mention.configure({ HTMLAttributes: { - class: "mention-node", - style: "color: #0A7CFF !important; font-weight: 500 !important;", + class: 'mention-node', + style: 'color: #0A7CFF !important; font-weight: 500 !important;', onanimationend: 'this.classList.add("shimmer-done")', }, renderText: ({ node }) => { // Try to find the recipient by ID to get their name const recipient = recipients.find((r) => r.id === node.attrs.id); return ( - recipient?.name.split(" ")[0] ?? node.attrs.label ?? node.attrs.id + recipient?.name.split(' ')[0] ?? node.attrs.label ?? node.attrs.id ); }, renderHTML: ({ node }) => { // Try to find the recipient by ID to get their name const recipient = recipients.find((r) => r.id === node.attrs.id); const label = - recipient?.name.split(" ")[0] ?? node.attrs.label ?? node.attrs.id; + recipient?.name.split(' ')[0] ?? node.attrs.label ?? node.attrs.id; return [ - "span", + 'span', { - "data-type": "mention", - "data-id": node.attrs.id, - "data-label": label, - class: "mention-node", - style: "color: #0A7CFF !important; font-weight: 500 !important;", + 'data-type': 'mention', + 'data-id': node.attrs.id, + 'data-label': label, + class: 'mention-node', + style: 'color: #0A7CFF !important; font-weight: 500 !important;', }, label, ]; @@ -103,16 +103,16 @@ export const MessageInput = forwardRef< items: ({ query }: { query: string }) => { if (!query) return []; - const searchText = query.toLowerCase().replace(/^@/, ""); + const searchText = query.toLowerCase().replace(/^@/, ''); return recipients .filter((recipient) => { - const [firstName] = recipient.name.split(" "); + const [firstName] = recipient.name.split(' '); return firstName.toLowerCase().startsWith(searchText); }) .slice(0, 5) .map((match) => ({ id: match.id, - label: match.name.split(" ")[0], + label: match.name.split(' ')[0], })); }, render: () => { @@ -128,14 +128,14 @@ export const MessageInput = forwardRef< onStart: (props: SuggestionProps) => { const { editor } = props; component = { - element: document.createElement("div"), + element: document.createElement('div'), update: (props) => { if (!props.query) return; const match = props.items.find( (item) => item.label.toLowerCase() === - props.query.toLowerCase().replace(/^@/, "") + props.query.toLowerCase().replace(/^@/, ''), ); if (match) { @@ -148,7 +148,7 @@ export const MessageInput = forwardRef< .deleteRange({ from: start, to: end }) .insertContent([ { - type: "mention", + type: 'mention', attrs: { id: match.id, label: match.label }, }, ]) @@ -166,22 +166,22 @@ export const MessageInput = forwardRef< }, }; }, - char: "@", + char: '@', allowSpaces: false, - decorationClass: "suggestion", + decorationClass: 'suggestion', }, }), ], content: message, - autofocus: !isMobileView && !isNewChat ? "end" : false, + autofocus: !isMobileView && !isNewChat ? 'end' : false, onUpdate: ({ editor }) => { if (editor.view?.dom) { const element = editor.view.dom as HTMLElement; const height = Math.min(200, Math.max(32, element.scrollHeight)); const containerHeight = height + 32; // Add padding (16px top + 16px bottom) document.documentElement.style.setProperty( - "--dynamic-height", - `${containerHeight}px` + '--dynamic-height', + `${containerHeight}px`, ); } setMessage(editor.getHTML()); @@ -191,7 +191,7 @@ export const MessageInput = forwardRef< // Delay focus slightly to ensure view is ready setTimeout(() => { if (editor.view?.dom) { - editor.commands.focus("end"); + editor.commands.focus('end'); } }, 0); } @@ -199,10 +199,12 @@ export const MessageInput = forwardRef< editorProps: { attributes: { class: - "w-full bg-background border border-muted-foreground/20 rounded-[18px] pl-4 pr-10 py-2 text-base leading-tight focus:outline-none focus:border-[#007AFF] focus:ring-1 focus:ring-[#007AFF] disabled:opacity-50 prose prose-sm prose-neutral dark:prose-invert max-w-none flex items-center", - enterKeyHint: "send", - style: "min-height: 36px; max-height: 200px; overflow-y: auto;", - contentEditable: (attachments.length === 0 && uploadQueue.length === 0).toString(), + 'w-full bg-background border border-muted-foreground/20 rounded-[18px] pl-4 pr-10 py-2 text-base leading-tight focus:outline-none focus:border-[#007AFF] focus:ring-1 focus:ring-[#007AFF] disabled:opacity-50 prose prose-sm prose-neutral dark:prose-invert max-w-none flex items-center', + enterKeyHint: 'send', + style: 'min-height: 36px; max-height: 200px; overflow-y: auto;', + contentEditable: ( + attachments.length === 0 && uploadQueue.length === 0 + ).toString(), }, handleKeyDown: (view, event) => { // Disable keyboard input when uploading @@ -210,7 +212,7 @@ export const MessageInput = forwardRef< event.preventDefault(); return true; } - if (event.key === "Enter" && !event.shiftKey) { + if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); handleSubmit(); if (isMobileView && view.dom) { @@ -285,11 +287,11 @@ export const MessageInput = forwardRef< if (disabled) { return; } - + if (attachments.length > 0) { // Send attachments with a placeholder message const currentMessage = message.trim(); - setMessage(""); + setMessage(''); if (editor && !editor.isDestroyed) { editor.commands.clearContent(); } @@ -309,11 +311,11 @@ export const MessageInput = forwardRef< focus: () => { // Focus editor at end of content if (editor && !editor.isDestroyed && editor.view && editor.view.dom) { - editor.commands.focus("end"); + editor.commands.focus('end'); } }, }), - [editor] + [editor], ); // Effects @@ -342,11 +344,19 @@ export const MessageInput = forwardRef< // Focus editor at end of content useEffect(() => { - if (editor && !editor.isDestroyed && editor.view && editor.view.dom && conversationId && !isMobileView && !isNewChat) { + if ( + editor && + !editor.isDestroyed && + editor.view && + editor.view.dom && + conversationId && + !isMobileView && + !isNewChat + ) { // Use a small timeout to ensure the view is fully mounted const timer = setTimeout(() => { if (editor && !editor.isDestroyed && editor.view && editor.view.dom) { - editor.commands.focus("end"); + editor.commands.focus('end'); } }, 0); return () => clearTimeout(timer); @@ -359,7 +369,7 @@ export const MessageInput = forwardRef< if (editor && !editor.isDestroyed && editor.view && editor.view.dom) { const element = editor.view.dom as HTMLElement; // Force reflow to get accurate scrollHeight - element.style.height = "auto"; + element.style.height = 'auto'; // Get the scroll height including all content const contentHeight = element.scrollHeight; // Set the height considering padding and ensuring we don't exceed max height @@ -368,10 +378,10 @@ export const MessageInput = forwardRef< // Handle height for both mobile and desktop element.style.height = `${height}px`; - element.style.overflowY = height >= 200 ? "auto" : "hidden"; + element.style.overflowY = height >= 200 ? 'auto' : 'hidden'; document.documentElement.style.setProperty( - "--dynamic-height", - `${containerHeight}px` + '--dynamic-height', + `${containerHeight}px`, ); } }; @@ -382,30 +392,36 @@ export const MessageInput = forwardRef< } // Update height on editor changes - editor.on("update", updateHeight); + editor.on('update', updateHeight); // Update height on window resize - window.addEventListener("resize", updateHeight); + window.addEventListener('resize', updateHeight); // Initial height calculation with small delay to ensure view is mounted const timer = setTimeout(updateHeight, 0); return () => { clearTimeout(timer); - window.removeEventListener("resize", updateHeight); + window.removeEventListener('resize', updateHeight); if (editor && !editor.isDestroyed) { - editor.off("update", updateHeight); + editor.off('update', updateHeight); } }; }, [editor, isMobileView]); // Reset editor height when message is cleared (e.g. after sending) useEffect(() => { - if (message === "" && editor && !editor.isDestroyed && editor.view && editor.view.dom) { + if ( + message === '' && + editor && + !editor.isDestroyed && + editor.view && + editor.view.dom + ) { const element = editor.view.dom as HTMLElement; if (element) { - element.style.height = "32px"; - document.documentElement.style.setProperty("--dynamic-height", "64px"); + element.style.height = '32px'; + document.documentElement.style.setProperty('--dynamic-height', '64px'); } } }, [message, editor]); @@ -424,7 +440,7 @@ export const MessageInput = forwardRef< }; const handleEscape = (event: KeyboardEvent) => { - if (event.key === "Escape") { + if (event.key === 'Escape') { if (showEmojiPicker) { setShowEmojiPicker(false); } else if (editor && !editor.isDestroyed) { @@ -433,12 +449,12 @@ export const MessageInput = forwardRef< } }; - document.addEventListener("mousedown", handleClickOutside); - document.addEventListener("keydown", handleEscape); + document.addEventListener('mousedown', handleClickOutside); + document.addEventListener('keydown', handleEscape); return () => { - document.removeEventListener("mousedown", handleClickOutside); - document.removeEventListener("keydown", handleEscape); + document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('keydown', handleEscape); }; }, [showEmojiPicker, editor]); @@ -450,6 +466,7 @@ export const MessageInput = forwardRef<
{attachments.map((attachment, index) => (
key={index} className="relative w-20 h-16 rounded-md bg-muted overflow-hidden group" > @@ -465,8 +482,11 @@ export const MessageInput = forwardRef<
)} + {/* biome-ignore lint/a11y/useButtonType: */}
)} - +
- - + + {/* Camera/Image button */} {!editor?.getText().trim() && ( )} - +
- 0 || uploadQueue.length > 0) ? 'opacity-50 pointer-events-none' : ''}`} + 0 || uploadQueue.length > 0 ? 'opacity-50 pointer-events-none' : ''}`} /> {/* Show send button when there's text or attachments */} {(editor?.getText().trim() || attachments.length > 0) && ( )}
- + {/* Show emoji picker for desktop */} - {!isMobileView && attachments.length === 0 && uploadQueue.length === 0 && ( - - )} - + {!isMobileView && + attachments.length === 0 && + uploadQueue.length === 0 && ( + + )} + {showEmojiPicker && !isMobileView && (
)} diff --git a/components/typing-bubble.tsx b/components/typing-bubble.tsx index ea91272..484a080 100644 --- a/components/typing-bubble.tsx +++ b/components/typing-bubble.tsx @@ -1,6 +1,5 @@ - -import { cn } from "@/lib/utils"; -import { Logo } from "./logo"; +import { cn } from '@/lib/utils'; +import { Logo } from './logo'; interface TypingBubbleProps { senderName?: string; @@ -15,45 +14,38 @@ const typingAnimation = ` } `; -export function TypingBubble({ senderName = "Supermemory", isMobileView = false }: TypingBubbleProps) { +export function TypingBubble({ + senderName = 'Supermemory', + isMobileView = false, +}: TypingBubbleProps) { return (
- {/* Avatar with logo */} -
- -
- {/* Typing bubble */} -
- {/* Sender name */} -
- {senderName} -
- +
{/* Bubble with typing dots */}
@@ -62,4 +54,3 @@ export function TypingBubble({ senderName = "Supermemory", isMobileView = false
); } - diff --git a/components/ui/button.tsx b/components/ui/button.tsx index 7b63ff1..36496a2 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -1,56 +1,56 @@ -import * as React from 'react'; -import { Slot } from '@radix-ui/react-slot'; -import { cva, type VariantProps } from 'class-variance-authority'; +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" -import { cn } from '@/lib/utils'; +import { cn } from "@/lib/utils" const buttonVariants = cva( - 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { variants: { variant: { - default: 'bg-primary text-primary-foreground hover:bg-primary/90', + default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: - 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: - 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", secondary: - 'bg-secondary text-secondary-foreground hover:bg-secondary/80', - ghost: 'hover:bg-accent hover:text-accent-foreground', - link: 'text-primary underline-offset-4 hover:underline', + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", }, size: { - default: 'h-10 px-4 py-2', - sm: 'h-9 rounded-md px-3', - lg: 'h-11 rounded-md px-8', - icon: 'h-10 w-10', + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", }, }, defaultVariants: { - variant: 'default', - size: 'default', + variant: "default", + size: "default", }, - }, -); + } +) export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { - asChild?: boolean; + asChild?: boolean } const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : 'button'; + const Comp = asChild ? Slot : "button" return ( - ); - }, -); -Button.displayName = 'Button'; + ) + } +) +Button.displayName = "Button" -export { Button, buttonVariants }; +export { Button, buttonVariants } diff --git a/components/ui/form.tsx b/components/ui/form.tsx new file mode 100644 index 0000000..e9c0ec3 --- /dev/null +++ b/components/ui/form.tsx @@ -0,0 +1,178 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + FormProvider, + useFormContext, + type ControllerProps, + type FieldPath, + type FieldValues, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext(null) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + if (!itemContext) { + throw new Error("useFormField should be used within ") + } + + const fieldState = getFieldState(fieldContext.name, formState) + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext(null) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +