init
This commit is contained in:
commit
10673f4d13
9
.env
Normal file
9
.env
Normal 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
39
.gitignore
vendored
Normal 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
40
README.md
Normal 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
8
SCHEMA.sql
Normal 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
28
TODO
Normal 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
246
app/api/chat/route.js
Normal 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
21
components.json
Normal 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
91
components/chat.jsx
Normal 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
102
components/nav-user.jsx
Normal 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
182
components/sidebar-app.jsx
Normal 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
33
components/ui/avatar.jsx
Normal 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 }
|
92
components/ui/breadcrumb.jsx
Normal file
92
components/ui/breadcrumb.jsx
Normal 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
48
components/ui/button.jsx
Normal 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
50
components/ui/card.jsx
Normal 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 }
|
153
components/ui/chat-input.jsx
Normal file
153
components/ui/chat-input.jsx
Normal 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 };
|
56
components/ui/chat-message-area.jsx
Normal file
56
components/ui/chat-message-area.jsx
Normal 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";
|
262
components/ui/chat-message.jsx
Normal file
262
components/ui/chat-message.jsx
Normal 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
94
components/ui/dialog.jsx
Normal 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,
|
||||
}
|
158
components/ui/dropdown-menu.jsx
Normal file
158
components/ui/dropdown-menu.jsx
Normal 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
19
components/ui/input.jsx
Normal 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
16
components/ui/label.jsx
Normal 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 }
|
339
components/ui/markdown-content.jsx
Normal file
339
components/ui/markdown-content.jsx
Normal 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";
|
29
components/ui/radio-group.jsx
Normal file
29
components/ui/radio-group.jsx
Normal 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 }
|
38
components/ui/scroll-area.jsx
Normal file
38
components/ui/scroll-area.jsx
Normal 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
119
components/ui/select.jsx
Normal 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,
|
||||
}
|
23
components/ui/separator.jsx
Normal file
23
components/ui/separator.jsx
Normal 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
108
components/ui/sheet.jsx
Normal 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
620
components/ui/sidebar.jsx
Normal 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,
|
||||
}
|
14
components/ui/skeleton.jsx
Normal file
14
components/ui/skeleton.jsx
Normal 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 }
|
18
components/ui/textarea.jsx
Normal file
18
components/ui/textarea.jsx
Normal 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
28
components/ui/tooltip.jsx
Normal 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
14
eslint.config.mjs
Normal 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
19
hooks/use-mobile.jsx
Normal 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
|
||||
}
|
111
hooks/use-scroll-to-bottom.js
Normal file
111
hooks/use-scroll-to-bottom.js
Normal 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];
|
||||
}
|
35
hooks/use-textarea-resize.js
Normal file
35
hooks/use-textarea-resize.js
Normal 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
7
jsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
}
|
||||
}
|
46
lib/solana/PrivyEmbeddedWallet.js
Normal file
46
lib/solana/PrivyEmbeddedWallet.js
Normal 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
13
lib/utils.js
Normal 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
6
next.config.mjs
Normal file
@ -0,0 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
export default nextConfig;
|
14657
package-lock.json
generated
Normal file
14657
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
60
package.json
Normal file
60
package.json
Normal 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
41
pages/_app.js
Normal 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
13
pages/_document.js
Normal 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
86
pages/api/test.js
Normal 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
121
pages/chat/[chatid].js
Normal 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
66
pages/chat/index.js
Normal 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
5
pages/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
export default function Home() {
|
||||
return (
|
||||
<div>Homee</div>
|
||||
);
|
||||
}
|
128
pages/onboarding/delegate.js
Normal file
128
pages/onboarding/delegate.js
Normal 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
108
pages/onboarding/index.js
Normal 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
108
pages/onboarding/login.js
Normal 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
8
postcss.config.mjs
Normal file
@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
147
server/checkpointToVercelAI.js
Normal file
147
server/checkpointToVercelAI.js
Normal 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
3
server/checkpointer.js
Normal 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
7
server/db.js
Normal 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
102
server/graph.js
Normal 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;
|
32
server/tools/solana/transfer.js
Normal file
32
server/tools/solana/transfer.js
Normal 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
70
server/tools/testing.js
Normal 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",
|
||||
},
|
||||
);
|
177
solana-agent-kit/agent/index.js
Normal file
177
solana-agent-kit/agent/index.js
Normal 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)
|
||||
}
|
||||
|
||||
}
|
37
solana-agent-kit/constants/index.js
Normal file
37
solana-agent-kit/constants/index.js
Normal 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"
|
7
solana-agent-kit/index.js
Normal file
7
solana-agent-kit/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { SolanaAgentKit } from "./agent"
|
||||
import { createSolanaTools } from "./langchain"
|
||||
|
||||
export { SolanaAgentKit, createSolanaTools }
|
||||
|
||||
// Export action system
|
||||
export * from "./wallet/EmbeddedWallet"
|
38
solana-agent-kit/langchain/agent/create_image.js
Normal file
38
solana-agent-kit/langchain/agent/create_image.js
Normal 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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
2
solana-agent-kit/langchain/agent/index.js
Normal file
2
solana-agent-kit/langchain/agent/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./create_image"
|
||||
export * from "./wallet_address"
|
15
solana-agent-kit/langchain/agent/wallet_address.js
Normal file
15
solana-agent-kit/langchain/agent/wallet_address.js
Normal 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()
|
||||
}
|
||||
}
|
51
solana-agent-kit/langchain/gibwork/create_task.js
Normal file
51
solana-agent-kit/langchain/gibwork/create_task.js
Normal 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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
1
solana-agent-kit/langchain/gibwork/index.js
Normal file
1
solana-agent-kit/langchain/gibwork/index.js
Normal file
@ -0,0 +1 @@
|
||||
export * from "./create_task"
|
38
solana-agent-kit/langchain/helius/get_all_assets.js
Normal file
38
solana-agent-kit/langchain/helius/get_all_assets.js
Normal 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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
2
solana-agent-kit/langchain/helius/index.js
Normal file
2
solana-agent-kit/langchain/helius/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./get_all_assets"
|
||||
export * from "./parse_transaction"
|
33
solana-agent-kit/langchain/helius/parse_transaction.js
Normal file
33
solana-agent-kit/langchain/helius/parse_transaction.js
Normal 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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
51
solana-agent-kit/langchain/index.js
Normal file
51
solana-agent-kit/langchain/index.js
Normal 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),
|
||||
]
|
||||
}
|
34
solana-agent-kit/langchain/jupiter/fetch_price.js
Normal file
34
solana-agent-kit/langchain/jupiter/fetch_price.js
Normal 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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
3
solana-agent-kit/langchain/jupiter/index.js
Normal file
3
solana-agent-kit/langchain/jupiter/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./fetch_price"
|
||||
export * from "./trade"
|
||||
export * from "./stake"
|
35
solana-agent-kit/langchain/jupiter/stake.js
Normal file
35
solana-agent-kit/langchain/jupiter/stake.js
Normal 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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
48
solana-agent-kit/langchain/jupiter/trade.js
Normal file
48
solana-agent-kit/langchain/jupiter/trade.js
Normal 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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
1
solana-agent-kit/langchain/pumpfun/index.js
Normal file
1
solana-agent-kit/langchain/pumpfun/index.js
Normal file
@ -0,0 +1 @@
|
||||
export * from "./launch_pumpfun_token"
|
79
solana-agent-kit/langchain/pumpfun/launch_pumpfun_token.js
Normal file
79
solana-agent-kit/langchain/pumpfun/launch_pumpfun_token.js
Normal 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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
2
solana-agent-kit/langchain/rugcheck/index.js
Normal file
2
solana-agent-kit/langchain/rugcheck/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./token_report_summary"
|
||||
export * from "./token_report_detailed"
|
32
solana-agent-kit/langchain/rugcheck/token_report_detailed.js
Normal file
32
solana-agent-kit/langchain/rugcheck/token_report_detailed.js
Normal 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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
32
solana-agent-kit/langchain/rugcheck/token_report_summary.js
Normal file
32
solana-agent-kit/langchain/rugcheck/token_report_summary.js
Normal 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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
1
solana-agent-kit/langchain/sns/index.js
Normal file
1
solana-agent-kit/langchain/sns/index.js
Normal file
@ -0,0 +1 @@
|
||||
export * from "./resolve_domain";
|
36
solana-agent-kit/langchain/sns/resolve_domain.js
Normal file
36
solana-agent-kit/langchain/sns/resolve_domain.js
Normal 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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
37
solana-agent-kit/langchain/solana/balance.js
Normal file
37
solana-agent-kit/langchain/solana/balance.js
Normal 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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
44
solana-agent-kit/langchain/solana/balance_other.js
Normal file
44
solana-agent-kit/langchain/solana/balance_other.js
Normal 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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
33
solana-agent-kit/langchain/solana/close_empty_accounts.js
Normal file
33
solana-agent-kit/langchain/solana/close_empty_accounts.js
Normal 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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
20
solana-agent-kit/langchain/solana/get_tps.js
Normal file
20
solana-agent-kit/langchain/solana/get_tps.js
Normal 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}`
|
||||
}
|
||||
}
|
||||
}
|
5
solana-agent-kit/langchain/solana/index.js
Normal file
5
solana-agent-kit/langchain/solana/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
export * from "./get_tps"
|
||||
export * from "./balance"
|
||||
export * from "./balance_other"
|
||||
export * from "./close_empty_accounts"
|
||||
export * from "./transfer"
|
49
solana-agent-kit/langchain/solana/transfer.js
Normal file
49
solana-agent-kit/langchain/solana/transfer.js
Normal 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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
1
solana-agent-kit/langchain/tiplink/index.js
Normal file
1
solana-agent-kit/langchain/tiplink/index.js
Normal file
@ -0,0 +1 @@
|
||||
export * from "./tiplink"
|
50
solana-agent-kit/langchain/tiplink/tiplink.js
Normal file
50
solana-agent-kit/langchain/tiplink/tiplink.js
Normal 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"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
33
solana-agent-kit/tools/agent/create_image.js
Normal file
33
solana-agent-kit/tools/agent/create_image.js
Normal 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}`)
|
||||
}
|
||||
}
|
8
solana-agent-kit/tools/agent/get_wallet_address.js
Normal file
8
solana-agent-kit/tools/agent/get_wallet_address.js
Normal 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()
|
||||
}
|
2
solana-agent-kit/tools/agent/index.js
Normal file
2
solana-agent-kit/tools/agent/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./create_image"
|
||||
export * from "./get_wallet_address"
|
79
solana-agent-kit/tools/gibwork/create_gibwork_task.js
Normal file
79
solana-agent-kit/tools/gibwork/create_gibwork_task.js
Normal 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}`)
|
||||
}
|
||||
}
|
1
solana-agent-kit/tools/gibwork/index.js
Normal file
1
solana-agent-kit/tools/gibwork/index.js
Normal file
@ -0,0 +1 @@
|
||||
export * from "./create_gibwork_task"
|
50
solana-agent-kit/tools/helius/get_assets_by_owner.js
Normal file
50
solana-agent-kit/tools/helius/get_assets_by_owner.js
Normal 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}`)
|
||||
}
|
||||
}
|
39
solana-agent-kit/tools/helius/helius_transaction_parsing.js
Normal file
39
solana-agent-kit/tools/helius/helius_transaction_parsing.js
Normal 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}`)
|
||||
}
|
||||
}
|
2
solana-agent-kit/tools/helius/index.js
Normal file
2
solana-agent-kit/tools/helius/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./get_assets_by_owner"
|
||||
export * from "./helius_transaction_parsing"
|
9
solana-agent-kit/tools/index.js
Normal file
9
solana-agent-kit/tools/index.js
Normal 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"
|
26
solana-agent-kit/tools/jupiter/fetch_price.js
Normal file
26
solana-agent-kit/tools/jupiter/fetch_price.js
Normal 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}`)
|
||||
}
|
||||
}
|
3
solana-agent-kit/tools/jupiter/index.js
Normal file
3
solana-agent-kit/tools/jupiter/index.js
Normal 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
Loading…
x
Reference in New Issue
Block a user