Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion frontend/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type React from "react"
import type { Metadata } from "next"
import { Analytics } from "@vercel/analytics/next"
import "./globals.css"
import { Providers } from "@/components/provider"
import AIAssistant from "@/components/ai-assistant"

export const metadata: Metadata = {
title: "Kivo - Smart Wallet",
Expand Down Expand Up @@ -34,7 +36,10 @@ export default function RootLayout({
return (
<html lang="en">
<body className="font-sans antialiased bg-background text-foreground">
{children}
<Providers>
{children}
<AIAssistant />
</Providers>
<Analytics />
</body>
</html>
Expand Down
106 changes: 79 additions & 27 deletions frontend/components/ai-assistant.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,96 @@
"use client"

import { motion } from "framer-motion"
import { MessageCircle } from "lucide-react"
import { MessageCircle, Zap, CheckCircle, AlertTriangle } from "lucide-react"
import { useState } from "react"
import { useAIAgent, AgentSuggestion } from "@/hooks/use-ai-agent"
import Button from "./ui/button"

export default function AIAssistant() {
const [showTooltip, setShowTooltip] = useState(false)
const [isOpen, setIsOpen] = useState(false)
const { suggestions, executeAction } = useAIAgent()
const [executingId, setExecutingId] = useState<string | null>(null)

const handleExecute = async (suggestion: AgentSuggestion) => {
if (!suggestion) return
setExecutingId(suggestion.id)
try {
await executeAction(suggestion)
} catch (error) {
console.error("Execution failed", error)
} finally {
setExecutingId(null)
}
}

const getRiskColor = (riskLevel?: "low" | "medium" | "high") => {
switch (riskLevel) {
case "low":
return "text-green-500"
case "medium":
return "text-yellow-500"
case "high":
return "text-red-500"
default:
return "text-gray-500"
}
}

return (
<motion.div
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.6, duration: 0.5 }}
className="fixed bottom-6 right-6 z-30"
>
<div className="fixed bottom-6 right-6 z-30">
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
onClick={() => setIsOpen(!isOpen)}
className="relative p-4 rounded-full bg-gradient-to-br from-primary to-accent text-primary-foreground shadow-lg hover:shadow-xl transition-shadow"
style={{
animation: "float-pulse 2s ease-in-out infinite",
}}
>
<MessageCircle size={24} />
<Zap size={24} />
</motion.button>

{/* Tooltip */}
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{
opacity: showTooltip ? 1 : 0,
scale: showTooltip ? 1 : 0.8,
}}
transition={{ duration: 0.2 }}
className="absolute bottom-full right-0 mb-3 bg-card border border-border rounded-lg px-4 py-2 text-sm font-semibold text-foreground whitespace-nowrap shadow-lg pointer-events-none"
>
Ask Kivo AI (Coming Soon)
</motion.div>
</motion.div>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
className="absolute bottom-full right-0 mb-3 w-80 bg-card border border-border rounded-lg shadow-lg"
>
<div className="p-4 border-b border-border">
<h3 className="font-semibold text-foreground">AI Agent</h3>
<p className="text-sm text-muted-foreground">Arbitrage Opportunities</p>
</div>
<div className="p-2 max-h-80 overflow-y-auto">
{suggestions.length === 0 && (
<div className="text-center py-4 text-sm text-muted-foreground">
Scanning for opportunities...
</div>
)}
{suggestions.map((suggestion) => (
<div key={suggestion.id} className="p-3 mb-2 rounded-lg bg-background border border-border">
<div className="flex justify-between items-start">
<div>
<p className="font-semibold text-sm">{suggestion.title}</p>
<p className="text-xs text-muted-foreground">{suggestion.description}</p>
</div>
<div className={`flex items-center text-xs ${getRiskColor(suggestion.riskLevel)}`}>
<AlertTriangle size={12} className="mr-1" />
{suggestion.riskLevel}
</div>
</div>
<div className="flex justify-between items-center mt-2">
<p className="text-xs font-bold text-green-500">{suggestion.estimatedSavings}</p>
<Button
className="px-4 py-2 text-xs font-semibold text-white bg-blue-500 rounded hover:bg-blue-600"
onClick={() => handleExecute(suggestion)}
disabled={executingId === suggestion.id}
>
{executingId === suggestion.id ? "Executing..." : "Execute"}
</Button>
</div>
</div>
))}
</div>
</motion.div>
)}
</div>
)
}
74 changes: 74 additions & 0 deletions frontend/components/provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"use client"

import { PrivyProvider, usePrivy } from '@privy-io/react-auth';
import { createAlchemySmartAccountClient } from '@alchemy/aa-alchemy';
import { WalletClientSigner } from '@alchemy/aa-core';
import { sepolia } from 'viem/chains';
import { createContext, useContext, useEffect, useState } from 'react';
import { createWalletClient, custom } from 'viem';

const SmartAccountContext = createContext<{ smartAccount: any | null }>({
smartAccount: null,
});

export const useSmartAccount = () => {
return useContext(SmartAccountContext);
};

export function Providers({ children }: { children: React.ReactNode }) {
return (
<PrivyProvider
appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID || ""}
config={{
loginMethods: ['email', 'google', 'twitter', 'wallet'],
appearance: { theme: 'light' },
embeddedWallets: {
ethereum: {
createOnLogin: 'users-without-wallets',
}
},
}}
>
<AlchemyAccountProvider>
{children}
</AlchemyAccountProvider>
</PrivyProvider>
);
}

function AlchemyAccountProvider({ children }: { children: React.ReactNode }) {
const { user, authenticated } = usePrivy();
const [smartAccount, setSmartAccount] = useState<any | null>(null);

useEffect(() => {
if (authenticated && user && user.wallet) {
createSmartAccount();
}
}, [authenticated, user]);

const createSmartAccount = async () => {
const privyEmbeddedProvider = user.wallet.getEthereumProvider();
const privyWalletClient = createWalletClient({
transport: custom(privyEmbeddedProvider),
});
const privySigner = new WalletClientSigner(privyWalletClient, 'privy');

const chain = sepolia;
const alchemyRpcUrl = `https://eth-sepolia.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_API_KEY}`;

const client = await createAlchemySmartAccountClient({
chain,
owner: privySigner,
rpcUrl: alchemyRpcUrl,
entryPointAddress: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
});

setSmartAccount(client);
};

return (
<SmartAccountContext.Provider value={{ smartAccount }}>
{children}
</SmartAccountContext.Provider>
);
}
135 changes: 41 additions & 94 deletions frontend/hooks/use-ai-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,119 +2,67 @@

import { useWalletStore } from "@/store/wallet-store"
import { useWallet } from "./user-privy-auth"
import { ArbitrageAgent } from "@/lib/AI"
import { AcrossBridgeService } from "@/lib/acrossBridge"
import { useEffect, useMemo, useState } from "react"

export interface AgentSuggestion {
id: string
title: string
description: string
confidence: number
type: "OPTIMIZE_SWAP" | "SECURITY_ALERT" | "COST_SAVING"
type: "OPTIMIZE_SWAP" | "SECURITY_ALERT" | "COST_SAVING" | "ARBITRAGE"
recommendedAction?: {
type: string
params: Record<string, unknown>
}
estimatedSavings?: string
riskLevel?: "low" | "medium" | "high"
opportunity?: any
}

