Skip to content

Commit

Permalink
Merge pull request #145 from NeurProjects/feat/degen-mode
Browse files Browse the repository at this point in the history
Configurable degen mode to skip confirmation prompts
  • Loading branch information
backwardsmap authored Jan 22, 2025
2 parents 13f7681 + b99ab57 commit 1ed310c
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 11 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
31 changes: 31 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "degenMode" BOOLEAN NOT NULL DEFAULT false;
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ model User {
updatedAt DateTime @updatedAt
earlyAccess Boolean @default(false)
degenMode Boolean @default(false)
wallets Wallet[]
conversations Conversation[]
Expand Down
3 changes: 2 additions & 1 deletion src/ai/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
79 changes: 76 additions & 3 deletions src/app/(user)/account/account-content.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use client';

import { startTransition, useOptimistic } from 'react';

import { useRouter } from 'next/navigation';

import {
Expand All @@ -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';
Expand All @@ -20,15 +24,24 @@ 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';
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';
Expand All @@ -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,
Expand Down Expand Up @@ -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 (
<div className="flex flex-1 flex-col py-8">
<div className="w-full px-8">
Expand Down Expand Up @@ -191,6 +225,38 @@ export function AccountContent() {
)}
</div>
</div>
<div>
<div className="flex items-center gap-1.5">
<Label className="text-xs text-muted-foreground">
Degen Mode
</Label>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="h-3.5 w-3.5 text-muted-foreground/70" />
</TooltipTrigger>
<TooltipContent>
<p>
Enable Degen Mode to skip confirmation prompts
for on-chain actions
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="mt-1 flex h-8 items-center justify-between">
<span className={cn('text-sm font-medium')}>
{optimisticUser.degenMode ? 'Enabled' : 'Disabled'}
</span>
<Switch
checked={optimisticUser.degenMode}
onCheckedChange={async (checked) => {
await handleUpdateUser({ degenMode: checked });
}}
aria-label="Toggle degen mode"
/>
</div>
</div>
</div>
</div>
</CardContent>
Expand Down Expand Up @@ -223,9 +289,16 @@ export function AccountContent() {
<div>
<p className="text-sm font-medium">Wallet</p>
<p className="text-xs text-muted-foreground">
{linkedSolanaWallet?.address
? `${linkedSolanaWallet?.address}`
: 'Not connected'}
<span className="hidden sm:inline">
{linkedSolanaWallet?.address
? linkedSolanaWallet?.address
: 'Not connected'}
</span>
<span className="sm:hidden">
{linkedSolanaWallet?.address
? truncate(linkedSolanaWallet?.address)
: 'Not connected'}
</span>
</p>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/app/(user)/account/loading-skeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ export function LoadingStateSkeleton() {
</div>
<div>
<Label className="text-xs text-muted-foreground">
Connected Wallet
Early Access Program
</Label>
<div className="mt-1">
<Skeleton className="h-6 w-full" />
</div>
</div>
<div>
<Label className="text-xs text-muted-foreground">
Synapses
Degen Mode
</Label>
<div className="mt-1 flex flex-col items-center">
<Skeleton className="h-8 w-full" />
Expand Down
9 changes: 8 additions & 1 deletion src/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
29 changes: 29 additions & 0 deletions src/components/ui/switch.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName

export { Switch }
37 changes: 34 additions & 3 deletions src/server/actions/user.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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) {
Expand All @@ -108,6 +112,8 @@ export const verifyUser = actionClient.action<
where: { privyId: claims.userId },
select: {
id: true,
degenMode: true,
privyId: true,
wallets: {
select: {
publicKey: true,
Expand All @@ -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 {
Expand Down Expand Up @@ -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' };
}
}
8 changes: 7 additions & 1 deletion src/types/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 1ed310c

Please sign in to comment.