├── .gitattributes ├── .github ├── CODEOWNERS ├── renovate.json └── workflows │ └── ci.yml ├── .eslintrc.json ├── src ├── styles │ └── globals.css ├── components │ ├── toaster.tsx │ ├── markdown.tsx │ ├── sidebar-footer.tsx │ ├── providers.tsx │ ├── tailwind-indicator.tsx │ ├── chat-list.tsx │ ├── footer.tsx │ ├── ui │ │ ├── AuthScreen.tsx │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── separator.tsx │ │ ├── toaster.tsx │ │ ├── input.tsx │ │ ├── tooltip.tsx │ │ ├── badge.tsx │ │ ├── switch.tsx │ │ ├── button.tsx │ │ ├── sheet.tsx │ │ ├── dialog.tsx │ │ ├── select.tsx │ │ ├── dropdown-menu.tsx │ │ ├── codeblock.tsx │ │ ├── use-toast.ts │ │ ├── alert-dialog.tsx │ │ ├── toast.tsx │ │ └── icons.tsx │ ├── external-link.tsx │ ├── chat-scroll-anchor.tsx │ ├── theme-toggle.tsx │ ├── sidebar.tsx │ ├── button-scroll-to-bottom.tsx │ ├── chat-message-actions.tsx │ ├── sidebar-list.tsx │ ├── sidebar-item.tsx │ ├── clear-history.tsx │ ├── header.tsx │ ├── user-menu.tsx │ ├── chat-message.tsx │ ├── prompt-form.tsx │ ├── chat.tsx │ ├── chat-panel.tsx │ ├── empty-screen.tsx │ └── sidebar-actions.tsx ├── lib │ ├── utils.ts │ ├── fonts.ts │ ├── types.ts │ ├── hooks │ │ ├── use-at-bottom.tsx │ │ ├── use-enter-submit.tsx │ │ ├── use-local-storage.ts │ │ └── use-copy-to-clipboard.tsx │ ├── redact.ts │ ├── auditLog.ts │ └── analytics.ts └── pages │ ├── _document.tsx │ ├── api │ ├── hello.ts │ ├── audit-log.ts │ ├── redact.ts │ └── chat.ts │ ├── _app.tsx │ ├── index.tsx │ └── chat.tsx ├── public ├── favicon.ico ├── vercel.svg ├── next.svg └── pangea-openai.svg ├── assets ├── redirect.png ├── authN_token.png ├── pangea_login.png ├── open_ai_login.png ├── create_new_token.png └── copy_openai_secret.png ├── postcss.config.js ├── next.config.js ├── .vscode ├── settings.json └── launch.json ├── components.json ├── .env.example ├── .gitignore ├── tsconfig.json ├── .devcontainer └── devcontainer.json ├── tailwind.config.ts ├── LICENSE.txt ├── package.json └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @kenany @snpranav @vanpangea 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangeacyber/secure-chatgpt/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/components/toaster.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | export { Toaster } from 'react-hot-toast' 4 | -------------------------------------------------------------------------------- /assets/redirect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangeacyber/secure-chatgpt/HEAD/assets/redirect.png -------------------------------------------------------------------------------- /assets/authN_token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangeacyber/secure-chatgpt/HEAD/assets/authN_token.png -------------------------------------------------------------------------------- /assets/pangea_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangeacyber/secure-chatgpt/HEAD/assets/pangea_login.png -------------------------------------------------------------------------------- /assets/open_ai_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangeacyber/secure-chatgpt/HEAD/assets/open_ai_login.png -------------------------------------------------------------------------------- /assets/create_new_token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangeacyber/secure-chatgpt/HEAD/assets/create_new_token.png -------------------------------------------------------------------------------- /assets/copy_openai_secret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangeacyber/secure-chatgpt/HEAD/assets/copy_openai_secret.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | } 5 | 6 | module.exports = nextConfig 7 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["local>pangeacyber/.github:renovate-config"], 4 | "automerge": true, 5 | "automergeStrategy": "rebase", 6 | "ignorePaths": [] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.insertSpaces": false, 3 | "files.trimTrailingWhitespace": true, 4 | "workbench.editorAssociations": { 5 | "*.md": "vscode.markdown.preview.editor" 6 | }, 7 | "markdown.extension.preview.autoShowPreviewToSide": true 8 | } -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/fonts.ts: -------------------------------------------------------------------------------- 1 | import { JetBrains_Mono as FontMono, Inter as FontSans } from 'next/font/google' 2 | 3 | export const fontSans = FontSans({ 4 | subsets: ['latin'], 5 | variable: '--font-sans' 6 | }) 7 | 8 | export const fontMono = FontMono({ 9 | subsets: ['latin'], 10 | variable: '--font-mono' 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/markdown.tsx: -------------------------------------------------------------------------------- 1 | import { FC, memo } from 'react' 2 | import ReactMarkdown, { Options } from 'react-markdown' 3 | 4 | export const MemoizedReactMarkdown: FC = memo( 5 | ReactMarkdown, 6 | (prevProps, nextProps) => 7 | prevProps.children === nextProps.children && 8 | prevProps.className === nextProps.className 9 | ) 10 | -------------------------------------------------------------------------------- /src/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /src/components/sidebar-footer.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils' 2 | 3 | export function SidebarFooter({ 4 | children, 5 | className, 6 | ...props 7 | }: React.ComponentProps<'div'>) { 8 | return ( 9 |
13 | {children} 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import { type Message } from 'ai' 2 | 3 | export interface Chat extends Record { 4 | id: string 5 | title: string 6 | createdAt: Date 7 | userId: string 8 | path: string 9 | messages: Message[] 10 | sharePath?: string 11 | } 12 | 13 | export type ServerActionResult = Promise< 14 | | Result 15 | | { 16 | error: string 17 | } 18 | > 19 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "gray", 10 | "cssVariables": false 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /src/components/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { ThemeProvider as NextThemesProvider, type ThemeProviderProps } from 'next-themes' 5 | 6 | import { TooltipProvider } from '@/components/ui/tooltip' 7 | 8 | export function Providers({ children, ...props }: ThemeProviderProps) { 9 | return ( 10 | 11 | {children} 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # 2 | # PUBLIC ENVIRONMENT VARIABLES, EXPOSED TO THE BROWSER 3 | # 4 | NEXT_PUBLIC_PANGEA_DOMAIN="" 5 | NEXT_PUBLIC_AUTHN_CLIENT_TOKEN="" 6 | NEXT_PUBLIC_AUTHN_HOSTED_LOGIN_URL="" 7 | 8 | # 9 | # PRIVATE ENVIRONMENT VARIABLES, NOT EXPOSED TO THE BROWSER 10 | # ONLY AVAILABLE ON THE SERVER 11 | # 12 | PANGEA_TOKEN="" 13 | PANGEA_DOMAIN="" 14 | OPENAI_API_KEY="" 15 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run application", 6 | "type": "node", 7 | "request": "launch", 8 | "cwd": "${workspaceFolder}", 9 | "console": "integratedTerminal", 10 | "runtimeExecutable": "npm", 11 | "runtimeArgs": [ 12 | "run", 13 | "dev" 14 | ], 15 | "skipFiles": [ 16 | "/**" 17 | ] 18 | } 19 | ] 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.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "paths": { 17 | "@/*": ["./src/*"] 18 | } 19 | }, 20 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/api/audit-log.ts: -------------------------------------------------------------------------------- 1 | import auditLog from '@/lib/auditLog' 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.body) { 7 | const auditResp = await auditLog({...req.body}) 8 | if (auditResp != false) { 9 | res.status(200).json({ message: "sucess" }) 10 | } else { 11 | res.status(403).json({message: "No PANGEA_TOKEN found on server."}) 12 | } 13 | } else { 14 | res.status(400).json({message: 'Bad request. Unable to log message.'}) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css' 2 | import type { AppProps } from 'next/app' 3 | import { AuthProvider } from "@pangeacyber/react-auth"; 4 | 5 | export default function App({ Component, pageProps }: AppProps) { 6 | const hostedLoginURL = process?.env?.NEXT_PUBLIC_AUTHN_HOSTED_LOGIN_URL || ""; 7 | const authConfig = { 8 | clientToken: process?.env?.NEXT_PUBLIC_AUTHN_CLIENT_TOKEN || "", 9 | domain: process?.env?.NEXT_PUBLIC_PANGEA_DOMAIN || "", 10 | }; 11 | 12 | return ( 13 | 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/hooks/use-at-bottom.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export function useAtBottom(offset = 0) { 4 | const [isAtBottom, setIsAtBottom] = React.useState(false) 5 | 6 | React.useEffect(() => { 7 | const handleScroll = () => { 8 | setIsAtBottom( 9 | window.innerHeight + window.scrollY >= 10 | document.body.offsetHeight - offset 11 | ) 12 | } 13 | 14 | window.addEventListener('scroll', handleScroll, { passive: true }) 15 | handleScroll() 16 | 17 | return () => { 18 | window.removeEventListener('scroll', handleScroll) 19 | } 20 | }, [offset]) 21 | 22 | return isAtBottom 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/api/redact.ts: -------------------------------------------------------------------------------- 1 | import redactText from "@/lib/redact"; 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | 5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 6 | if (req.body && "message" in req.body) { 7 | const redactedBody = await redactText(req.body.message as string) 8 | if (redactedBody == false) { 9 | return res.status(403).json({"error": "PANGEA_TOKEN is missing"}) 10 | } 11 | res.status(200).json({ message: redactedBody }) 12 | } else { 13 | res.status(400).json({message: 'Bad request. Unable to redact text.'}) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/tailwind-indicator.tsx: -------------------------------------------------------------------------------- 1 | export function TailwindIndicator() { 2 | if (process.env.NODE_ENV === 'production') return null 3 | 4 | return ( 5 |
6 |
xs
7 |
sm
8 |
md
9 |
lg
10 |
xl
11 |
2xl
12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import { Inter } from 'next/font/google' 3 | import { CallbackParams, useAuth } from '@pangeacyber/react-auth' 4 | import { useEffect } from 'react'; 5 | import { useRouter } from 'next/router'; 6 | import AuthScreen from '@/components/ui/AuthScreen'; 7 | 8 | const inter = Inter({ subsets: ['latin'] }) 9 | 10 | export default function Home() { 11 | const { user, authenticated, login } = useAuth(); 12 | const router = useRouter(); 13 | 14 | useEffect(() => { 15 | if(authenticated) { 16 | router.push('/chat') 17 | } 18 | }, [user, authenticated]) 19 | 20 | return ( 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/hooks/use-enter-submit.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, type RefObject } from 'react' 2 | 3 | export function useEnterSubmit(): { 4 | formRef: RefObject 5 | onKeyDown: (event: React.KeyboardEvent) => void 6 | } { 7 | const formRef = useRef(null) 8 | 9 | const handleKeyDown = ( 10 | event: React.KeyboardEvent 11 | ): void => { 12 | if ( 13 | event.key === 'Enter' && 14 | !event.shiftKey && 15 | !event.nativeEvent.isComposing 16 | ) { 17 | formRef.current?.requestSubmit() 18 | event.preventDefault() 19 | } 20 | } 21 | 22 | return { formRef, onKeyDown: handleKeyDown } 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/hooks/use-local-storage.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | export const useLocalStorage = ( 4 | key: string, 5 | initialValue: T 6 | ): [T, (value: T) => void] => { 7 | const [storedValue, setStoredValue] = useState(initialValue) 8 | 9 | useEffect(() => { 10 | // Retrieve from localStorage 11 | const item = window.localStorage.getItem(key) 12 | if (item) { 13 | setStoredValue(JSON.parse(item)) 14 | } 15 | }, [key]) 16 | 17 | const setValue = (value: T) => { 18 | // Save state 19 | setStoredValue(value) 20 | // Save to localStorage 21 | window.localStorage.setItem(key, JSON.stringify(value)) 22 | } 23 | return [storedValue, setValue] 24 | } 25 | -------------------------------------------------------------------------------- /src/components/chat-list.tsx: -------------------------------------------------------------------------------- 1 | import { type Message } from 'ai' 2 | 3 | import { Separator } from '@/components/ui/separator' 4 | import { ChatMessage } from '@/components/chat-message' 5 | 6 | export interface ChatList { 7 | messages: Message[] 8 | } 9 | 10 | export function ChatList({ messages }: ChatList) { 11 | if (!messages.length) { 12 | return null 13 | } 14 | 15 | return ( 16 |
17 | {messages.map((message, index) => ( 18 |
19 | 20 | {index < messages.length - 1 && ( 21 | 22 | )} 23 |
24 | ))} 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | import { ExternalLink } from '@/components/external-link' 5 | 6 | export function FooterText({ className, ...props }: React.ComponentProps<'p'>) { 7 | return ( 8 |

15 | Open source AI chatbot built with{' '} 16 | Next.js and{' '} 17 | 18 | Pangea 19 | 20 | . 21 |

22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/components/ui/AuthScreen.tsx: -------------------------------------------------------------------------------- 1 | // AuthScreen.js 2 | import React from 'react'; 3 | 4 | const AuthScreen = ({login}: any) => { 5 | return ( 6 |
7 |
8 | Logo 9 |

Welcome to Secure ChatGPT

10 | 15 |
16 |
17 | ); 18 | }; 19 | 20 | export default AuthScreen -------------------------------------------------------------------------------- /src/components/external-link.tsx: -------------------------------------------------------------------------------- 1 | export function ExternalLink({ 2 | href, 3 | children 4 | }: { 5 | href: string 6 | children: React.ReactNode 7 | }) { 8 | return ( 9 | 14 | {children} 15 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/redact.ts: -------------------------------------------------------------------------------- 1 | import { PangeaConfig, RedactService } from "pangea-node-sdk"; 2 | 3 | export default async function redactText(inputText: string) { 4 | if(process.env.PANGEA_TOKEN) { 5 | const token = process.env.PANGEA_TOKEN as string; 6 | const config = new PangeaConfig({ domain: process.env.PANGEA_DOMAIN }); 7 | const redact = new RedactService(token, config); 8 | 9 | const response = await redact.redact(inputText); 10 | 11 | if (response.success) { 12 | return response.result.redacted_text; 13 | } else { 14 | // If error return input text 15 | console.log("Error", response.status, response.result); 16 | return inputText; 17 | } 18 | } else { 19 | return false; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/hooks/use-copy-to-clipboard.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | export interface useCopyToClipboardProps { 6 | timeout?: number 7 | } 8 | 9 | export function useCopyToClipboard({ 10 | timeout = 2000 11 | }: useCopyToClipboardProps) { 12 | const [isCopied, setIsCopied] = React.useState(false) 13 | 14 | const copyToClipboard = (value: string) => { 15 | if (typeof window === 'undefined' || !navigator.clipboard?.writeText) { 16 | return 17 | } 18 | 19 | if (!value) { 20 | return 21 | } 22 | 23 | navigator.clipboard.writeText(value).then(() => { 24 | setIsCopied(true) 25 | 26 | setTimeout(() => { 27 | setIsCopied(false) 28 | }, timeout) 29 | }) 30 | } 31 | 32 | return { isCopied, copyToClipboard } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /src/components/chat-scroll-anchor.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { useInView } from 'react-intersection-observer' 5 | 6 | import { useAtBottom } from '@/lib/hooks/use-at-bottom' 7 | 8 | interface ChatScrollAnchorProps { 9 | trackVisibility?: boolean 10 | } 11 | 12 | export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) { 13 | const isAtBottom = useAtBottom() 14 | const { ref, entry, inView } = useInView({ 15 | trackVisibility, 16 | delay: 100, 17 | rootMargin: '0px 0px -150px 0px' 18 | }) 19 | 20 | React.useEffect(() => { 21 | if (isAtBottom && trackVisibility && !inView) { 22 | entry?.target.scrollIntoView({ 23 | block: 'start' 24 | }) 25 | } 26 | }, [inView, entry, isAtBottom, trackVisibility]) 27 | 28 | return
29 | } 30 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "mcr.microsoft.com/devcontainers/universal:2", 3 | "hostRequirements": { 4 | "cpus": 4 5 | }, 6 | "waitFor": "onCreateCommand", 7 | "postCreateCommand": "sudo chown -R $USER:$USER /usr/local/bin/; curl -L -o /usr/local/bin/pangea \"https://github.com/pangeacyber/pangea-cli/releases/latest/download/pangea-$(uname -s)-$(uname -m)\" && chmod +x /usr/local/bin/pangea; npm install", 8 | "postAttachCommand": { 9 | "server": "npm run dev" 10 | }, 11 | "customizations": { 12 | "codespaces": { 13 | "openFiles": [ 14 | "README.md", 15 | ".env.example" 16 | ] 17 | } 18 | }, 19 | "portsAttributes": { 20 | "3000": { 21 | "label": "Application", 22 | "onAutoForward": "openPreview" 23 | } 24 | }, 25 | "forwardPorts": [3000] 26 | } -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |