diff --git a/package.json b/package.json index 3946837..644ce54 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.6", "@remixicon/react": "^4.6.0", "@solana/spl-token": "^0.4.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e95ac2..59ef399 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,9 @@ importers: '@radix-ui/react-slot': specifier: ^1.1.1 version: 1.1.1(@types/react@19.0.7)(react@19.0.0) + '@radix-ui/react-switch': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-tooltip': specifier: ^1.1.6 version: 1.1.6(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -1747,6 +1750,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-switch@1.1.2': + resolution: {integrity: sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g==} + 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 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-tooltip@1.1.6': resolution: {integrity: sha512-TLB5D8QLExS1uDn7+wH/bjEmRurNMTzNrtq7IjaS4kjion9NtzsTGkvR5+i7yc9q01Pi2KMM2cN3f8UG4IvvXA==} peerDependencies: @@ -9337,6 +9353,21 @@ snapshots: optionalDependencies: '@types/react': 19.0.7 + '@radix-ui/react-switch@1.1.2(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.7)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.7)(react@19.0.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.7)(react@19.0.0) + '@radix-ui/react-use-previous': 1.1.0(@types/react@19.0.7)(react@19.0.0) + '@radix-ui/react-use-size': 1.1.0(@types/react@19.0.7)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.7 + '@types/react-dom': 19.0.3(@types/react@19.0.7) + '@radix-ui/react-tooltip@1.1.6(@types/react-dom@19.0.3(@types/react@19.0.7))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.1 diff --git a/prisma/migrations/20250120222231_user_degen_mode/migration.sql b/prisma/migrations/20250120222231_user_degen_mode/migration.sql new file mode 100644 index 0000000..a61e676 --- /dev/null +++ b/prisma/migrations/20250120222231_user_degen_mode/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "degenMode" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7f86ac4..4089cf1 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -15,6 +15,7 @@ model User { updatedAt DateTime @updatedAt earlyAccess Boolean @default(false) + degenMode Boolean @default(false) wallets Wallet[] conversations Conversation[] diff --git a/src/ai/providers.tsx b/src/ai/providers.tsx index ae3a0c2..eae4ab5 100644 --- a/src/ai/providers.tsx +++ b/src/ai/providers.tsx @@ -48,6 +48,7 @@ Confirmation Handling: 1. Always call the \`askForConfirmation\` tool to request explicit user confirmation. 2. STOP your response immediately after calling \`askForConfirmation\` without providing any additional information or context. 3. Wait for the user to explicitly confirm or reject the action in a separate response. + 4. Never ask for confirmation if the user has enabled \`degenMode\`. - Post-Confirmation Execution: - If the user confirms: 1. Only proceed with executing the tool in a new response after the confirmation. @@ -188,7 +189,7 @@ Your Task: Analyze the user's message and return the appropriate tools as a **JSON array of strings**. Rules: -- Only include the askForConfirmation tool if the user's message requires a transaction signature +- Only include the askForConfirmation tool if the user's message requires a transaction signature or if they are creating an action. - Only return the toolsets in the format: ["toolset1", "toolset2", ...]. - Do not add any text, explanations, or comments outside the array. - Be minimal — include only the toolsets necessary to handle the request. diff --git a/src/app/(user)/account/account-content.tsx b/src/app/(user)/account/account-content.tsx index 646f22b..08201db 100644 --- a/src/app/(user)/account/account-content.tsx +++ b/src/app/(user)/account/account-content.tsx @@ -1,5 +1,7 @@ 'use client'; +import { startTransition, useOptimistic } from 'react'; + import { useRouter } from 'next/navigation'; import { @@ -12,6 +14,8 @@ import { usePrivy, } from '@privy-io/react-auth'; import { useSolanaWallets } from '@privy-io/react-auth/solana'; +import { HelpCircle } from 'lucide-react'; +import { mutate } from 'swr'; import { WalletCard } from '@/components/dashboard/wallet-card'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; @@ -20,6 +24,13 @@ import { Card, CardContent } from '@/components/ui/card'; import { CopyableText } from '@/components/ui/copyable-text'; import { Label } from '@/components/ui/label'; import { Separator } from '@/components/ui/separator'; +import { Switch } from '@/components/ui/switch'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/tooltip'; import { useUser } from '@/hooks/use-user'; import { useEmbeddedWallets } from '@/hooks/use-wallets'; import { cn } from '@/lib/utils'; @@ -27,8 +38,10 @@ import { formatPrivyId, formatUserCreationDate, formatWalletAddress, + truncate, } from '@/lib/utils/format'; import { getUserID, grantDiscordRole } from '@/lib/utils/grant-discord-role'; +import { type UserUpdateData, updateUser } from '@/server/actions/user'; import { EmbeddedWallet } from '@/types/db'; import { LoadingStateSkeleton } from './loading-skeleton'; @@ -49,6 +62,16 @@ export function AccountContent() { unlinkWallet, } = useUser(); + const [optimisticUser, updateOptimisticUser] = useOptimistic( + { + degenMode: user?.degenMode || false, + }, + (state, update: UserUpdateData) => ({ + ...state, + ...update, + }), + ); + const { data: embeddedWallets = [], error: walletsError, @@ -121,6 +144,17 @@ export function AccountContent() { ...legacyWallets.map((w) => w.publicKey), ]; + const handleUpdateUser = async (data: UserUpdateData) => { + startTransition(() => { + updateOptimisticUser(data); + }); + + const result = await updateUser(data); + if (result.success) { + await mutate(`user-${userData.privyId}`); + } + }; + return (
@@ -191,6 +225,38 @@ export function AccountContent() { )}
+
+
+ + + + + + + +

