This commit is contained in:
Reihan 2025-02-17 15:21:20 +07:00
commit 10673f4d13
142 changed files with 24721 additions and 0 deletions

9
.env Normal file
View File

@ -0,0 +1,9 @@
POSTGRES_DB_URL=postgres://postgres:eMd9hliASrN1yuNOYSk7LtOdlLRnnlnhUF31JKww6zQ=@supabase-pgdb-1:5432/solana
NEXT_PUBLIC_PRIVY_APP_ID=cm6nnbni501avu3k6sfxrc8ts
PRIVY_APP_SECRET=2j5KefComuF3Fm3YbDPh2udnSjfi2uYrSihG7PmXij7JDC45sobRGCoYLSYS21sRyW3De4SbFmw9mtTiyqNtoHub
PRIVY_WALLET_SIGNING_KEY=wallet-auth:MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgDJtG6Ejxyshi9/g1Qb/qWaWpRNEoMcDnF1MhE8r/3SWhRANCAAQl03/3auCjNZm1GHjdpAyw73ZoMlAAVfnXtVlDRSJCheupk6A0PQNdy/NL7mOfspiLxKnvq/mfurI2HGfthVDI
OPENAI_API_KEY="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"
SOLSCAN_API_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkQXQiOjE3MzAwNDg4Njc1ODUsImVtYWlsIjoic2hpYWxvdGhAZ21haWwuY29tIiwiYWN0aW9uIjoidG9rZW4tYXBpIiwiYXBpVmVyc2lvbiI6InYyIiwiaWF0IjoxNzMwMDQ4ODY3fQ.JKKAyBvlfB68zMidRipBfXv2l-a_eEIA3gAU5wxusyQ
BIRDEYE_API_KEY=26e4687aad6f4630a3cd76f85a147657
RPC_URL=https://mainnet.helius-rpc.com/?api-key=22e4b61a-a7f2-47ab-bbc6-48d095a9ceb4
HELIUS_API_KEY=22e4b61a-a7f2-47ab-bbc6-48d095a9ceb4

39
.gitignore vendored Normal file
View File

@ -0,0 +1,39 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

40
README.md Normal file
View File

@ -0,0 +1,40 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/pages/api-reference/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) instead of React pages.
This project uses [`next/font`](https://nextjs.org/docs/pages/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn-pages-router) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/pages/building-your-application/deploying) for more details.

8
SCHEMA.sql Normal file
View File

@ -0,0 +1,8 @@
CREATE TABLE users (
userid TEXT PRIMARY KEY,
publickey TEXT NOT NULL,
last_signedin_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
tg_userid TEXT DEFAULT NULL,
delegated BOOLEAN DEFAULT FALSE
);

28
TODO Normal file
View File

@ -0,0 +1,28 @@
- show user wallet
- show assets
- add export walet hook
- add table to store chat ids for user
- user id
- chat id
- created at
- title
- create chat title ai function
- trim messages from ai node
- clean up code
- transfer function:
- to: address or domain name
- mint: default native sol
- add clear errors sand artifacts
- trade function:
- automatically check which platform if pf or jup
- from
- to
- usd value buy in
- token buy in
- slippage
- implement image upload for pump fun creation. local minio s3
- create pf token

246
app/api/chat/route.js Normal file
View File

@ -0,0 +1,246 @@
import { NextResponse } from "next/server";
import { ulid } from "ulid";
import { Command } from "@langchain/langgraph";
import { isAIMessage } from "@langchain/core/messages";
import checkpointer from "@/server/checkpointer";
import { PrivyClient } from '@privy-io/server-auth';
import { PublicKey } from '@solana/web3.js';
import { SolanaAgentKit } from '@/solana-agent-kit';
import pool from "@/server/db";
import { createPrivyEmbeddedWallet } from '@/lib/solana/PrivyEmbeddedWallet';
import getGraph from "@/server/graph";
export const dynamic = "force-dynamic";
const PRIVY_APP_ID = process.env.NEXT_PUBLIC_PRIVY_APP_ID;
const PRIVY_APP_SECRET = process.env.PRIVY_APP_SECRET;
const PRIVY_WALLET_SIGNING_KEY = process.env.PRIVY_WALLET_SIGNING_KEY;
const RPC_URL = process.env.RPC_URL;
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const HELIUS_API_KEY = process.env.HELIUS_API_KEY;
// TODO
const SYSTEMprompt = `You are a senior Solana software engineer and market data expert. You have access to Solana Defi information. You could also analyze github repos and tweet profiles and posts and search twitter.
Your goal is to give you clean and concise responses to users to help them on your Solana journey. You must analyze carefully the user input, then reason about the possible routes you could take using the tools available to you, dont return information not asked for by the user. All tool calls are visible to the user so DO NOT repeat what is said in the tools.
YOU MUST draw your own conclusions and analysis from the tool responses. Only your analysis must be show the user. Keep it concise and small. If user asks for list of tokens trending the charts will be showm, DO NOT LIST RETURN THEM TO THE USER! KEEP RESPONSES SHORT!`;
export async function POST(req) {
try {
const body = await req.json();
console.log(body);
const { message, thread_id, approveObj = {} } = body;
console.log(thread_id);
if (!thread_id) {
return NextResponse.json({ error: "Unauthorized. Please sign in" }, { status: 401 });
}
if (!message && approveObj == {}) {
return NextResponse.json({ error: "Empty message" }, { status: 401 });
}
const cookieAuthToken = req.cookies.get("privy-id-token");
console.log(cookieAuthToken);
if (!cookieAuthToken) {
console.log("No authentication token found.");
return NextResponse.json({ error: "Unauthorized: No auth token" }, { status: 401 });
}
const PRIVY_SERVER_CLIENT = new PrivyClient(PRIVY_APP_ID, PRIVY_APP_SECRET, {
walletApi: {
authorizationPrivateKey: PRIVY_WALLET_SIGNING_KEY,
},
});
console.log("Verifying auth token...");
const claims = await PRIVY_SERVER_CLIENT.verifyAuthToken(cookieAuthToken.value);
const userId = claims.userId;
console.log("Authenticated user ID:", userId);
const dbRes = await pool.query("SELECT userid, publickey, tg_userid, delegated FROM users WHERE userid = $1", [userId]);
if (dbRes.rows.length === 1) {
const { userid, publickey } = dbRes.rows[0];
console.log("User found in database:", userid, "with public key:", publickey);
const walletAdapter = new createPrivyEmbeddedWallet(
PRIVY_SERVER_CLIENT,
new PublicKey(publickey)
);
const solAgentKit = new SolanaAgentKit(walletAdapter, RPC_URL, {
OPENAI_API_KEY,
HELIUS_API_KEY
});
const graph = getGraph(solAgentKit)
const input = {
role: "user",
content: message ?? "",
};
const config = { configurable: { thread_id } };
let passToGraph = { messages: [input] };
const checkpointerData = await checkpointer.get(config);
if (
typeof checkpointerData?.channel_values === "object" &&
"branch:agent:condition:checkApproval" in checkpointerData.channel_values
) {
console.log("=".repeat(75));
console.log(`${"=".repeat(25)} Graph interrupted for user input ${"=".repeat(25)}`);
if (Object.keys(approveObj).length == 0) {
return NextResponse.json({ error: "Please accept or reject the tools" }, { status: 401 });
}
passToGraph = new Command({ resume: approveObj });
}
const transformStream = new ReadableStream({
async start(controller) {
const textEncoder = new TextEncoder();
try {
for await (const event of await graph.stream(passToGraph, config)) {
if (event.__interrupt__) {
console.log("=".repeat(75));
console.log("=" + "=".repeat(25) + " Interrupted for permission " + "=".repeat(25));
console.log(JSON.stringify(event.__interrupt__));
console.log("=".repeat(75));
const toolCalls = event.__interrupt__[0].value.map((tool) => ({
id: tool.id,
name: tool.name,
args: Object.fromEntries(Object.entries(tool.args).filter(([key]) => key !== "debug")),
}));
console.log(toolCalls);
controller.enqueue(`f:{"messageId":"${event.__interrupt__[0].ns[0]}"}\n`);
const interrupter_tool_id = ulid();
controller.enqueue(
`9:${JSON.stringify({
toolCallId: interrupter_tool_id,
toolName: "interrupter",
args: { toolCalls },
})}\n`,
);
controller.enqueue(
`a:${JSON.stringify({
toolCallId: interrupter_tool_id,
result: "Your input is required to proceed",
})}\n`,
);
controller.enqueue(
`e:${JSON.stringify({
finishReason: "tool-calls",
usage: { promptTokens: 0, completionTokens: 0 },
isContinued: false,
})}\n`,
);
} else if (event.checkApproval) {
continue;
} else if (event.agent) {
console.log("=".repeat(75));
console.log("=" + "=".repeat(25) + " Agent node " + "=".repeat(25));
const message = event.agent.messages[0];
console.log(message);
if (message.content) {
controller.enqueue(`f:{"messageId":"${message.id}"}\n`);
controller.enqueue(`0:${JSON.stringify(message.content)}\n`);
controller.enqueue(
`e:${JSON.stringify({
finishReason: "stop",
usage: {
promptTokens: message.usage_metadata.input_tokens,
completionTokens: message.usage_metadata.output_tokens,
},
isContinued: false,
})}\n`,
);
}
} else if (event.allToolsNode) {
console.log("=".repeat(75));
console.log("=" + "=".repeat(25) + " All tools node " + "=".repeat(25));
console.log(event.allToolsNode);
console.log("=".repeat(75));
for (const toolMessage of event.allToolsNode.messages) {
controller.enqueue(`f:{"messageId":"${toolMessage.id}"}\n`);
controller.enqueue(
textEncoder.encode(
`9:${JSON.stringify({
toolCallId: toolMessage.tool_call_id,
toolName: toolMessage.name,
args: { artifact: toolMessage.artifact },
})}\n`,
),
);
controller.enqueue(
textEncoder.encode(
`a:${JSON.stringify({
toolCallId: toolMessage.tool_call_id,
result: toolMessage.content,
})}\n`,
),
);
controller.enqueue(
`e:${JSON.stringify({
finishReason: "tool-calls",
usage: { promptTokens: 0, completionTokens: 0 },
isContinued: false,
})}\n`,
);
}
}
}
const checkpointerData = await checkpointer.get(config);
if (
typeof checkpointerData?.channel_values === "object" &&
!("branch:agent:condition:checkApproval" in checkpointerData.channel_values)
) {
const lastMessage = checkpointerData?.channel_values?.messages?.at(-1);
const lastMessage_hasToolCalls = lastMessage?.tool_calls?.length > 0;
const lastMessage_isAIMessageFlag = lastMessage ? isAIMessage(lastMessage) : false;
if (lastMessage_isAIMessageFlag && !lastMessage_hasToolCalls) {
console.log("Breaking loop: Last message is an AI message with no tool calls.");
// console.log(checkpointerData);
controller.enqueue(`d:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n`);
}
}
} catch (error) {
controller.enqueue(`3:${JSON.stringify(error.message)}\n`);
controller.error(error);
} finally {
controller.close();
}
},
});
const response = new Response(transformStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
"x-vercel-ai-data-stream": "v1",
},
});
return response;
} else {
console.log("User not found in database.");
return NextResponse.json({ error: "User not found" }, { status: 404 });
}
} catch (e) {
console.log(e);
return NextResponse.json({ error: "Error while processing your request. Please try again later" }, { status: 500 });
}
}

21
components.json Normal file
View File

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": false,
"tailwind": {
"config": "tailwind.config.mjs",
"css": "styles/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

91
components/chat.jsx Normal file
View File

@ -0,0 +1,91 @@
"use client"
import {
ChatInput,
ChatInputSubmit,
ChatInputTextArea
} from "@/components/ui/chat-input"
import {
ChatMessage,
ChatMessageAvatar,
ChatMessageContent
} from "@/components/ui/chat-message"
import { ChatMessageArea } from "@/components/ui/chat-message-area"
import { useChat } from "ai/react"
export function Chat({ className,thread_id,initialMessages, ...props }) {
const {
messages,
input,
handleInputChange,
handleSubmit,
isLoading,
stop
} = useChat({
id: thread_id,
api: "/api/chat",
sendExtraMessageFields: true,
experimental_prepareRequestBody({ messages, id, requestData }) {
console.log(requestData);
return { message: messages[messages.length - 1].content, thread_id, approveObj:requestData };
},
initialMessages: initialMessages,
onFinish: message => {
console.log("onFinish", message, completion);
},
onToolCall: tool => console.log(tool)
})
console.log("messages",messages);
const handleSubmitMessage = () => {
if (isLoading) {
return
}
handleSubmit()
}
return (
<div className="flex-1 flex flex-col h-full overflow-y-auto" {...props}>
<ChatMessageArea scrollButtonAlignment="center">
<div className="max-w-2xl mx-auto w-full px-4 py-8 space-y-4 text-white">
{messages.map(message => {
if (message.role !== "user") {
return (
<ChatMessage key={message.id} id={message.id}>
<ChatMessageAvatar />
<ChatMessageContent content={message.content} toolInvocations={message.toolInvocations} interruptSubmitter={handleSubmit}/>
</ChatMessage>
)
}
if (!message.content) return null;
return (
<ChatMessage
key={message.id}
id={message.id}
variant="bubble"
type="outgoing"
>
<ChatMessageContent content={message.content} />
</ChatMessage>
)
})}
</div>
</ChatMessageArea>
<div className="px-2 py-4 max-w-2xl mx-auto w-full">
<ChatInput
value={input}
onChange={handleInputChange}
onSubmit={handleSubmitMessage}
loading={isLoading}
onStop={stop}
>
<ChatInputTextArea placeholder="Type a message..." />
<ChatInputSubmit />
</ChatInput>
</div>
</div>
)
}

102
components/nav-user.jsx Normal file
View File

@ -0,0 +1,102 @@
"use client"
import {
BadgeCheck,
Bell,
ChevronsUpDown,
CreditCard,
LogOut,
Sparkles
} from "lucide-react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from "@/components/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar
} from "@/components/ui/sidebar"
export function NavUser({ user }) {
const { isMobile } = useSidebar()
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align="start"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Sparkles />
Upgrade to Pro
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<BadgeCheck />
Account
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCard />
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<Bell />
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}

182
components/sidebar-app.jsx Normal file
View File

@ -0,0 +1,182 @@
"use client"
import { Button } from "@/components/ui/button"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarRail
} from "@/components/ui/sidebar"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "@/components/ui/tooltip"
import { NavUser } from "@/components/nav-user"
import { MessageCircle, SquarePen } from "lucide-react"
// This is sample data.
const data = {
user: {
name: "John Doe",
email: "m@example.com",
avatar: "/avatar-1.png"
},
recentChats: [
{
title: "Project Planning Assistant",
date: new Date(2024, 2, 20),
url: "#"
},
{
title: "Code Review Helper",
date: new Date(2024, 2, 19),
url: "#"
},
{
title: "Bug Analysis Chat",
date: new Date(2024, 2, 18),
url: "#"
}
],
lastWeekChats: [
{
title: "API Design Discussion",
date: new Date(2024, 2, 15),
url: "#"
},
{
title: "Database Schema Planning",
date: new Date(2024, 2, 14),
url: "#"
}
],
lastMonthChats: [
{
title: "Architecture Overview",
date: new Date(2024, 1, 28),
url: "#"
},
{
title: "Performance Optimization",
date: new Date(2024, 1, 25),
url: "#"
}
],
previousChats: [
{
title: "Initial Project Setup",
date: new Date(2023, 11, 15),
url: "#"
},
{
title: "Requirements Analysis",
date: new Date(2023, 11, 10),
url: "#"
}
]
}
export function SidebarApp({ ...props }) {
return (
<Sidebar className="border-r-0" {...props}>
<SidebarHeader>
<div className="flex items-center justify-between p-2">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary">
<MessageCircle className="h-5 w-5 text-primary-foreground" />
</div>
<span className="text-lg font-semibold">simple-ai</span>
</div>
{/* New Chat Button */}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="ghost">
<SquarePen className="h-5 w-5" />
<span className="sr-only">New Chat</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>New Chat</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</SidebarHeader>
<SidebarContent>
<div className="flex flex-col gap-4">
{/* Recent Chats */}
<SidebarGroup>
<SidebarGroupLabel>Recent</SidebarGroupLabel>
<SidebarMenu>
{data.recentChats.map(chat => (
<SidebarMenuItem key={chat.title}>
<SidebarMenuButton className="w-full justify-start">
<MessageCircle className="mr-2 h-4 w-4" />
{chat.title}
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
{/* Previous 7 Days */}
<SidebarGroup>
<SidebarGroupLabel>Previous 7 Days</SidebarGroupLabel>
<SidebarMenu>
{data.lastWeekChats.map(chat => (
<SidebarMenuItem key={chat.title}>
<SidebarMenuButton className="w-full justify-start">
<MessageCircle className="mr-2 h-4 w-4" />
{chat.title}
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
{/* Previous 30 Days */}
<SidebarGroup>
<SidebarGroupLabel>Previous 30 Days</SidebarGroupLabel>
<SidebarMenu>
{data.lastMonthChats.map(chat => (
<SidebarMenuItem key={chat.title}>
<SidebarMenuButton className="w-full justify-start">
<MessageCircle className="mr-2 h-4 w-4" />
{chat.title}
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
{/* Previous Years */}
<SidebarGroup>
<SidebarGroupLabel>Previous Years</SidebarGroupLabel>
<SidebarMenu>
{data.previousChats.map(chat => (
<SidebarMenuItem key={chat.title}>
<SidebarMenuButton className="w-full justify-start">
<MessageCircle className="mr-2 h-4 w-4" />
{chat.title}
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
</div>
</SidebarContent>
<SidebarRail />
<SidebarFooter>
<NavUser user={data.user} />
</SidebarFooter>
</Sidebar>
)
}

33
components/ui/avatar.jsx Normal file
View File

@ -0,0 +1,33 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
{...props} />
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props} />
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props} />
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@ -0,0 +1,92 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
const Breadcrumb = React.forwardRef(
({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />
)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props} />
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props} />
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
(<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props} />)
);
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props} />
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
{...props}>
{children ?? <ChevronRight />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({
className,
...props
}) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

48
components/ui/button.jsx Normal file
View File

@ -0,0 +1,48 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring 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 shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
(<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props} />)
);
})
Button.displayName = "Button"
export { Button, buttonVariants }

50
components/ui/card.jsx Normal file
View File

@ -0,0 +1,50 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
{...props} />
))
Card.displayName = "Card"
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props} />
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props} />
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props} />
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props} />
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -0,0 +1,153 @@
"use client";;
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import { useTextareaResize } from "@/hooks/use-textarea-resize";
import { ArrowUpIcon } from "lucide-react";
import { createContext, useContext } from "react";
const ChatInputContext = createContext({});
function ChatInput({
children,
className,
variant = "default",
value,
onChange,
onSubmit,
loading,
onStop,
rows = 1
}) {
const contextValue = {
value,
onChange,
onSubmit,
loading,
onStop,
variant,
rows,
};
return (
(<ChatInputContext.Provider value={contextValue}>
<div
className={cn(variant === "default" &&
"flex flex-col items-end w-full p-2 rounded-2xl border border-input bg-transparent focus-within:ring-1 focus-within:ring-ring focus-within:outline-none", variant === "unstyled" && "flex items-start gap-2 w-full", className)}>
{children}
</div>
</ChatInputContext.Provider>)
);
}
ChatInput.displayName = "ChatInput";
function ChatInputTextArea({
onSubmit: onSubmitProp,
value: valueProp,
onChange: onChangeProp,
className,
variant: variantProp,
...props
}) {
const context = useContext(ChatInputContext);
const value = valueProp ?? context.value ?? "";
const onChange = onChangeProp ?? context.onChange;
const onSubmit = onSubmitProp ?? context.onSubmit;
const rows = context.rows ?? 1;
// Convert parent variant to textarea variant unless explicitly overridden
const variant =
variantProp ?? (context.variant === "default" ? "unstyled" : "default");
const textareaRef = useTextareaResize(value, rows);
const handleKeyDown = (e) => {
if (!onSubmit) {
return;
}
if (e.key === "Enter" && !e.shiftKey) {
if (typeof value !== "string" || value.trim().length === 0) {
return;
}
e.preventDefault();
onSubmit();
}
};
return (
(<Textarea
ref={textareaRef}
{...props}
value={value}
onChange={onChange}
onKeyDown={handleKeyDown}
className={cn(
"max-h-[400px] min-h-0 resize-none overflow-x-hidden",
variant === "unstyled" &&
"border-none focus-visible:ring-0 focus-visible:ring-offset-0 shadow-none",
className
)}
rows={rows} />)
);
}
ChatInputTextArea.displayName = "ChatInputTextArea";
function ChatInputSubmit({
onSubmit: onSubmitProp,
loading: loadingProp,
onStop: onStopProp,
className,
...props
}) {
const context = useContext(ChatInputContext);
const loading = loadingProp ?? context.loading;
const onStop = onStopProp ?? context.onStop;
const onSubmit = onSubmitProp ?? context.onSubmit;
if (loading && onStop) {
return (
(<Button
onClick={onStop}
className={cn("shrink-0 rounded-full p-1.5 h-fit border dark:border-zinc-600", className)}
{...props}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="currentColor"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="Stop">
<title>Stop</title>
<rect x="6" y="6" width="12" height="12" />
</svg>
</Button>)
);
}
const isDisabled =
typeof context.value !== "string" || context.value.trim().length === 0;
return (
(<Button
className={cn("shrink-0 rounded-full p-1.5 h-fit border dark:border-zinc-600", className)}
disabled={isDisabled}
onClick={(event) => {
event.preventDefault();
if (!isDisabled) {
onSubmit?.();
}
}}
{...props}>
<ArrowUpIcon />
</Button>)
);
}
ChatInputSubmit.displayName = "ChatInputSubmit";
export { ChatInput, ChatInputTextArea, ChatInputSubmit };

View File

@ -0,0 +1,56 @@
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import { useScrollToBottom } from "@/hooks/use-scroll-to-bottom";
import { ChevronDown } from "lucide-react";
export function ScrollButton({
onClick,
alignment = "right",
className
}) {
const alignmentClasses = {
left: "left-4",
center: "left-1/2 -translate-x-1/2",
right: "right-4",
};
return (
(<Button
variant="secondary"
size="icon"
className={cn(
"absolute bottom-4 rounded-full shadow-lg hover:bg-secondary",
alignmentClasses[alignment],
className
)}
onClick={onClick}>
<ChevronDown className="h-4 w-4" />
</Button>)
);
}
export function ChatMessageArea({
children,
className,
scrollButtonAlignment = "right"
}) {
const [containerRef, showScrollButton, scrollToBottom] =
useScrollToBottom();
return (
(<ScrollArea className="flex-1 relative">
<div ref={containerRef}>
<div className={cn(className, "min-h-0")}>{children}</div>
</div>
{showScrollButton && (
<ScrollButton
onClick={scrollToBottom}
alignment={scrollButtonAlignment}
className="absolute bottom-4 rounded-full shadow-lg hover:bg-secondary" />
)}
</ScrollArea>)
);
}
ChatMessageArea.displayName = "ChatMessageArea";

View File

@ -0,0 +1,262 @@
import { cn } from "@/lib/utils";
import { MarkdownContent } from "@/components/ui/markdown-content";
import { cva } from "class-variance-authority";
import { SparklesIcon, UserIcon, WrenchIcon } from "lucide-react";
import React from "react";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Button } from "@/components/ui/button";
const chatMessageVariants = cva("flex gap-4 w-full", {
variants: {
variant: {
default: "",
bubble: "",
full: "p-5",
},
type: {
incoming: "justify-start mr-auto",
outgoing: "justify-end ml-auto",
tool: "justify-end ml-auto",
},
},
compoundVariants: [
{
variant: "full",
type: "outgoing",
className: "bg-muted",
},
{
variant: "full",
type: "incoming",
className: "bg-background",
},
{
variant: "full",
type: "tool",
className: "bg-accent",
},
],
defaultVariants: {
variant: "default",
type: "incoming",
},
});
const ChatMessageContext = React.createContext(null);
const useChatMessage = () => {
const context = React.useContext(ChatMessageContext);
return context;
};
const ChatMessage = React.forwardRef((
{
className,
variant = "default",
type = "incoming",
id,
children,
...props
},
ref,
) => {
return (
<ChatMessageContext.Provider value={{ variant, type, id }}>
<div
ref={ref}
className={cn(chatMessageVariants({ variant, type, className }))}
{...props}>
{children}
</div>
</ChatMessageContext.Provider>
);
});
ChatMessage.displayName = "ChatMessage";
// Avatar component
const chatMessageAvatarVariants = cva(
"w-8 h-8 flex items-center rounded-full justify-center ring-1 shrink-0 bg-transparent overflow-hidden",
{
variants: {
type: {
incoming: "ring-border",
outgoing: "ring-muted-foreground/30",
tool: "ring-accent-foreground/30",
},
},
defaultVariants: {
type: "incoming",
},
}
);
const ChatMessageAvatar = React.forwardRef(({ className, icon: iconProps, imageSrc, ...props }, ref) => {
const context = useChatMessage();
const type = context?.type ?? "incoming";
const icon =
iconProps ?? (type === "incoming" ? <SparklesIcon /> : type === "tool" ? <WrenchIcon /> : <UserIcon />);
return (
<div
ref={ref}
className={cn(chatMessageAvatarVariants({ type, className }))}
{...props}>
{imageSrc ? (
<img src={imageSrc} alt="Avatar" className="h-full w-full object-cover" />
) : (
<div className="translate-y-px [&_svg]:size-4 [&_svg]:shrink-0">
{icon}
</div>
)}
</div>
);
});
ChatMessageAvatar.displayName = "ChatMessageAvatar";
// Content component
const chatMessageContentVariants = cva("flex flex-col gap-2", {
variants: {
variant: {
default: "",
bubble: "rounded-xl px-3 py-2",
full: "",
},
type: {
incoming: "",
outgoing: "",
tool: "",
},
},
compoundVariants: [
{
variant: "bubble",
type: "incoming",
className: "bg-secondary text-secondary-foreground",
},
{
variant: "bubble",
type: "outgoing",
className: "bg-primary text-primary-foreground",
},
{
variant: "bubble",
type: "tool",
className: "bg-accent text-accent-foreground",
},
],
defaultVariants: {
variant: "default",
type: "incoming",
},
});
const InterrupterCard = ({ invocationId, args, onDecision }) => {
const [decisions, setDecisions] = React.useState(
args.toolCalls.reduce((acc, tool) => ({ ...acc, [tool.id]: null }), {})
);
console.log("decisions",decisions);
const handleDecision = (key, decision) => {
const newDecisions = { ...decisions, [key]: decision };
setDecisions(newDecisions);
if (Object.values(newDecisions).every(value => value !== null)) {
onDecision(newDecisions);
}
};
console.log("args",args);
return (
<Card>
<CardHeader>
<CardTitle>Interrupter</CardTitle>
<CardDescription>Invocation ID: {invocationId}</CardDescription>
</CardHeader>
<CardContent>
{args['toolCalls'].map((tool) => (
<div key={tool.id} className="flex justify-between items-center mb-2">
<div>
<p className="font-semibold">Function: {tool.name}</p>
<ul className="list-disc">
{Object.entries(tool.args).map(([key, value]) => (
<li key={key}>
{key}: {value}
</li>
))}
</ul>
</div>
<div>
<Button variant="outline" onClick={() => handleDecision(tool.id, true)} disabled={decisions[tool.id] !== null} className="mr-2">
Approve
</Button>
<Button variant="outline" onClick={() => handleDecision(tool.id, false)} disabled={decisions[tool.id] !== null}>
Deny
</Button>
</div>
</div>
))}
</CardContent>
</Card>
);
};
const ChatMessageContent = React.forwardRef(({ className, content, toolInvocations, id: idProp,interruptSubmitter, children, ...props }, ref) => {
const context = useChatMessage();
const variant = context?.variant ?? "default";
const type = context?.type ?? "incoming";
const id = idProp ?? context?.id ?? "";
return (
<div ref={ref} className={cn(chatMessageContentVariants({ variant, type, className }))} {...props}>
{content.length > 0 && <MarkdownContent id={id} content={content} />}
{toolInvocations && toolInvocations.length > 0 && (
<div className="space-y-4 mt-4">
{toolInvocations.map((toolInvocation) => (
toolInvocation.toolName === "interrupter" ? (
<InterrupterCard
key={toolInvocation.toolCallId}
invocationId={toolInvocation.toolCallId}
args={toolInvocation.args}
onDecision={(decisions) => {
console.log(decisions);
interruptSubmitter(null,{
data: decisions,
allowEmptySubmit: true,
});
}
}
/>
) : (
<Card key={toolInvocation.toolCallId}>
<CardHeader>
<CardTitle>{toolInvocation.toolName}</CardTitle>
<CardDescription>Invocation ID: {toolInvocation.toolCallId}</CardDescription>
</CardHeader>
<CardContent>
<p>Function: {toolInvocation.toolName}</p>
<p>Parameters: {JSON.stringify(toolInvocation.args)}</p>
</CardContent>
<CardFooter>
<p>Status: {toolInvocation.state}</p>
</CardFooter>
</Card>
)
))}
</div>
)}
{children}
</div>
);
});
ChatMessageContent.displayName = "ChatMessageContent";
export { ChatMessage, ChatMessageAvatar, ChatMessageContent };

