├── .cursorrules ├── .dockerignore ├── .env.example ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── CHANGELOGS.md ├── CONTRIBUTING.md ├── Dockerfile.local ├── LICENSE.md ├── LOCAL_DEV.md ├── README.md ├── SECURITY.md ├── components.json ├── docker-compose.yml ├── eslint.config.mjs ├── local_entrypoint.sh ├── next.config.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── prisma ├── migrations │ ├── 20241223223656_inital │ │ └── migration.sql │ ├── 20241228165314_rules_actions │ │ └── migration.sql │ ├── 20250102065518_add_message_tokens │ │ └── migration.sql │ ├── 20250102200737_message_tokens_drop_unique_user_id │ │ └── migration.sql │ ├── 20250103193523_token_stats_multiple_types │ │ └── migration.sql │ ├── 20250104044437_ │ │ └── migration.sql │ ├── 20250104053417_ │ │ └── migration.sql │ ├── 20250108170751_add_telegram_chat_table │ │ └── migration.sql │ ├── 20250109220033_privy_embedded │ │ └── migration.sql │ ├── 20250109224129_composite_wallet_key │ │ └── migration.sql │ ├── 20250113001512_add_failure_tracking_actions │ │ └── migration.sql │ ├── 20250115064130_add_saved_prompts │ │ └── migration.sql │ ├── 20250118210411_modify_message_content_string │ │ └── migration.sql │ ├── 20250120202224_add_action_starttime │ │ └── migration.sql │ ├── 20250120222231_user_degen_mode │ │ └── migration.sql │ ├── 20250123201805_add_conversation_read │ │ └── migration.sql │ ├── 20250124072441_add_subscriptions │ │ └── migration.sql │ ├── 20250127070601_add_user_referral_code │ │ └── migration.sql │ ├── 20250131222120_change_sub_amount_to_decimal │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── icons │ └── solscan.svg ├── integrations │ ├── defined_fi.svg │ ├── dexscreener.svg │ ├── dialect.svg │ ├── jupiter.svg │ ├── magic_eden.svg │ └── pump_fun.svg ├── letter.svg ├── letter_w.svg ├── logo.png ├── logo.svg ├── logo_w.png ├── logo_w.svg ├── product.png └── product_dark.png ├── src ├── ai │ ├── generic │ │ ├── action.tsx │ │ ├── jina.tsx │ │ ├── telegram.tsx │ │ └── util.tsx │ ├── providers.tsx │ └── solana │ │ ├── birdeye.tsx │ │ ├── bundle.tsx │ │ ├── chart.tsx │ │ ├── cookie.tsx │ │ ├── defined-fi.tsx │ │ ├── dexscreener.tsx │ │ ├── jupiter.tsx │ │ ├── magic-eden.tsx │ │ ├── pumpfun.tsx │ │ └── solana.tsx ├── app │ ├── (misc) │ │ └── refresh │ │ │ ├── page.tsx │ │ │ └── refresh-content.tsx │ ├── (user) │ │ ├── account │ │ │ ├── account-content.tsx │ │ │ ├── loading-skeleton.tsx │ │ │ └── page.tsx │ │ ├── chat │ │ │ ├── [id] │ │ │ │ ├── chat-interface.tsx │ │ │ │ ├── chat-skeleton.tsx │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── faq │ │ │ └── page.tsx │ │ ├── home │ │ │ ├── components │ │ │ │ ├── integration-card.tsx │ │ │ │ └── integrations-grid.tsx │ │ │ ├── conversation-input.tsx │ │ │ ├── data │ │ │ │ ├── integrations.ts │ │ │ │ └── suggestions.ts │ │ │ ├── home-content.tsx │ │ │ ├── page.tsx │ │ │ └── suggestion-card.tsx │ │ ├── layout.tsx │ │ ├── memories │ │ │ └── page.tsx │ │ └── saved-prompts │ │ │ ├── components │ │ │ ├── delete-prompt-dialog.tsx │ │ │ ├── edit-prompt-dialog.tsx │ │ │ └── filter-dropdown.tsx │ │ │ ├── page.tsx │ │ │ └── types │ │ │ └── prompt.ts │ ├── api │ │ ├── actions │ │ │ ├── [id] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── chat │ │ │ ├── [conversationId] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── conversations │ │ │ └── route.ts │ │ ├── cron │ │ │ ├── minute │ │ │ │ └── route.ts │ │ │ └── subscription │ │ │ │ └── route.ts │ │ ├── discord │ │ │ └── grant-role │ │ │ │ └── route.ts │ │ └── wallet │ │ │ └── [address] │ │ │ └── portfolio │ │ │ └── route.ts │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components │ ├── action-emitter.tsx │ ├── bundle-list.tsx │ ├── coming-soon.tsx │ ├── confimation.tsx │ ├── cookie-fun │ │ ├── cookie-agent.tsx │ │ └── cookie-tweet.tsx │ ├── dashboard │ │ ├── app-sidebar-automations.tsx │ │ ├── app-sidebar-conversations.tsx │ │ ├── app-sidebar-user.tsx │ │ ├── app-sidebar.tsx │ │ ├── integration-card.tsx │ │ └── wallet-card.tsx │ ├── dynamic-image.tsx │ ├── eap-transaction-checker.tsx │ ├── floating-wallet.tsx │ ├── logo.tsx │ ├── message │ │ ├── pumpfun-launch.tsx │ │ ├── token-grid.tsx │ │ ├── tool-result.tsx │ │ └── wallet-portfolio.tsx │ ├── page-loading.tsx │ ├── page-maintenance.tsx │ ├── price-chart.tsx │ ├── provider-auth.tsx │ ├── provider-theme.tsx │ ├── referral-section.tsx │ ├── saved-prompts-menu.tsx │ ├── subscription │ │ └── subscription-section.tsx │ ├── theme-toggle.tsx │ ├── top-trader.tsx │ ├── transfer-dialog.tsx │ ├── tweet-card.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── ai-particles-background.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── animated-beam.tsx │ │ ├── animated-list.tsx │ │ ├── animated-shiny-text.tsx │ │ ├── animated-subscribe-button.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── banner.tsx │ │ ├── bento-grid.tsx │ │ ├── blur-fade.tsx │ │ ├── blur-in.tsx │ │ ├── border-beam.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── chart.tsx │ │ ├── circle.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── copyable-text.tsx │ │ ├── dialog.tsx │ │ ├── dot-pattern.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── integrations-background.tsx │ │ ├── label.tsx │ │ ├── marquee.tsx │ │ ├── number-ticker.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── rainbow-button.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── shimmer-button.tsx │ │ ├── shine-border.tsx │ │ ├── shiny-button.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── textarea.tsx │ │ ├── tooltip.tsx │ │ ├── tweet-card.tsx │ │ └── typing-animation.tsx ├── hooks │ ├── store │ │ └── conversations.ts │ ├── use-actions.ts │ ├── use-conversations.ts │ ├── use-media-query.ts │ ├── use-mobile.tsx │ ├── use-polling.ts │ ├── use-user.ts │ ├── use-wallet-portfolio.ts │ └── use-wallets.ts ├── lib │ ├── constants.ts │ ├── debug.ts │ ├── events.ts │ ├── format.ts │ ├── placeholder.ts │ ├── prisma.ts │ ├── safe-action.ts │ ├── solana │ │ ├── PrivyEmbeddedWallet.ts │ │ ├── helius.ts │ │ ├── index.ts │ │ ├── integrations │ │ │ ├── defined_fi.ts │ │ │ ├── dexscreener.ts │ │ │ ├── dialect.ts │ │ │ ├── jupiter.ts │ │ │ └── magic_eden.ts │ │ └── wallet-generator.ts │ ├── upload.ts │ ├── utils.ts │ └── utils │ │ ├── ai.ts │ │ ├── format.ts │ │ ├── grant-discord-role.ts │ │ └── known-addresses.json ├── middleware.ts ├── server │ ├── actions │ │ ├── action.ts │ │ ├── ai.ts │ │ ├── birdeye.ts │ │ ├── bundle.ts │ │ ├── chart.ts │ │ ├── charts.ts │ │ ├── conversation.ts │ │ ├── cookie.ts │ │ ├── eap.ts │ │ ├── jupiter.ts │ │ ├── orchestrator.ts │ │ ├── saved-prompt.ts │ │ ├── subscription.ts │ │ ├── telegram.ts │ │ ├── user.ts │ │ └── wallet.ts │ ├── db │ │ └── queries.ts │ └── utils │ │ └── index.ts ├── store │ └── action-store.ts ├── test.ts └── types │ ├── action.ts │ ├── bundle.ts │ ├── chart.ts │ ├── db.ts │ ├── global.d.ts │ ├── helius │ ├── fungibleToken.ts │ ├── nonFungibleToken.ts │ └── portfolio.ts │ ├── phantom.d.ts │ ├── subscription.ts │ └── util.ts ├── tailwind.config.ts ├── tsconfig.json └── vercel.json /.cursorrules: -------------------------------------------------------------------------------- 1 | You are an expert in TypeScript, Node.js, Next.js App Router, React, Shadcn UI, Radix UI, Tailwind CSS, and performance optimization. 2 | 3 | Code Style & Structure: Use concise, technical TypeScript with clear examples. Prefer functional, declarative patterns over classes. Avoid code duplication, favor iteration & modularization. Use descriptive variable names (e.g., isLoading, hasError). Structure files: components, subcomponents, helpers, static content, types. Use lowercase with dashes for directories (e.g., components/auth-wizard). Favor named exports for components. 4 | 5 | TypeScript Usage: Use TypeScript for all code; prefer interfaces over types. Avoid enums; use maps instead. Use functional components with TypeScript interfaces. Use the `function` keyword for pure functions. Avoid unnecessary curly braces in conditionals; use concise syntax. 6 | 7 | UI & Styling: Use Shadcn UI, Radix, and Tailwind CSS. Implement responsive design with Tailwind, mobile-first. Optimize images (WebP, size data, lazy loading). 8 | 9 | Performance Optimization: Minimize 'use client', 'useEffect', 'setState'; favor React Server Components (RSC). Wrap client components in Suspense with a fallback. Use dynamic loading for non-critical components. Optimize Web Vitals (LCP, CLS, FID). 10 | 11 | Next.js Specifics: Use `next-safe-action` for server actions: 12 | - Create type-safe actions with validation using Zod. 13 | - Use `action` function for actions. 14 | - Return `ActionResponse` type with error handling. 15 | 16 | Example Action: 17 | 'use server' 18 | import { actionClient, ActionResponse } from "@/lib/safe-action"; 19 | import { z } from 'zod' 20 | 21 | const schema = z.object({ 22 | value: z.string() 23 | }) 24 | 25 | export const someAction = actionClient 26 | .schema(schema) 27 | .action(async (input): Promise => { 28 | try { 29 | return { success: true, data: /* result */ } 30 | } catch (error) { 31 | return { success: false, error: error instanceof AppError ? error : appErrors.UNEXPECTED_ERROR } 32 | } 33 | }) 34 | 35 | Use `useQueryState` for query state management. 36 | 37 | Example: 38 | 'use client' 39 | import { useQueryState } from 'nuqs' 40 | 41 | export function Demo() { 42 | const [name, setName] = useQueryState('name') 43 | return ( 44 | <> 45 | setName(e.target.value)} /> 46 | 47 |

Hello, {name || 'anonymous visitor'}!

48 | 49 | ) 50 | } 51 | 52 | Key Conventions: Use `nuqs` for URL query state management. Optimize Web Vitals (LCP, CLS, FID). Limit 'use client' to small Web API components. Avoid 'use client' for data fetching or state management. 53 | 54 | Additional Notes: All server actions are in `src/server/actions`. Use sonner for toasts: 55 | import { toast } from "sonner" 56 | toast("Transaction Sent") 57 | Use pnpm for package management. -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | local-db 2 | **/node_modules/ 3 | .pnpm-store 4 | **/build/ 5 | **/dist/ 6 | .git/ 7 | .github/ 8 | **/.next/ -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_PRIVY_APP_ID= 2 | NEXT_PUBLIC_EAP_RECEIVE_WALLET_ADDRESS= 3 | NEXT_PUBLIC_HELIUS_RPC_URL= 4 | NEXT_PUBLIC_SUB_LAMPORTS=1000000 5 | NEXT_PUBLIC_SUB_ENABLED=false 6 | NEXT_PUBLIC_TRIAL_LAMPORTS=0 7 | NEXT_PUBLIC_TRIAL_ENABLED=false 8 | NEXT_PUBLIC_NEUR_MINT=3N2ETvNpPNAxhcaXgkhKoY1yDnQfs41Wnxsx5qNJpump 9 | NEXT_PUBLIC_DISABLED_TOOLS=[] #["getTokenOrders", "holders", "resolveWalletAddressFromDomain"] 10 | OPENAI_API_KEY= 11 | PRIVY_APP_SECRET= 12 | WALLET_ENCRYPTION_KEY= 13 | NEXT_PUBLIC_IMGBB_API_KEY= 14 | ANTHROPIC_API_KEY= 15 | DATABASE_URL= 16 | DIRECT_URL= 17 | CRON_SECRET= 18 | HELIUS_API_KEY= 19 | # coingecko api key and base url 20 | CG_API_KEY= 21 | CG_BASE_URL= 22 | TELEGRAM_BOT_TOKEN= 23 | TELEGRAM_BOT_USERNAME= 24 | DISCORD_BOT_TOKEN= 25 | DISCORD_GUILD_ID= 26 | DISCORD_ROLE_ID= 27 | PRIVY_SIGNING_KEY= 28 | BIRDEYE_API_KEY= 29 | COOKIE_FUN_API_KEY= 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | .local 16 | 17 | # next.js 18 | /.next/ 19 | /out/ 20 | 21 | # production 22 | /build 23 | 24 | # misc 25 | .DS_Store 26 | *.pem 27 | 28 | # debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | .pnpm-debug.log* 33 | 34 | # env files (can opt-in for committing if needed) 35 | .env* 36 | !.env.example 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | next-env.d.ts 44 | .env*.local 45 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | .pnpm-store 4 | 5 | # Next.js 6 | .next 7 | out 8 | 9 | # Production 10 | build 11 | dist 12 | 13 | # Debug 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | .pnpm-debug.log* 18 | 19 | # Local env files 20 | .env* 21 | 22 | # Vercel 23 | .vercel 24 | 25 | # TypeScript 26 | *.tsbuildinfo 27 | next-env.d.ts 28 | 29 | # Cache 30 | .cache -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 80, 6 | "tabWidth": 2, 7 | "useTabs": false, 8 | "bracketSpacing": true, 9 | "bracketSameLine": false, 10 | "arrowParens": "always", 11 | "endOfLine": "lf", 12 | "plugins": [ 13 | "@trivago/prettier-plugin-sort-imports", 14 | "prettier-plugin-tailwindcss" 15 | ], 16 | "tailwindConfig": "./tailwind.config.ts", 17 | "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"], 18 | "importOrder": [ 19 | "^(react/(.*)$)|^(react$)", 20 | "^(next/(.*)$)|^(next$)", 21 | "", 22 | "^@/(.*)$", 23 | "^[./]" 24 | ], 25 | "importOrderSeparation": true, 26 | "importOrderSortSpecifiers": true 27 | } 28 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": "explicit" 6 | }, 7 | "[typescript]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | }, 10 | "[typescriptreact]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | }, 13 | "[javascript]": { 14 | "editor.defaultFormatter": "esbenp.prettier-vscode" 15 | }, 16 | "[javascriptreact]": { 17 | "editor.defaultFormatter": "esbenp.prettier-vscode" 18 | }, 19 | "[json]": { 20 | "editor.defaultFormatter": "esbenp.prettier-vscode" 21 | }, 22 | "[jsonc]": { 23 | "editor.defaultFormatter": "esbenp.prettier-vscode" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Dockerfile.local: -------------------------------------------------------------------------------- 1 | FROM node:20 AS base 2 | ENV PNPM_HOME="/pnpm" 3 | ENV PATH="$PNPM_HOME:$PATH" 4 | RUN corepack enable 5 | 6 | FROM base AS dev 7 | COPY . /usr/src/app 8 | WORKDIR /usr/src/app 9 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile 10 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm npx prisma generate -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 neur.studio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | neur-app: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile.local 8 | target: dev 9 | container_name: "neur-app" 10 | depends_on: 11 | neur-db: 12 | condition: service_healthy 13 | expose: 14 | - "3000" 15 | ports: 16 | - "3000:3000" 17 | working_dir: /usr/src/app 18 | command: "/bin/bash ./local_entrypoint.sh" 19 | env_file: 20 | - .env 21 | environment: 22 | - DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@neur-db:5432/neurdb 23 | - DIRECT_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@neur-db:5432/neurdb 24 | volumes: 25 | - .:/usr/src/app 26 | - root_node_modules:/usr/src/app/node_modules 27 | - webapp_next:/usr/src/app/.next 28 | 29 | neur-studio: 30 | build: 31 | context: . 32 | dockerfile: Dockerfile.local 33 | target: dev 34 | container_name: "neur-studio" 35 | depends_on: 36 | neur-db: 37 | condition: service_healthy 38 | environment: 39 | - DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@neur-db:5432/neurdb 40 | expose: 41 | - "5555" 42 | ports: 43 | - "5555:5555" 44 | working_dir: /usr/src/app 45 | command: "pnpm npx prisma studio" 46 | volumes: 47 | - .:/usr/src/app 48 | - root_node_modules:/usr/src/app/node_modules 49 | - webapp_next:/usr/src/app/.next 50 | 51 | neur-db: 52 | image: "postgres:15" 53 | container_name: "neur-db" 54 | healthcheck: 55 | test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] 56 | interval: 10s 57 | timeout: 5s 58 | retries: 5 59 | environment: 60 | - POSTGRES_USER=${POSTGRES_USER} 61 | - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} 62 | ports: 63 | - "5432:5432" 64 | volumes: 65 | - "neur-db-data:/var/lib/postgresql/data" 66 | 67 | volumes: 68 | neur-db-data: 69 | root_node_modules: 70 | webapp_next: 71 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | { 15 | ignores: ['src/components/ui/*', 'src/components/ui/**/*', 'src/hooks/*', 'src/hooks/**/*'] 16 | }, 17 | { 18 | rules: { 19 | "@typescript-eslint/no-unused-vars": "off", 20 | "@typescript-eslint/no-explicit-any": "off", 21 | "@typescript-eslint/no-require-imports": "off", 22 | "@typescript-eslint/no-var-requires": "off" 23 | } 24 | } 25 | ]; 26 | 27 | export default eslintConfig; 28 | -------------------------------------------------------------------------------- /local_entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | pnpm run generate 3 | pnpm run migrate 4 | pnpm run dev -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next'; 2 | 3 | const nextConfig: NextConfig = { 4 | images: { 5 | remotePatterns: [ 6 | { 7 | protocol: 'https', 8 | hostname: '*', 9 | }, 10 | ], 11 | }, 12 | webpack: (config) => { 13 | config.module.rules.push({ 14 | test: /\.json$/, 15 | type: 'json', 16 | }); 17 | return config; 18 | }, 19 | }; 20 | 21 | export default nextConfig; 22 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20241223223656_inital/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "Visibility" AS ENUM ('PRIVATE', 'PUBLIC'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "users" ( 6 | "id" TEXT NOT NULL, 7 | "privyId" TEXT NOT NULL, 8 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | "updatedAt" TIMESTAMP(3) NOT NULL, 10 | "earlyAccess" BOOLEAN NOT NULL DEFAULT false, 11 | 12 | CONSTRAINT "users_pkey" PRIMARY KEY ("id") 13 | ); 14 | 15 | -- CreateTable 16 | CREATE TABLE "wallets" ( 17 | "id" TEXT NOT NULL, 18 | "ownerId" TEXT NOT NULL, 19 | "name" TEXT NOT NULL, 20 | "publicKey" TEXT NOT NULL, 21 | "encryptedPrivateKey" TEXT NOT NULL, 22 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 23 | "updatedAt" TIMESTAMP(3) NOT NULL, 24 | 25 | CONSTRAINT "wallets_pkey" PRIMARY KEY ("id") 26 | ); 27 | 28 | -- CreateTable 29 | CREATE TABLE "conversations" ( 30 | "id" TEXT NOT NULL, 31 | "userId" TEXT NOT NULL, 32 | "title" TEXT NOT NULL, 33 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 34 | "updatedAt" TIMESTAMP(3) NOT NULL, 35 | "visibility" "Visibility" NOT NULL DEFAULT 'PRIVATE', 36 | 37 | CONSTRAINT "conversations_pkey" PRIMARY KEY ("id") 38 | ); 39 | 40 | -- CreateTable 41 | CREATE TABLE "messages" ( 42 | "id" TEXT NOT NULL, 43 | "conversationId" TEXT NOT NULL, 44 | "role" TEXT NOT NULL, 45 | "content" JSONB NOT NULL, 46 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 47 | 48 | CONSTRAINT "messages_pkey" PRIMARY KEY ("id") 49 | ); 50 | 51 | -- CreateIndex 52 | CREATE UNIQUE INDEX "users_privyId_key" ON "users"("privyId"); 53 | 54 | -- CreateIndex 55 | CREATE UNIQUE INDEX "wallets_ownerId_key" ON "wallets"("ownerId"); 56 | 57 | -- AddForeignKey 58 | ALTER TABLE "wallets" ADD CONSTRAINT "wallets_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 59 | 60 | -- AddForeignKey 61 | ALTER TABLE "conversations" ADD CONSTRAINT "conversations_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 62 | 63 | -- AddForeignKey 64 | ALTER TABLE "messages" ADD CONSTRAINT "messages_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "conversations"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 65 | -------------------------------------------------------------------------------- /prisma/migrations/20241228165314_rules_actions/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "Operator" AS ENUM ('eq', 'lt', 'gt', 'contains'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "rules" ( 6 | "id" TEXT NOT NULL, 7 | "userId" TEXT NOT NULL, 8 | "name" VARCHAR(255) NOT NULL, 9 | "field" VARCHAR(255) NOT NULL, 10 | "operator" "Operator" NOT NULL, 11 | "value" VARCHAR(255) NOT NULL, 12 | "triggered" BOOLEAN NOT NULL DEFAULT false, 13 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 14 | "updatedAt" TIMESTAMP(3) NOT NULL, 15 | 16 | CONSTRAINT "rules_pkey" PRIMARY KEY ("id") 17 | ); 18 | 19 | -- CreateTable 20 | CREATE TABLE "actions" ( 21 | "id" TEXT NOT NULL, 22 | "userId" TEXT NOT NULL, 23 | "conversationId" TEXT NOT NULL, 24 | "triggeredBy" INTEGER[], 25 | "stoppedBy" INTEGER[], 26 | "frequency" INTEGER, 27 | "maxExecutions" INTEGER, 28 | "description" VARCHAR(255) NOT NULL, 29 | "actionType" VARCHAR(255) NOT NULL, 30 | "params" JSONB, 31 | "timesExecuted" INTEGER NOT NULL DEFAULT 0, 32 | "lastExecutedAt" TIMESTAMP(3), 33 | "triggered" BOOLEAN NOT NULL DEFAULT false, 34 | "paused" BOOLEAN NOT NULL DEFAULT false, 35 | "completed" BOOLEAN NOT NULL DEFAULT false, 36 | "priority" INTEGER NOT NULL DEFAULT 0, 37 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 38 | "updatedAt" TIMESTAMP(3) NOT NULL, 39 | 40 | CONSTRAINT "actions_pkey" PRIMARY KEY ("id") 41 | ); 42 | 43 | -- CreateIndex 44 | CREATE INDEX "triggeredBy_idx" ON "actions"("triggeredBy"); 45 | 46 | -- CreateIndex 47 | CREATE INDEX "stoppedBy_idx" ON "actions"("stoppedBy"); 48 | 49 | -- AddForeignKey 50 | ALTER TABLE "rules" ADD CONSTRAINT "rules_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 51 | 52 | -- AddForeignKey 53 | ALTER TABLE "actions" ADD CONSTRAINT "actions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 54 | 55 | -- AddForeignKey 56 | ALTER TABLE "actions" ADD CONSTRAINT "actions_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "conversations"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 57 | -------------------------------------------------------------------------------- /prisma/migrations/20250102065518_add_message_tokens/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "token_stats" ( 3 | "id" TEXT NOT NULL, 4 | "userId" TEXT NOT NULL, 5 | "messageIds" TEXT[], 6 | "totalTokens" INTEGER NOT NULL, 7 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | "updatedAt" TIMESTAMP(3) NOT NULL, 9 | 10 | CONSTRAINT "token_stats_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateIndex 14 | CREATE UNIQUE INDEX "token_stats_userId_key" ON "token_stats"("userId"); 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "token_stats" ADD CONSTRAINT "token_stats_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 18 | -------------------------------------------------------------------------------- /prisma/migrations/20250102200737_message_tokens_drop_unique_user_id/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "token_stats_userId_key"; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250103193523_token_stats_multiple_types/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `completionTokens` to the `token_stats` table without a default value. This is not possible if the table is not empty. 5 | - Added the required column `promptTokens` to the `token_stats` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "token_stats" ADD COLUMN "completionTokens" INTEGER NOT NULL, 10 | ADD COLUMN "promptTokens" INTEGER NOT NULL; 11 | -------------------------------------------------------------------------------- /prisma/migrations/20250104044437_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "users" ADD COLUMN "telegramId" TEXT; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250104053417_/migration.sql: -------------------------------------------------------------------------------- 1 | -- This is an empty migration. -------------------------------------------------------------------------------- /prisma/migrations/20250108170751_add_telegram_chat_table/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `telegramId` on the `users` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "users" DROP COLUMN "telegramId"; 9 | 10 | -- CreateTable 11 | CREATE TABLE "telegram_chats" ( 12 | "id" TEXT NOT NULL, 13 | "userId" TEXT NOT NULL, 14 | "username" TEXT NOT NULL, 15 | "chatId" TEXT, 16 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 17 | 18 | CONSTRAINT "telegram_chats_pkey" PRIMARY KEY ("id") 19 | ); 20 | 21 | -- CreateIndex 22 | CREATE UNIQUE INDEX "telegram_chats_userId_key" ON "telegram_chats"("userId"); 23 | 24 | -- AddForeignKey 25 | ALTER TABLE "telegram_chats" ADD CONSTRAINT "telegram_chats_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 26 | -------------------------------------------------------------------------------- /prisma/migrations/20250109220033_privy_embedded/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "WalletSource" AS ENUM ('CUSTOM', 'PRIVY'); 3 | 4 | -- CreateEnum 5 | CREATE TYPE "Chain" AS ENUM ('SOLANA'); 6 | 7 | -- DropIndex 8 | DROP INDEX "wallets_ownerId_key"; 9 | 10 | -- AlterTable 11 | ALTER TABLE "wallets" ADD COLUMN "active" BOOLEAN NOT NULL DEFAULT true, 12 | ADD COLUMN "chain" "Chain" NOT NULL DEFAULT 'SOLANA', 13 | ADD COLUMN "delegated" BOOLEAN NOT NULL DEFAULT false, 14 | ADD COLUMN "walletSource" "WalletSource" NOT NULL DEFAULT 'CUSTOM', 15 | ALTER COLUMN "encryptedPrivateKey" DROP NOT NULL; 16 | -------------------------------------------------------------------------------- /prisma/migrations/20250109224129_composite_wallet_key/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[ownerId,publicKey]` on the table `wallets` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- CreateIndex 8 | CREATE UNIQUE INDEX "wallets_ownerId_publicKey_key" ON "wallets"("ownerId", "publicKey"); 9 | -------------------------------------------------------------------------------- /prisma/migrations/20250113001512_add_failure_tracking_actions/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "actions" ADD COLUMN "lastFailureAt" TIMESTAMP(3), 3 | ADD COLUMN "lastSuccessAt" TIMESTAMP(3); 4 | -------------------------------------------------------------------------------- /prisma/migrations/20250115064130_add_saved_prompts/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "saved_prompts" ( 3 | "id" TEXT NOT NULL, 4 | "userId" TEXT NOT NULL, 5 | "title" VARCHAR(255) NOT NULL, 6 | "content" TEXT NOT NULL, 7 | "usageFrequency" INTEGER NOT NULL DEFAULT 0, 8 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | "updatedAt" TIMESTAMP(3) NOT NULL, 10 | "lastUsedAt" TIMESTAMP(3), 11 | "isFavorite" BOOLEAN NOT NULL DEFAULT false, 12 | 13 | CONSTRAINT "saved_prompts_pkey" PRIMARY KEY ("id") 14 | ); 15 | 16 | -- CreateIndex 17 | CREATE INDEX "idx_userId_lastUsedAt" ON "saved_prompts"("userId", "lastUsedAt"); 18 | 19 | -- AddForeignKey 20 | ALTER TABLE "saved_prompts" ADD CONSTRAINT "saved_prompts_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 21 | -------------------------------------------------------------------------------- /prisma/migrations/20250118210411_modify_message_content_string/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "messages" ADD COLUMN "experimental_attachments" JSONB, 3 | ADD COLUMN "toolInvocations" JSONB, 4 | ALTER COLUMN "content" DROP NOT NULL, 5 | ALTER COLUMN "content" SET DATA TYPE TEXT; 6 | -------------------------------------------------------------------------------- /prisma/migrations/20250120202224_add_action_starttime/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "actions" ADD COLUMN "startTime" TIMESTAMP(3); 3 | ALTER TABLE "actions" ADD COLUMN "name" VARCHAR(255); 4 | UPDATE "actions" SET name = description; 5 | -------------------------------------------------------------------------------- /prisma/migrations/20250120222231_user_degen_mode/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "users" ADD COLUMN "degenMode" BOOLEAN NOT NULL DEFAULT false; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250123201805_add_conversation_read/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "conversations" ADD COLUMN "lastMessageAt" TIMESTAMP(3), 3 | ADD COLUMN "lastReadAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP; 4 | -------------------------------------------------------------------------------- /prisma/migrations/20250124072441_add_subscriptions/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "BillingCycle" AS ENUM ('MONTHLY', 'YEARLY'); 3 | 4 | -- CreateEnum 5 | CREATE TYPE "PaymentStatus" AS ENUM ('SUCCESS', 'FAILED', 'PENDING'); 6 | 7 | -- CreateTable 8 | CREATE TABLE "subscriptions" ( 9 | "id" TEXT NOT NULL, 10 | "userId" TEXT NOT NULL, 11 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 12 | "active" BOOLEAN NOT NULL DEFAULT true, 13 | "startDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 14 | "nextPaymentDate" TIMESTAMP(3) NOT NULL, 15 | "endDate" TIMESTAMP(3), 16 | "billingCycle" "BillingCycle" NOT NULL DEFAULT 'MONTHLY', 17 | 18 | CONSTRAINT "subscriptions_pkey" PRIMARY KEY ("id") 19 | ); 20 | 21 | -- CreateTable 22 | CREATE TABLE "subscription_payments" ( 23 | "id" TEXT NOT NULL, 24 | "subscriptionId" TEXT NOT NULL, 25 | "paymentDate" TIMESTAMP(3) NOT NULL, 26 | "amount" DOUBLE PRECISION NOT NULL, 27 | "status" "PaymentStatus" NOT NULL, 28 | "failureReason" TEXT, 29 | "failureCode" INTEGER, 30 | "transactionHash" TEXT, 31 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 32 | 33 | CONSTRAINT "subscription_payments_pkey" PRIMARY KEY ("id") 34 | ); 35 | 36 | -- CreateIndex 37 | CREATE UNIQUE INDEX "subscriptions_userId_key" ON "subscriptions"("userId"); 38 | 39 | -- AddForeignKey 40 | ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 41 | 42 | -- AddForeignKey 43 | ALTER TABLE "subscription_payments" ADD CONSTRAINT "subscription_payments_subscriptionId_fkey" FOREIGN KEY ("subscriptionId") REFERENCES "subscriptions"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 44 | -------------------------------------------------------------------------------- /prisma/migrations/20250127070601_add_user_referral_code/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - A unique constraint covering the columns `[referralCode]` on the table `users` will be added. If there are existing duplicate values, this will fail. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "users" ADD COLUMN "referralCode" TEXT, 9 | ADD COLUMN "referringUserId" TEXT; 10 | 11 | -- CreateIndex 12 | CREATE UNIQUE INDEX "users_referralCode_key" ON "users"("referralCode"); 13 | -------------------------------------------------------------------------------- /prisma/migrations/20250131222120_change_sub_amount_to_decimal/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to alter the column `amount` on the `subscription_payments` table. The data in that column could be lost. The data in that column will be cast from `DoublePrecision` to `Decimal(65,30)`. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "subscription_payments" ALTER COLUMN "amount" SET DATA TYPE DECIMAL(65,30); 9 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /public/letter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/letter_w.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeurProjects/neur-app/2713bd44b2b4069d12a184d73d7c7e906ed6e748/public/logo.png -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/logo_w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeurProjects/neur-app/2713bd44b2b4069d12a184d73d7c7e906ed6e748/public/logo_w.png -------------------------------------------------------------------------------- /public/logo_w.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/product.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeurProjects/neur-app/2713bd44b2b4069d12a184d73d7c7e906ed6e748/public/product.png -------------------------------------------------------------------------------- /public/product_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeurProjects/neur-app/2713bd44b2b4069d12a184d73d7c7e906ed6e748/public/product_dark.png -------------------------------------------------------------------------------- /src/ai/generic/util.tsx: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | import { Button } from '@/components/ui/button'; 4 | import { Card } from '@/components/ui/card'; 5 | import { type ToolActionResult } from '@/types/util'; 6 | 7 | interface ConfirmDenyProps { 8 | message: string; 9 | } 10 | 11 | export const utilTools = { 12 | askForConfirmation: { 13 | displayName: '⚠️ Confirmation', 14 | description: 'Confirm the execution of a function on behalf of the user.', 15 | parameters: z.object({ 16 | message: z.string().describe('The message to ask for confirmation'), 17 | }), 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /src/ai/solana/birdeye.tsx: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | import TopTrader from '@/components/top-trader'; 4 | import { 5 | BirdeyeTimeframe, 6 | BirdeyeTrader, 7 | getTopTraders, 8 | } from '@/server/actions/birdeye'; 9 | 10 | export const birdeyeTools = { 11 | getTopTraders: { 12 | displayName: '📈 Top Traders', 13 | isCollapsible: true, 14 | isExpandedByDefault: true, 15 | description: 'Get top traders on Solana DEXes given a timeframe', 16 | parameters: z.object({ 17 | timeframe: z 18 | .nativeEnum(BirdeyeTimeframe) 19 | .describe('The timeframe to search for'), 20 | }), 21 | requiredEnvVars: ['BIRDEYE_API_KEY'], 22 | execute: async ({ timeframe }: { timeframe: BirdeyeTimeframe }) => { 23 | try { 24 | const traders = await getTopTraders({ timeframe }); 25 | 26 | return { 27 | success: true, 28 | data: traders, 29 | }; 30 | } catch (error) { 31 | return { 32 | success: false, 33 | error: 34 | error instanceof Error ? error.message : 'Failed to search traders', 35 | }; 36 | } 37 | }, 38 | render: (result: unknown) => { 39 | const typedResult = result as { 40 | success: boolean; 41 | data?: BirdeyeTrader[]; 42 | error?: string; 43 | }; 44 | 45 | if (!typedResult.success) { 46 | return ( 47 |
48 |
49 |

50 | Error: {typedResult.error} 51 |

52 |
53 |
54 | ); 55 | } 56 | 57 | if (!typedResult.data?.length) { 58 | return ( 59 |
60 |
61 |

No traders found

62 |
63 |
64 | ); 65 | } 66 | 67 | return ( 68 |
69 | {typedResult.data.map((trader, index) => ( 70 | 71 | ))} 72 |
73 | ); 74 | }, 75 | }, 76 | }; 77 | -------------------------------------------------------------------------------- /src/app/(misc)/refresh/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Suspense } from 'react'; 4 | 5 | import PageLoading from '@/components/page-loading'; 6 | 7 | import RefreshContent from './refresh-content'; 8 | 9 | export default function RefreshPage() { 10 | return ( 11 | }> 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/(misc)/refresh/refresh-content.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect } from 'react'; 4 | 5 | import { useRouter, useSearchParams } from 'next/navigation'; 6 | 7 | import { usePrivy } from '@privy-io/react-auth'; 8 | 9 | import PageLoading from '@/components/page-loading'; 10 | 11 | export default function RefreshContent() { 12 | const { getAccessToken } = usePrivy(); 13 | const router = useRouter(); 14 | const searchParams = useSearchParams(); 15 | 16 | useEffect(() => { 17 | async function refreshAndRedirect() { 18 | try { 19 | // Try to refresh access token 20 | const token = await getAccessToken(); 21 | // Get original target path, default to home if not provided 22 | const redirectUri = searchParams.get('redirect_uri') || '/'; 23 | 24 | if (token) { 25 | // User is authenticated, redirect to original target path 26 | router.replace(redirectUri); 27 | } else { 28 | // User is not authenticated, redirect to home (login) with original target path 29 | router.replace( 30 | `/${redirectUri !== '/' ? `?redirect_uri=${redirectUri}` : ''}`, 31 | ); 32 | } 33 | } catch (error) { 34 | console.error('Error refreshing token:', error); 35 | // Redirect to home on error 36 | router.replace('/'); 37 | } 38 | } 39 | 40 | refreshAndRedirect(); 41 | }, [getAccessToken, router, searchParams]); 42 | 43 | return ; 44 | } 45 | -------------------------------------------------------------------------------- /src/app/(user)/account/page.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Account page component 3 | * @file User account page with profile information and social account connections 4 | */ 5 | import { Metadata } from 'next'; 6 | 7 | import { AccountContent } from './account-content'; 8 | 9 | export const metadata: Metadata = { 10 | title: 'Account', 11 | description: 'A place to manage your account and settings', 12 | }; 13 | 14 | export default function AccountPage() { 15 | return ( 16 |
17 | {/* Page header */} 18 |
19 |
20 |

Account

21 |
22 |
23 | 24 | {/* Page content */} 25 | 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/app/(user)/chat/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | 3 | import { Metadata } from 'next'; 4 | import { notFound } from 'next/navigation'; 5 | 6 | import { verifyUser } from '@/server/actions/user'; 7 | import { 8 | dbGetConversation, 9 | dbGetConversationMessages, 10 | } from '@/server/db/queries'; 11 | 12 | import ChatInterface from './chat-interface'; 13 | import { ChatSkeleton } from './chat-skeleton'; 14 | 15 | /** 16 | * Generates metadata for the chat page based on conversation details 17 | */ 18 | export async function generateMetadata({ 19 | params, 20 | }: { 21 | params: Promise<{ id: string }>; 22 | }): Promise { 23 | const { id } = await params; 24 | const conversation = await dbGetConversation({ conversationId: id }); 25 | 26 | if (!conversation) { 27 | return { 28 | title: 'Chat Not Found', 29 | description: 'The requested chat conversation could not be found.', 30 | }; 31 | } 32 | 33 | return { 34 | title: `Chat - ${conversation.title || 'Untitled Conversation'}`, 35 | description: `Chat conversation: ${conversation.title || 'Untitled Conversation'}`, 36 | }; 37 | } 38 | 39 | /** 40 | * Component responsible for fetching and validating chat data 41 | * Handles authentication, data loading, and access control 42 | */ 43 | async function ChatData({ params }: { params: Promise<{ id: string }> }) { 44 | const { id } = await params; 45 | const conversation = await dbGetConversation({ conversationId: id }); 46 | 47 | if (!conversation) { 48 | return notFound(); 49 | } 50 | 51 | // Verify user authentication and access rights 52 | const authResponse = await verifyUser(); 53 | const userId = authResponse?.data?.data?.id; 54 | 55 | // Check if user has access to private conversation 56 | if ( 57 | conversation.visibility === 'PRIVATE' && 58 | (!userId || conversation.userId !== userId) 59 | ) { 60 | return notFound(); 61 | } 62 | 63 | // Load conversation messages 64 | const messagesFromDB = await dbGetConversationMessages({ 65 | conversationId: id, 66 | }); 67 | 68 | if (!messagesFromDB) { 69 | return notFound(); 70 | } 71 | 72 | return ; 73 | } 74 | 75 | /** 76 | * Main chat page component with loading state handling 77 | */ 78 | export default function ChatPage({ 79 | params, 80 | }: { 81 | params: Promise<{ id: string }>; 82 | }) { 83 | return ( 84 | }> 85 | 86 | 87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /src/app/(user)/chat/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function ChatLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode; 5 | }) { 6 | return ( 7 |
8 | {children} 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/app/(user)/faq/page.tsx: -------------------------------------------------------------------------------- 1 | import { EAPTransactionChecker } from '@/components/eap-transaction-checker'; 2 | import { 3 | Accordion, 4 | AccordionContent, 5 | AccordionItem, 6 | AccordionTrigger, 7 | } from '@/components/ui/accordion'; 8 | 9 | interface FaqItem { 10 | id: string; 11 | question: string; 12 | answer: string | React.ReactNode; 13 | } 14 | 15 | const faqItems: FaqItem[] = [ 16 | { 17 | id: 'item-1', 18 | question: 'I paid for Early Access Program, but still not showing up?', 19 | answer: ( 20 |
21 | 22 | It usually takes 5~30 seconds for the EAP to be granted to your 23 | account. 24 |
25 | If the EAP is not granted, please paste your transaction hash into the 26 | transaction checker below. 27 |
28 | 29 |
30 | ), 31 | }, 32 | { 33 | id: 'item-2', 34 | question: 'Can I export my embedded wallet?', 35 | answer: ( 36 |
37 | 38 | Unfortunately, to ensure a maximum level of security, we currently do 39 | not support exporting your embedded wallet. 40 |
41 | We will be integrating with famous Embedded Wallet providers soon so 42 | you can have absolute control over your wallet. 43 |
44 |
45 | ), 46 | }, 47 | { 48 | id: 'item-3', 49 | question: 'How can I become EAP Verified in Discord?', 50 | answer: ( 51 |
52 | 53 | On the bottom left, tap your wallet, then tap `Account`. Next, you 54 | should see a `Connect` button next to Discord. Tap on that and connect 55 | to the Discord server. 56 |
57 | Once that is completed, you should now be `EAP VERIFIED` and see 58 | custom Discord channels for EAP users. Your name will also be color 59 | differentiated from other users. 60 |
61 |
62 | ), 63 | }, 64 | ]; 65 | 66 | export default function FaqPage() { 67 | return ( 68 |
69 |
70 |
71 |

FAQ

72 |
73 |
74 | 75 | 76 | {faqItems.map((item) => ( 77 | 78 | {item.question} 79 | {item.answer} 80 | 81 | ))} 82 | 83 |
84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /src/app/(user)/home/components/integration-card.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | 3 | import { motion } from 'framer-motion'; 4 | 5 | import type { Integration } from '../data/integrations'; 6 | 7 | interface IntegrationCardProps { 8 | item: Integration; 9 | index: number; 10 | onClick?: () => void; 11 | } 12 | 13 | interface IntegrationCardStyles extends React.CSSProperties { 14 | '--integration-primary': string; 15 | '--integration-secondary': string; 16 | } 17 | 18 | export function IntegrationCard({ 19 | item, 20 | index, 21 | onClick, 22 | }: IntegrationCardProps) { 23 | return ( 24 | 46 | 57 | {item.label} 64 | 65 | 66 |
67 | 71 | {item.label} 72 | 73 | {item.description && ( 74 | 78 | {item.description} 79 | 80 | )} 81 |
82 | 83 | {/* Theme color overlay on hover */} 84 |
90 | 91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /src/app/(user)/home/components/integrations-grid.tsx: -------------------------------------------------------------------------------- 1 | import { INTEGRATIONS } from '../data/integrations'; 2 | import { IntegrationCard } from './integration-card'; 3 | 4 | export function IntegrationsGrid() { 5 | return ( 6 |
7 | {INTEGRATIONS.map((item, index) => ( 8 | { 13 | // TODO: Implement integration click handler 14 | console.log(`Clicked ${item.label}`); 15 | }} 16 | /> 17 | ))} 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/app/(user)/home/data/integrations.ts: -------------------------------------------------------------------------------- 1 | export interface IntegrationTheme { 2 | primary: string; 3 | secondary: string; 4 | } 5 | 6 | export interface Integration { 7 | icon: string; 8 | label: string; 9 | description?: string; 10 | theme: IntegrationTheme; 11 | } 12 | 13 | export const INTEGRATIONS: Integration[] = [ 14 | { 15 | icon: 'integrations/pump_fun.svg', 16 | label: 'pump.fun', 17 | description: 'Discover new tokens, launch tokens', 18 | theme: { 19 | primary: '#10B981', // Green 20 | secondary: '#10B981', // Green 21 | }, 22 | }, 23 | { 24 | icon: 'integrations/jupiter.svg', 25 | label: 'Jupiter', 26 | description: 'Swap tokens & DCA, Limit orders', 27 | theme: { 28 | primary: '#16A34A', // Green 29 | secondary: '#22C55E', // Light green 30 | }, 31 | }, 32 | { 33 | icon: 'integrations/magic_eden.svg', 34 | label: 'Magic Eden', 35 | description: 'Explore the best NFT collections', 36 | theme: { 37 | primary: '#9333EA', // Purple 38 | secondary: '#A855F7', // Light purple 39 | }, 40 | }, 41 | { 42 | icon: 'integrations/dialect.svg', 43 | label: 'Dialect', 44 | description: 'Create and share blinks', 45 | theme: { 46 | primary: '#0EA5E9', // Blue 47 | secondary: '#38BDF8', // Light blue 48 | }, 49 | }, 50 | { 51 | icon: 'integrations/dexscreener.svg', 52 | label: 'DexScreener', 53 | description: 'Discover trending tokens', 54 | theme: { 55 | primary: '#64748B', // Gray 56 | secondary: '#94A3B8', // Light gray 57 | }, 58 | }, 59 | { 60 | icon: 'integrations/defined_fi.svg', 61 | label: 'Defined Fi', 62 | description: 'Discover unbiassed trending tokens', 63 | theme: { 64 | primary: '#B0EECF', // Orange 65 | secondary: '#181432', // White 66 | }, 67 | }, 68 | ]; 69 | -------------------------------------------------------------------------------- /src/app/(user)/home/data/suggestions.ts: -------------------------------------------------------------------------------- 1 | export interface Suggestion { 2 | id: string; 3 | title: string; 4 | subtitle: string; 5 | } 6 | 7 | export const SUGGESTIONS: Suggestion[] = [ 8 | { 9 | id: 'launch-token', 10 | title: 'Launch a new token', 11 | subtitle: 'deploy a new token on pump.fun', 12 | }, 13 | { 14 | id: 'swap-sol-usdc', 15 | title: 'Swap 1 SOL for USDC', 16 | subtitle: 'using Jupiter to swap on Solana', 17 | }, 18 | { 19 | id: 'solana-trends', 20 | title: "What's trending on Solana?", 21 | subtitle: 'find the current market trends', 22 | }, 23 | { 24 | id: 'price-feed', 25 | title: "What's the price of SOL?", 26 | subtitle: 'find the current price of SOL', 27 | }, 28 | { 29 | id: 'top-gainers-last-24h', 30 | title: 'Top gainers in the last 24h', 31 | subtitle: 'find the top gainers in the last 24 hours', 32 | }, 33 | { 34 | id: 'check-my-wallet', 35 | title: 'Check my wallet', 36 | subtitle: 'check the portfolio of your wallet', 37 | }, 38 | // { 39 | // id: 'sell-everything-buy-neur', 40 | // title: 'Sell everything and buy $NEUR', 41 | // subtitle: 'swap all your tokens for $NEUR', 42 | // }, 43 | // { 44 | // id: 'phantom-updates', 45 | // title: 'Any updates from @phantom recently?', 46 | // subtitle: 'summarize the latest tweets from @phantom', 47 | // }, 48 | // { 49 | // id: "toly-updates", 50 | // title: "What has toly been doing recently?", 51 | // subtitle: "summarize his recent tweets" 52 | // }, 53 | ]; 54 | 55 | export function getRandomSuggestions(count: number): Suggestion[] { 56 | // Ensure we don't request more items than available 57 | const safeCount = Math.min(count, SUGGESTIONS.length); 58 | const startIndex = Math.floor(Date.now() / 1000) % SUGGESTIONS.length; 59 | 60 | // Create a rotated copy of the array starting from startIndex 61 | const rotatedSuggestions = [ 62 | ...SUGGESTIONS.slice(startIndex), 63 | ...SUGGESTIONS.slice(0, startIndex), 64 | ]; 65 | 66 | // Return only the first safeCount items 67 | return rotatedSuggestions.slice(0, safeCount); 68 | } 69 | -------------------------------------------------------------------------------- /src/app/(user)/home/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | import { HomeContent } from './home-content'; 4 | 5 | export const metadata: Metadata = { 6 | title: 'Home', 7 | description: 'Your AI assistant for everything Solana', 8 | }; 9 | 10 | export default function HomePage() { 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/(user)/home/suggestion-card.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | 3 | import type { Suggestion } from './data/suggestions'; 4 | 5 | interface SuggestionCardProps extends Suggestion { 6 | /** @default 0 */ 7 | delay?: number; 8 | /** @default false */ 9 | useSubtitle?: boolean; 10 | onSelect: (text: string) => void; 11 | } 12 | 13 | export function SuggestionCard({ 14 | title, 15 | subtitle, 16 | delay = 0, 17 | useSubtitle = false, 18 | onSelect, 19 | }: SuggestionCardProps) { 20 | return ( 21 |
22 | onSelect(useSubtitle ? subtitle : title)} 32 | className="text-left" 33 | > 34 |
{title}
35 |
36 | {subtitle} 37 |
38 |
39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/app/(user)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { cookies } from 'next/headers'; 2 | 3 | import { AppSidebar } from '@/components/dashboard/app-sidebar'; 4 | import { SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'; 5 | 6 | export default async function UserLayout({ 7 | children, 8 | }: { 9 | children: React.ReactNode; 10 | }) { 11 | const cookieStore = await cookies(); 12 | const defaultOpen = cookieStore.get('sidebar:state')?.value !== 'false'; 13 | 14 | return ( 15 | 16 | 17 |
18 | 19 | {children} 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/app/(user)/memories/page.tsx: -------------------------------------------------------------------------------- 1 | import { Brain } from 'lucide-react'; 2 | 3 | import { ComingSoonPage } from '@/components/coming-soon'; 4 | 5 | export default function MemoriesPage() { 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/(user)/saved-prompts/components/delete-prompt-dialog.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AlertDialog, 3 | AlertDialogAction, 4 | AlertDialogCancel, 5 | AlertDialogContent, 6 | AlertDialogDescription, 7 | AlertDialogFooter, 8 | AlertDialogHeader, 9 | AlertDialogTitle, 10 | } from '@/components/ui/alert-dialog'; 11 | 12 | import { PromptAction } from '../types/prompt'; 13 | 14 | interface DeletePromptDialogProps { 15 | promptAction: PromptAction; 16 | onOpenChange: VoidFunction; 17 | handleDeletePrompt: () => Promise; 18 | } 19 | 20 | export const DeletePromptDialog = ({ 21 | promptAction, 22 | onOpenChange, 23 | handleDeletePrompt, 24 | }: DeletePromptDialogProps) => ( 25 | promptAction.action !== null && onOpenChange()} 28 | > 29 | 30 | 31 | Are you absolutely sure? 32 | 33 | This action cannot be undone. This will permanently delete the saved 34 | prompt. 35 | 36 | 37 | 38 | Cancel 39 | 40 | Continue 41 | 42 | 43 | 44 | 45 | ); 46 | -------------------------------------------------------------------------------- /src/app/(user)/saved-prompts/components/filter-dropdown.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | 5 | import { Check, Filter } from 'lucide-react'; 6 | 7 | import { Button } from '@/components/ui/button'; 8 | import { 9 | Command, 10 | CommandEmpty, 11 | CommandGroup, 12 | CommandItem, 13 | CommandList, 14 | } from '@/components/ui/command'; 15 | import { 16 | Popover, 17 | PopoverContent, 18 | PopoverTrigger, 19 | } from '@/components/ui/popover'; 20 | import { cn } from '@/lib/utils'; 21 | 22 | import { FilterOption, FilterValue } from '../types/prompt'; 23 | 24 | interface FilterDropdownProps { 25 | disabled: boolean; 26 | filter: FilterValue; 27 | filterOptions: FilterOption[]; 28 | updateFilter: (value: FilterValue) => void; 29 | } 30 | 31 | export function FilterDropdown({ 32 | disabled, 33 | filter, 34 | filterOptions, 35 | updateFilter, 36 | }: FilterDropdownProps) { 37 | const [open, setOpen] = useState(false); 38 | 39 | return ( 40 | 41 | 42 | 55 | 56 | 57 | 58 | 59 | No framework found. 60 | 61 | {filterOptions.map((filterOption) => ( 62 | { 66 | updateFilter(filterOption.value); 67 | setOpen(false); 68 | }} 69 | > 70 | {filterOption.label} 71 | 79 | 80 | ))} 81 | 82 | 83 | 84 | 85 | 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /src/app/(user)/saved-prompts/types/prompt.ts: -------------------------------------------------------------------------------- 1 | export interface FilterOption { 2 | value: FilterValue; 3 | label: string; 4 | } 5 | 6 | export type FilterValue = 7 | | 'recentlyUsed' 8 | | 'editedRecently' 9 | | 'latest' 10 | | 'oldest' 11 | | 'favorites'; 12 | 13 | export interface PromptAction { 14 | action: 'update' | 'delete' | 'save' | null; 15 | id: string | null; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/api/actions/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | 3 | import { verifyUser } from '@/server/actions/user'; 4 | import { dbDeleteAction, dbUpdateAction } from '@/server/db/queries'; 5 | 6 | export async function DELETE(req: NextRequest) { 7 | try { 8 | const session = await verifyUser(); 9 | const userId = session?.data?.data?.id; 10 | 11 | const id = req.nextUrl.pathname.split('/').pop(); 12 | 13 | if (!id) { 14 | return NextResponse.json({ error: 'Missing action ID' }, { status: 400 }); 15 | } 16 | 17 | if (!userId) { 18 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 19 | } 20 | 21 | const result = await dbDeleteAction({ id, userId }); 22 | 23 | if (!result) { 24 | return NextResponse.json( 25 | { error: 'Failed to delete action' }, 26 | { status: 400 }, 27 | ); 28 | } 29 | 30 | return NextResponse.json({ success: true }); 31 | } catch (error) { 32 | console.error('Error deleting action:', error); 33 | return NextResponse.json( 34 | { error: 'Internal Server Error' }, 35 | { status: 500 }, 36 | ); 37 | } 38 | } 39 | 40 | export async function PATCH(req: NextRequest) { 41 | try { 42 | const session = await verifyUser(); 43 | const userId = session?.data?.data?.id; 44 | 45 | const id = req.nextUrl.pathname.split('/').pop(); 46 | 47 | if (!id) { 48 | return NextResponse.json({ error: 'Missing action ID' }, { status: 400 }); 49 | } 50 | 51 | if (!userId) { 52 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 53 | } 54 | 55 | const data = await req.json(); 56 | const result = await dbUpdateAction({ id, userId, data }); 57 | 58 | if (!result) { 59 | return NextResponse.json( 60 | { error: 'Failed to update action' }, 61 | { status: 400 }, 62 | ); 63 | } 64 | 65 | return NextResponse.json({ 66 | success: true, 67 | data: result, 68 | }); 69 | } catch (error) { 70 | console.error('Error updating action:', error); 71 | return NextResponse.json( 72 | { error: 'Internal Server Error', success: false }, 73 | { status: 500 }, 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/app/api/actions/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | 3 | import { verifyUser } from '@/server/actions/user'; 4 | import { dbGetUserActions } from '@/server/db/queries'; 5 | 6 | export async function GET(req: NextRequest) { 7 | try { 8 | const session = await verifyUser(); 9 | const userId = session?.data?.data?.id; 10 | 11 | if (!userId) { 12 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 13 | } 14 | 15 | const actions = await dbGetUserActions({ userId }); 16 | return NextResponse.json(actions); 17 | } catch (error) { 18 | console.error('Error fetching actions:', error); 19 | return NextResponse.json( 20 | { error: 'Internal Server Error' }, 21 | { status: 500 }, 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/api/chat/[conversationId]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | 3 | import { verifyUser } from '@/server/actions/user'; 4 | import { 5 | dbGetConversation, 6 | dbGetConversationMessages, 7 | } from '@/server/db/queries'; 8 | 9 | export async function GET( 10 | req: NextRequest, 11 | { params }: { params: Promise<{ conversationId: string }> }, 12 | ) { 13 | const session = await verifyUser(); 14 | const userId = session?.data?.data?.id; 15 | const publicKey = session?.data?.data?.publicKey; 16 | 17 | if (!userId) { 18 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 19 | } 20 | 21 | if (!publicKey) { 22 | console.error('[chat/route] No public key found'); 23 | return NextResponse.json({ error: 'No public key found' }, { status: 400 }); 24 | } 25 | 26 | const { conversationId } = await params; 27 | 28 | if (!conversationId) { 29 | return NextResponse.json( 30 | { error: 'Missing conversationId' }, 31 | { status: 401 }, 32 | ); 33 | } 34 | 35 | try { 36 | const messages = await dbGetConversationMessages({ 37 | conversationId, 38 | }); 39 | 40 | if (!messages || messages.length === 0) { 41 | return NextResponse.json( 42 | { error: 'Conversation messages not found' }, 43 | { status: 404 }, 44 | ); 45 | } 46 | 47 | return NextResponse.json(messages); 48 | } catch (error) { 49 | console.error( 50 | `[chat/[conversationId]/route] Error fetching conversation: ${error}`, 51 | ); 52 | return NextResponse.json( 53 | { error: 'Internal Server Error' }, 54 | { status: 500 }, 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/api/conversations/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | 3 | import { verifyUser } from '@/server/actions/user'; 4 | import { dbGetConversations } from '@/server/db/queries'; 5 | 6 | export async function GET(req: NextRequest) { 7 | try { 8 | const session = await verifyUser(); 9 | const userId = session?.data?.data?.id; 10 | 11 | if (!userId) { 12 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 13 | } 14 | 15 | const conversations = await dbGetConversations({ userId }); 16 | return NextResponse.json(conversations); 17 | } catch (error) { 18 | console.error('Error fetching conversations:', error); 19 | return NextResponse.json( 20 | { error: 'Internal Server Error' }, 21 | { status: 500 }, 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/api/cron/minute/route.ts: -------------------------------------------------------------------------------- 1 | import { IS_TRIAL_ENABLED } from '@/lib/utils'; 2 | import { processAction } from '@/server/actions/action'; 3 | import { dbGetActions } from '@/server/db/queries'; 4 | 5 | export const maxDuration = 300; 6 | export const dynamic = 'force-dynamic'; // static by default, unless reading the request 7 | 8 | export async function GET(request: Request) { 9 | const authHeader = request.headers.get('authorization'); 10 | if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) { 11 | return new Response('Unauthorized', { 12 | status: 401, 13 | }); 14 | } 15 | 16 | // Minute cron job 17 | // Get all Actions that are not completed or paused 18 | const actions = await dbGetActions({ 19 | triggered: true, 20 | completed: false, 21 | paused: false, 22 | }); 23 | 24 | console.log(`[cron/action] Fetched ${actions.length} actions`); 25 | 26 | // This job runs every minute minute, but we only need to process actions that are ready to be processed, based on their frequency 27 | // Filter the actions to only include those that are ready to be processed based on their lastExecutedAt and frequency 28 | const now = new Date(); 29 | const actionsToProcess = actions.filter((action) => { 30 | // Filter out actions where user is not EAP or does not have an active subscription (allow all during trial mode) 31 | if ( 32 | !action.user || 33 | (!action.user.earlyAccess && 34 | !action.user.subscription?.active && 35 | !IS_TRIAL_ENABLED) 36 | ) { 37 | return false; 38 | } 39 | 40 | // Filter out actions without a frequency 41 | if (!action.frequency) { 42 | return false; 43 | } 44 | 45 | // If the action has never been executed, it should be processed now 46 | // This means that the first time this job sees an action, it will process it 47 | if (!action.lastExecutedAt) { 48 | return true; 49 | } 50 | 51 | // Next execution time is the last execution time plus the frequency (seconds) * 1000 52 | const nextExecutionAt = new Date( 53 | action.lastExecutedAt.getTime() + action.frequency * 1000, 54 | ); 55 | 56 | return now >= nextExecutionAt; 57 | }); 58 | 59 | await Promise.all( 60 | actionsToProcess.map((action) => 61 | processAction(action).catch((error) => { 62 | console.error(`Error processing action ${action.id}:`, error); 63 | }), 64 | ), 65 | ); 66 | 67 | console.log(`[cron/action] Processed ${actionsToProcess.length} actions`); 68 | 69 | return Response.json({ success: true }); 70 | } 71 | -------------------------------------------------------------------------------- /src/app/api/discord/grant-role/route.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | import { getUserData } from '@/server/actions/user'; 4 | 5 | const DISCORD_API_BASE_URL = 'https://discordapp.com/api'; 6 | const BOT_TOKEN = process.env.DISCORD_BOT_TOKEN; 7 | const GUILD_ID = process.env.DISCORD_GUILD_ID; 8 | const ROLE_ID = process.env.DISCORD_ROLE_ID; 9 | 10 | export async function POST(req: Request) { 11 | if (!BOT_TOKEN || !GUILD_ID || !ROLE_ID) { 12 | throw new Error('Discord environment variables not set'); 13 | } 14 | 15 | const userData = await getUserData(); 16 | const hasEarlyAccess = userData?.data?.data?.earlyAccess; 17 | 18 | if (!hasEarlyAccess) { 19 | return new Response('User does not have early access', { status: 403 }); 20 | } 21 | 22 | const { userId } = await req.json(); 23 | 24 | if (!userId) { 25 | return new Response('User ID is required', { status: 400 }); 26 | } 27 | 28 | try { 29 | const url = `${DISCORD_API_BASE_URL}/guilds/${GUILD_ID}/members/${userId}/roles/${ROLE_ID}`; 30 | 31 | await fetch(url, { 32 | method: 'PUT', 33 | headers: { 34 | Authorization: `Bot ${BOT_TOKEN}`, 35 | }, 36 | }); 37 | return new Response('Role granted successfully', { status: 200 }); 38 | } catch (error) { 39 | return new Response('Failed to grant role', { status: 500 }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/app/api/wallet/[address]/portfolio/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from 'next/server'; 2 | 3 | import { searchWalletAssets } from '@/lib/solana/helius'; 4 | import { transformToPortfolio } from '@/types/helius/portfolio'; 5 | 6 | export async function GET( 7 | request: NextRequest, 8 | { params }: { params: Promise<{ address: string }> }, 9 | ) { 10 | try { 11 | const { address } = await params; 12 | const { fungibleTokens, nonFungibleTokens } = 13 | await searchWalletAssets(address); 14 | const portfolio = transformToPortfolio( 15 | address, 16 | fungibleTokens, 17 | nonFungibleTokens, 18 | ); 19 | 20 | return Response.json(portfolio); 21 | } catch (error) { 22 | console.error('Error fetching wallet portfolio:', error); 23 | return new Response( 24 | JSON.stringify({ error: 'Failed to fetch wallet portfolio' }), 25 | { 26 | status: 500, 27 | }, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { Geist, Geist_Mono } from 'next/font/google'; 3 | 4 | import { Analytics } from '@vercel/analytics/react'; 5 | import { SpeedInsights } from '@vercel/speed-insights/next'; 6 | 7 | import AuthProviders from '@/components/provider-auth'; 8 | import { ThemeProvider } from '@/components/provider-theme'; 9 | import { Toaster } from '@/components/ui/sonner'; 10 | import { cn } from '@/lib/utils'; 11 | 12 | import './globals.css'; 13 | 14 | const geistSans = Geist({ 15 | variable: '--font-geist-sans', 16 | subsets: ['latin'], 17 | }); 18 | 19 | const geistMono = Geist_Mono({ 20 | variable: '--font-geist-mono', 21 | subsets: ['latin'], 22 | }); 23 | 24 | export const metadata: Metadata = { 25 | title: { 26 | template: '%s | Neur', 27 | default: 'Neur - The Intelligent Copilot for Solana', 28 | }, 29 | description: 'The Intelligent Copilot elevating your Solana experience.', 30 | 31 | icons: { 32 | icon: '/logo.svg', 33 | }, 34 | }; 35 | 36 | export default function RootLayout({ 37 | children, 38 | }: Readonly<{ children: React.ReactNode }>) { 39 | return ( 40 | 41 | 47 | 48 | 54 |
55 | {children} 56 | 57 |
58 |
59 |
60 | 61 | 62 | 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/components/action-emitter.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect } from 'react'; 4 | 5 | import { EVENTS } from '@/lib/events'; 6 | 7 | interface ActionEmitterProps { 8 | actionId: string; 9 | } 10 | 11 | export function ActionEmitter({ actionId }: ActionEmitterProps) { 12 | useEffect(() => { 13 | if (actionId) { 14 | console.log( 15 | '[ActionEmitter] Emitting action created event for:', 16 | actionId, 17 | ); 18 | window.dispatchEvent(new CustomEvent(EVENTS.ACTION_CREATED)); 19 | } 20 | }, [actionId]); 21 | 22 | return null; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/coming-soon.tsx: -------------------------------------------------------------------------------- 1 | import { LucideIcon } from 'lucide-react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | interface ComingSoonPageProps { 6 | icon: LucideIcon; 7 | title?: string; 8 | className?: string; 9 | } 10 | 11 | export function ComingSoonPage({ 12 | icon: Icon, 13 | title = 'Coming Soon', 14 | className, 15 | }: ComingSoonPageProps) { 16 | return ( 17 |
25 | {/* Gradient Overlay */} 26 |
27 |
28 | {/* Glowing Background Effect */} 29 |
30 |
31 |
32 | 33 | {/* Content */} 34 |
35 |
36 |
37 | 38 |
39 | 40 |
41 | 42 |
43 |

{title}

44 |

45 | Coming soon... Stay tuned for updates! 46 |

47 |
48 |
49 |
50 |
51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/components/confimation.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui/button'; 2 | import { Card } from '@/components/ui/card'; 3 | import { cn } from '@/lib/utils'; 4 | 5 | export const Confirmation = ({ 6 | message, 7 | result, 8 | toolCallId, 9 | addResultUtility, 10 | }: { 11 | message: string; 12 | result: string | undefined; 13 | toolCallId: string; 14 | addResultUtility: (result: string) => void; 15 | }) => { 16 | return ( 17 |
18 |
19 |
20 |
28 | 29 | ⚠️ Confirmation 30 | 31 | 32 | {toolCallId.slice(0, 9)} 33 | 34 |
35 |
36 |
37 | {!message && ( 38 |
39 |
40 |
41 | )} 42 | {message && ( 43 | 44 |
45 |

{message}

46 |
47 | 48 |
49 | {result === 'deny' && ( 50 | 53 | )} 54 | {result === 'confirm' && ( 55 | 58 | )} 59 | {!result && addResultUtility && ( 60 | 69 | )} 70 | {!result && addResultUtility && ( 71 | 80 | )} 81 |
82 |
83 | )} 84 |
85 |
86 | ); 87 | }; 88 | -------------------------------------------------------------------------------- /src/components/dynamic-image.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import type { ImageProps } from 'next/image'; 4 | import Image from 'next/image'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | interface DynamicImageProps extends Omit { 9 | lightSrc: ImageProps['src']; 10 | darkSrc: ImageProps['src']; 11 | className?: string; 12 | } 13 | 14 | export function DynamicImage({ 15 | lightSrc, 16 | darkSrc, 17 | alt, 18 | className, 19 | width, 20 | height, 21 | ...props 22 | }: DynamicImageProps) { 23 | return ( 24 | <> 25 | {alt} 33 | {alt} 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/eap-transaction-checker.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | 5 | import { Loader2 } from 'lucide-react'; 6 | 7 | import { 8 | AlertDialog, 9 | AlertDialogCancel, 10 | AlertDialogContent, 11 | AlertDialogDescription, 12 | AlertDialogFooter, 13 | AlertDialogHeader, 14 | AlertDialogTitle, 15 | } from '@/components/ui/alert-dialog'; 16 | import { Button } from '@/components/ui/button'; 17 | import { Input } from '@/components/ui/input'; 18 | import { checkEAPTransaction } from '@/server/actions/eap'; 19 | 20 | export function EAPTransactionChecker() { 21 | const [txHash, setTxHash] = useState(''); 22 | const [isChecking, setIsChecking] = useState(false); 23 | const [result, setResult] = useState<{ 24 | success: boolean; 25 | message: string; 26 | } | null>(null); 27 | 28 | async function handleCheck() { 29 | setIsChecking(true); 30 | try { 31 | const response = await checkEAPTransaction({ txHash }); 32 | if (response?.data?.success) { 33 | setResult({ 34 | success: true, 35 | message: `Transaction verified successfully. EAP should be granted to your account.`, 36 | }); 37 | } else { 38 | console.log('response', response); 39 | setResult({ 40 | success: false, 41 | message: 42 | 'Failed to verify transaction. Contact support if you think this is an error.', 43 | }); 44 | } 45 | } catch (error) { 46 | setResult({ 47 | success: false, 48 | message: 49 | 'Failed to verify transaction. Contact support if you think this is an error.', 50 | }); 51 | } finally { 52 | setIsChecking(false); 53 | } 54 | } 55 | 56 | return ( 57 |
58 |
59 | setTxHash(e.target.value)} 63 | /> 64 | 74 |
75 | 76 | setResult(null)}> 77 | 78 | 79 | 80 | {result?.success ? 'Transaction Verified' : 'Verification Failed'} 81 | 82 | {result?.message} 83 | 84 | 85 | Close 86 | 87 | 88 | 89 |
90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /src/components/logo.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | import { DynamicImage } from './dynamic-image'; 6 | 7 | export default function Logo({ 8 | width = 100, 9 | height = width, 10 | className, 11 | }: { 12 | width?: number; 13 | height?: number; 14 | className?: string; 15 | }) { 16 | return ( 17 | 25 | ); 26 | } 27 | 28 | interface BrandProps { 29 | className?: string; 30 | } 31 | 32 | export function Brand({ className }: BrandProps) { 33 | return ( 34 | 35 |
36 | 37 | Neur 38 |
39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/message/tool-result.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | 5 | import * as Collapsible from '@radix-ui/react-collapsible'; 6 | import { ChevronDown } from 'lucide-react'; 7 | 8 | import { DefaultToolResultRenderer, getToolConfig } from '@/ai/providers'; 9 | import { cn } from '@/lib/utils'; 10 | 11 | interface ToolResultProps { 12 | toolName: string; 13 | result: unknown; 14 | header: React.ReactNode; 15 | } 16 | 17 | export function ToolResult({ toolName, result, header }: ToolResultProps) { 18 | const config = getToolConfig(toolName); 19 | const isCollapsible = config?.isCollapsible === true; 20 | const [isOpen, setIsOpen] = useState( 21 | config?.isExpandedByDefault ?? !isCollapsible, 22 | ); 23 | 24 | const content = config?.render 25 | ? config?.render(result) 26 | : DefaultToolResultRenderer({ result }); 27 | if (!content) return null; 28 | 29 | const headerContent = ( 30 |
31 | {header} 32 | {isCollapsible && ( 33 | 39 | )} 40 |
41 | ); 42 | 43 | if (!isCollapsible) { 44 | return ( 45 |
46 |
47 | {headerContent} 48 |
49 |
{content}
50 |
51 | ); 52 | } 53 | 54 | return ( 55 | 60 | 61 |
62 | {headerContent} 63 |
64 |
65 | 66 | 67 |
{content}
68 |
69 |
70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /src/components/page-loading.tsx: -------------------------------------------------------------------------------- 1 | import Logo from './logo'; 2 | 3 | export default function PageLoading() { 4 | return ( 5 |
6 |
7 | 8 | 9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/page-maintenance.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import { DynamicImage } from './dynamic-image'; 4 | 5 | export default function MaintenanceIndex() { 6 | return ( 7 |
8 | 19 |
23 | 32 |
33 | 34 |
35 |

36 | Follow{' '} 37 | 42 | @neur_sh 43 | {' '} 44 | for updates on our launch 45 |

46 |
47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/components/provider-auth.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { PrivyProvider } from '@privy-io/react-auth'; 4 | import { toSolanaWalletConnectors } from '@privy-io/react-auth/solana'; 5 | import { useTheme } from 'next-themes'; 6 | 7 | import { RPC_URL } from '@/lib/constants'; 8 | 9 | const solanaConnectors = toSolanaWalletConnectors({ 10 | shouldAutoConnect: false, 11 | }); 12 | 13 | export default function AuthProviders({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) { 18 | const { resolvedTheme } = useTheme(); 19 | 20 | return ( 21 | 36 | {children} 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/components/provider-theme.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import { ThemeProvider as NextThemesProvider } from 'next-themes'; 6 | 7 | export function ThemeProvider({ 8 | children, 9 | ...props 10 | }: React.ComponentProps) { 11 | return {children}; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/saved-prompts-menu.tsx: -------------------------------------------------------------------------------- 1 | import { SavedPrompt } from '@prisma/client'; 2 | import { Loader2 } from 'lucide-react'; 3 | 4 | interface SavedPromptsMenuProps { 5 | input: string; 6 | isFetchingSavedPrompts: boolean; 7 | savedPrompts: SavedPrompt[]; 8 | filteredPrompts: SavedPrompt[]; 9 | onPromptClick: (subtitle: string) => void; 10 | updatePromptLastUsedAt: (id: string) => Promise; 11 | onHomeScreen?: boolean; 12 | } 13 | 14 | export const SavedPromptsMenu = ({ 15 | input, 16 | isFetchingSavedPrompts, 17 | savedPrompts, 18 | filteredPrompts, 19 | onPromptClick, 20 | updatePromptLastUsedAt, 21 | onHomeScreen = false, 22 | }: SavedPromptsMenuProps) => ( 23 |
27 |

Saved Prompts

28 | {isFetchingSavedPrompts ? ( 29 |
30 | 31 |
32 | ) : savedPrompts.length === 0 ? ( 33 |
34 | No prompts saved yet 35 |
36 | ) : filteredPrompts.length === 0 ? ( 37 |
38 | No match found 39 |
40 | ) : ( 41 | filteredPrompts.map((filteredPrompt) => ( 42 |
{ 44 | onPromptClick(filteredPrompt.content); 45 | updatePromptLastUsedAt(filteredPrompt.id); 46 | }} 47 | key={filteredPrompt.id} 48 | className="flex cursor-pointer flex-col gap-1.5 rounded-[0.5rem] bg-primary/10 p-2 text-left 49 | transition-colors duration-200 hover:bg-primary/5" 50 | > 51 |

{filteredPrompt.title}

52 |
53 | {filteredPrompt.content} 54 |
55 |
56 | )) 57 | )} 58 |
59 | ); 60 | -------------------------------------------------------------------------------- /src/components/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect, useState } from 'react'; 4 | 5 | import { Monitor, Moon, Sun } from 'lucide-react'; 6 | import { useTheme } from 'next-themes'; 7 | 8 | import { Skeleton } from '@/components/ui/skeleton'; 9 | 10 | export function ThemeToggle() { 11 | const [mounted, setMounted] = useState(false); 12 | const { theme, setTheme } = useTheme(); 13 | 14 | useEffect(() => { 15 | setMounted(true); 16 | }, []); 17 | 18 | if (!mounted) { 19 | return null; 20 | } 21 | 22 | return ( 23 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/components/top-trader.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { formatNumber } from '@/lib/format'; 4 | import { BirdeyeTrader } from '@/server/actions/birdeye'; 5 | 6 | export default function TopTrader({ 7 | trader, 8 | rank, 9 | }: { 10 | trader: BirdeyeTrader; 11 | rank: number; 12 | }) { 13 | return ( 14 |
15 |
16 |
17 |
18 | #{rank} 19 |
20 |
21 |
22 |
23 | 29 | {trader.address.slice(0, 4)}...{trader.address.slice(-4)} 30 | 31 | = 0 34 | ? 'bg-green-500/10 text-green-500' 35 | : 'bg-red-500/10 text-red-500' 36 | }`} 37 | > 38 | {trader.pnl >= 0 ? '+' : ''} 39 | {formatNumber(trader.pnl, 'percent', 2)} 40 | 41 |
42 |
43 | Vol: {formatNumber(trader.volume, 'currency')} 44 | 45 | Trades: {formatNumber(trader.tradeCount, 'number')} 46 |
47 |
48 |
49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/components/tweet-card.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { TweetProps, useTweet } from 'react-tweet'; 4 | 5 | import { 6 | MagicTweet, 7 | TweetNotFound, 8 | TweetSkeleton, 9 | } from '@/components/ui/tweet-card'; 10 | 11 | export const ClientTweetCard = ({ 12 | id, 13 | apiUrl, 14 | fallback = , 15 | components, 16 | fetchOptions, 17 | onError, 18 | ...props 19 | }: TweetProps & { className?: string }) => { 20 | const { data, error, isLoading } = useTweet(id, apiUrl, fetchOptions); 21 | 22 | if (isLoading) return fallback; 23 | if (error || !data) { 24 | const NotFound = components?.TweetNotFound || TweetNotFound; 25 | return ; 26 | } 27 | 28 | return ; 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import * as AccordionPrimitive from '@radix-ui/react-accordion'; 6 | import { ChevronDown } from 'lucide-react'; 7 | 8 | import { cn } from '@/lib/utils'; 9 | 10 | const Accordion = AccordionPrimitive.Root; 11 | 12 | const AccordionItem = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )); 22 | AccordionItem.displayName = 'AccordionItem'; 23 | 24 | const AccordionTrigger = React.forwardRef< 25 | React.ElementRef, 26 | React.ComponentPropsWithoutRef 27 | >(({ className, children, ...props }, ref) => ( 28 | 29 | svg]:rotate-180', 33 | className, 34 | )} 35 | {...props} 36 | > 37 | {children} 38 | 39 | 40 | 41 | )); 42 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; 43 | 44 | const AccordionContent = React.forwardRef< 45 | React.ElementRef, 46 | React.ComponentPropsWithoutRef 47 | >(({ className, children, ...props }, ref) => ( 48 | 53 |
{children}
54 |
55 | )); 56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName; 57 | 58 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; 59 | -------------------------------------------------------------------------------- /src/components/ui/ai-particles-background.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useCallback } from 'react'; 4 | 5 | import { useTheme } from 'next-themes'; 6 | import Particles from 'react-particles'; 7 | import { loadSlim } from 'tsparticles-slim'; 8 | 9 | export function AiParticlesBackground() { 10 | const { theme } = useTheme(); 11 | const isDark = theme === 'dark'; 12 | 13 | const particlesInit = useCallback(async (engine: any) => { 14 | await loadSlim(engine); 15 | }, []); 16 | 17 | return ( 18 | 112 | ); 113 | } 114 | -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { type VariantProps, cva } from 'class-variance-authority'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | 7 | const alertVariants = cva( 8 | 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground', 9 | { 10 | variants: { 11 | variant: { 12 | default: 'bg-background text-foreground', 13 | destructive: 14 | 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', 15 | success: 16 | 'border-green-500/50 text-green-600 dark:border-green-500 [&>svg]:text-green-600', 17 | }, 18 | }, 19 | defaultVariants: { 20 | variant: 'default', 21 | }, 22 | }, 23 | ); 24 | 25 | const Alert = React.forwardRef< 26 | HTMLDivElement, 27 | React.HTMLAttributes & VariantProps 28 | >(({ className, variant, ...props }, ref) => ( 29 |
35 | )); 36 | Alert.displayName = 'Alert'; 37 | 38 | const AlertTitle = React.forwardRef< 39 | HTMLParagraphElement, 40 | React.HTMLAttributes 41 | >(({ className, ...props }, ref) => ( 42 |
47 | )); 48 | AlertTitle.displayName = 'AlertTitle'; 49 | 50 | const AlertDescription = React.forwardRef< 51 | HTMLParagraphElement, 52 | React.HTMLAttributes 53 | >(({ className, ...props }, ref) => ( 54 |
59 | )); 60 | AlertDescription.displayName = 'AlertDescription'; 61 | 62 | export { Alert, AlertTitle, AlertDescription }; 63 | -------------------------------------------------------------------------------- /src/components/ui/animated-list.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { ReactElement, useEffect, useMemo, useState } from 'react'; 4 | 5 | import { AnimatePresence, motion } from 'framer-motion'; 6 | 7 | export interface AnimatedListProps { 8 | className?: string; 9 | children: React.ReactNode; 10 | delay?: number; 11 | } 12 | 13 | export const AnimatedList = React.memo( 14 | ({ className, children, delay = 1000 }: AnimatedListProps) => { 15 | const [index, setIndex] = useState(0); 16 | const childrenArray = useMemo( 17 | () => React.Children.toArray(children), 18 | [children], 19 | ); 20 | 21 | useEffect(() => { 22 | if (index < childrenArray.length - 1) { 23 | const timeout = setTimeout(() => { 24 | setIndex((prevIndex) => prevIndex + 1); 25 | }, delay); 26 | 27 | return () => clearTimeout(timeout); 28 | } 29 | }, [index, delay, childrenArray.length]); 30 | 31 | const itemsToShow = useMemo(() => { 32 | const result = childrenArray.slice(0, index + 1).reverse(); 33 | return result; 34 | }, [index, childrenArray]); 35 | 36 | return ( 37 |
38 | 39 | {itemsToShow.map((item) => ( 40 | 41 | {item} 42 | 43 | ))} 44 | 45 |
46 | ); 47 | }, 48 | ); 49 | 50 | AnimatedList.displayName = 'AnimatedList'; 51 | 52 | export function AnimatedListItem({ children }: { children: React.ReactNode }) { 53 | const animations = { 54 | initial: { scale: 0, opacity: 0 }, 55 | animate: { scale: 1, opacity: 1, originY: 0 }, 56 | exit: { scale: 0, opacity: 0 }, 57 | transition: { type: 'spring', stiffness: 350, damping: 40 }, 58 | }; 59 | 60 | return ( 61 | 62 | {children} 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/components/ui/animated-shiny-text.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, FC, ReactNode } from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | interface AnimatedShinyTextProps { 6 | children: ReactNode; 7 | className?: string; 8 | shimmerWidth?: number; 9 | } 10 | 11 | const AnimatedShinyText: FC = ({ 12 | children, 13 | className, 14 | shimmerWidth = 100, 15 | }) => { 16 | return ( 17 |

35 | {children} 36 |

37 | ); 38 | }; 39 | 40 | export default AnimatedShinyText; 41 | -------------------------------------------------------------------------------- /src/components/ui/animated-subscribe-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useState } from 'react'; 4 | 5 | import { AnimatePresence, motion } from 'framer-motion'; 6 | 7 | interface AnimatedSubscribeButtonProps { 8 | buttonColor: string; 9 | buttonTextColor?: string; 10 | subscribeStatus: boolean; 11 | initialText: React.ReactElement | string; 12 | changeText: React.ReactElement | string; 13 | } 14 | 15 | export const AnimatedSubscribeButton: React.FC< 16 | AnimatedSubscribeButtonProps 17 | > = ({ 18 | buttonColor, 19 | subscribeStatus, 20 | buttonTextColor, 21 | changeText, 22 | initialText, 23 | }) => { 24 | const [isSubscribed, setIsSubscribed] = useState(subscribeStatus); 25 | 26 | return ( 27 | 28 | {isSubscribed ? ( 29 | setIsSubscribed(false)} 32 | initial={{ opacity: 0 }} 33 | animate={{ opacity: 1 }} 34 | exit={{ opacity: 0 }} 35 | > 36 | 43 | {changeText} 44 | 45 | 46 | ) : ( 47 | setIsSubscribed(true)} 51 | initial={{ opacity: 0 }} 52 | animate={{ opacity: 1 }} 53 | exit={{ opacity: 0 }} 54 | > 55 | 61 | {initialText} 62 | 63 | 64 | )} 65 | 66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import * as AvatarPrimitive from '@radix-ui/react-avatar'; 6 | 7 | import { Placeholder } from '@/lib/placeholder'; 8 | import { cn } from '@/lib/utils'; 9 | 10 | const Avatar = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )); 23 | Avatar.displayName = AvatarPrimitive.Root.displayName; 24 | 25 | const AvatarImage = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef & { 28 | fallbackText?: string; 29 | } 30 | >(({ className, fallbackText, ...props }, ref) => ( 31 | { 36 | (e.target as HTMLImageElement).src = Placeholder.avatar(); 37 | if (props.onError) props.onError(e); 38 | }} 39 | /> 40 | )); 41 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 42 | 43 | const AvatarFallback = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, ...props }, ref) => ( 47 | 55 | )); 56 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 57 | 58 | export { Avatar, AvatarImage, AvatarFallback }; 59 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { type VariantProps, cva } from 'class-variance-authority'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | 7 | const badgeVariants = cva( 8 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', 14 | secondary: 15 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', 16 | destructive: 17 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', 18 | outline: 'text-foreground', 19 | }, 20 | }, 21 | defaultVariants: { 22 | variant: 'default', 23 | }, 24 | }, 25 | ); 26 | 27 | export interface BadgeProps 28 | extends React.HTMLAttributes, 29 | VariantProps {} 30 | 31 | function Badge({ className, variant, ...props }: BadgeProps) { 32 | return ( 33 |
34 | ); 35 | } 36 | 37 | export { Badge, badgeVariants }; 38 | -------------------------------------------------------------------------------- /src/components/ui/banner.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect, useState } from 'react'; 4 | 5 | import { X } from 'lucide-react'; 6 | 7 | const bannerStateKey = 'dd-banner:visible'; 8 | 9 | interface BannerProps { 10 | children: React.ReactNode; 11 | } 12 | 13 | export function Banner({ children }: BannerProps) { 14 | const [isVisible, setIsVisible] = useState(false); 15 | 16 | // Persist banner state in localStorage 17 | useEffect(() => { 18 | const bannerState = localStorage.getItem(bannerStateKey); 19 | if (bannerState !== 'false') { 20 | setIsVisible(true); 21 | } 22 | }, []); 23 | 24 | const handleDismiss = () => { 25 | setIsVisible(false); 26 | localStorage.setItem(bannerStateKey, 'false'); 27 | }; 28 | 29 | if (!isVisible) { 30 | return null; 31 | } 32 | 33 | return ( 34 |
35 |
36 |

{children}

37 | 45 |
46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/components/ui/bento-grid.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | import { ArrowRightIcon } from '@radix-ui/react-icons'; 4 | 5 | import { Button } from '@/components/ui/button'; 6 | import { cn } from '@/lib/utils'; 7 | 8 | const BentoGrid = ({ 9 | children, 10 | className, 11 | }: { 12 | children: ReactNode; 13 | className?: string; 14 | }) => { 15 | return ( 16 |
22 | {children} 23 |
24 | ); 25 | }; 26 | 27 | const BentoCard = ({ 28 | name, 29 | className, 30 | background, 31 | Icon, 32 | description, 33 | href, 34 | cta, 35 | }: { 36 | name: string; 37 | className: string; 38 | background: ReactNode; 39 | Icon: any; 40 | description: string; 41 | href?: string; 42 | cta?: string; 43 | }) => ( 44 |
55 |
{background}
56 |
62 | 63 |

64 | {name} 65 |

66 |

67 | {description} 68 |

69 |
70 | 71 | {href && cta && ( 72 |
77 | 88 |
89 | )} 90 |
91 |
92 | ); 93 | 94 | export { BentoCard, BentoGrid }; 95 | -------------------------------------------------------------------------------- /src/components/ui/blur-fade.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useRef } from 'react'; 4 | 5 | import { 6 | AnimatePresence, 7 | UseInViewOptions, 8 | Variants, 9 | motion, 10 | useInView, 11 | } from 'framer-motion'; 12 | 13 | type MarginType = UseInViewOptions['margin']; 14 | 15 | interface BlurFadeProps { 16 | children: React.ReactNode; 17 | className?: string; 18 | variant?: { 19 | hidden: { y: number }; 20 | visible: { y: number }; 21 | }; 22 | duration?: number; 23 | delay?: number; 24 | yOffset?: number; 25 | inView?: boolean; 26 | inViewMargin?: MarginType; 27 | blur?: string; 28 | } 29 | 30 | export default function BlurFade({ 31 | children, 32 | className, 33 | variant, 34 | duration = 0.4, 35 | delay = 0, 36 | yOffset = 6, 37 | inView = false, 38 | inViewMargin = '-50px', 39 | blur = '6px', 40 | }: BlurFadeProps) { 41 | const ref = useRef(null); 42 | const inViewResult = useInView(ref, { once: true, margin: inViewMargin }); 43 | const isInView = !inView || inViewResult; 44 | const defaultVariants: Variants = { 45 | hidden: { y: yOffset, opacity: 0, filter: `blur(${blur})` }, 46 | visible: { y: -yOffset, opacity: 1, filter: `blur(0px)` }, 47 | }; 48 | const combinedVariants = variant || defaultVariants; 49 | return ( 50 | 51 | 64 | {children} 65 | 66 | 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/components/ui/blur-in.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { motion } from 'framer-motion'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | 7 | interface BlurInProps { 8 | word: string; 9 | className?: string; 10 | variant?: { 11 | hidden: { filter: string; opacity: number }; 12 | visible: { filter: string; opacity: number }; 13 | }; 14 | duration?: number; 15 | } 16 | const BlurIn = ({ word, className, variant, duration = 1 }: BlurInProps) => { 17 | const defaultVariants = { 18 | hidden: { filter: 'blur(10px)', opacity: 0 }, 19 | visible: { filter: 'blur(0px)', opacity: 1 }, 20 | }; 21 | const combinedVariants = variant || defaultVariants; 22 | 23 | return ( 24 | 34 | {word} 35 | 36 | ); 37 | }; 38 | 39 | export default BlurIn; 40 | -------------------------------------------------------------------------------- /src/components/ui/border-beam.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | 3 | interface BorderBeamProps { 4 | className?: string; 5 | size?: number; 6 | duration?: number; 7 | borderWidth?: number; 8 | anchor?: number; 9 | colorFrom?: string; 10 | colorTo?: string; 11 | delay?: number; 12 | } 13 | 14 | export const BorderBeam = ({ 15 | className, 16 | size = 200, 17 | duration = 15, 18 | anchor = 90, 19 | borderWidth = 1.5, 20 | colorFrom = '#ffaa40', 21 | colorTo = '#9c40ff', 22 | delay = 0, 23 | }: BorderBeamProps) => { 24 | return ( 25 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { Slot } from '@radix-ui/react-slot'; 4 | import { type VariantProps, cva } from 'class-variance-authority'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | const buttonVariants = cva( 9 | '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', 10 | { 11 | variants: { 12 | variant: { 13 | default: 14 | 'bg-primary text-primary-foreground shadow hover:bg-primary/90', 15 | destructive: 16 | 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', 17 | outline: 18 | 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', 19 | secondary: 20 | 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', 21 | ghost: 'hover:bg-accent hover:text-accent-foreground', 22 | link: 'text-primary underline-offset-4 hover:underline', 23 | }, 24 | size: { 25 | default: 'h-9 px-4 py-2', 26 | sm: 'h-8 rounded-md px-3 text-xs', 27 | lg: 'h-10 rounded-md px-8', 28 | icon: 'h-9 w-9', 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: 'default', 33 | size: 'default', 34 | }, 35 | }, 36 | ); 37 | 38 | export interface ButtonProps 39 | extends React.ButtonHTMLAttributes, 40 | VariantProps { 41 | asChild?: boolean; 42 | } 43 | 44 | const Button = React.forwardRef( 45 | ({ className, variant, size, asChild = false, ...props }, ref) => { 46 | const Comp = asChild ? Slot : 'button'; 47 | return ( 48 | 53 | ); 54 | }, 55 | ); 56 | Button.displayName = 'Button'; 57 | 58 | export { Button, buttonVariants }; 59 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = 'Card'; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = 'CardHeader'; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
41 | )); 42 | CardTitle.displayName = 'CardTitle'; 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLDivElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |
53 | )); 54 | CardDescription.displayName = 'CardDescription'; 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |
61 | )); 62 | CardContent.displayName = 'CardContent'; 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )); 74 | CardFooter.displayName = 'CardFooter'; 75 | 76 | export { 77 | Card, 78 | CardHeader, 79 | CardFooter, 80 | CardTitle, 81 | CardDescription, 82 | CardContent, 83 | }; 84 | -------------------------------------------------------------------------------- /src/components/ui/circle.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { forwardRef } from 'react'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | 7 | export const Circle = forwardRef< 8 | HTMLDivElement, 9 | { 10 | className?: string; 11 | children?: React.ReactNode; 12 | style?: React.CSSProperties; 13 | } 14 | >(({ className, children, style }, ref) => { 15 | return ( 16 |
24 | {children} 25 |
26 | ); 27 | }); 28 | 29 | Circle.displayName = 'Circle'; 30 | -------------------------------------------------------------------------------- /src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'; 6 | 7 | const Collapsible = CollapsiblePrimitive.Root; 8 | 9 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; 10 | 11 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; 12 | 13 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }; 14 | -------------------------------------------------------------------------------- /src/components/ui/copyable-text.tsx: -------------------------------------------------------------------------------- 1 | import bs58 from 'bs58'; 2 | import { Copy, ExternalLink } from 'lucide-react'; 3 | 4 | import { Button } from '@/components/ui/button'; 5 | 6 | interface Props { 7 | text: string; 8 | /** 9 | * Whether to show Solscan link 10 | */ 11 | showSolscan?: boolean; 12 | } 13 | 14 | /** 15 | * Copyable text component with clipboard support and Solscan link 16 | */ 17 | export const CopyableText = ({ text, showSolscan = false }: Props) => { 18 | const handleCopy = (text: string) => { 19 | navigator.clipboard.writeText(text); 20 | }; 21 | 22 | // Validate if it's a valid bs58 address 23 | const isValidBs58 = (text: string): boolean => { 24 | try { 25 | const decoded = bs58.decode(text); 26 | return decoded.length === 32; // Solana address should be 32 bytes 27 | } catch { 28 | return false; 29 | } 30 | }; 31 | 32 | const isValidBase58 = isValidBs58(text); 33 | const shouldShowSolscanLink = showSolscan && isValidBase58; 34 | 35 | return ( 36 |
37 |
38 | {text} 39 |
40 |
41 | 49 | {shouldShowSolscanLink && ( 50 | 64 | )} 65 |
66 |
67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /src/components/ui/dot-pattern.tsx: -------------------------------------------------------------------------------- 1 | import { useId } from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | interface DotPatternProps { 6 | width?: any; 7 | height?: any; 8 | x?: any; 9 | y?: any; 10 | cx?: any; 11 | cy?: any; 12 | cr?: any; 13 | className?: string; 14 | [key: string]: any; 15 | } 16 | export function DotPattern({ 17 | width = 16, 18 | height = 16, 19 | x = 0, 20 | y = 0, 21 | cx = 1, 22 | cy = 1, 23 | cr = 1, 24 | className, 25 | ...props 26 | }: DotPatternProps) { 27 | const id = useId(); 28 | 29 | return ( 30 | 53 | ); 54 | } 55 | 56 | export default DotPattern; 57 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ); 18 | }, 19 | ); 20 | Input.displayName = 'Input'; 21 | 22 | export { Input }; 23 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import * as LabelPrimitive from '@radix-ui/react-label'; 6 | import { type VariantProps, cva } from 'class-variance-authority'; 7 | 8 | import { cn } from '@/lib/utils'; 9 | 10 | const labelVariants = cva( 11 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', 12 | ); 13 | 14 | const Label = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef & 17 | VariantProps 18 | >(({ className, ...props }, ref) => ( 19 | 24 | )); 25 | Label.displayName = LabelPrimitive.Root.displayName; 26 | 27 | export { Label }; 28 | -------------------------------------------------------------------------------- /src/components/ui/marquee.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | 3 | interface MarqueeProps { 4 | className?: string; 5 | reverse?: boolean; 6 | pauseOnHover?: boolean; 7 | children?: React.ReactNode; 8 | vertical?: boolean; 9 | repeat?: number; 10 | [key: string]: any; 11 | } 12 | 13 | export default function Marquee({ 14 | className, 15 | reverse, 16 | pauseOnHover = false, 17 | children, 18 | vertical = false, 19 | repeat = 4, 20 | ...props 21 | }: MarqueeProps) { 22 | return ( 23 |
34 | {Array(repeat) 35 | .fill(0) 36 | .map((_, i) => ( 37 |
46 | {children} 47 |
48 | ))} 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/components/ui/number-ticker.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect, useRef } from 'react'; 4 | 5 | import { animate, useInView, useMotionValue } from 'framer-motion'; 6 | 7 | import { cn } from '@/lib/utils'; 8 | 9 | export default function NumberTicker({ 10 | value, 11 | direction = 'up', 12 | delay = 0, 13 | className, 14 | decimalPlaces = 0, 15 | duration = 2, // duration in seconds 16 | }: { 17 | value: number; 18 | direction?: 'up' | 'down'; 19 | className?: string; 20 | delay?: number; // delay in s 21 | decimalPlaces?: number; 22 | duration?: number; // duration in s 23 | }) { 24 | const ref = useRef(null); 25 | const motionValue = useMotionValue(direction === 'down' ? value : 0); 26 | const isInView = useInView(ref, { once: true, margin: '0px' }); 27 | 28 | useEffect(() => { 29 | if (!isInView) return; 30 | 31 | const timeoutId = setTimeout(() => { 32 | const controls = animate(motionValue, direction === 'down' ? 0 : value, { 33 | duration, 34 | ease: 'easeOut', 35 | onUpdate: (latest) => { 36 | if (ref.current) { 37 | ref.current.textContent = Intl.NumberFormat('en-US', { 38 | minimumFractionDigits: decimalPlaces, 39 | maximumFractionDigits: decimalPlaces, 40 | }).format(Number(latest.toFixed(decimalPlaces))); 41 | } 42 | }, 43 | }); 44 | 45 | return () => controls.stop(); 46 | }, delay * 1000); 47 | 48 | return () => clearTimeout(timeoutId); 49 | }, [motionValue, isInView, delay, value, direction, duration, decimalPlaces]); 50 | 51 | return ( 52 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import * as PopoverPrimitive from '@radix-ui/react-popover'; 6 | 7 | import { cn } from '@/lib/utils'; 8 | 9 | const Popover = PopoverPrimitive.Root; 10 | 11 | const PopoverTrigger = PopoverPrimitive.Trigger; 12 | 13 | const PopoverAnchor = PopoverPrimitive.Anchor; 14 | 15 | const PopoverContent = React.forwardRef< 16 | React.ComponentRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( 19 | 20 | 30 | 31 | )); 32 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 33 | 34 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; 35 | -------------------------------------------------------------------------------- /src/components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import * as ProgressPrimitive from '@radix-ui/react-progress'; 6 | 7 | import { cn } from '@/lib/utils'; 8 | 9 | const Progress = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, value, ...props }, ref) => ( 13 | 21 | 25 | 26 | )); 27 | Progress.displayName = ProgressPrimitive.Root.displayName; 28 | 29 | export { Progress }; 30 | -------------------------------------------------------------------------------- /src/components/ui/radio-group.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'; 6 | import { Circle } from 'lucide-react'; 7 | 8 | import { cn } from '@/lib/utils'; 9 | 10 | const RadioGroup = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => { 14 | return ( 15 | 20 | ); 21 | }); 22 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName; 23 | 24 | const RadioGroupItem = React.forwardRef< 25 | React.ElementRef, 26 | React.ComponentPropsWithoutRef 27 | >(({ className, ...props }, ref) => { 28 | return ( 29 | 37 | 38 | 39 | 40 | 41 | ); 42 | }); 43 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName; 44 | 45 | export { RadioGroup, RadioGroupItem }; 46 | -------------------------------------------------------------------------------- /src/components/ui/rainbow-button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | interface RainbowButtonProps 6 | extends React.ButtonHTMLAttributes {} 7 | 8 | export function RainbowButton({ 9 | children, 10 | className, 11 | ...props 12 | }: RainbowButtonProps) { 13 | return ( 14 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; 6 | 7 | import { cn } from '@/lib/utils'; 8 | 9 | const ScrollArea = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, children, ...props }, ref) => ( 13 | 18 | 19 | {children} 20 | 21 | 22 | 23 | 24 | )); 25 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; 26 | 27 | const ScrollBar = React.forwardRef< 28 | React.ElementRef, 29 | React.ComponentPropsWithoutRef 30 | >(({ className, orientation = 'vertical', ...props }, ref) => ( 31 | 44 | 45 | 46 | )); 47 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; 48 | 49 | export { ScrollArea, ScrollBar }; 50 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import * as SeparatorPrimitive from '@radix-ui/react-separator'; 6 | 7 | import { cn } from '@/lib/utils'; 8 | 9 | const Separator = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >( 13 | ( 14 | { className, orientation = 'horizontal', decorative = true, ...props }, 15 | ref, 16 | ) => ( 17 | 28 | ), 29 | ); 30 | Separator.displayName = SeparatorPrimitive.Root.displayName; 31 | 32 | export { Separator }; 33 | -------------------------------------------------------------------------------- /src/components/ui/shine-border.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | type TColorProp = string | string[]; 6 | 7 | interface ShineBorderProps { 8 | borderRadius?: number; 9 | borderWidth?: number; 10 | duration?: number; 11 | color?: TColorProp; 12 | className?: string; 13 | children: React.ReactNode; 14 | } 15 | 16 | /** 17 | * @name Shine Border 18 | * @description It is an animated background border effect component with easy to use and configurable props. 19 | * @param borderRadius defines the radius of the border. 20 | * @param borderWidth defines the width of the border. 21 | * @param duration defines the animation duration to be applied on the shining border 22 | * @param color a string or string array to define border color. 23 | * @param className defines the class name to be applied to the component 24 | * @param children contains react node elements. 25 | */ 26 | export default function ShineBorder({ 27 | borderRadius = 8, 28 | borderWidth = 1, 29 | duration = 14, 30 | color = '#000000', 31 | className, 32 | children, 33 | }: ShineBorderProps) { 34 | return ( 35 |
46 |
58 | {children} 59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/components/ui/shiny-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | 5 | import { 6 | type AnimationProps, 7 | type HTMLMotionProps, 8 | motion, 9 | } from 'framer-motion'; 10 | 11 | import { cn } from '@/lib/utils'; 12 | 13 | const animationProps = { 14 | initial: { '--x': '100%', scale: 0.8 }, 15 | animate: { '--x': '-100%', scale: 1 }, 16 | whileTap: { scale: 0.95 }, 17 | transition: { 18 | repeat: Infinity, 19 | repeatType: 'loop', 20 | repeatDelay: 1, 21 | type: 'spring', 22 | stiffness: 20, 23 | damping: 15, 24 | mass: 2, 25 | scale: { 26 | type: 'spring', 27 | stiffness: 200, 28 | damping: 5, 29 | mass: 0.5, 30 | }, 31 | }, 32 | } as AnimationProps; 33 | 34 | interface ShinyButtonProps extends HTMLMotionProps<'button'> { 35 | children: React.ReactNode; 36 | className?: string; 37 | } 38 | 39 | const ShinyButton = React.forwardRef( 40 | ({ children, className, ...props }, ref) => { 41 | return ( 42 | 51 | 58 | {children} 59 | 60 | 67 | 68 | ); 69 | }, 70 | ); 71 | 72 | ShinyButton.displayName = 'ShinyButton'; 73 | 74 | export default ShinyButton; 75 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ); 13 | } 14 | 15 | export { Skeleton }; 16 | -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useTheme } from 'next-themes'; 4 | import { Toaster as Sonner } from 'sonner'; 5 | 6 | type ToasterProps = React.ComponentProps; 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = 'system' } = useTheme(); 10 | 11 | return ( 12 | 28 | ); 29 | }; 30 | 31 | export { Toaster }; 32 | -------------------------------------------------------------------------------- /src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import * as SwitchPrimitives from '@radix-ui/react-switch'; 6 | 7 | import { cn } from '@/lib/utils'; 8 | 9 | const Switch = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 26 | 27 | )); 28 | Switch.displayName = SwitchPrimitives.Root.displayName; 29 | 30 | export { Switch }; 31 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | const Textarea = React.forwardRef< 6 | HTMLTextAreaElement, 7 | React.ComponentProps<'textarea'> 8 | >(({ className, ...props }, ref) => { 9 | return ( 10 |