export function useAIAgent() {
const { account } = useWalletStore()
const { getChainBalances, activeChain } = useWallet()

const generateSuggestions = (): AgentSuggestion[] => {
// TODO: Replace with real AI endpoint call
// For now, generate deterministic mock suggestions based on balances

const balances = getChainBalances()
const suggestions: AgentSuggestion[] = []

// Suggestion 1: Optimize swap
if (balances.USDC && Number.parseFloat(balances.USDC) > 100) {
suggestions.push({
id: "sug-1",
title: "Optimize USDC→ETH Swap",
description: `Swap ${balances.USDC} USDC to ETH on Base. Estimated 0.3% fee savings vs DEX average.`,
confidence: 0.92,
type: "OPTIMIZE_SWAP",
recommendedAction: {
type: "swap",
params: { fromToken: "USDC", toToken: "ETH", chainId: activeChain },
},
estimatedSavings: "$3.75",
})
const { smartAccount } = useWallet()
const [suggestions, setSuggestions] = useState<AgentSuggestion[]>([])

const bridgeService = useMemo(() => new AcrossBridgeService(), [])
const agent = useMemo(() => {
if (smartAccount) {
// IMPORTANT: Replace with your actual Alchemy API key
const alchemyApiKey = "YOUR_ALCHEMY_API_KEY"
return new ArbitrageAgent(bridgeService, smartAccount, alchemyApiKey)
}

// Suggestion 2: Cost saving via chain switch
if (activeChain === "polygon") {
suggestions.push({
id: "sug-2",
title: "Move Assets to Base for Lower Fees",
description: "Base network has 90% lower transaction costs. Consider moving liquidity there.",
confidence: 0.85,
type: "COST_SAVING",
riskLevel: "low",
estimatedSavings: "$15-25/month",
})
}

// Suggestion 3: Security alert (mock)
suggestions.push({
id: "sug-3",
title: "Account Security Checkup",
description: "Your wallet is secure. No unusual activity detected. Keep using trusted apps.",
confidence: 0.95,
type: "SECURITY_ALERT",
riskLevel: "low",
})

return suggestions
}

const askAgent = (question: string): AgentSuggestion | null => {
// TODO: Send question to real AI endpoint
// For now, deterministic parsing of common patterns

const lower = question.toLowerCase()

if (lower.includes("optimize") || lower.includes("swap")) {
return {
id: "chat-opt",
title: "Swap Optimization",
description: "I found a better swap route on Base with 0.25% slippage.",
confidence: 0.88,
type: "OPTIMIZE_SWAP",
estimatedSavings: "$2.50",
}
}

if (lower.includes("fee") || lower.includes("cost")) {
return {
id: "chat-cost",
title: "Cost Reduction Strategy",
description: "Switch to Optimism for 70% lower fees on your next transaction.",
confidence: 0.82,
type: "COST_SAVING",
estimatedSavings: "$5.00",
return null
}, [smartAccount, bridgeService])

useEffect(() => {
if (agent) {
const fetchOpportunities = async () => {
const opportunities = await agent.scanOpportunities()
const arbitrageSuggestions = opportunities.map((opp, index) => ({
id: `arb-${index}`,
title: `Arbitrage ${opp.token}`,
description: `From chain ${opp.fromChain} to ${opp.toChain} for ${opp.profitPercent.toFixed(2)}% profit.`,
confidence: 0.95,
type: "ARBITRAGE",
riskLevel: "medium",
estimatedSavings: `$${opp.estimatedProfit.toFixed(2)}`,
opportunity: opp,
}))
setSuggestions(arbitrageSuggestions)
}
}

if (lower.includes("secure") || lower.includes("safety")) {
return {
id: "chat-sec",
title: "Security Status",
description: "Your account is secure. No risks detected. All connections are verified.",
confidence: 0.99,
type: "SECURITY_ALERT",
riskLevel: "low",
}
fetchOpportunities()
const interval = setInterval(fetchOpportunities, agent.config.scanInterval)
return () => clearInterval(interval)
}

return null
}
}, [agent])

const executeAction = async (suggestion: AgentSuggestion) => {
// TODO: Execute real on-chain action via relayer
// Return simulated transaction
if (suggestion.type === "ARBITRAGE" && agent && suggestion.opportunity) {
return await agent.executeArbitrage(suggestion.opportunity)
}
// TODO: Execute other types of actions
return {
txHash: `0x${Math.random().toString(16).slice(2, 66)}`,
status: "pending",
Expand All @@ -123,8 +71,7 @@ export function useAIAgent() {
}

return {
generateSuggestions,
askAgent,
suggestions,
executeAction,
}
}
Loading