94
components/ui/dialog.jsx Normal file
View File

@ -0,0 +1,94 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props} />
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}>
{children}
<DialogPrimitive.Close
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}) => (
<div
className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
{...props} />
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}) => (
<div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props} />
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props} />
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props} />
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@ -0,0 +1,158 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props} />
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props} />
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
inset && "pl-8",
className
)}
{...props} />
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...props} />
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props} />
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}) => {
return (
(<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props} />)
);
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

19
components/ui/input.jsx Normal file
View File

@ -0,0 +1,19 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
return (
(<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props} />)
);
})
Input.displayName = "Input"
export { Input }

16
components/ui/label.jsx Normal file
View File

@ -0,0 +1,16 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@ -0,0 +1,339 @@
import { cn } from "@/lib/utils";
import { marked } from "marked";
import { Suspense, isValidElement, memo, useMemo } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
const DEFAULT_PRE_BLOCK_CLASS =
"my-4 overflow-x-auto w-fit rounded-xl bg-zinc-950 text-zinc-50 dark:bg-zinc-900 border border-border p-4";
const extractTextContent = node => {
if (typeof node === "string") {
return node;
}
if (Array.isArray(node)) {
return node.map(extractTextContent).join("");
}
if (isValidElement(node)) {
return extractTextContent(node.props.children);
}
return "";
};
const HighlightedPre = memo(async ({
children,
className,
language,
...props
}) => {
const { codeToTokens, bundledLanguages } = await import("shiki");
const code = extractTextContent(children);
if (!(language in bundledLanguages)) {
return (
(<pre {...props} className={cn(DEFAULT_PRE_BLOCK_CLASS, className)}>
<code className="whitespace-pre-wrap">{children}</code>
</pre>)
);
}
const { tokens } = await codeToTokens(code, {
lang: language,
themes: {
light: "github-dark",
dark: "github-dark",
},
});
return (
(<pre {...props} className={cn(DEFAULT_PRE_BLOCK_CLASS, className)}>
<code className="whitespace-pre-wrap">
{tokens.map((line, lineIndex) => (
<span
key={`line-${
// biome-ignore lint/suspicious/noArrayIndexKey: Needed for react key
lineIndex
}`}>
{line.map((token, tokenIndex) => {
const style =
typeof token.htmlStyle === "string"
? undefined
: token.htmlStyle;
return (
(<span
key={`token-${
// biome-ignore lint/suspicious/noArrayIndexKey: Needed for react key
tokenIndex
}`}
style={style}>
{token.content}
</span>)
);
})}
{lineIndex !== tokens.length - 1 && "\n"}
</span>
))}
</code>
</pre>)
);
});
HighlightedPre.displayName = "HighlightedPre";
const CodeBlock = ({
children,
language,
className,
...props
}) => {
return (
(<Suspense
fallback={
<pre {...props} className={cn(DEFAULT_PRE_BLOCK_CLASS, className)}>
<code className="whitespace-pre-wrap">{children}</code>
</pre>
}>
<HighlightedPre language={language} {...props}>
{children}
</HighlightedPre>
</Suspense>)
);
};
CodeBlock.displayName = "CodeBlock";
const components = {
h1: ({
children,
...props
}) => (
<h1 className="mt-2 scroll-m-20 text-4xl font-bold" {...props}>
{children}
</h1>
),
h2: ({
children,
...props
}) => (
<h2
className="mt-8 scroll-m-20 border-b pb-2 text-2xl font-semibold tracking-tight first:mt-0"
{...props}>
{children}
</h2>
),
h3: ({
children,
...props
}) => (
<h3
className="mt-4 scroll-m-20 text-xl font-semibold tracking-tight"
{...props}>
{children}
</h3>
),
h4: ({
children,
...props
}) => (
<h4
className="mt-4 scroll-m-20 text-lg font-semibold tracking-tight"
{...props}>
{children}
</h4>
),
h5: ({
children,
...props
}) => (
<h5
className="mt-4 scroll-m-20 text-lg font-semibold tracking-tight"
{...props}>
{children}
</h5>
),
h6: ({
children,
...props
}) => (
<h6
className="mt-4 scroll-m-20 text-base font-semibold tracking-tight"
{...props}>
{children}
</h6>
),
p: ({
children,
...props
}) => (
<p className="leading-6 [&:not(:first-child)]:mt-4" {...props}>
{children}
</p>
),
strong: ({
children,
...props
}) => (
<span className="font-semibold" {...props}>
{children}
</span>
),
a: ({
children,
...props
}) => (
<a
className="font-medium underline underline-offset-4"
target="_blank"
rel="noreferrer"
{...props}>
{children}
</a>
),
ol: ({
children,
...props
}) => (
<ol className="my-4 ml-6 list-decimal" {...props}>
{children}
</ol>
),
ul: ({
children,
...props
}) => (
<ul className="my-4 ml-6 list-disc" {...props}>
{children}
</ul>
),
li: ({
children,
...props
}) => (
<li className="mt-2" {...props}>
{children}
</li>
),
blockquote: ({
children,
...props
}) => (
<blockquote className="mt-4 border-l-2 pl-6 italic" {...props}>
{children}
</blockquote>
),
hr: (props) => (
<hr className="my-4 md:my-8" {...props} />
),
table: ({
children,
...props
}) => (
<div className="my-6 w-full overflow-y-auto">
<table
className="relative w-full overflow-hidden border-none text-sm"
{...props}>
{children}
</table>
</div>
),
tr: ({
children,
...props
}) => (
<tr className="last:border-b-none m-0 border-b" {...props}>
{children}
</tr>
),
th: ({
children,
...props
}) => (
<th
className="px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right"
{...props}>
{children}
</th>
),
td: ({
children,
...props
}) => (
<td
className="px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right"
{...props}>
{children}
</td>
),
img: ({
alt,
...props
}) => (
// biome-ignore lint/a11y/useAltText: alt is not required
(<img className="rounded-md" alt={alt} {...props} />)
),
code: ({ children, node, className, ...props }) => {
const match = /language-(\w+)/.exec(className || "");
if (match) {
return (
(<CodeBlock language={match[1]} className={className} {...props}>
{children}
</CodeBlock>)
);
}
return (
(<code
className={cn("rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm", className)}
{...props}>
{children}
</code>)
);
},
pre: ({ children }) => <>{children}</>,
};
function parseMarkdownIntoBlocks(markdown) {
if (!markdown) {
return [];
}
const tokens = marked.lexer(markdown);
return tokens.map((token) => token.raw);
}
const MemoizedMarkdownBlock = memo(({
content,
className
}) => {
return (
(<ReactMarkdown remarkPlugins={[remarkGfm]} components={components} className={className}>
{content}
</ReactMarkdown>)
);
}, (prevProps, nextProps) => {
if (prevProps.content !== nextProps.content) {
return false;
}
return true;
});
MemoizedMarkdownBlock.displayName = "MemoizedMarkdownBlock";
export const MarkdownContent = memo(({
content,
id,
className
}) => {
const blocks = useMemo(() => parseMarkdownIntoBlocks(content || ""), [content]);
return blocks.map((block, index) => (
<MemoizedMarkdownBlock
content={block}
className={className}
key={`${id}-block_${
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
index
}`} />
));
});
MarkdownContent.displayName = "MarkdownContent";

View File

@ -0,0 +1,29 @@
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const RadioGroup = React.forwardRef(({ className, ...props }, ref) => {
return (<RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />);
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef(({ className, ...props }, ref) => {
return (
(<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-3.5 w-3.5 fill-primary" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>)
);
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }

View File

@ -0,0 +1,38 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

119
components/ui/select.jsx Normal file
View File

@ -0,0 +1,119 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn("p-1", position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]")}>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props} />
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props} />
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@ -0,0 +1,23 @@
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef((
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props} />
))
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

108
components/ui/sheet.jsx Normal file
View File

@ -0,0 +1,108 @@
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva } from "class-variance-authority";
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref} />
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
)
const SheetContent = React.forwardRef(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
<SheetPrimitive.Close
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
{children}
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}) => (
<div
className={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
{...props} />
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
...props
}) => (
<div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props} />
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props} />
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props} />
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

620
components/ui/sidebar.jsx Normal file
View File

@ -0,0 +1,620 @@
"use client";
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva } from "class-variance-authority";
import { PanelLeft } from "lucide-react"
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import { Sheet, SheetContent } from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
const SIDEBAR_COOKIE_NAME = "sidebar_state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
const SidebarContext = React.createContext(null)
function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
}
const SidebarProvider = React.forwardRef((
{
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
},
ref
) => {
const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = React.useState(false)
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen)
const open = openProp ?? _open
const setOpen = React.useCallback((value) => {
const openState = typeof value === "function" ? value(open) : value
if (setOpenProp) {
setOpenProp(openState)
} else {
_setOpen(openState)
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
}, [setOpenProp, open])
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile
? setOpenMobile((open) => !open)
: setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile])
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault()
toggleSidebar()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown);
}, [toggleSidebar])
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed"
const contextValue = React.useMemo(() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}), [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar])
return (
(<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style
}
}
className={cn(
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
className
)}
ref={ref}
{...props}>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>)
);
})
SidebarProvider.displayName = "SidebarProvider"
const Sidebar = React.forwardRef((
{
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
},
ref
) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
if (collapsible === "none") {
return (
(<div
className={cn(
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
className
)}
ref={ref}
{...props}>
{children}
</div>)
);
}
if (isMobile) {
return (
(<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE
}
}
side={side}>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>)
);
}
return (
(<div
ref={ref}
className="group peer hidden text-sidebar-foreground md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
"relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
)} />
<div
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
className
)}
{...props}>
<div
data-sidebar="sidebar"
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow">
{children}
</div>
</div>
</div>)
);
})
Sidebar.displayName = "Sidebar"
const SidebarTrigger = React.forwardRef(({ className, onClick, ...props }, ref) => {
const { toggleSidebar } = useSidebar()
return (
(<Button
ref={ref}
data-sidebar="trigger"
variant="ghost"
size="icon"
className={cn("h-7 w-7", className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
}}
{...props}>
<PanelLeft />
<span className="sr-only">Toggle Sidebar</span>
</Button>)
);
})
SidebarTrigger.displayName = "SidebarTrigger"
const SidebarRail = React.forwardRef(({ className, ...props }, ref) => {
const { toggleSidebar } = useSidebar()
return (
(<button
ref={ref}
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
)}
{...props} />)
);
})
SidebarRail.displayName = "SidebarRail"
const SidebarInset = React.forwardRef(({ className, ...props }, ref) => {
return (
(<main
ref={ref}
className={cn(
"relative flex min-h-svh flex-1 flex-col bg-background",
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className
)}
{...props} />)
);
})
SidebarInset.displayName = "SidebarInset"
const SidebarInput = React.forwardRef(({ className, ...props }, ref) => {
return (
(<Input
ref={ref}
data-sidebar="input"
className={cn(
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
className
)}
{...props} />)
);
})
SidebarInput.displayName = "SidebarInput"
const SidebarHeader = React.forwardRef(({ className, ...props }, ref) => {
return (
(<div
ref={ref}
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props} />)
);
})
SidebarHeader.displayName = "SidebarHeader"
const SidebarFooter = React.forwardRef(({ className, ...props }, ref) => {
return (
(<div
ref={ref}
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props} />)
);
})
SidebarFooter.displayName = "SidebarFooter"
const SidebarSeparator = React.forwardRef(({ className, ...props }, ref) => {
return (
(<Separator
ref={ref}
data-sidebar="separator"
className={cn("mx-2 w-auto bg-sidebar-border", className)}
{...props} />)
);
})
SidebarSeparator.displayName = "SidebarSeparator"
const SidebarContent = React.forwardRef(({ className, ...props }, ref) => {
return (
(<div
ref={ref}
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
)}
{...props} />)
);
})
SidebarContent.displayName = "SidebarContent"
const SidebarGroup = React.forwardRef(({ className, ...props }, ref) => {
return (
(<div
ref={ref}
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props} />)
);
})
SidebarGroup.displayName = "SidebarGroup"
const SidebarGroupLabel = React.forwardRef(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "div"
return (
(<Comp
ref={ref}
data-sidebar="group-label"
className={cn(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
)}
{...props} />)
);
})
SidebarGroupLabel.displayName = "SidebarGroupLabel"
const SidebarGroupAction = React.forwardRef(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
(<Comp
ref={ref}
data-sidebar="group-action"
className={cn(
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props} />)
);
})
SidebarGroupAction.displayName = "SidebarGroupAction"
const SidebarGroupContent = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props} />
))
SidebarGroupContent.displayName = "SidebarGroupContent"
const SidebarMenu = React.forwardRef(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props} />
))
SidebarMenu.displayName = "SidebarMenu"
const SidebarMenuItem = React.forwardRef(({ className, ...props }, ref) => (
<li
ref={ref}
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props} />
))
SidebarMenuItem.displayName = "SidebarMenuItem"
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const SidebarMenuButton = React.forwardRef((
{
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
},
ref
) => {
const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar()
const button = (
<Comp
ref={ref}
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props} />
)
if (!tooltip) {
return button
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
}
}
return (
(<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip} />
</Tooltip>)
);
})
SidebarMenuButton.displayName = "SidebarMenuButton"
const SidebarMenuAction = React.forwardRef(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
(<Comp
ref={ref}
data-sidebar="menu-action"
className={cn(
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
className
)}
{...props} />)
);
})
SidebarMenuAction.displayName = "SidebarMenuAction"
const SidebarMenuBadge = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="menu-badge"
className={cn(
"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props} />
))
SidebarMenuBadge.displayName = "SidebarMenuBadge"
const SidebarMenuSkeleton = React.forwardRef(({ className, showIcon = false, ...props }, ref) => {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, [])
return (
(<div
ref={ref}
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}>
{showIcon && (
<Skeleton className="size-4 rounded-md" data-sidebar="menu-skeleton-icon" />
)}
<Skeleton
className="h-4 max-w-[--skeleton-width] flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width
}
} />
</div>)
);
})
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
const SidebarMenuSub = React.forwardRef(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu-sub"
className={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props} />
))
SidebarMenuSub.displayName = "SidebarMenuSub"
const SidebarMenuSubItem = React.forwardRef(({ ...props }, ref) => <li ref={ref} {...props} />)
SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
const SidebarMenuSubButton = React.forwardRef(
({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
(<Comp
ref={ref}
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props} />)
);
}
)
SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
}