+ Enable Degen Mode to skip confirmation prompts + for on-chain actions +

+
+
+
+
+
+ + {optimisticUser.degenMode ? 'Enabled' : 'Disabled'} + + { + await handleUpdateUser({ degenMode: checked }); + }} + aria-label="Toggle degen mode" + /> +
+
@@ -223,9 +289,16 @@ export function AccountContent() {

Wallet

- {linkedSolanaWallet?.address - ? `${linkedSolanaWallet?.address}` - : 'Not connected'} + + {linkedSolanaWallet?.address + ? linkedSolanaWallet?.address + : 'Not connected'} + + + {linkedSolanaWallet?.address + ? truncate(linkedSolanaWallet?.address) + : 'Not connected'} +

diff --git a/src/app/(user)/account/loading-skeleton.tsx b/src/app/(user)/account/loading-skeleton.tsx index 3fdb035..23397a3 100644 --- a/src/app/(user)/account/loading-skeleton.tsx +++ b/src/app/(user)/account/loading-skeleton.tsx @@ -37,7 +37,7 @@ export function LoadingStateSkeleton() {
@@ -45,7 +45,7 @@ export function LoadingStateSkeleton() {
diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index a783a3f..bb13fd6 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -45,6 +45,7 @@ export async function POST(req: Request) { const session = await verifyUser(); const userId = session?.data?.data?.id; const publicKey = session?.data?.data?.publicKey; + const degenMode = session?.data?.data?.degenMode; if (!userId) { return new Response('Unauthorized', { status: 401 }); @@ -124,6 +125,7 @@ export async function POST(req: Request) { `User Solana wallet public key: ${publicKey}`, `User ID: ${userId}`, `Conversation ID: ${conversationId}`, + `Degen Mode: ${degenMode}`, ].join('\n\n'); // Filter out empty messages and ensure sorting by createdAt ascending @@ -168,7 +170,12 @@ export async function POST(req: Request) { // Exclude the confirmation tool if we are handling a confirmation const { toolsRequired, usage: orchestratorUsage } = - await getToolsFromOrchestrator(relevant, confirmationHandled); + await getToolsFromOrchestrator( + relevant, + degenMode || confirmationHandled, + ); + + console.log('toolsRequired', toolsRequired); logWithTiming( startTime, diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx new file mode 100644 index 0000000..5f4117f --- /dev/null +++ b/src/components/ui/switch.tsx @@ -0,0 +1,29 @@ +"use client" + +import * as React from "react" +import * as SwitchPrimitives from "@radix-ui/react-switch" + +import { cn } from "@/lib/utils" + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +Switch.displayName = SwitchPrimitives.Root.displayName + +export { Switch } diff --git a/src/server/actions/user.ts b/src/server/actions/user.ts index d150b25..ba15c3b 100644 --- a/src/server/actions/user.ts +++ b/src/server/actions/user.ts @@ -1,9 +1,8 @@ 'use server'; +import { revalidateTag } from 'next/cache'; import { cookies } from 'next/headers'; -import { Wallet } from '@prisma/client'; -import { Email } from '@privy-io/react-auth'; import { PrivyClient } from '@privy-io/server-auth'; import { WalletWithMetadata } from '@privy-io/server-auth'; import { z } from 'zod'; @@ -92,7 +91,12 @@ const getOrCreateUser = actionClient }); export const verifyUser = actionClient.action< - ActionResponse<{ id: string; publicKey?: string }> + ActionResponse<{ + id: string; + degenMode: boolean; + publicKey?: string; + privyId: string; + }> >(async () => { const token = (await cookies()).get('privy-token')?.value; if (!token) { @@ -108,6 +112,8 @@ export const verifyUser = actionClient.action< where: { privyId: claims.userId }, select: { id: true, + degenMode: true, + privyId: true, wallets: { select: { publicKey: true, @@ -130,7 +136,9 @@ export const verifyUser = actionClient.action< success: true, data: { id: user.id, + privyId: user.privyId, publicKey: user.wallets[0]?.publicKey, + degenMode: user.degenMode, }, }; } catch { @@ -240,3 +248,26 @@ export const syncEmbeddedWallets = actionClient.action< export const getPrivyClient = actionClient.action( async () => PRIVY_SERVER_CLIENT, ); + +export type UserUpdateData = { + degenMode?: boolean; +}; +export async function updateUser(data: UserUpdateData) { + try { + const authResult = await verifyUser(); + const userId = authResult?.data?.data?.id; + const privyId = authResult?.data?.data?.privyId; + + if (!userId) { + return { success: false, error: 'UNAUTHORIZED' }; + } + await prisma.user.update({ + where: { id: userId }, + data, + }); + revalidateTag(`user-${privyId}`); + return { success: true }; + } catch (error) { + return { success: false, error: 'Failed to update user' }; + } +} diff --git a/src/types/db.ts b/src/types/db.ts index d9b22af..e24496a 100644 --- a/src/types/db.ts +++ b/src/types/db.ts @@ -32,7 +32,13 @@ export type PrismaUser = _PrismaUser & { export type NeurUser = Pick< PrismaUser, - 'id' | 'privyId' | 'createdAt' | 'updatedAt' | 'earlyAccess' | 'wallets' + | 'id' + | 'privyId' + | 'createdAt' + | 'updatedAt' + | 'earlyAccess' + | 'wallets' + | 'degenMode' > & { privyUser: PrivyUser; hasEAP: boolean;