View File

@ -0,0 +1,14 @@
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}) {
return (
(<div
className={cn("animate-pulse rounded-md bg-primary/10", className)}
{...props} />)
);
}
export { Skeleton }

View File

@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef(({ className, ...props }, ref) => {
return (
(<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props} />)
);
})
Textarea.displayName = "Textarea"
export { Textarea }

28
components/ui/tooltip.jsx Normal file
View File

@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props} />
</TooltipPrimitive.Portal>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

14
eslint.config.mjs Normal file
View File

@ -0,0 +1,14 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [...compat.extends("next/core-web-vitals")];
export default eslintConfig;

19
hooks/use-mobile.jsx Normal file
View File

@ -0,0 +1,19 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange);
}, [])
return !!isMobile
}

View File

@ -0,0 +1,111 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
export function useScrollToBottom() {
const containerRef = useRef(null);
const [showScrollButton, setShowScrollButton] = useState(false);
const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
const isUserScrolling = useRef(false);
const isGrowing = useRef(false);
const getViewport = useCallback((element) => {
return element?.closest("[data-radix-scroll-area-viewport]");
}, []);
const isAtBottom = useCallback((viewport) => {
const { scrollTop, scrollHeight, clientHeight } = viewport;
return Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
}, []);
const updateScrollState = useCallback((viewport) => {
const { scrollHeight, clientHeight } = viewport;
const hasScrollableContent = scrollHeight > clientHeight;
const atBottom = isAtBottom(viewport);
setShowScrollButton(hasScrollableContent && !atBottom);
if (!isUserScrolling.current) {
setShouldAutoScroll(atBottom);
}
}, [isAtBottom]);
useEffect(() => {
const container = containerRef.current;
const viewport = getViewport(container);
if (!container || !viewport) {
return;
}
updateScrollState(viewport);
const handleScroll = () => {
if (!isUserScrolling.current) {
updateScrollState(viewport);
}
};
const handleTouchStart = () => {
isUserScrolling.current = true;
};
const handleTouchEnd = () => {
isUserScrolling.current = false;
updateScrollState(viewport);
};
let growthTimeout;
const observer = new MutationObserver(() => {
isGrowing.current = true;
window.clearTimeout(growthTimeout);
if (shouldAutoScroll && !isUserScrolling.current) {
viewport.scrollTo({
top: viewport.scrollHeight,
behavior: "instant",
});
}
updateScrollState(viewport);
growthTimeout = window.setTimeout(() => {
isGrowing.current = false;
}, 100);
});
viewport.addEventListener("scroll", handleScroll, { passive: true });
viewport.addEventListener("touchstart", handleTouchStart);
viewport.addEventListener("touchend", handleTouchEnd);
observer.observe(container, {
childList: true,
subtree: true,
attributes: true,
characterData: true,
});
return () => {
window.clearTimeout(growthTimeout);
observer.disconnect();
viewport.removeEventListener("scroll", handleScroll);
viewport.removeEventListener("touchstart", handleTouchStart);
viewport.removeEventListener("touchend", handleTouchEnd);
};
}, [getViewport, updateScrollState, shouldAutoScroll]);
const scrollToBottom = () => {
const viewport = getViewport(containerRef.current);
if (!viewport) {
return;
}
setShouldAutoScroll(true);
viewport.scrollTo({
top: viewport.scrollHeight,
behavior: isGrowing.current ? "instant" : "smooth",
});
};
return [containerRef, showScrollButton, scrollToBottom];
}

View File

@ -0,0 +1,35 @@
"use client";;
import { useLayoutEffect, useRef } from "react";
export function useTextareaResize(
value,
rows = 1,
) {
const textareaRef = useRef(null);
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useLayoutEffect(() => {
const textArea = textareaRef.current;
if (textArea) {
// Get the line height to calculate minimum height based on rows
const computedStyle = window.getComputedStyle(textArea);
const lineHeight = Number.parseInt(computedStyle.lineHeight, 10) || 20;
const padding =
Number.parseInt(computedStyle.paddingTop, 10) +
Number.parseInt(computedStyle.paddingBottom, 10);
// Calculate minimum height based on rows
const minHeight = lineHeight * rows + padding;
// Reset height to auto first to get the correct scrollHeight
textArea.style.height = "0px";
const scrollHeight = Math.max(textArea.scrollHeight, minHeight);
// Set the final height
textArea.style.height = `${scrollHeight + 2}px`;
}
}, [textareaRef, value, rows]);
return textareaRef;
}

7
jsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./*"]
}
}
}

View File

@ -0,0 +1,46 @@
import { Transaction } from '@solana/web3.js';
function createPrivyEmbeddedWallet(privyClient, publicKey) {
const secretKey = new Uint8Array(0);
async function signTransaction(transaction) {
try {
const request = {
address: publicKey.toBase58(),
chainType: 'solana',
method: 'signTransaction',
params: { transaction },
};
const { data } = await privyClient.walletApi.rpc(request);
return data.signedTransaction;
} catch (error) {
throw new Error(
`Failed to sign transaction: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
async function signAllTransactions(transactions) {
try {
return transactions.map((tx) => {
if (tx instanceof Transaction) {
tx.partialSign({ publicKey });
}
return tx;
});
} catch (error) {
throw new Error(
`Failed to sign transactions: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
}
return {
publicKey,
secretKey,
signTransaction,
signAllTransactions,
};
}
export { createPrivyEmbeddedWallet };

13
lib/utils.js Normal file
View File

@ -0,0 +1,13 @@
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge"
export function cn(...inputs) {
return twMerge(clsx(inputs));
}
export function chunkArray(arr, chunkSize) {
const chunks = []
for (let i = 0; i < arr.length; i += chunkSize) {
chunks.push(arr.slice(i, i + chunkSize))
}
return chunks
}

6
next.config.mjs Normal file
View File

@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
};
export default nextConfig;

14657
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

60
package.json Normal file
View File

@ -0,0 +1,60 @@
{
"name": "my-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@bonfida/spl-name-service": "^3.0.8",
"@radix-ui/react-scroll-area": "^1.2.3",
"marked": "^15.0.7",
"react-markdown": "^9.0.3",
"remark-gfm": "^4.0.1",
"shiki": "^2.4.1",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@langchain/langgraph": "^0.2.46",
"@langchain/langgraph-checkpoint-postgres": "^0.0.3",
"@privy-io/react-auth": "^2.3.0",
"@privy-io/server-auth": "^1.18.5",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-radio-group": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-slot": "^1.1.2",
"@solana/spl-token": "^0.4.12",
"@solana/web3.js": "^1.98.0",
"@tiplink/api": "^0.3.1",
"ai": "^4.1.41",
"bs58": "^6.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"langchain": "^0.3.15",
"lucide-react": "^0.474.0",
"next": "15.1.6",
"openai": "^4.83.0",
"pg": "^8.13.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"rpc-websockets": "7.11.0",
"sonner": "^1.7.4",
"swr": "^2.3.2",
"tailwind-merge": "^3.0.1",
"tailwindcss-animate": "^1.0.7",
"ulid": "^2.3.0",
"use-sync-external-store": "^1.4.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"eslint": "^9",
"eslint-config-next": "15.1.6",
"postcss": "^8",
"tailwindcss": "^3.4.1"
}
}

41
pages/_app.js Normal file
View File

@ -0,0 +1,41 @@
import "@/styles/globals.css";
import {PrivyProvider} from '@privy-io/react-auth';
import {toSolanaWalletConnectors} from '@privy-io/react-auth/solana';
export default function App({ Component, pageProps }) {
const solanaConnectors = toSolanaWalletConnectors({
shouldAutoConnect: true,
});
return (
<PrivyProvider
appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID || ""}
config={{
appearance: {
theme: 'dark',
landingHeader: 'Onchain AI at your fingertips',
accentColor: '',
showWalletLoginFirst: true,
logo: '/images/logooo.png',
walletChainType: 'solana-only',
},
externalWallets: {
solana: {
connectors: solanaConnectors
}
},
embeddedWallets: {
solana: {
createOnLogin: 'all-users',
},
},
solanaClusters: [
{
name: 'mainnet-beta',
rpcUrl: 'https://api.mainnet-beta.solana.com',
}
]
}} >
<Component {...pageProps} />
</PrivyProvider>
);
}

13
pages/_document.js Normal file
View File

@ -0,0 +1,13 @@
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="en">
<Head />
<body className="antialiased">
<Main />
<NextScript />
</body>
</Html>
);
}

86
pages/api/test.js Normal file
View File

@ -0,0 +1,86 @@
import { PrivyClient } from '@privy-io/server-auth';
import { PublicKey } from '@solana/web3.js';
import { SolanaAgentKit } from '@/solana-agent-kit'
import pool from "@/server/db";
import { createPrivyEmbeddedWallet } from '@/lib/solana/PrivyEmbeddedWallet';
import {TOKENS} from "@/solana-agent-kit/constants"
const PRIVY_APP_ID = process.env.NEXT_PUBLIC_PRIVY_APP_ID;
const PRIVY_APP_SECRET = process.env.PRIVY_APP_SECRET;
const PRIVY_WALLET_SIGNING_KEY = process.env.PRIVY_WALLET_SIGNING_KEY;
const RPC_URL = process.env.RPC_URL;
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const HELIUS_API_KEY = process.env.HELIUS_API_KEY;
export default async function handler(req, res) {
console.log("Received request:", req.method, req.url);
const cookieAuthToken = req.cookies["privy-id-token"];
if (!cookieAuthToken) {
console.log("No authentication token found.");
return res.status(401).json({ error: "Unauthorized: No auth token" });
}
try {
const PRIVY_SERVER_CLIENT = new PrivyClient(PRIVY_APP_ID, PRIVY_APP_SECRET, {
walletApi: {
authorizationPrivateKey: PRIVY_WALLET_SIGNING_KEY,
},
});
console.log("Verifying auth token...");
const claims = await PRIVY_SERVER_CLIENT.verifyAuthToken(cookieAuthToken);
const userId = claims.userId;
console.log("Authenticated user ID:", userId);
const dbRes = await pool.query("SELECT userid, publickey, tg_userid, delegated FROM users WHERE userid = $1", [userId]);
if (dbRes.rows.length === 1) {
const { userid, publickey } = dbRes.rows[0];
console.log("User found in database:", userid, "with public key:", publickey);
const walletAdapter = new createPrivyEmbeddedWallet(
PRIVY_SERVER_CLIENT,
new PublicKey(publickey)
);
const agent = new SolanaAgentKit(walletAdapter, RPC_URL, {
OPENAI_API_KEY,
HELIUS_API_KEY
});
console.log("Performing trade...");
// working
// const signature = await agent.transfer(new PublicKey('FM3ZZEPrfqwwVAnfBhfXq2ExitG58dUfiymcrYd9SLeD'),0.001)
// const signature = await agent.createImage("happy nature")
// const signature = await agent.getWalletAddress()
// const signature = await agent.createGibworkTask("Testing my agent","Testing my agent","Testing my agent",["TEST"],"So11111111111111111111111111111111111111112",0.055)
// {
// status: 'success',
// taskId: 'eae20ca6-019f-4a8c-a1aa-83fb7727a739',
// signature: '4ezTKYMcnopRimFxRsgydrgMndy9WcojsczeCAhZR64psqS1DMQ582qMRKvmn75P1QeaVYBX2aG4HHVu3G2B7XuK'
// }
// const signature = await agent.getAllAssetsbyOwner('ADG9zh4E2CoieEAqQn8YamhLhs5pkUwEysaD4LTqvvTx',20)
// const signature = await agent.heliusParseTransactions('52Cy4FTEnS5KfQg7WuizFa2Yu6ieYAAPLwwvvqQJzh5J7V8vycMiB8yffkfHoNZvrKNc5fbEZCW8iyo4Pq4F4Xeu')
// const signature = await agent.fetchTokenPrice('B5WTLaRwaUQpKk7ir1wniNB6m5o8GgMrimhKMYan2R6B')
// const signature = await agent.stake(0.0001)
// const signature = await agent.trade(TOKENS.USDC,0.0001,TOKENS.SOL)
// write my own implementation
// const signature = await agent.launchPumpFunToken('test','test','test','https://pump.mypinata.cloud/ipfs/QmRCWhURvy2Bnt3CbKjZVvEA5QcGy7iMd1XcmVv95V3fwP?img-width=800&img-dpr=2&img-onerror=redirect')
// const signature = await agent.fetchTokenDetailedReport(TOKENS.BONK)
// const signature = await agent.fetchTokenReportSummary(TOKENS.BONK)
// const signature = await agent.resolveSolDomain("balls.sol")
// const signature = await agent.closeEmptyTokenAccounts()
console.log("Trade completed with signature:", signature);
return res.status(200).json({ success: true, signature });
} else {
console.log("User not found in database.");
return res.status(404).json({ error: "User not found" });
}
} catch (error) {
console.error("Error during request handling:", error);
return res.status(500).json({ error: "Internal Server Error", details: error.message });
}
}

121
pages/chat/[chatid].js Normal file
View File

@ -0,0 +1,121 @@
import pool from "@/server/db";
import { PrivyClient } from "@privy-io/server-auth";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
} from "@/components/ui/breadcrumb";
import { Separator } from "@/components/ui/separator";
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { Chat } from "@/components/chat";
import { SidebarApp } from "@/components/sidebar-app";
import { checkpointToVercelAI } from "@/server/checkpointToVercelAI";
export const getServerSideProps = async ({ req,params }) => {
const cookieAuthToken = req.cookies["privy-id-token"];
if (!cookieAuthToken) {
return {
redirect: {
destination: "/onboarding",
permanent: false,
},
};
}
const PRIVY_APP_ID = process.env.NEXT_PUBLIC_PRIVY_APP_ID;
const PRIVY_APP_SECRET = process.env.PRIVY_APP_SECRET;
const client = new PrivyClient(PRIVY_APP_ID, PRIVY_APP_SECRET);
try {
const claims = await client.verifyAuthToken(cookieAuthToken);
const userId = claims.userId;
const dbRes = await pool.query("SELECT userid, publickey, tg_userid, delegated FROM users WHERE userid = $1", [userId]);
if (dbRes.rows.length === 0) {
return {
redirect: {
destination: "/onboarding",
permanent: false,
},
};
}
const { delegated } = dbRes.rows[0];
if (!delegated) {
return {
redirect: {
destination: "/onboarding",
permanent: false,
},
};
}
const { chatid } = params;
const thread_id = `${userId}-${chatid}`
const initialMessages = await checkpointToVercelAI(thread_id)
// initial messages logic here
return {
props: {
user: dbRes.rows[0],
thread_id,
initialMessages
},
};
} catch (error) {
console.error("Auth verification failed", error);
return {
redirect: {
destination: "/onboarding",
permanent: false,
},
};
}
};
export default function Page({thread_id, initialMessages}) {
return (
<div className="dark">
<SidebarProvider>
<SidebarApp />
<SidebarInset className="flex flex-col h-screen overflow-y-auto">
<header className="sticky top-0 flex h-14 shrink-0 items-center gap-2 bg-background">
<div className="flex flex-1 items-center gap-2 px-3">
<SidebarTrigger />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbPage className="line-clamp-1">
Project Management & Task Tracking
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
<Chat thread_id={thread_id} initialMessages={initialMessages}/>
</SidebarInset>
</SidebarProvider>
</div>
);
}
// export default function ChatPage({ user }) {
// return (
// <div>
// <h1>Welcome to the Chat Page</h1>
// <p>Wallet: {user.publickey}</p>
// <p>Delegated: {user.delegated ? "Yes" : "No"}</p>
// </div>
// );
// }

66
pages/chat/index.js Normal file
View File

@ -0,0 +1,66 @@
import pool from "@/server/db";
import { PrivyClient } from "@privy-io/server-auth";
import { ulid } from "ulid";
export const getServerSideProps = async ({ req }) => {
const cookieAuthToken = req.cookies["privy-id-token"];
if (!cookieAuthToken) {
return {
redirect: {
destination: "/onboarding",
permanent: false,
},
};
}
const PRIVY_APP_ID = process.env.NEXT_PUBLIC_PRIVY_APP_ID;
const PRIVY_APP_SECRET = process.env.PRIVY_APP_SECRET;
const client = new PrivyClient(PRIVY_APP_ID, PRIVY_APP_SECRET);
try {
const claims = await client.verifyAuthToken(cookieAuthToken);
const userId = claims.userId;
const dbRes = await pool.query("SELECT userid, publickey, tg_userid, delegated FROM users WHERE userid = $1", [userId]);
if (dbRes.rows.length === 0) {
return {
redirect: {
destination: "/onboarding",
permanent: false,
},
};
}
const { delegated } = dbRes.rows[0];
if (!delegated) {
return {
redirect: {
destination: "/onboarding",
permanent: false,
},
};
}
const chatid = ulid()
return {
redirect: {
destination: `/chat/${chatid}`,
permanent: false,
},
};
} catch (error) {
console.error("Auth verification failed", error);
return {
redirect: {
destination: "/onboarding",
permanent: false,
},
};
}
};
export default function Page() {
return (<div></div> );
}

5
pages/index.js Normal file
View File

@ -0,0 +1,5 @@
export default function Home() {
return (
<div>Homee</div>
);
}

View File

@ -0,0 +1,128 @@
import { useRouter } from "next/router";
import { PrivyClient } from "@privy-io/server-auth";
import { useDelegatedActions } from "@privy-io/react-auth";
import pool from "@/server/db";
export const getServerSideProps = async ({ req }) => {
const cookieAuthToken = req.cookies["privy-id-token"];
if (!cookieAuthToken) {
return {
redirect: {
destination: "/onboarding/login",
permanent: false,
},
};
}
const PRIVY_APP_ID = process.env.NEXT_PUBLIC_PRIVY_APP_ID;
const PRIVY_APP_SECRET = process.env.PRIVY_APP_SECRET;
const client = new PrivyClient(PRIVY_APP_ID, PRIVY_APP_SECRET);
try {
const claims = await client.verifyAuthToken(cookieAuthToken);
const userId = claims.userId;
const dbRes = await pool.query("SELECT * FROM users WHERE userid = $1", [userId]);
if (dbRes.rows.length === 0) {
const user = await client.getUser(userId);
const privyWallets = user.linkedAccounts.filter(
(account) => account.type === "wallet" && account.walletClientType === "privy" && account.chainType === "solana"
);
const embeddedWallet = privyWallets[0]?.address || null;
const delegated = privyWallets[0]?.delegated || false;
await pool.query(
"INSERT INTO users (userid, publickey, last_signedin_at, created_at, delegated) VALUES ($1, $2, NOW(), NOW(), $3)",
[userId, embeddedWallet, delegated]
);
if (delegated) {
return {
redirect: {
destination: "/chat",
permanent: false,
},
};
} else {
return {
props: { user: { delegated: false, publickey: embeddedWallet } },
};
}
}
const { delegated } = dbRes.rows[0];
if (delegated) {
return {
redirect: {
destination: "/chat",
permanent: false,
},
};
} else {
const user = await client.getUser(userId);
const privyWallets = user.linkedAccounts.filter(
(account) => account.type === "wallet" && account.walletClientType === "privy" && account.chainType === "solana"
);
const updatedDelegated = privyWallets[0]?.delegated || false;
if (updatedDelegated) {
await pool.query("UPDATE users SET delegated = TRUE WHERE userid = $1", [userId]);
return {
redirect: {
destination: "/chat",
permanent: false,
},
};
} else {
return {
props: { user: { delegated: false, publickey: privyWallets[0]?.address || null } },
};
}
}
} catch (error) {
console.error("Auth verification failed", error);
return {
redirect: {
destination: "/onboarding/login",
permanent: false,
},
};
}
};
export default function DelegatePage({ user }) {
const router = useRouter();
const { delegateWallet } = useDelegatedActions();
const handleDelegate = async () => {
try {
if (user.publickey) {
console.log(`Delegating wallet ${user.publickey}...`);
await delegateWallet({
address: user.publickey,
chainType: "solana",
});
router.push("/onboarding");
}
} catch (error) {
console.error("Delegation failed:", error);
}
};
return (
<div>
<h1>Delegate your Wallet</h1>
<p>Wallet: {user.publickey}</p>
<button onClick={handleDelegate} className="bg-violet-600 hover:bg-violet-700 py-3 px-6 text-white rounded-lg">
Delegate
</button>
</div>
);
}

108
pages/onboarding/index.js Normal file
View File

@ -0,0 +1,108 @@
import { useRouter } from "next/router";
import { PrivyClient } from "@privy-io/server-auth";
import pool from "@/server/db";
export const getServerSideProps = async ({ req }) => {
const cookieAuthToken = req.cookies["privy-id-token"];
if (!cookieAuthToken) {
return {
redirect: {
destination: "/onboarding/login",
permanent: false,
},
};
}
const PRIVY_APP_ID = process.env.NEXT_PUBLIC_PRIVY_APP_ID;
const PRIVY_APP_SECRET = process.env.PRIVY_APP_SECRET;
const client = new PrivyClient(PRIVY_APP_ID, PRIVY_APP_SECRET);
try {
const claims = await client.verifyAuthToken(cookieAuthToken);
const userId = claims.userId;
const dbRes = await pool.query("SELECT * FROM users WHERE userid = $1", [userId]);
if (dbRes.rows.length === 0) {
const user = await client.getUser(userId);
const privyWallets = user.linkedAccounts.filter(
(account) => account.type === "wallet" && account.walletClientType === "privy" && account.chainType === "solana"
);
const embeddedWallet = privyWallets[0]?.address || null;
const delegated = privyWallets[0]?.delegated || false;
await pool.query(
"INSERT INTO users (userid, publickey, last_signedin_at, created_at, delegated) VALUES ($1, $2, NOW(), NOW(), $3)",
[userId, embeddedWallet, delegated]
);
if (delegated) {
return {
redirect: {
destination: "/chat",
permanent: false,
},
};
} else {
return {
redirect: {
destination: "/onboarding/delegate",
permanent: false,
},
};
}
}
const { delegated } = dbRes.rows[0];
if (delegated) {
return {
redirect: {
destination: "/chat",
permanent: false,
},
};
} else {
const user = await client.getUser(userId);
const privyWallets = user.linkedAccounts.filter(
(account) => account.type === "wallet" && account.walletClientType === "privy" && account.chainType === "solana"
);
const updatedDelegated = privyWallets[0]?.delegated || false;
if (updatedDelegated) {
await pool.query("UPDATE users SET delegated = TRUE WHERE userid = $1", [userId]);
return {
redirect: {
destination: "/chat",
permanent: false,
},
};
} else {
return {
redirect: {
destination: "/onboarding/delegate",
permanent: false,
},
};
}
}
} catch (error) {
console.error("Auth verification failed", error);
return {
redirect: {
destination: "/onboarding/login",
permanent: false,
},
};
}
};
export default function Onboarding() {
const router = useRouter();
return <div>Redirecting...</div>;
}

108
pages/onboarding/login.js Normal file
View File

@ -0,0 +1,108 @@
import { useLogin } from "@privy-io/react-auth";
import { PrivyClient } from "@privy-io/server-auth";
import pool from "@/server/db";
import { useRouter } from "next/router";
export const getServerSideProps = async ({ req }) => {
const cookieAuthToken = req.cookies["privy-id-token"];
if (!cookieAuthToken) {
return { props: {} };
}
const PRIVY_APP_ID = process.env.NEXT_PUBLIC_PRIVY_APP_ID;
const PRIVY_APP_SECRET = process.env.PRIVY_APP_SECRET;
const client = new PrivyClient(PRIVY_APP_ID, PRIVY_APP_SECRET);
try {
const claims = await client.verifyAuthToken(cookieAuthToken);
const userId = claims.userId;
const dbRes = await pool.query("SELECT * FROM users WHERE userid = $1", [userId]);
if (dbRes.rows.length === 0) {
const user = await client.getUser(userId);
const privyWallets = user.linkedAccounts.filter(
(account) => account.type === "wallet" && account.walletClientType === "privy" && account.chainType === "solana"
);
const embeddedWallet = privyWallets[0]?.address || null;
const delegated = privyWallets[0]?.delegated || false;
await pool.query(
"INSERT INTO users (userid, publickey, last_signedin_at, created_at, delegated) VALUES ($1, $2, NOW(), NOW(), $3)",
[userId, embeddedWallet, delegated]
);
if (delegated) {
return {
redirect: {
destination: "/chat",
permanent: false,
},
};
} else {
return {
redirect: {
destination: "/onboarding/delegate",
permanent: false,
},
};
}
}
const { delegated } = dbRes.rows[0];
if (delegated) {
return {
redirect: {
destination: "/chat",
permanent: false,
},
};
} else {
const user = await client.getUser(userId);
const privyWallets = user.linkedAccounts.filter(
(account) => account.type === "wallet" && account.walletClientType === "privy" && account.chainType === "solana"
);
const updatedDelegated = privyWallets[0]?.delegated || false;
if (updatedDelegated) {
await pool.query("UPDATE users SET delegated = TRUE WHERE userid = $1", [userId]);
return {
redirect: {
destination: "/chat",
permanent: false,
},
};
} else {
return {
redirect: {
destination: "/onboarding/delegate",
permanent: false,
},
};
}
}
} catch (error) {
console.error("Auth verification failed", error);
return { props: {} };
}
};
export default function Login() {
const router = useRouter();
const { login } = useLogin({
onComplete: () => {
router.reload();
},
});
return (
<div>
<button onClick={login}>Log in</button>
</div>
);
}

8
postcss.config.mjs Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,147 @@
import { isAIMessage, isHumanMessage,isToolMessage } from "@langchain/core/messages";
import graph from "@/server/graph";
import checkpointer from "@/server/checkpointer";
import { ulid } from "ulid";
const interrupterMessageCreator = (id, toolCalls) => {
const template = {
id: id,
createdAt: new Date().toISOString(),
role: "assistant",
content: "",
parts: [
{
type: "tool-invocation",
toolInvocation: {
state: "result",
step: 0,
toolCallId: "interrupter",
toolName: "interrupter",
args: {
toolCalls: toolCalls,
},
result: "Your input is required to proceed",
},
},
],
toolInvocations: [
{
state: "result",
step: 0,
toolCallId: "01JM87QQ72ZDYY01389N4JTV6P",
toolName: "interrupter",
args: {
toolCalls: toolCalls,
},
result: "Your input is required to proceed",
},
],
revisionId: ulid(),
};
return template;
};
const humanMessageCreator = (message) => {
const template = {
id: message.id,
createdAt: new Date().toISOString(),
role: "user",
content: message.content,
parts: [
{
type: "text",
text: message.content,
},
],
};
return template;
};
const aiMessageCreator = (id, content, tools) => {
const template = {
id: id,
createdAt: new Date().toISOString(),
role: "assistant",
content: content,
parts: [],
revisionId: ulid(),
};
if (tools.length != 0) {
template.toolInvocations = tools;
}
tools.forEach((tool) => {
template.parts.push({
type: "tool-invocation",
toolInvocation: tool,
});
});
template.parts.push({
type: "text",
text: content,
});
return template;
};
const toolMessageCreator = (message, step) => {
const template = {
state: "result",
step: step,
toolCallId: message.tool_call_id,
toolName: message.name,
args: message.artifact ? { artifact: message.artifact } : {},
result: message.content,
};
return template;
};
export async function checkpointToVercelAI(thread_id) {
const config = { configurable: { thread_id } };
const checkpointerData = await checkpointer.get(config);
const vercel_messages = [
{
id: "1",
content: "Hi! How can i assist you today?",
role: "assistant",
},
];
if (!checkpointerData) {
return vercel_messages;
}
let tools_grouped = [];
for (const message of checkpointerData.channel_values.messages) {
if (isHumanMessage(message)) {
vercel_messages.push(humanMessageCreator(message));
} else if (isToolMessage(message)) {
tools_grouped.push(toolMessageCreator(message, tools_grouped.length));
} else if (isAIMessage(message) && message.content != "") {
vercel_messages.push(aiMessageCreator(message.id, message.content, tools_grouped));
tools_grouped = [];
}
}
if (typeof checkpointerData?.channel_values === "object" && "branch:agent:condition:checkApproval" in checkpointerData.channel_values) {
let interrupterTool;
for await (const event of await graph.stream(null, config)) {
interrupterTool = event.__interrupt__;
}
const toolCalls = interrupterTool[0].value.map((tool) => ({
id: tool.id,
name: tool.name,
args: Object.fromEntries(Object.entries(tool.args).filter(([key]) => key !== "debug")),
}));
const id = interrupterTool[0].ns[0];
const interruptMessage = interrupterMessageCreator(id, toolCalls);
vercel_messages.push(interruptMessage);
}
return vercel_messages;
}

3
server/checkpointer.js Normal file
View File

@ -0,0 +1,3 @@
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
const checkpointer = PostgresSaver.fromConnString(process.env.POSTGRES_DB_URL);
export default checkpointer;

7
server/db.js Normal file
View File

@ -0,0 +1,7 @@
import pkg from 'pg';
const { Pool } = pkg
const pool = new Pool({
connectionString: process.env.POSTGRES_DB_URL,
});
export default pool;

102
server/graph.js Normal file
View File

@ -0,0 +1,102 @@
import { approveRequiredTool1 , approveRequiredTool2, regularTool1, regularTool2} from "@/server/tools/testing";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { ChatOpenAI } from "@langchain/openai";
import {
StateGraph,
MessagesAnnotation,
END,
START,
interrupt,
} from "@langchain/langgraph";
import checkpointer from "@/server/checkpointer";
import { transferSolanaTool } from "@/server/tools/solana/transfer";
const getGraph = (solAgentKit) => {
const transferSolTool = transferSolanaTool(solAgentKit)
const approveRequiredTools = [approveRequiredTool1, approveRequiredTool2, transferSolTool];
const allTools = [regularTool1, regularTool2, ...approveRequiredTools];
const allToolsNode = new ToolNode(allTools);
const model = new ChatOpenAI({
model: "gpt-4o",
temperature: 0,
apiKey: process.env.OPENAI_API_KEY,
});
const modelWithTools = model.bindTools(allTools);
function shouldContinue(state) {
const lastMessage = state.messages[state.messages.length - 1];
if (
"tool_calls" in lastMessage &&
Array.isArray(lastMessage.tool_calls) &&
lastMessage.tool_calls?.length
) {
return "checkApproval";
}
return END;
}
async function callModel(state) {
const messages = state.messages;
const response = await modelWithTools.invoke(messages);
return { messages: [response] };
}
function checkApproval(state) {
const lastMessage = state.messages[state.messages.length - 1];
// Identify tool calls that require approval
const toolCallsNeedingApproval = lastMessage.tool_calls.filter((tc) =>
approveRequiredTools.some((tool) => tool.name === tc.name),
);
if (toolCallsNeedingApproval.length === 0) {
return { messages: [lastMessage] }; // No approval needed, return as is
}
// Reset only the tools requiring approval
const resetToolCalls = toolCallsNeedingApproval.map((tc) => ({
...tc,
args: {
...tc.args,
debug: false,
},
}));
// Interrupt with only the reset tool calls to request user approval
const toolCallsListReviewed = interrupt(resetToolCalls);
// Process user input and update debug values
const updatedToolCalls = lastMessage.tool_calls.map((tc) =>
toolCallsListReviewed[tc.id] !== undefined
? { ...tc, args: { ...tc.args, debug: toolCallsListReviewed[tc.id] } }
: tc,
);
const updatedMessage = {
role: "ai",
content: lastMessage.content,
tool_calls: updatedToolCalls,
id: lastMessage.id,
};
console.log(updatedMessage);
return { messages: [updatedMessage] };
}
const workflow = new StateGraph(MessagesAnnotation)
.addNode("agent", callModel)
.addNode("checkApproval", checkApproval)
.addNode("allToolsNode", allToolsNode)
.addEdge(START, "agent")
.addConditionalEdges("agent", shouldContinue, ["checkApproval", END])
.addEdge("checkApproval", "allToolsNode")
.addEdge("allToolsNode", "agent");
const graph = workflow.compile({
checkpointer,
});
return graph
}
export default getGraph;

View File

@ -0,0 +1,32 @@
import { PublicKey } from '@solana/web3.js';
import { tool } from "@langchain/core/tools";
import { z } from 'zod';
export const transferSolanaTool = (agent) => tool(
async ({ recipient, amount, debug = false }) => {
if (!debug) {
return `transferSolanaTool call failed. User denied the requiest`;
}
try {
const recipientPubKey = new PublicKey(recipient);
const signature = await agent.transfer(recipientPubKey, amount);
return `Transfer to ${recipient} successful. Signature: ${signature}`;
} catch (error) {
if (debug) {
return `Transfer failed with error: ${error.message}`;
}
return 'Transfer failed.';
}
},
{
name: 'transferSolanaTool',
description: 'Transfers a specified amount of Solana to the provided recipient address.',
schema: z.object({
recipient: z.string().describe('The recipient public key as a string'),
amount: z.number().min(0).describe('The amount of Solana to transfer'),
debug: z.boolean().default(false).describe('Keep false'),
}),
}
);

70
server/tools/testing.js Normal file
View File

@ -0,0 +1,70 @@
import { tool } from "@langchain/core/tools";
import { z } from "zod";
export const approveRequiredTool1 = tool(
({ testParam, debug = false }) => {
if (debug) {
return `approveRequiredTool1 called successfully with ${testParam}`;
}
return `approveRequiredTool1 call failed with ${testParam}. User denied the requiest`;
},
{
name: "approveRequiredTool1",
description: "Call when user ask for the function approveRequiredTool1",
schema: z.object({
testParam: z
.string()
.describe("Some random string you come up with just for testing"),
debug: z
.boolean()
.default(false)
.describe("User controlled variable, you cannot control this"),
}),
},
);
export const approveRequiredTool2 = tool(
({ testParam, debug = false }) => {
if (debug) {
return `approveRequiredTool2 called successfully with ${testParam}`;
}
return `approveRequiredTool2 call failed with ${testParam}. User denied the requiest`;
},
{
name: "approveRequiredTool2",
description: "Call when user ask for the function approveRequiredTool2",
schema: z.object({
testParam: z
.string()
.describe("Some random string you come up with just for testing"),
debug: z
.boolean()
.default(false)
.describe("User controlled variable, you cannot control this"),
}),
},
);
export const regularTool1 = tool(
(_) => {
return ["regularTool1 called successfully", "artifact data"];
},
{
name: "regularTool1",
description: "Call when user ask for the function regularTool1",
schema: z.string(),
responseFormat: "content_and_artifact",
},
);
export const regularTool2 = tool(
(_) => {
return ["regularTool2 called successfully", "artifact data"];
},
{
name: "regularTool2",
description: "Call when user ask for the function regularTool2",
schema: z.string(),
responseFormat: "content_and_artifact",
},
);

View File

@ -0,0 +1,177 @@
import { Connection, PublicKey } from "@solana/web3.js"
import {
closeEmptyTokenAccounts,
create_TipLink,
create_gibwork_task,
fetchPrice,
fetchTokenDetailedReport,
fetchTokenReportSummary,
getAssetsByOwner,
getTPS,
get_balance,
get_balance_other,
launchPumpFunToken,
parseTransaction,
resolveSolDomain,
stakeWithJup,
trade,
transfer,
create_image,
get_wallet_address
} from "../tools"
import { DEFAULT_OPTIONS } from "../constants"
/**
* Main class for interacting with Solana blockchain
* Provides a unified interface for token operations, NFT management, trading and more
*
* @class SolanaAgentKit
* @property {Connection} connection - Solana RPC connection
* @property {WalletAdapter} wallet - Wallet that implements WalletAdapter for signing transactions
* @property {PublicKey} wallet_address - Public key of the wallet
* @property {Config} config - Configuration object
*/
export class SolanaAgentKit {
constructor(wallet, rpc_url, configOrKey) {
this.connection = new Connection(
rpc_url || "https://api.mainnet-beta.solana.com"
)
this.wallet = wallet
this.wallet_address = this.wallet.publicKey
// Handle both old and new patterns
if (typeof configOrKey === "string" || configOrKey === null) {
this.config = { OPENAI_API_KEY: configOrKey || "" }
} else {
this.config = configOrKey
}
}
getAnchorWallet() {
const adapter = this.wallet
return {
publicKey: adapter.publicKey,
signTransaction: adapter.signTransaction.bind(adapter),
signAllTransactions: adapter.signAllTransactions.bind(adapter),
payer: adapter
}
}
async getBalance(token_address) {
return get_balance(this, token_address)
}
async getBalanceOther(walletAddress, tokenAddress) {
return get_balance_other(this, walletAddress, tokenAddress)
}
async createImage(prompt) {
return create_image(this, prompt)
}
async getWalletAddress() {
return get_wallet_address(this)
}
async transfer(to, amount, mint) {
return transfer(this, to, amount, mint)
}
async resolveSolDomain(domain) {
return resolveSolDomain(this, domain)
}
async getPrimaryDomain(account) {
return getPrimaryDomain(this, account)
}
async trade(
outputMint,
inputAmount,
inputMint,
slippageBps = DEFAULT_OPTIONS.SLIPPAGE_BPS
) {
return trade(this, outputMint, inputAmount, inputMint, slippageBps)
}
async getTPS() {
return getTPS(this)
}
async fetchTokenPrice(mint) {
return fetchPrice(new PublicKey(mint))
}
async launchPumpFunToken(
tokenName,
tokenTicker,
description,
imageUrl,
options
) {
return launchPumpFunToken(
this,
tokenName,
tokenTicker,
description,
imageUrl,
options
)
}
async stake(amount) {
return stakeWithJup(this, amount)
}
async createGibworkTask(
title,
content,
requirements,
tags,
tokenMintAddress,
tokenAmount,
payer
) {
return create_gibwork_task(
this,
title,
content,
requirements,
tags,
new PublicKey(tokenMintAddress),
tokenAmount,
payer ? new PublicKey(payer) : undefined
)
}
async createTiplink(amount, splmintAddress) {
return create_TipLink(this, amount, splmintAddress)
}
async closeEmptyTokenAccounts() {
return closeEmptyTokenAccounts(this)
}
async fetchTokenReportSummary(mint) {
return fetchTokenReportSummary(mint)
}
async fetchTokenDetailedReport(mint) {
return fetchTokenDetailedReport(mint)
}
async heliusParseTransactions(transactionId) {
return parseTransaction(this, transactionId)
}
async getAllAssetsbyOwner(owner, limit) {
return getAssetsByOwner(this, owner, limit)
}
}

View File

@ -0,0 +1,37 @@
import { PublicKey } from "@solana/web3.js"
/**
* Common token addresses used across the toolkit
*/
export const TOKENS = {
USDC: new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"),
USDT: new PublicKey("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"),
USDS: new PublicKey("USDSwr9ApdHk5bvJKMjzff41FfuX8bSxdKcR81vTwcA"),
SOL: new PublicKey("So11111111111111111111111111111111111111112"),
jitoSOL: new PublicKey("J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn"),
bSOL: new PublicKey("bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1"),
mSOL: new PublicKey("mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So"),
BONK: new PublicKey("DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263")
}
/**
* Default configuration options
* @property {number} SLIPPAGE_BPS - Default slippage tolerance in basis points (300 = 3%)
* @property {number} TOKEN_DECIMALS - Default number of decimals for new tokens
* @property {number} LEVERAGE_BPS - Default leverage for trading PERP
*/
export const DEFAULT_OPTIONS = {
SLIPPAGE_BPS: 300,
TOKEN_DECIMALS: 9,
RERERRAL_FEE: 200,
// 10000 = x1, 50000 = x5, 100000 = x10, 1000000 = x100
LEVERAGE_BPS: 50000
}
/**
* Jupiter API URL
*/
export const JUP_API = "https://quote-api.jup.ag/v6"
export const JUP_REFERRAL_ADDRESS =
"REFER4ZgmyYx9c6He5XfaTMiGfdLwRnkV4RPp9t9iF3"

View File

@ -0,0 +1,7 @@
import { SolanaAgentKit } from "./agent"
import { createSolanaTools } from "./langchain"
export { SolanaAgentKit, createSolanaTools }
// Export action system
export * from "./wallet/EmbeddedWallet"

View File

@ -0,0 +1,38 @@
import { Tool } from "langchain/tools"
import { create_image } from "../../tools/agent"
export class SolanaCreateImageTool extends Tool {
name = "solana_create_image"
description =
"Create an image using OpenAI's DALL-E. Input should be a string prompt for the image."
constructor(solanaKit) {
super()
this.solanaKit = solanaKit
}
validateInput(input) {
if (typeof input !== "string" || input.trim().length === 0) {
throw new Error("Input must be a non-empty string prompt")
}
}
async _call(input) {
try {
this.validateInput(input)
const result = await create_image(this.solanaKit, input.trim())
return JSON.stringify({
status: "success",
message: "Image created successfully",
...result
})
} catch (error) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR"
})
}
}
}

View File

@ -0,0 +1,2 @@
export * from "./create_image"
export * from "./wallet_address"

View File

@ -0,0 +1,15 @@
import { Tool } from "langchain/tools"
export class SolanaGetWalletAddressTool extends Tool {
name = "solana_get_wallet_address"
description = `Get the wallet address of the agent`
constructor(solanaKit) {
super()
this.solanaKit = solanaKit
}
async _call(_input) {
return this.solanaKit.wallet_address.toString()
}
}

View File

@ -0,0 +1,51 @@
import { Tool } from "langchain/tools"
export class SolanaCreateGibworkTask extends Tool {
name = "create_gibwork_task"
description = `Create a task on Gibwork.
Inputs (input is a JSON string):
title: string, title of the task (required)
content: string, description of the task (required)
requirements: string, requirements to complete the task (required)
tags: string[], list of tags associated with the task (required)
payer: string, payer address (optional, defaults to agent wallet)
tokenMintAddress: string, the mint address of the token, e.g., "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN" (required)
amount: number, payment amount (required)
`
constructor(solanaSdk) {
super()
this.solanaSdk = solanaSdk
}
async _call(input) {
try {
const parsedInput = JSON.parse(input)
const taskData = await this.solanaSdk.createGibworkTask(
parsedInput.title,
parsedInput.content,
parsedInput.requirements,
parsedInput.tags,
parsedInput.tokenMintAddress,
parsedInput.amount,
parsedInput.payer
)
const response = {
status: "success",
taskId: taskData.taskId,
signature: taskData.signature
}
return JSON.stringify(response)
} catch (err) {
return JSON.stringify({
status: "error",
message: err.message,
code: err.code || "CREATE_TASK_ERROR"
})
}
}
}

View File

@ -0,0 +1 @@
export * from "./create_task"

View File

@ -0,0 +1,38 @@
import { Tool } from "langchain/tools"
import { PublicKey } from "@solana/web3.js"
export class SolanaGetAllAssetsByOwner extends Tool {
name = "solana_get_all_assets_by_owner"
description = `Get all assets owned by a specific wallet address.
Inputs:
- owner: string, the wallet address of the owner, e.g., "4Be9CvxqHW6BYiRAxW9Q3xu1ycTMWaL5z8NX4HR3ha7t" (required)
- limit: number, the maximum number of assets to retrieve (optional)`
constructor(solanaKit) {
super()
this.solanaKit = solanaKit
}
async _call(input) {
try {
const { owner, limit } = JSON.parse(input)
const ownerPubkey = new PublicKey(owner)
const assets = await this.solanaKit.getAllAssetsbyOwner(
ownerPubkey,
limit
)
return JSON.stringify({
status: "success",
message: "Assets retrieved successfully",
assets: assets
})
} catch (error) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR"
})
}
}
}

View File

@ -0,0 +1,2 @@
export * from "./get_all_assets"
export * from "./parse_transaction"

View File

@ -0,0 +1,33 @@
import { Tool } from "langchain/tools"
export class SolanaParseTransactionHeliusTool extends Tool {
name = "solana_parse_transaction_helius"
description = `Parse a Solana transaction using Helius API.
Inputs:
- transactionId: string, the ID of the transaction to parse, e.g., "5h3k...9d2k" (required).`
constructor(solanaKit) {
super()
this.solanaKit = solanaKit
}
async _call(input) {
try {
const transactionId = input.trim()
const parsedTransaction = await this.solanaKit.heliusParseTransactions(
transactionId
)
return JSON.stringify({
status: "success",
message: "transaction parsed successfully",
transaction: parsedTransaction
})
} catch (error) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "NOt able to Parse transaction"
})
}
}
}

View File

@ -0,0 +1,51 @@
export * from "./gibwork"
export * from "./jupiter"
export * from "./solana"
export * from "./agent"
export * from "./pumpfun"
export * from "./rugcheck"
export * from "./tiplink"
export * from "./sns"
export * from "./helius"
import {
SolanaBalanceTool,
SolanaBalanceOtherTool,
SolanaTransferTool,
SolanaTradeTool,
SolanaGetWalletAddressTool,
SolanaPumpfunTokenLaunchTool,
SolanaCreateImageTool,
SolanaTPSCalculatorTool,
SolanaStakeTool,
SolanaFetchPriceTool,
SolanaCreateGibworkTask,
SolanaTipLinkTool,
SolanaCloseEmptyTokenAccounts,
SolanaFetchTokenReportSummaryTool,
SolanaFetchTokenDetailedReportTool,
SolanaParseTransactionHeliusTool,
SolanaGetAllAssetsByOwner
} from "./index"
export function createSolanaTools(solanaKit) {
return [
new SolanaBalanceTool(solanaKit),
new SolanaBalanceOtherTool(solanaKit),
new SolanaTransferTool(solanaKit),
new SolanaTradeTool(solanaKit),
new SolanaGetWalletAddressTool(solanaKit),
new SolanaPumpfunTokenLaunchTool(solanaKit),
new SolanaCreateImageTool(solanaKit),
new SolanaTPSCalculatorTool(solanaKit),
new SolanaStakeTool(solanaKit),
new SolanaFetchPriceTool(solanaKit),
new SolanaCreateGibworkTask(solanaKit),
new SolanaTipLinkTool(solanaKit),
new SolanaCloseEmptyTokenAccounts(solanaKit),
new SolanaFetchTokenReportSummaryTool(solanaKit),
new SolanaFetchTokenDetailedReportTool(solanaKit),
new SolanaParseTransactionHeliusTool(solanaKit),
new SolanaGetAllAssetsByOwner(solanaKit),
]
}

View File

@ -0,0 +1,34 @@
import { Tool } from "langchain/tools"
/**
* Tool to fetch the price of a token in USDC
*/
export class SolanaFetchPriceTool extends Tool {
name = "solana_fetch_price"
description = `Fetch the price of a given token in USDC.
Inputs:
- tokenId: string, the mint address of the token, e.g., "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN"`
constructor(solanaKit) {
super()
this.solanaKit = solanaKit
}
async _call(input) {
try {
const price = await this.solanaKit.fetchTokenPrice(input.trim())
return JSON.stringify({
status: "success",
tokenId: input.trim(),
priceInUSDC: price
})
} catch (error) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR"
})
}
}
}

View File

@ -0,0 +1,3 @@
export * from "./fetch_price"
export * from "./trade"
export * from "./stake"

View File

@ -0,0 +1,35 @@
import { Tool } from "langchain/tools"
export class SolanaStakeTool extends Tool {
name = "solana_stake"
description = `This tool can be used to stake your SOL (Solana), also called as SOL staking or liquid staking.
Inputs ( input is a JSON string ):
amount: number, eg 1 or 0.01 (required)`
constructor(solanaKit) {
super()
this.solanaKit = solanaKit
}
async _call(input) {
try {
const parsedInput = JSON.parse(input) || Number(input)
const tx = await this.solanaKit.stake(parsedInput.amount)
return JSON.stringify({
status: "success",
message: "Staked successfully",
transaction: tx,
amount: parsedInput.amount
})
} catch (error) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR"
})
}
}
}

View File

@ -0,0 +1,48 @@
import { Tool } from "langchain/tools"
import { PublicKey } from "@solana/web3.js"
export class SolanaTradeTool extends Tool {
name = "solana_trade"
description = `This tool can be used to swap tokens to another token ( It uses Jupiter Exchange ).
Inputs ( input is a JSON string ):
outputMint: string, eg "So11111111111111111111111111111111111111112" or "SENDdRQtYMWaQrBroBrJ2Q53fgVuq95CV9UPGEvpCxa" (required)
inputAmount: number, eg 1 or 0.01 (required)
inputMint?: string, eg "So11111111111111111111111111111111111111112" (optional)
slippageBps?: number, eg 100 (optional)`
constructor(solanaKit) {
super()
this.solanaKit = solanaKit
}
async _call(input) {
try {
const parsedInput = JSON.parse(input)
const tx = await this.solanaKit.trade(
new PublicKey(parsedInput.outputMint),
parsedInput.inputAmount,
parsedInput.inputMint
? new PublicKey(parsedInput.inputMint)
: new PublicKey("So11111111111111111111111111111111111111112"),
parsedInput.slippageBps
)
return JSON.stringify({
status: "success",
message: "Trade executed successfully",
transaction: tx,
inputAmount: parsedInput.inputAmount,
inputToken: parsedInput.inputMint || "SOL",
outputToken: parsedInput.outputMint
})
} catch (error) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR"
})
}
}
}

View File

@ -0,0 +1 @@
export * from "./launch_pumpfun_token"

View File

@ -0,0 +1,79 @@
import { Tool } from "langchain/tools"
export class SolanaPumpfunTokenLaunchTool extends Tool {
name = "solana_launch_pumpfun_token"
description = `This tool can be used to launch a token on Pump.fun,
do not use this tool for any other purpose, or for creating SPL tokens.
If the user asks you to chose the parameters, you should generate valid values.
For generating the image, you can use the solana_create_image tool.
Inputs:
tokenName: string, eg "PumpFun Token",
tokenTicker: string, eg "PUMP",
description: string, eg "PumpFun Token is a token on the Solana blockchain",
imageUrl: string, eg "https://i.imgur.com/UFm07Np_d.png`
constructor(solanaKit) {
super()
this.solanaKit = solanaKit
}
validateInput(input) {
if (!input.tokenName || typeof input.tokenName !== "string") {
throw new Error("tokenName is required and must be a string")
}
if (!input.tokenTicker || typeof input.tokenTicker !== "string") {
throw new Error("tokenTicker is required and must be a string")
}
if (!input.description || typeof input.description !== "string") {
throw new Error("description is required and must be a string")
}
if (!input.imageUrl || typeof input.imageUrl !== "string") {
throw new Error("imageUrl is required and must be a string")
}
if (
input.initialLiquiditySOL !== undefined &&
typeof input.initialLiquiditySOL !== "number"
) {
throw new Error("initialLiquiditySOL must be a number when provided")
}
}
async _call(input) {
try {
// Parse and normalize input
input = input.trim()
const parsedInput = JSON.parse(input)
this.validateInput(parsedInput)
// Launch token with validated input
await this.solanaKit.launchPumpFunToken(
parsedInput.tokenName,
parsedInput.tokenTicker,
parsedInput.description,
parsedInput.imageUrl,
{
twitter: parsedInput.twitter,
telegram: parsedInput.telegram,
website: parsedInput.website,
initialLiquiditySOL: parsedInput.initialLiquiditySOL
}
)
return JSON.stringify({
status: "success",
message: "Token launched successfully on Pump.fun",
tokenName: parsedInput.tokenName,
tokenTicker: parsedInput.tokenTicker
})
} catch (error) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR"
})
}
}
}

View File

@ -0,0 +1,2 @@
export * from "./token_report_summary"
export * from "./token_report_detailed"

View File

@ -0,0 +1,32 @@
import { Tool } from "langchain/tools"
export class SolanaFetchTokenDetailedReportTool extends Tool {
name = "solana_fetch_token_detailed_report"
description = `Fetches a detailed report for a specific token from RugCheck.
Inputs:
- mint: string, the mint address of the token, e.g., "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN" (required).`
constructor(solanaKit) {
super()
this.solanaKit = solanaKit
}
async _call(input) {
try {
const mint = input.trim()
const detailedReport = await this.solanaKit.fetchTokenDetailedReport(mint)
return JSON.stringify({
status: "success",
message: "Detailed token report fetched successfully",
report: detailedReport
})
} catch (error) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "FETCH_TOKEN_DETAILED_REPORT_ERROR"
})
}
}
}

View File

@ -0,0 +1,32 @@
import { Tool } from "langchain/tools"
export class SolanaFetchTokenReportSummaryTool extends Tool {
name = "solana_fetch_token_report_summary"
description = `Fetches a summary report for a specific token from RugCheck.
Inputs:
- mint: string, the mint address of the token, e.g., "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN" (required).`
constructor(solanaKit) {
super()
this.solanaKit = solanaKit
}
async _call(input) {
try {
const mint = input.trim()
const report = await this.solanaKit.fetchTokenReportSummary(mint)
return JSON.stringify({
status: "success",
message: "Token report summary fetched successfully",
report
})
} catch (error) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "FETCH_TOKEN_REPORT_SUMMARY_ERROR"
})
}
}
}

View File

@ -0,0 +1 @@
export * from "./resolve_domain";

View File

@ -0,0 +1,36 @@
import { Tool } from "langchain/tools"
export class SolanaResolveDomainTool extends Tool {
name = "solana_resolve_domain"
description = `Resolve ONLY .sol domain names to a Solana PublicKey.
This tool is exclusively for .sol domains.
DO NOT use this for other domain types like .blink, .bonk, etc.
Inputs:
domain: string, eg "pumpfun.sol" (required)
`
constructor(solanaKit) {
super()
this.solanaKit = solanaKit
}
async _call(input) {
try {
const domain = input.trim()
const publicKey = await this.solanaKit.resolveSolDomain(domain)
return JSON.stringify({
status: "success",
message: "Domain resolved successfully",
publicKey: publicKey.toBase58()
})
} catch (error) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR"
})
}
}
}

View File

@ -0,0 +1,37 @@
import { PublicKey } from "@solana/web3.js"
import { Tool } from "langchain/tools"
export class SolanaBalanceTool extends Tool {
name = "solana_balance"
description = `Get the balance of a Solana wallet or token account.
If you want to get the balance of your wallet, you don't need to provide the tokenAddress.
If no tokenAddress is provided, the balance will be in SOL.
Inputs ( input is a JSON string ):
tokenAddress: string, eg "So11111111111111111111111111111111111111112" (optional)`
constructor(solanaKit) {
super()
this.solanaKit = solanaKit
}
async _call(input) {
try {
const tokenAddress = input ? new PublicKey(input) : undefined
const balance = await this.solanaKit.getBalance(tokenAddress)
return JSON.stringify({
status: "success",
balance,
token: input || "SOL"
})
} catch (error) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR"
})
}
}
}

View File

@ -0,0 +1,44 @@
import { PublicKey } from "@solana/web3.js"
import { Tool } from "langchain/tools"
export class SolanaBalanceOtherTool extends Tool {
name = "solana_balance_other"
description = `Get the balance of a Solana wallet or token account which is different from the agent's wallet.
If no tokenAddress is provided, the SOL balance of the wallet will be returned.
Inputs ( input is a JSON string ):
walletAddress: string, eg "GDEkQF7UMr7RLv1KQKMtm8E2w3iafxJLtyXu3HVQZnME" (required)
tokenAddress: string, eg "SENDdRQtYMWaQrBroBrJ2Q53fgVuq95CV9UPGEvpCxa" (optional)`
constructor(solanaKit) {
super()
this.solanaKit = solanaKit
}
async _call(input) {
try {
const { walletAddress, tokenAddress } = JSON.parse(input)
const tokenPubKey = tokenAddress ? new PublicKey(tokenAddress) : undefined
const balance = await this.solanaKit.getBalanceOther(
new PublicKey(walletAddress),
tokenPubKey
)
return JSON.stringify({
status: "success",
balance,
wallet: walletAddress,
token: tokenAddress || "SOL"
})
} catch (error) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR"
})
}
}
}

View File

@ -0,0 +1,33 @@
import { Tool } from "langchain/tools"
export class SolanaCloseEmptyTokenAccounts extends Tool {
name = "close_empty_token_accounts"
description = `Close all empty spl-token accounts and reclaim the rent`
constructor(solanaKit) {
super()
this.solanaKit = solanaKit
}
async _call() {
try {
const { signature, size } = await this.solanaKit.closeEmptyTokenAccounts()
return JSON.stringify({
status: "success",
message: `${size} accounts closed successfully. ${
size === 48
? "48 accounts can be closed in a single transaction try again to close more accounts"
: ""
}`,
signature
})
} catch (error) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR"
})
}
}
}

View File

@ -0,0 +1,20 @@
import { Tool } from "langchain/tools"
export class SolanaTPSCalculatorTool extends Tool {
name = "solana_get_tps"
description = "Get the current TPS of the Solana network"
constructor(solanaKit) {
super()
this.solanaKit = solanaKit
}
async _call(_input) {
try {
const tps = await this.solanaKit.getTPS()
return `Solana (mainnet-beta) current transactions per second: ${tps}`
} catch (error) {
return `Error fetching TPS: ${error.message}`
}
}
}

View File

@ -0,0 +1,5 @@
export * from "./get_tps"
export * from "./balance"
export * from "./balance_other"
export * from "./close_empty_accounts"
export * from "./transfer"

View File

@ -0,0 +1,49 @@
import { PublicKey } from "@solana/web3.js"
import { Tool } from "langchain/tools"
export class SolanaTransferTool extends Tool {
name = "solana_transfer"
description = `Transfer tokens or SOL to another address ( also called as wallet address ).
Inputs ( input is a JSON string ):
to: string, eg "8x2dR8Mpzuz2YqyZyZjUbYWKSWesBo5jMx2Q9Y86udVk" (required)
amount: number, eg 1 (required)
mint?: string, eg "So11111111111111111111111111111111111111112" or "SENDdRQtYMWaQrBroBrJ2Q53fgVuq95CV9UPGEvpCxa" (optional)`
constructor(solanaKit) {
super()
this.solanaKit = solanaKit
}
async _call(input) {
try {
const parsedInput = JSON.parse(input)
const recipient = new PublicKey(parsedInput.to)
const mintAddress = parsedInput.mint
? new PublicKey(parsedInput.mint)
: undefined
const tx = await this.solanaKit.transfer(
recipient,
parsedInput.amount,
mintAddress
)
return JSON.stringify({
status: "success",
message: "Transfer completed successfully",
amount: parsedInput.amount,
recipient: parsedInput.to,
token: parsedInput.mint || "SOL",
transaction: tx
})
} catch (error) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR"
})
}
}
}

View File

@ -0,0 +1 @@
export * from "./tiplink"

View File

@ -0,0 +1,50 @@
import { PublicKey } from "@solana/web3.js"
import { Tool } from "langchain/tools"
export class SolanaTipLinkTool extends Tool {
name = "solana_tiplink"
description = `Create a TipLink for transferring SOL or SPL tokens.
Input is a JSON string with:
- amount: number (required) - Amount to transfer
- splmintAddress: string (optional) - SPL token mint address`
constructor(solanaKit) {
super()
this.solanaKit = solanaKit
}
async _call(input) {
try {
const parsedInput = JSON.parse(input)
if (!parsedInput.amount) {
throw new Error("Amount is required")
}
const amount = parseFloat(parsedInput.amount)
const splmintAddress = parsedInput.splmintAddress
? new PublicKey(parsedInput.splmintAddress)
: undefined
const { url, signature } = await this.solanaKit.createTiplink(
amount,
splmintAddress
)
return JSON.stringify({
status: "success",
url,
signature,
amount,
tokenType: splmintAddress ? "SPL" : "SOL",
message: `TipLink created successfully`
})
} catch (error) {
return JSON.stringify({
status: "error",
message: error.message,
code: error.code || "UNKNOWN_ERROR"
})
}
}
}

View File

@ -0,0 +1,33 @@
import OpenAI from "openai"
/**
* Generate an image using OpenAI's DALL-E
* @param agent SolanaAgentKit instance
* @param prompt Text description of the image to generate
* @param size Image size ('256x256', '512x512', or '1024x1024') (default: '1024x1024')
* @param n Number of images to generate (default: 1)
* @returns Object containing the generated image URLs
*/
export async function create_image(agent, prompt, size = "1024x1024", n = 1) {
try {
if (!agent.config.OPENAI_API_KEY) {
throw new Error("OpenAI API key not found in agent configuration")
}
const openai = new OpenAI({
apiKey: agent.config.OPENAI_API_KEY
})
const response = await openai.images.generate({
prompt,
n,
size
})
return {
images: response.data.map(img => img.url)
}
} catch (error) {
throw new Error(`Image generation failed: ${error.message}`)
}
}

View File

@ -0,0 +1,8 @@
/**
* Get the agents wallet address
* @param agent - SolanaAgentKit instance
* @returns string
*/
export function get_wallet_address(agent) {
return agent.wallet_address.toBase58()
}

View File

@ -0,0 +1,2 @@
export * from "./create_image"
export * from "./get_wallet_address"

View File

@ -0,0 +1,79 @@
import { VersionedTransaction } from "@solana/web3.js"
/**
* Create an new task on Gibwork
* @param agent SolanaAgentKit instance
* @param title Title of the task
* @param content Description of the task
* @param requirements Requirements to complete the task
* @param tags List of tags associated with the task
* @param payer Payer address for the task (default: agent wallet address)
* @param tokenMintAddress Token mint address for payment
* @param tokenAmount Payment amount for the task
* @returns Object containing task creation transaction and generated taskId
*/
export async function create_gibwork_task(
agent,
title,
content,
requirements,
tags,
tokenMintAddress,
tokenAmount,
payer
) {
try {
const apiResponse = await fetch(
"https://api2.gib.work/tasks/public/transaction",
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
title: title,
content: content,
requirements: requirements,
tags: tags,
payer: payer?.toBase58() || agent.wallet.publicKey.toBase58(),
token: {
mintAddress: tokenMintAddress.toBase58(),
amount: tokenAmount
}
})
}
)
const responseData = await apiResponse.json()
if (!responseData.taskId && !responseData.serializedTransaction) {
throw new Error(`${responseData.message}`)
}
const serializedTransaction = Buffer.from(
responseData.serializedTransaction,
"base64"
)
const tx = VersionedTransaction.deserialize(serializedTransaction)
const signedTx = await agent.wallet.signTransaction(tx)
const signature = await agent.connection.sendTransaction(signedTx, {
preflightCommitment: "confirmed",
maxRetries: 3
})
const latestBlockhash = await agent.connection.getLatestBlockhash()
await agent.connection.confirmTransaction({
signature,
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight
})
return {
status: "success",
taskId: responseData.taskId,
signature: signature
}
} catch (err) {
throw new Error(`${err.message}`)
}
}

View File

@ -0,0 +1 @@
export * from "./create_gibwork_task"

View File

@ -0,0 +1,50 @@
/**
* Fetch assets by owner using the Helius Digital Asset Standard (DAS) API
* @param agent SolanaAgentKit instance
* @param ownerPublicKey Owner's Solana wallet PublicKey
* @param limit Number of assets to retrieve per request
* @returns Assets owned by the specified address
*/
export async function getAssetsByOwner(agent, ownerPublicKey, limit) {
try {
const apiKey = agent.config.HELIUS_API_KEY
if (!apiKey) {
throw new Error("HELIUS_API_KEY not found in environment variables")
}
const url = `https://mainnet.helius-rpc.com/?api-key=${apiKey}`
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
jsonrpc: "2.0",
id: "get-assets",
method: "getAssetsByOwner",
params: {
ownerAddress: ownerPublicKey.toString(),
page: 1,
limit: limit,
displayOptions: {
showFungible: true
}
}
})
})
if (!response.ok) {
throw new Error(
`Failed to fetch: ${response.status} - ${response.statusText}`
)
}
const data = await response.json()
return data.result.items
} catch (error) {
console.error("Error retrieving assets: ", error.message)
throw new Error(`Assets retrieval failed: ${error.message}`)
}
}

View File

@ -0,0 +1,39 @@
/**
* Parse a Solana transaction using the Helius Enhanced Transactions API
* @param agent SolanaAgentKit instance
* @param transactionId The transaction ID to parse
* @returns Parsed transaction data
*/
export async function parseTransaction(agent, transactionId) {
try {
const apiKey = agent.config.HELIUS_API_KEY
if (!apiKey) {
throw new Error("HELIUS_API_KEY not found in environment variables")
}
const url = `https://api.helius.xyz/v0/transactions/?api-key=${apiKey}`
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
transactions: [transactionId]
})
})
if (!response.ok) {
throw new Error(
`Failed to fetch: ${response.status} - ${response.statusText}`
)
}
const data = await response.json()
return data
} catch (error) {
console.error("Error parsing transaction: ", error.message)
throw new Error(`Transaction parsing failed: ${error.message}`)
}
}

View File

@ -0,0 +1,2 @@
export * from "./get_assets_by_owner"
export * from "./helius_transaction_parsing"

View File

@ -0,0 +1,9 @@
export * from "./sns"
export * from "./gibwork"
export * from "./jupiter"
export * from "./solana"
export * from "./agent"
export * from "./pumpfun"
export * from "./rugcheck"
export * from "./tiplink"
export * from "./helius"

View File

@ -0,0 +1,26 @@
/**
* Fetch the price of a given token quoted in USDC using Jupiter API
* @param tokenId The token mint address
* @returns The price of the token quoted in USDC
*/
export async function fetchPrice(tokenId) {
try {
const response = await fetch(`https://api.jup.ag/price/v2?ids=${tokenId}`)
if (!response.ok) {
throw new Error(`Failed to fetch price: ${response.statusText}`)
}
const data = await response.json()
const price = data.data[tokenId.toBase58()]?.price
if (!price) {
throw new Error("Price data not available for the given token.")
}
return price
} catch (error) {
throw new Error(`Price fetch failed: ${error.message}`)
}
}

View File

@ -0,0 +1,3 @@
export * from "./fetch_price"
export * from "./stake_with_jup"
export * from "./trade"

Some files were not shown because too many files have changed in this diff Show More