├── .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 |
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 |
9 |
Welcome to Secure ChatGPT
10 |
{
11 | login()
12 | }}>
13 | Login to Continue
14 |
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 |
22 |
26 |
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 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = 'Textarea'
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/src/components/theme-toggle.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { useTheme } from 'next-themes'
5 |
6 | import { Button } from '@/components/ui/button'
7 | import { IconMoon, IconSun } from '@/components/ui/icons'
8 |
9 | export function ThemeToggle() {
10 | const { setTheme, theme } = useTheme()
11 | const [_, startTransition] = React.useTransition()
12 |
13 | return (
14 | {
18 | startTransition(() => {
19 | setTheme(theme === 'light' ? 'dark' : 'light')
20 | })
21 | }}
22 | >
23 | {!theme ? null : theme === 'dark' ? (
24 |
25 | ) : (
26 |
27 | )}
28 | Toggle theme
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as SeparatorPrimitive from '@radix-ui/react-separator'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = 'horizontal', decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Toast,
3 | ToastClose,
4 | ToastDescription,
5 | ToastProvider,
6 | ToastTitle,
7 | ToastViewport,
8 | } from "@/components/ui/toast"
9 | import { useToast } from "@/components/ui/use-toast"
10 |
11 | export function Toaster() {
12 | const { toasts } = useToast()
13 |
14 | return (
15 |
16 | {toasts.map(function ({ id, title, description, action, ...props }) {
17 | return (
18 |
19 |
20 | {title && {title} }
21 | {description && (
22 | {description}
23 | )}
24 |
25 | {action}
26 |
27 |
28 | )
29 | })}
30 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cn } from '@/lib/utils'
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = 'Input'
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/src/pages/api/chat.ts:
--------------------------------------------------------------------------------
1 | import OpenAI from 'openai';
2 | import { OpenAIStream, StreamingTextResponse } from 'ai';
3 |
4 | // Create an OpenAI API client (that's edge friendly!)
5 | const openai = new OpenAI({
6 | apiKey: process.env.OPENAI_API_KEY,
7 | });
8 |
9 | // IMPORTANT! Set the runtime to edge
10 | export const runtime = 'edge';
11 |
12 | export default async function POST(req: Request) {
13 | // Extract the `prompt` from the body of the request
14 | const { messages } = await req.json();
15 |
16 | // Ask OpenAI for a streaming completion given the prompt
17 | const response = await openai.chat.completions.create({
18 | model: "gpt-3.5-turbo",
19 | messages: messages,
20 | stream: true,
21 | max_tokens: 256
22 | });
23 |
24 | // Convert the response into a friendly text-stream
25 | const stream = OpenAIStream(response);
26 |
27 | // Respond with the stream
28 | return new StreamingTextResponse(stream);
29 | }
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | darkMode: ["class"],
4 | content: [
5 | './pages/**/*.{ts,tsx}',
6 | './components/**/*.{ts,tsx}',
7 | './app/**/*.{ts,tsx}',
8 | './src/**/*.{ts,tsx}',
9 | ],
10 | theme: {
11 | container: {
12 | center: true,
13 | padding: "2rem",
14 | screens: {
15 | "2xl": "1400px",
16 | },
17 | },
18 | extend: {
19 | keyframes: {
20 | "accordion-down": {
21 | from: { height: 0 },
22 | to: { height: "var(--radix-accordion-content-height)" },
23 | },
24 | "accordion-up": {
25 | from: { height: "var(--radix-accordion-content-height)" },
26 | to: { height: 0 },
27 | },
28 | },
29 | animation: {
30 | "accordion-down": "accordion-down 0.2s ease-out",
31 | "accordion-up": "accordion-up 0.2s ease-out",
32 | },
33 | },
34 | },
35 | plugins: [require("tailwindcss-animate")],
36 | }
--------------------------------------------------------------------------------
/src/components/sidebar.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | import { Button } from '@/components/ui/button'
6 | import {
7 | Sheet,
8 | SheetContent,
9 | SheetHeader,
10 | SheetTitle,
11 | SheetTrigger
12 | } from '@/components/ui/sheet'
13 | import { IconSidebar } from '@/components/ui/icons'
14 |
15 | export interface SidebarProps {
16 | children?: React.ReactNode
17 | }
18 |
19 | export function Sidebar({ children }: SidebarProps) {
20 | return (
21 |
22 |
23 |
24 |
25 | Toggle Sidebar
26 |
27 |
28 |
29 |
30 | Chat History
31 |
32 | {children}
33 |
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/button-scroll-to-bottom.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | import { cn } from '@/lib/utils'
6 | import { useAtBottom } from '@/lib/hooks/use-at-bottom'
7 | import { Button, type ButtonProps } from '@/components/ui/button'
8 | import { IconArrowDown } from '@/components/ui/icons'
9 |
10 | export function ButtonScrollToBottom({ className, ...props }: ButtonProps) {
11 | const isAtBottom = useAtBottom()
12 |
13 | return (
14 |
23 | window.scrollTo({
24 | top: document.body.offsetHeight,
25 | behavior: 'smooth'
26 | })
27 | }
28 | {...props}
29 | >
30 |
31 | Scroll to bottom
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/src/lib/auditLog.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import { PangeaConfig, AuditService, PangeaErrors } from "pangea-node-sdk";
4 |
5 | export default async function auditLog({
6 | message,
7 | actor,
8 | user_id,
9 | session_id
10 | }: any) {
11 | if(process.env.PANGEA_TOKEN) {
12 | const token = process.env.PANGEA_TOKEN as string;
13 | const config = new PangeaConfig({ domain: process.env.PANGEA_DOMAIN });
14 | const audit = new AuditService(token, config);
15 |
16 | const data = {
17 | "message": message,
18 | "actor": actor,
19 | "user_id": user_id,
20 | "timestamp": new Date(Date.now()).toISOString(),
21 | "session_id": session_id
22 | };
23 |
24 | try {
25 | const logResponse = await audit.log(data, { verbose: true });
26 | console.log("Response: %s", logResponse.result);
27 | } catch (err) {
28 | if (err instanceof PangeaErrors.APIError) {
29 | console.log(err.summary, err.pangeaResponse);
30 | } else {
31 | throw err;
32 | }
33 | }
34 | } else {
35 | return false;
36 | }
37 | }
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Pangea Cyber
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as TooltipPrimitive from '@radix-ui/react-tooltip'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/src/components/chat-message-actions.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { type Message } from 'ai'
4 |
5 | import { Button } from '@/components/ui/button'
6 | import { IconCheck, IconCopy } from '@/components/ui/icons'
7 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
8 | import { cn } from '@/lib/utils'
9 |
10 | interface ChatMessageActionsProps extends React.ComponentProps<'div'> {
11 | message: Message
12 | }
13 |
14 | export function ChatMessageActions({
15 | message,
16 | className,
17 | ...props
18 | }: ChatMessageActionsProps) {
19 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
20 |
21 | const onCopy = () => {
22 | if (isCopied) return
23 | copyToClipboard(message.content)
24 | }
25 |
26 | return (
27 |
34 |
35 | {isCopied ? : }
36 | Copy message
37 |
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/sidebar-list.tsx:
--------------------------------------------------------------------------------
1 | // import { getChats, removeChat, shareChat } from '@/app/actions'
2 | // import { SidebarActions } from '@/components/sidebar-actions'
3 | // import { SidebarItem } from '@/components/sidebar-item'
4 |
5 | // export interface SidebarListProps {
6 | // userId?: string
7 | // }
8 |
9 | // export async function SidebarList({ userId }: SidebarListProps) {
10 | // const chats = await getChats(userId)
11 |
12 | // return (
13 | //
14 | // {chats?.length ? (
15 | //
16 | // {chats.map(
17 | // chat =>
18 | // chat && (
19 | //
20 | //
25 | //
26 | // )
27 | // )}
28 | //
29 | // ) : (
30 | //
31 | //
No chat history
32 | //
33 | // )}
34 | //
35 | // )
36 | // }
37 |
--------------------------------------------------------------------------------
/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { cva, type VariantProps } from 'class-variance-authority'
3 |
4 | import { cn } from '@/lib/utils'
5 |
6 | const badgeVariants = cva(
7 | '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',
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
13 | secondary:
14 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
15 | destructive:
16 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
17 | outline: 'text-foreground'
18 | }
19 | },
20 | defaultVariants: {
21 | variant: 'default'
22 | }
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SwitchPrimitives from "@radix-ui/react-switch"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Switch = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 |
23 |
24 | ))
25 | Switch.displayName = SwitchPrimitives.Root.displayName
26 |
27 | export { Switch }
28 |
--------------------------------------------------------------------------------
/src/pages/chat.tsx:
--------------------------------------------------------------------------------
1 | import { nanoid } from 'ai'
2 | import { Chat } from '@/components/chat'
3 | import { useCompletion } from 'ai/react';
4 | import { ThemeProvider as NextThemesProvider } from 'next-themes'
5 | import { TooltipProvider } from '@/components/ui/tooltip';
6 | import { Providers } from '@/components/providers'
7 | import { Header } from '@/components/header'
8 | import { useAuth } from '@pangeacyber/react-auth';
9 | import AuthScreen from '@/components/ui/AuthScreen';
10 | import { useRouter } from 'next/router';
11 | import { useEffect } from 'react';
12 | import { Toaster } from "@/components/ui/toaster"
13 |
14 | export default function Completion() {
15 |
16 | const id = nanoid()
17 | const {authenticated, error, logout, loading, user} = useAuth()
18 | const router = useRouter();
19 |
20 |
21 | useEffect(() => {
22 | console.log(user)
23 | if (!loading && !error && !authenticated) {
24 | router.push("/");
25 | }
26 | }, [error, authenticated, loading]);
27 |
28 | return (
29 | <>
30 | {authenticated ? (
31 |
32 |
33 |
34 |
35 |
36 | {/* */}
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | ): (
45 | <>>
46 | )}
47 | >
48 | )
49 | }
--------------------------------------------------------------------------------
/src/lib/analytics.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest } from 'next'
2 | import type { NextFetchEvent, NextRequest } from 'next/server'
3 |
4 | export const initAnalytics = ({
5 | request,
6 | event
7 | }: {
8 | request: NextRequest | NextApiRequest | Request
9 | event?: NextFetchEvent
10 | }) => {
11 | const endpoint = process.env.VERCEL_URL
12 |
13 | return {
14 | track: async (eventName: string, data?: any) => {
15 | try {
16 | if (!endpoint && process.env.NODE_ENV === 'development') {
17 | console.log(
18 | `[Vercel Web Analytics] Track "${eventName}"` +
19 | (data ? ` with data ${JSON.stringify(data || {})}` : '')
20 | )
21 | return
22 | }
23 |
24 | const headers: { [key: string]: string } = {}
25 | Object.entries(request.headers).map(([key, value]) => {
26 | headers[key] = value
27 | })
28 |
29 | const body = {
30 | o: headers.referer,
31 | ts: new Date().getTime(),
32 | r: '',
33 | en: eventName,
34 | ed: data
35 | }
36 |
37 | const promise = fetch(
38 | `https://${process.env.VERCEL_URL}/_vercel/insights/event`,
39 | {
40 | headers: {
41 | 'content-type': 'application/json',
42 | 'user-agent': headers['user-agent'] as string,
43 | 'x-forwarded-for': headers['x-forwarded-for'] as string,
44 | 'x-va-server': '1'
45 | },
46 | body: JSON.stringify(body),
47 | method: 'POST'
48 | }
49 | )
50 |
51 | if (event) {
52 | event.waitUntil(promise)
53 | }
54 | {
55 | await promise
56 | }
57 | } catch (err) {
58 | console.error(err)
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/components/sidebar-item.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import Link from 'next/link'
4 | import { usePathname } from 'next/navigation'
5 |
6 | import { type Chat } from '@/lib/types'
7 | import { cn } from '@/lib/utils'
8 | import { buttonVariants } from '@/components/ui/button'
9 | import { IconMessage, IconUsers } from '@/components/ui/icons'
10 | import {
11 | Tooltip,
12 | TooltipContent,
13 | TooltipTrigger
14 | } from '@/components/ui/tooltip'
15 |
16 | interface SidebarItemProps {
17 | chat: Chat
18 | children: React.ReactNode
19 | }
20 |
21 | export function SidebarItem({ chat, children }: SidebarItemProps) {
22 | const pathname = usePathname()
23 | const isActive = pathname === chat.path
24 |
25 | if (!chat?.id) return null
26 |
27 | return (
28 |
29 |
30 | {chat.sharePath ? (
31 |
32 |
36 |
37 |
38 | This is a shared chat.
39 |
40 | ) : (
41 |
42 | )}
43 |
44 |
52 |
56 | {chat.title}
57 |
58 |
59 | {isActive &&
{children}
}
60 |
61 | )
62 | }
63 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "secure-chatgpt-revamp-v2",
3 | "version": "0.1.0",
4 | "private": true,
5 | "license": "MIT",
6 | "scripts": {
7 | "dev": "next dev",
8 | "build": "next build",
9 | "start": "next start",
10 | "lint": "next lint"
11 | },
12 | "dependencies": {
13 | "@pangeacyber/react-auth": "^0.0.17",
14 | "@radix-ui/react-alert-dialog": "^1.1.6",
15 | "@radix-ui/react-dialog": "^1.1.6",
16 | "@radix-ui/react-dropdown-menu": "^2.1.6",
17 | "@radix-ui/react-icons": "^1.3.2",
18 | "@radix-ui/react-label": "^2.1.2",
19 | "@radix-ui/react-select": "^2.1.6",
20 | "@radix-ui/react-separator": "^1.1.2",
21 | "@radix-ui/react-slot": "^1.1.2",
22 | "@radix-ui/react-switch": "^1.1.3",
23 | "@radix-ui/react-toast": "^1.2.6",
24 | "@radix-ui/react-tooltip": "^1.1.8",
25 | "@types/node": "22.14.0",
26 | "@vercel/analytics": "^1.5.0",
27 | "@vercel/kv": "^3.0.0",
28 | "@vercel/og": "^0.6.8",
29 | "ai": "3.4.33",
30 | "autoprefixer": "10.4.21",
31 | "axios": "^1.8.4",
32 | "class-variance-authority": "^0.7.1",
33 | "clsx": "^2.1.1",
34 | "eslint": "8.57.1",
35 | "eslint-config-next": "15.2.4",
36 | "focus-trap-react": "^11.0.3",
37 | "next": "15.2.4",
38 | "next-themes": "0.4.6",
39 | "openai": "4.76.0",
40 | "pangea-node-sdk": "^4.4.0",
41 | "postcss": "8.5.3",
42 | "react": "18.3.1",
43 | "react-dom": "18.3.1",
44 | "react-hot-toast": "^2.5.2",
45 | "react-intersection-observer": "^9.16.0",
46 | "react-markdown": "9.1.0",
47 | "react-syntax-highlighter": "^15.6.1",
48 | "react-textarea-autosize": "^8.5.9",
49 | "remark-gfm": "^4.0.1",
50 | "remark-math": "^6.0.0",
51 | "tailwind-merge": "^3.2.0",
52 | "tailwindcss-animate": "^1.0.7",
53 | "typescript": "5.8.3"
54 | },
55 | "devDependencies": {
56 | "@types/react": "18.3.20",
57 | "@types/react-dom": "18.3.6",
58 | "@types/react-syntax-highlighter": "15.5.13",
59 | "tailwindcss": "3.4.17"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { Slot } from '@radix-ui/react-slot'
3 | import { cva, type VariantProps } from 'class-variance-authority'
4 |
5 | import { cn } from '@/lib/utils'
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center rounded-md text-sm font-medium shadow ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | 'bg-primary text-primary-foreground shadow-md hover:bg-primary/90',
14 | destructive:
15 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
16 | outline:
17 | 'border border-input hover:bg-accent hover:text-accent-foreground',
18 | secondary:
19 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
20 | ghost: 'shadow-none hover:bg-accent hover:text-accent-foreground',
21 | link: 'text-primary underline-offset-4 shadow-none hover:underline'
22 | },
23 | size: {
24 | default: 'h-8 px-4 py-2',
25 | sm: 'h-8 rounded-md px-3',
26 | lg: 'h-11 rounded-md px-8',
27 | icon: 'h-8 w-8 p-0'
28 | }
29 | },
30 | defaultVariants: {
31 | variant: 'default',
32 | size: 'default'
33 | }
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : 'button'
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = 'Button'
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | pull_request:
9 | types:
10 | - opened
11 | - synchronize
12 | - reopened
13 | - ready_for_review
14 |
15 | merge_group:
16 |
17 | workflow_dispatch:
18 |
19 | permissions:
20 | contents: read
21 |
22 | concurrency:
23 | group: ${{ github.workflow }}-${{ github.event.number || github.ref }}
24 | cancel-in-progress: ${{ github.ref_name != 'main' }}
25 |
26 | env:
27 | NEXT_TELEMETRY_DISABLED: 1
28 | NEXT_PUBLIC_AUTHN_CLIENT_TOKEN: placeholder
29 | NEXT_PUBLIC_PANGEA_DOMAIN: placeholder
30 |
31 | jobs:
32 | prefetch:
33 | runs-on: ubuntu-latest
34 | steps:
35 | - name: Checkout code
36 | uses: actions/checkout@v4.2.2
37 |
38 | - name: Setup Node.js
39 | uses: actions/setup-node@v4.3.0
40 | with:
41 | node-version: 20
42 | cache: npm
43 |
44 | - name: Install dependencies
45 | run: npm ci
46 |
47 | build:
48 | needs: [prefetch]
49 | runs-on: ubuntu-latest
50 | steps:
51 | - name: Checkout code
52 | uses: actions/checkout@v4.2.2
53 |
54 | - name: Setup Node.js
55 | uses: actions/setup-node@v4.3.0
56 | with:
57 | node-version: 20
58 | cache: npm
59 |
60 | - name: Next.js cache
61 | uses: actions/cache@v4.2.3
62 | with:
63 | path: |
64 | ~/.npm
65 | ${{ github.workspace }}/.next/cache
66 | key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
67 | restore-keys: |
68 | ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
69 |
70 | - name: Install dependencies
71 | run: npm ci
72 |
73 | - name: Build
74 | run: npm run build
75 |
76 | lint:
77 | needs: [prefetch]
78 | runs-on: ubuntu-latest
79 | steps:
80 | - name: Checkout code
81 | uses: actions/checkout@v4.2.2
82 |
83 | - name: Setup Node.js
84 | uses: actions/setup-node@v4.3.0
85 | with:
86 | node-version: 20
87 | cache: npm
88 |
89 | - name: Install dependencies
90 | run: npm ci
91 |
92 | - name: Lint
93 | run: npm run lint
94 |
--------------------------------------------------------------------------------
/src/components/clear-history.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { useRouter } from 'next/navigation'
5 | import { toast } from 'react-hot-toast'
6 |
7 | import { ServerActionResult } from '@/lib/types'
8 | import { Button } from '@/components/ui/button'
9 | import {
10 | AlertDialog,
11 | AlertDialogAction,
12 | AlertDialogCancel,
13 | AlertDialogContent,
14 | AlertDialogDescription,
15 | AlertDialogFooter,
16 | AlertDialogHeader,
17 | AlertDialogTitle,
18 | AlertDialogTrigger
19 | } from '@/components/ui/alert-dialog'
20 | import { IconSpinner } from '@/components/ui/icons'
21 |
22 | interface ClearHistoryProps {
23 | clearChats: () => ServerActionResult
24 | }
25 |
26 | export function ClearHistory({ clearChats }: ClearHistoryProps) {
27 | const [open, setOpen] = React.useState(false)
28 | const [isPending, startTransition] = React.useTransition()
29 | const router = useRouter()
30 |
31 | return (
32 |
33 |
34 |
35 | {isPending && }
36 | Clear history
37 |
38 |
39 |
40 |
41 | Are you absolutely sure?
42 |
43 | This will permanently delete your chat history and remove your data
44 | from our servers.
45 |
46 |
47 |
48 | Cancel
49 | {
52 | event.preventDefault()
53 | startTransition(async () => {
54 | const result = await clearChats()
55 |
56 | if (result && 'error' in result) {
57 | toast.error(result.error)
58 | return
59 | }
60 |
61 | setOpen(false)
62 | router.push('/')
63 | })
64 | }}
65 | >
66 | {isPending && }
67 | Delete
68 |
69 |
70 |
71 |
72 | )
73 | }
74 |
--------------------------------------------------------------------------------
/src/components/header.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import Link from 'next/link'
3 |
4 | import { cn } from '@/lib/utils'
5 | import { useAuth } from '@pangeacyber/react-auth'
6 | import { Button, buttonVariants } from '@/components/ui/button'
7 | import { Sidebar } from '@/components/sidebar'
8 | import {
9 | IconGitHub,
10 | IconNextChat,
11 | IconSeparator,
12 | IconVercel
13 | } from '@/components/ui/icons'
14 | import { SidebarFooter } from '@/components/sidebar-footer'
15 | import { ThemeToggle } from '@/components/theme-toggle'
16 |
17 | export function Header() {
18 | const { authenticated, login } = useAuth()
19 |
20 | return (
21 |
73 | )
74 | }
75 |
--------------------------------------------------------------------------------
/src/components/user-menu.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import Image from 'next/image'
4 | import { useAuth } from '@pangeacyber/react-auth'
5 |
6 | import { Button } from '@/components/ui/button'
7 | import {
8 | DropdownMenu,
9 | DropdownMenuContent,
10 | DropdownMenuItem,
11 | DropdownMenuSeparator,
12 | DropdownMenuTrigger
13 | } from '@/components/ui/dropdown-menu'
14 | import { IconExternalLink } from '@/components/ui/icons'
15 |
16 | // export interface UserMenuProps {
17 | // user: Session['user']
18 | // }
19 |
20 | function getUserInitials(name: string) {
21 | const [firstName, lastName] = name.split(' ')
22 | return lastName ? `${firstName[0]}${lastName[0]}` : firstName.slice(0, 2)
23 | }
24 |
25 | export function UserMenu({ user }: any) {
26 | const { logout } = useAuth();
27 | return (
28 |
29 |
30 |
31 |
32 | {user?.image ? (
33 |
39 | ) : (
40 |
41 | {user?.name ? getUserInitials(user?.name) : null}
42 |
43 | )}
44 | {user?.name}
45 |
46 |
47 |
48 |
49 | {user?.name}
50 | {user?.email}
51 |
52 |
53 |
54 |
60 | Vercel Homepage
61 |
62 |
63 |
64 |
66 | logout()
67 | }
68 | className="text-xs"
69 | >
70 | Log Out
71 |
72 |
73 |
74 |
75 | )
76 | }
77 |
--------------------------------------------------------------------------------
/src/components/chat-message.tsx:
--------------------------------------------------------------------------------
1 | // Inspired by Chatbot-UI and modified to fit the needs of this project
2 | // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatMessage.tsx
3 |
4 | import { Message } from 'ai'
5 | import React from 'react'
6 | import remarkGfm from 'remark-gfm'
7 | import remarkMath from 'remark-math'
8 |
9 | import { cn } from '@/lib/utils'
10 | import { CodeBlock } from '@/components/ui/codeblock'
11 | import { MemoizedReactMarkdown } from '@/components/markdown'
12 | import { IconPangeaOpenAI, IconUser } from '@/components/ui/icons'
13 | import { ChatMessageActions } from '@/components/chat-message-actions'
14 |
15 | export interface ChatMessageProps {
16 | message: Message
17 | }
18 |
19 | export function ChatMessage({ message, ...props }: ChatMessageProps) {
20 | return (
21 |
25 |
33 | {message.role === 'user' ? : }
34 |
35 |
36 | {children}
42 | },
43 | code({ node, className, children, ...props }) {
44 | const childrenArray = React.Children.toArray(children)
45 | const firstChild = childrenArray[0] as React.ReactElement
46 | const firstChildAsString = React.isValidElement(firstChild)
47 | ? (firstChild as React.ReactElement).props.children
48 | : firstChild
49 |
50 | if (firstChildAsString === '▍') {
51 | return (
52 | ▍
53 | )
54 | }
55 |
56 | if (typeof firstChildAsString === "string") {
57 | childrenArray[0] = firstChildAsString.replace("`▍`", "▍")
58 | }
59 |
60 | const match = /language-(\w+)/.exec(className || '')
61 |
62 | if (typeof firstChildAsString === "string" && !firstChildAsString.includes("\n")) {
63 | return (
64 |
65 | {children}
66 |
67 | )
68 | }
69 |
70 | return (
71 |
77 | )
78 | }
79 | }}
80 | >
81 | {message.content}
82 |
83 |
84 |
85 |
86 | )
87 | }
88 |
--------------------------------------------------------------------------------
/src/components/prompt-form.tsx:
--------------------------------------------------------------------------------
1 | import { UseChatHelpers } from 'ai/react'
2 | import * as React from 'react'
3 | import Textarea from 'react-textarea-autosize'
4 |
5 | import { Button, buttonVariants } from '@/components/ui/button'
6 | import { IconArrowElbow, IconPlus } from '@/components/ui/icons'
7 | import {
8 | Tooltip,
9 | TooltipContent,
10 | TooltipTrigger
11 | } from '@/components/ui/tooltip'
12 | import { useEnterSubmit } from '@/lib/hooks/use-enter-submit'
13 | import { cn } from '@/lib/utils'
14 | import { useRouter } from 'next/navigation'
15 |
16 | export interface PromptProps
17 | extends Pick {
18 | onSubmit: (value: string) => Promise
19 | isLoading: boolean
20 | }
21 |
22 | export function PromptForm({
23 | onSubmit,
24 | input,
25 | setInput,
26 | isLoading
27 | }: PromptProps) {
28 | const { formRef, onKeyDown } = useEnterSubmit()
29 | const inputRef = React.useRef(null)
30 | const router = useRouter()
31 |
32 | React.useEffect(() => {
33 | if (inputRef.current) {
34 | inputRef.current.focus()
35 | }
36 | }, [])
37 |
38 | return (
39 |
98 | )
99 | }
100 |
--------------------------------------------------------------------------------
/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as SheetPrimitive from '@radix-ui/react-dialog'
5 |
6 | import { cn } from '@/lib/utils'
7 | import { IconClose } from '@/components/ui/icons'
8 |
9 | const Sheet = SheetPrimitive.Root
10 |
11 | const SheetTrigger = SheetPrimitive.Trigger
12 |
13 | const SheetClose = SheetPrimitive.Close
14 |
15 | const SheetPortal = ({
16 | children,
17 | ...props
18 | }: SheetPrimitive.DialogPortalProps) => (
19 |
22 |
23 | {children}
24 |
25 |
26 | )
27 | SheetPortal.displayName = SheetPrimitive.Portal.displayName
28 |
29 | const SheetOverlay = React.forwardRef<
30 | React.ElementRef,
31 | React.ComponentPropsWithoutRef
32 | >(({ className, children, ...props }, ref) => (
33 |
41 | ))
42 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
43 |
44 | const SheetContent = React.forwardRef<
45 | React.ElementRef,
46 | React.ComponentPropsWithoutRef
47 | >(({ className, children, ...props }, ref) => (
48 |
49 |
57 | {children}
58 |
59 |
60 | Close
61 |
62 |
63 |
64 | ))
65 | SheetContent.displayName = SheetPrimitive.Content.displayName
66 |
67 | const SheetHeader = ({
68 | className,
69 | ...props
70 | }: React.HTMLAttributes) => (
71 |
72 | )
73 | SheetHeader.displayName = 'SheetHeader'
74 |
75 | const SheetFooter = ({
76 | className,
77 | ...props
78 | }: React.HTMLAttributes) => (
79 |
86 | )
87 | SheetFooter.displayName = 'SheetFooter'
88 |
89 | const SheetTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
98 | ))
99 | SheetTitle.displayName = SheetPrimitive.Title.displayName
100 |
101 | const SheetDescription = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | SheetDescription.displayName = SheetPrimitive.Description.displayName
112 |
113 | export {
114 | Sheet,
115 | SheetTrigger,
116 | SheetClose,
117 | SheetContent,
118 | SheetHeader,
119 | SheetFooter,
120 | SheetTitle,
121 | SheetDescription
122 | }
123 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as DialogPrimitive from '@radix-ui/react-dialog'
5 |
6 | import { cn } from '@/lib/utils'
7 | import { IconClose } from '@/components/ui/icons'
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = ({
14 | children,
15 | ...props
16 | }: DialogPrimitive.DialogPortalProps) => (
17 |
18 |
19 | {children}
20 |
21 |
22 | )
23 | DialogPortal.displayName = DialogPrimitive.Portal.displayName
24 |
25 | const DialogOverlay = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
39 |
40 | const DialogContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, children, ...props }, ref) => (
44 |
45 |
46 |
54 | {children}
55 |
56 |
57 | Close
58 |
59 |
60 |
61 | ))
62 | DialogContent.displayName = DialogPrimitive.Content.displayName
63 |
64 | const DialogHeader = ({
65 | className,
66 | ...props
67 | }: React.HTMLAttributes) => (
68 |
75 | )
76 | DialogHeader.displayName = 'DialogHeader'
77 |
78 | const DialogFooter = ({
79 | className,
80 | ...props
81 | }: React.HTMLAttributes) => (
82 |
89 | )
90 | DialogFooter.displayName = 'DialogFooter'
91 |
92 | const DialogTitle = React.forwardRef<
93 | React.ElementRef,
94 | React.ComponentPropsWithoutRef
95 | >(({ className, ...props }, ref) => (
96 |
104 | ))
105 | DialogTitle.displayName = DialogPrimitive.Title.displayName
106 |
107 | const DialogDescription = React.forwardRef<
108 | React.ElementRef,
109 | React.ComponentPropsWithoutRef
110 | >(({ className, ...props }, ref) => (
111 |
116 | ))
117 | DialogDescription.displayName = DialogPrimitive.Description.displayName
118 |
119 | export {
120 | Dialog,
121 | DialogTrigger,
122 | DialogContent,
123 | DialogHeader,
124 | DialogFooter,
125 | DialogTitle,
126 | DialogDescription
127 | }
128 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as SelectPrimitive from '@radix-ui/react-select'
5 |
6 | import { cn } from '@/lib/utils'
7 | import {
8 | IconArrowDown,
9 | IconCheck,
10 | IconChevronUpDown
11 | } from '@/components/ui/icons'
12 |
13 | const Select = SelectPrimitive.Root
14 |
15 | const SelectGroup = SelectPrimitive.Group
16 |
17 | const SelectValue = SelectPrimitive.Value
18 |
19 | const SelectTrigger = React.forwardRef<
20 | React.ElementRef,
21 | React.ComponentPropsWithoutRef
22 | >(({ className, children, ...props }, ref) => (
23 |
31 | {children}
32 |
33 |
34 |
35 |
36 | ))
37 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
38 |
39 | const SelectContent = React.forwardRef<
40 | React.ElementRef,
41 | React.ComponentPropsWithoutRef
42 | >(({ className, children, position = 'popper', ...props }, ref) => (
43 |
44 |
54 |
61 | {children}
62 |
63 |
64 |
65 | ))
66 | SelectContent.displayName = SelectPrimitive.Content.displayName
67 |
68 | const SelectLabel = React.forwardRef<
69 | React.ElementRef,
70 | React.ComponentPropsWithoutRef
71 | >(({ className, ...props }, ref) => (
72 |
77 | ))
78 | SelectLabel.displayName = SelectPrimitive.Label.displayName
79 |
80 | const SelectItem = React.forwardRef<
81 | React.ElementRef,
82 | React.ComponentPropsWithoutRef
83 | >(({ className, children, ...props }, ref) => (
84 |
92 |
93 |
94 |
95 |
96 |
97 | {children}
98 |
99 | ))
100 | SelectItem.displayName = SelectPrimitive.Item.displayName
101 |
102 | const SelectSeparator = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
113 |
114 | export {
115 | Select,
116 | SelectGroup,
117 | SelectValue,
118 | SelectTrigger,
119 | SelectContent,
120 | SelectLabel,
121 | SelectItem,
122 | SelectSeparator
123 | }
124 |
--------------------------------------------------------------------------------
/src/components/chat.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { useChat, type Message } from 'ai/react'
4 |
5 | import { cn } from '@/lib/utils'
6 | import { ChatList } from '@/components/chat-list'
7 | import { ChatPanel } from '@/components/chat-panel'
8 | import { EmptyScreen } from '@/components/empty-screen'
9 | import { ChatScrollAnchor } from '@/components/chat-scroll-anchor'
10 | import { useLocalStorage } from '@/lib/hooks/use-local-storage'
11 | import {
12 | Dialog,
13 | DialogContent,
14 | DialogDescription,
15 | DialogFooter,
16 | DialogHeader,
17 | DialogTitle
18 | } from '@/components/ui/dialog'
19 | import { useState } from 'react'
20 | import { Button } from './ui/button'
21 | import { Input } from './ui/input'
22 | import { toast } from 'react-hot-toast'
23 | import axios from 'axios'
24 |
25 | const IS_PREVIEW = process.env.VERCEL_ENV === 'preview'
26 | export interface ChatProps extends React.ComponentProps<'div'> {
27 | initialMessages?: Message[]
28 | id?: string
29 | user?: Object
30 | logout?: any
31 | }
32 |
33 | export function Chat({ id, user, logout, initialMessages, className }: ChatProps) {
34 | const [redactStatus, setRedactStatus] = useState(false);
35 | const [auditLogStatus, setAuditLogStatus] = useState(false);
36 | const [previewToken, setPreviewToken] = useLocalStorage(
37 | 'ai-token',
38 | null
39 | )
40 | const [previewTokenDialog, setPreviewTokenDialog] = useState(IS_PREVIEW)
41 | const [previewTokenInput, setPreviewTokenInput] = useState(previewToken ?? '')
42 | const { messages, append, reload, stop, isLoading, input, setInput } =
43 | useChat({
44 | initialMessages,
45 | id,
46 | body: {
47 | id,
48 | previewToken
49 | },
50 | onResponse(response) {
51 | if (response.status === 401) {
52 | toast.error(response.statusText)
53 | }
54 | },
55 | async onFinish(message) {
56 | if (auditLogStatus) {
57 | await axios.post('/api/audit-log', {
58 | 'user_id': 'openai',
59 | 'session_id': id,
60 | message: message.content,
61 | actor: message.role
62 | })
63 | }
64 | }
65 | })
66 | return (
67 | <>
68 |
69 | {messages.length ? (
70 | <>
71 |
72 |
73 | >
74 | ) : (
75 |
76 | )}
77 |
78 |
94 |
95 |
96 |
97 |
98 |
99 | Enter your OpenAI Key
100 |
101 | If you have not obtained your OpenAI API key, you can do so by{' '}
102 |
106 | signing up
107 | {' '}
108 | on the OpenAI website. This is only necessary for preview
109 | environments so that the open source community can test the app.
110 | The token will be saved to your browser's local storage under
111 | the name ai-token.
112 |
113 |
114 | setPreviewTokenInput(e.target.value)}
118 | />
119 |
120 | {
122 | setPreviewToken(previewTokenInput)
123 | setPreviewTokenDialog(false)
124 | }}
125 | >
126 | Save Token
127 |
128 |
129 |
130 |
131 | >
132 | )
133 | }
134 |
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const DropdownMenu = DropdownMenuPrimitive.Root
9 |
10 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
11 |
12 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
13 |
14 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
15 |
16 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
17 |
18 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
19 |
20 | const DropdownMenuSubContent = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, ...props }, ref) => (
24 |
32 | ))
33 | DropdownMenuSubContent.displayName =
34 | DropdownMenuPrimitive.SubContent.displayName
35 |
36 | const DropdownMenuContent = React.forwardRef<
37 | React.ElementRef,
38 | React.ComponentPropsWithoutRef
39 | >(({ className, sideOffset = 4, ...props }, ref) => (
40 |
41 |
50 |
51 | ))
52 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
53 |
54 | const DropdownMenuItem = React.forwardRef<
55 | React.ElementRef,
56 | React.ComponentPropsWithoutRef & {
57 | inset?: boolean
58 | }
59 | >(({ className, inset, ...props }, ref) => (
60 |
69 | ))
70 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
71 |
72 | const DropdownMenuLabel = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef & {
75 | inset?: boolean
76 | }
77 | >(({ className, inset, ...props }, ref) => (
78 |
87 | ))
88 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
89 |
90 | const DropdownMenuSeparator = React.forwardRef<
91 | React.ElementRef,
92 | React.ComponentPropsWithoutRef
93 | >(({ className, ...props }, ref) => (
94 |
99 | ))
100 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
101 |
102 | const DropdownMenuShortcut = ({
103 | className,
104 | ...props
105 | }: React.HTMLAttributes) => {
106 | return (
107 |
111 | )
112 | }
113 | DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
114 |
115 | export {
116 | DropdownMenu,
117 | DropdownMenuTrigger,
118 | DropdownMenuContent,
119 | DropdownMenuItem,
120 | DropdownMenuLabel,
121 | DropdownMenuSeparator,
122 | DropdownMenuShortcut,
123 | DropdownMenuGroup,
124 | DropdownMenuPortal,
125 | DropdownMenuSub,
126 | DropdownMenuSubContent,
127 | DropdownMenuRadioGroup
128 | }
129 |
--------------------------------------------------------------------------------
/src/components/ui/codeblock.tsx:
--------------------------------------------------------------------------------
1 | // Inspired by Chatbot-UI and modified to fit the needs of this project
2 | // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Markdown/CodeBlock.tsx
3 |
4 | 'use client'
5 |
6 | import { FC, memo } from 'react'
7 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
8 | import { coldarkDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'
9 |
10 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
11 | import { IconCheck, IconCopy, IconDownload } from '@/components/ui/icons'
12 | import { Button } from '@/components/ui/button'
13 |
14 | interface Props {
15 | language: string
16 | value: string
17 | }
18 |
19 | interface languageMap {
20 | [key: string]: string | undefined
21 | }
22 |
23 | export const programmingLanguages: languageMap = {
24 | javascript: '.js',
25 | python: '.py',
26 | java: '.java',
27 | c: '.c',
28 | cpp: '.cpp',
29 | 'c++': '.cpp',
30 | 'c#': '.cs',
31 | ruby: '.rb',
32 | php: '.php',
33 | swift: '.swift',
34 | 'objective-c': '.m',
35 | kotlin: '.kt',
36 | typescript: '.ts',
37 | go: '.go',
38 | perl: '.pl',
39 | rust: '.rs',
40 | scala: '.scala',
41 | haskell: '.hs',
42 | lua: '.lua',
43 | shell: '.sh',
44 | sql: '.sql',
45 | html: '.html',
46 | css: '.css'
47 | // add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
48 | }
49 |
50 | export const generateRandomString = (length: number, lowercase = false) => {
51 | const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789' // excluding similar looking characters like Z, 2, I, 1, O, 0
52 | let result = ''
53 | for (let i = 0; i < length; i++) {
54 | result += chars.charAt(Math.floor(Math.random() * chars.length))
55 | }
56 | return lowercase ? result.toLowerCase() : result
57 | }
58 |
59 | const CodeBlock: FC = memo(({ language, value }) => {
60 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
61 |
62 | const downloadAsFile = () => {
63 | if (typeof window === 'undefined') {
64 | return
65 | }
66 | const fileExtension = programmingLanguages[language] || '.file'
67 | const suggestedFileName = `file-${generateRandomString(
68 | 3,
69 | true
70 | )}${fileExtension}`
71 | const fileName = window.prompt('Enter file name', suggestedFileName)
72 |
73 | if (!fileName) {
74 | // User pressed cancel on prompt.
75 | return
76 | }
77 |
78 | const blob = new Blob([value], { type: 'text/plain' })
79 | const url = URL.createObjectURL(blob)
80 | const link = document.createElement('a')
81 | link.download = fileName
82 | link.href = url
83 | link.style.display = 'none'
84 | document.body.appendChild(link)
85 | link.click()
86 | document.body.removeChild(link)
87 | URL.revokeObjectURL(url)
88 | }
89 |
90 | const onCopy = () => {
91 | if (isCopied) return
92 | copyToClipboard(value)
93 | }
94 |
95 | return (
96 |
97 |
98 |
{language}
99 |
100 |
106 |
107 | Download
108 |
109 |
115 | {isCopied ? : }
116 | Copy code
117 |
118 |
119 |
120 |
138 | {value}
139 |
140 |
141 | )
142 | })
143 | CodeBlock.displayName = 'CodeBlock'
144 |
145 | export { CodeBlock }
146 |
--------------------------------------------------------------------------------
/src/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | // Inspired by react-hot-toast library
2 | import * as React from "react"
3 |
4 | import type {
5 | ToastActionElement,
6 | ToastProps,
7 | } from "@/components/ui/toast"
8 |
9 | const TOAST_LIMIT = 1
10 | const TOAST_REMOVE_DELAY = 1000000
11 |
12 | type ToasterToast = ToastProps & {
13 | id: string
14 | title?: React.ReactNode
15 | description?: React.ReactNode
16 | action?: ToastActionElement
17 | }
18 |
19 | const actionTypes = {
20 | ADD_TOAST: "ADD_TOAST",
21 | UPDATE_TOAST: "UPDATE_TOAST",
22 | DISMISS_TOAST: "DISMISS_TOAST",
23 | REMOVE_TOAST: "REMOVE_TOAST",
24 | } as const
25 |
26 | let count = 0
27 |
28 | function genId() {
29 | count = (count + 1) % Number.MAX_VALUE
30 | return count.toString()
31 | }
32 |
33 | type ActionType = typeof actionTypes
34 |
35 | type Action =
36 | | {
37 | type: ActionType["ADD_TOAST"]
38 | toast: ToasterToast
39 | }
40 | | {
41 | type: ActionType["UPDATE_TOAST"]
42 | toast: Partial
43 | }
44 | | {
45 | type: ActionType["DISMISS_TOAST"]
46 | toastId?: ToasterToast["id"]
47 | }
48 | | {
49 | type: ActionType["REMOVE_TOAST"]
50 | toastId?: ToasterToast["id"]
51 | }
52 |
53 | interface State {
54 | toasts: ToasterToast[]
55 | }
56 |
57 | const toastTimeouts = new Map>()
58 |
59 | const addToRemoveQueue = (toastId: string) => {
60 | if (toastTimeouts.has(toastId)) {
61 | return
62 | }
63 |
64 | const timeout = setTimeout(() => {
65 | toastTimeouts.delete(toastId)
66 | dispatch({
67 | type: "REMOVE_TOAST",
68 | toastId: toastId,
69 | })
70 | }, TOAST_REMOVE_DELAY)
71 |
72 | toastTimeouts.set(toastId, timeout)
73 | }
74 |
75 | export const reducer = (state: State, action: Action): State => {
76 | switch (action.type) {
77 | case "ADD_TOAST":
78 | return {
79 | ...state,
80 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
81 | }
82 |
83 | case "UPDATE_TOAST":
84 | return {
85 | ...state,
86 | toasts: state.toasts.map((t) =>
87 | t.id === action.toast.id ? { ...t, ...action.toast } : t
88 | ),
89 | }
90 |
91 | case "DISMISS_TOAST": {
92 | const { toastId } = action
93 |
94 | // ! Side effects ! - This could be extracted into a dismissToast() action,
95 | // but I'll keep it here for simplicity
96 | if (toastId) {
97 | addToRemoveQueue(toastId)
98 | } else {
99 | state.toasts.forEach((toast) => {
100 | addToRemoveQueue(toast.id)
101 | })
102 | }
103 |
104 | return {
105 | ...state,
106 | toasts: state.toasts.map((t) =>
107 | t.id === toastId || toastId === undefined
108 | ? {
109 | ...t,
110 | open: false,
111 | }
112 | : t
113 | ),
114 | }
115 | }
116 | case "REMOVE_TOAST":
117 | if (action.toastId === undefined) {
118 | return {
119 | ...state,
120 | toasts: [],
121 | }
122 | }
123 | return {
124 | ...state,
125 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
126 | }
127 | }
128 | }
129 |
130 | const listeners: Array<(state: State) => void> = []
131 |
132 | let memoryState: State = { toasts: [] }
133 |
134 | function dispatch(action: Action) {
135 | memoryState = reducer(memoryState, action)
136 | listeners.forEach((listener) => {
137 | listener(memoryState)
138 | })
139 | }
140 |
141 | type Toast = Omit
142 |
143 | function toast({ ...props }: Toast) {
144 | const id = genId()
145 |
146 | const update = (props: ToasterToast) =>
147 | dispatch({
148 | type: "UPDATE_TOAST",
149 | toast: { ...props, id },
150 | })
151 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
152 |
153 | dispatch({
154 | type: "ADD_TOAST",
155 | toast: {
156 | ...props,
157 | id,
158 | open: true,
159 | onOpenChange: (open) => {
160 | if (!open) dismiss()
161 | },
162 | },
163 | })
164 |
165 | return {
166 | id: id,
167 | dismiss,
168 | update,
169 | }
170 | }
171 |
172 | function useToast() {
173 | const [state, setState] = React.useState(memoryState)
174 |
175 | React.useEffect(() => {
176 | listeners.push(setState)
177 | return () => {
178 | const index = listeners.indexOf(setState)
179 | if (index > -1) {
180 | listeners.splice(index, 1)
181 | }
182 | }
183 | }, [state])
184 |
185 | return {
186 | ...state,
187 | toast,
188 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
189 | }
190 | }
191 |
192 | export { useToast, toast }
193 |
--------------------------------------------------------------------------------
/src/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
5 |
6 | import { cn } from '@/lib/utils'
7 | import { buttonVariants } from '@/components/ui/button'
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12 |
13 | const AlertDialogPortal = ({
14 | children,
15 | ...props
16 | }: AlertDialogPrimitive.AlertDialogPortalProps) => (
17 |
18 |
19 | {children}
20 |
21 |
22 | )
23 | AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName
24 |
25 | const AlertDialogOverlay = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, children, ...props }, ref) => (
29 |
37 | ))
38 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
39 |
40 | const AlertDialogContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
45 |
46 |
54 |
55 | ))
56 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
57 |
58 | const AlertDialogHeader = ({
59 | className,
60 | ...props
61 | }: React.HTMLAttributes) => (
62 |
69 | )
70 | AlertDialogHeader.displayName = 'AlertDialogHeader'
71 |
72 | const AlertDialogFooter = ({
73 | className,
74 | ...props
75 | }: React.HTMLAttributes) => (
76 |
83 | )
84 | AlertDialogFooter.displayName = 'AlertDialogFooter'
85 |
86 | const AlertDialogTitle = React.forwardRef<
87 | React.ElementRef,
88 | React.ComponentPropsWithoutRef
89 | >(({ className, ...props }, ref) => (
90 |
95 | ))
96 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
97 |
98 | const AlertDialogDescription = React.forwardRef<
99 | React.ElementRef,
100 | React.ComponentPropsWithoutRef
101 | >(({ className, ...props }, ref) => (
102 |
107 | ))
108 | AlertDialogDescription.displayName =
109 | AlertDialogPrimitive.Description.displayName
110 |
111 | const AlertDialogAction = React.forwardRef<
112 | React.ElementRef,
113 | React.ComponentPropsWithoutRef
114 | >(({ className, ...props }, ref) => (
115 |
120 | ))
121 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
122 |
123 | const AlertDialogCancel = React.forwardRef<
124 | React.ElementRef,
125 | React.ComponentPropsWithoutRef
126 | >(({ className, ...props }, ref) => (
127 |
136 | ))
137 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
138 |
139 | export {
140 | AlertDialog,
141 | AlertDialogTrigger,
142 | AlertDialogContent,
143 | AlertDialogHeader,
144 | AlertDialogFooter,
145 | AlertDialogTitle,
146 | AlertDialogDescription,
147 | AlertDialogAction,
148 | AlertDialogCancel
149 | }
150 |
--------------------------------------------------------------------------------
/src/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Cross2Icon } from "@radix-ui/react-icons"
3 | import * as ToastPrimitives from "@radix-ui/react-toast"
4 | import { cva, type VariantProps } from "class-variance-authority"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ToastProvider = ToastPrimitives.Provider
9 |
10 | const ToastViewport = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
24 |
25 | const toastVariants = cva(
26 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border border-gray-200 p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full dark:border-gray-800",
27 | {
28 | variants: {
29 | variant: {
30 | default: "border bg-white text-gray-950 dark:bg-gray-950 dark:text-gray-50",
31 | destructive:
32 | "destructive group border-red-500 bg-red-500 text-gray-50 dark:border-red-900 dark:bg-red-900 dark:text-gray-50",
33 | },
34 | },
35 | defaultVariants: {
36 | variant: "default",
37 | },
38 | }
39 | )
40 |
41 | const Toast = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef &
44 | VariantProps
45 | >(({ className, variant, ...props }, ref) => {
46 | return (
47 |
52 | )
53 | })
54 | Toast.displayName = ToastPrimitives.Root.displayName
55 |
56 | const ToastAction = React.forwardRef<
57 | React.ElementRef,
58 | React.ComponentPropsWithoutRef
59 | >(({ className, ...props }, ref) => (
60 |
68 | ))
69 | ToastAction.displayName = ToastPrimitives.Action.displayName
70 |
71 | const ToastClose = React.forwardRef<
72 | React.ElementRef,
73 | React.ComponentPropsWithoutRef
74 | >(({ className, ...props }, ref) => (
75 |
84 |
85 |
86 | ))
87 | ToastClose.displayName = ToastPrimitives.Close.displayName
88 |
89 | const ToastTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
98 | ))
99 | ToastTitle.displayName = ToastPrimitives.Title.displayName
100 |
101 | const ToastDescription = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | ToastDescription.displayName = ToastPrimitives.Description.displayName
112 |
113 | type ToastProps = React.ComponentPropsWithoutRef
114 |
115 | type ToastActionElement = React.ReactElement
116 |
117 | export {
118 | type ToastProps,
119 | type ToastActionElement,
120 | ToastProvider,
121 | ToastViewport,
122 | Toast,
123 | ToastTitle,
124 | ToastDescription,
125 | ToastClose,
126 | ToastAction,
127 | }
128 |
--------------------------------------------------------------------------------
/src/components/chat-panel.tsx:
--------------------------------------------------------------------------------
1 | import { type UseChatHelpers } from 'ai/react'
2 |
3 | import { Button } from '@/components/ui/button'
4 | import { PromptForm } from '@/components/prompt-form'
5 | import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom'
6 | import { IconRefresh, IconStop } from '@/components/ui/icons'
7 | import { FooterText } from '@/components/footer'
8 | import axios from 'axios';
9 | import { Switch } from './ui/switch'
10 | import { Label } from './ui/label'
11 | import { useState } from 'react'
12 | import { useToast } from './ui/use-toast'
13 |
14 | export interface ChatPanelProps
15 | extends Pick<
16 | UseChatHelpers,
17 | | 'append'
18 | | 'isLoading'
19 | | 'reload'
20 | | 'messages'
21 | | 'stop'
22 | | 'input'
23 | | 'setInput'
24 | > {
25 | id?: string
26 | user?: Object
27 | logout?: any
28 | redactStatus: boolean
29 | setRedactStatus: any
30 | auditLogStatus: boolean
31 | setAuditLogStatus: any
32 | }
33 |
34 | export function ChatPanel({
35 | id,
36 | user,
37 | isLoading,
38 | stop,
39 | append,
40 | reload,
41 | input,
42 | setInput,
43 | messages,
44 | logout,
45 | redactStatus,
46 | setRedactStatus,
47 | auditLogStatus,
48 | setAuditLogStatus
49 | }: ChatPanelProps) {
50 | const { toast } = useToast()
51 |
52 | return (
53 |
54 |
55 |
56 |
57 | {isLoading ? (
58 | stop()}
61 | className="bg-background"
62 | >
63 |
64 | Stop generating
65 |
66 | ) : (
67 | messages?.length > 0 && (
68 | reload()}
71 | className="bg-background"
72 | >
73 |
74 | Regenerate response
75 |
76 | )
77 | )}
78 |
79 |
80 |
{
82 | console.log(auditLogStatus, redactStatus)
83 | if(auditLogStatus) {
84 | await axios.post('/api/audit-log', {
85 | message: value,
86 | user_id: (user as any).email as string,
87 | session_id: id,
88 | actor: 'user'
89 | }).catch(error => {
90 | if(error.response.status === 403) {
91 | toast({
92 | title: "Error: No PANGEA_TOKEN",
93 | description: "Please set PANGEA_TOKEN in server environment",
94 | color: "#E54D2E"
95 | })
96 | } else {
97 | toast({
98 | title: "Error: Unable to send Pangea Requests",
99 | description: "Please reach out on the Pangea.cloud slack if the error persists"
100 | })
101 | }
102 | })
103 | }
104 |
105 | let outputMessage = "";
106 | if(redactStatus) {
107 | const redactedText = await axios.post('/api/redact', {
108 | message: value
109 | }).catch(error => {
110 | if(error.response.status === 403) {
111 | toast({
112 | title: "Error: No PANGEA_TOKEN",
113 | description: "Please set PANGEA_TOKEN in server environment",
114 | color: "#E54D2E"
115 | })
116 | } else {
117 | toast({
118 | title: "Error: Unable to send Pangea Requests",
119 | description: "Please reach out on the Pangea.cloud slack if the error persists"
120 | })
121 | }
122 | })
123 |
124 | outputMessage = redactedText?.data.message;
125 |
126 | } else {
127 | outputMessage = value;
128 | }
129 |
130 | await append({
131 | id,
132 | content: outputMessage,
133 | role: 'user'
134 | })
135 | }}
136 | input={input}
137 | setInput={setInput}
138 | isLoading={isLoading}
139 | />
140 |
141 |
142 | {
143 | setRedactStatus(!redactStatus)
144 | }} />
145 | Redact
146 |
147 |
148 |
149 | {
150 | setAuditLogStatus(!auditLogStatus)
151 | }} />
152 | Audit Log
153 |
154 |
155 |
156 | {
157 | await logout();
158 | window.location.reload();
159 | }}>Logout
160 |
161 |
162 |
163 |
164 |
165 |
166 | )
167 | }
168 |
--------------------------------------------------------------------------------
/src/components/empty-screen.tsx:
--------------------------------------------------------------------------------
1 | import { UseChatHelpers } from 'ai/react'
2 |
3 | import { Button } from '@/components/ui/button'
4 | import { ExternalLink } from '@/components/external-link'
5 | import { IconArrowRight } from '@/components/ui/icons'
6 |
7 | // Note - All example messages below have been generated by ChatGPT as synthetic data. No real-world data has been used.
8 | const exampleMessages = [
9 | {
10 | heading: 'Securely sumarize a patient record in a hospital',
11 | message: `Summarize the following patient records into a paragraph to suggest which type of doctor to forward the patient to.
12 |
13 | Patient Record:
14 |
15 | \`\`\`
16 | First Name: John
17 | Last Name: Doe
18 | Gender: Male
19 | Date of Birth: 1982-08-07
20 | Nationality: American
21 | Residential Address: 5161 Sunset Lane, Peoria, IL 61616
22 | Phone Number: (309) 230-4795
23 | Email Address: johndoe82@gmail.com
24 | Occupation: Accountant
25 | Insurance Provider: Blue Cross Blue Shield
26 | Insurance Policy Number: BCBS4207896
27 | Marital Status: Married
28 | Emergency Contact: Jane Doe
29 | Emergency Contact Relationship: Wife
30 | Emergency Contact Number: (309) 230-4798
31 |
32 | Medical History:
33 | - Asthma (Chronic): Diagnosed in 1998
34 | - High Blood Pressure: Diagnosed in 2008
35 | - Broken Arm (Right): Healed in 2010
36 |
37 | Current Medications:
38 | - Ventolin (Asthma) - 2 puffs twice a day
39 | - Lisinopril (Blood Pressure) - 1 tablet daily
40 |
41 | Last Visit Date: 2021-11-12
42 | Visit Reason: Regular Check-up
43 | Doctor's Note: Patient's blood pressure higher than usual. Advised to decrease salt intake and monitor blood pressure at home.
44 | \`\`\`
45 | `
46 | },
47 | {
48 | heading: 'Ask it to lookup a revenue stats from credit card transaction history',
49 | message: `
50 | What is our total revenue for 2024
51 |
52 | \`\`\`
53 | 1. Transaction ID: 001
54 | User Email: testuser1@gmail.com
55 | User Phone: +1-213-555-0123
56 | Credit Card Type: Visa
57 | Credit Card Number: 4123456789101112
58 | Credit Card Expiry: 02/24
59 | Amount: $100.00
60 | Transaction Status: Success
61 | Stripe Payment ID: ch_1JhZ0VGPR4hFd2H65ed3fg87
62 |
63 | 2. Transaction ID: 002
64 | User Email: testuser2@hotmail.com
65 | User Phone: +1-213-555-0456
66 | Credit Card Type: MasterCard
67 | Credit Card Number: 5123456789101113
68 | Credit Card Expiry: 01/23
69 | Amount: $250.00
70 | Transaction Status: Failure - Insufficient funds
71 | Stripe Payment ID: ch_1JhXVPGR4hFd2H624hh7kl89
72 |
73 | 3. Transaction ID: 003
74 | User Email: jonnycache@gmail.com
75 | User Phone: +1-222-555-1111
76 | Credit Card Type: Visa
77 | Credit Card Number: 4123456789101112
78 | Credit Card Expiry: 05/24
79 | Amount: $200.00
80 | Transaction Status: Success
81 | Stripe Payment ID: ch_1JhZ0VGPR4DDsdDSeddsfj3
82 |
83 | 4. Transaction ID: 004
84 | User Email: bagels@hotmail.com
85 | User Phone: +1-213-555-0456
86 | Credit Card Type: MasterCard
87 | Credit Card Number: 5123456789101113
88 | Credit Card Expiry: 03/24
89 | Amount: $250.00
90 | Transaction Status: Success
91 | Stripe Payment ID: ch_1JhXVPGR4hFd2H624hh7kl89
92 | \`\`\`
93 | `
94 | },
95 | {
96 | heading: 'Analyze a dataset with sensitive info securely',
97 | message: `Find the favorite ice cream flavor in the dataset.
98 | \`\`\`
99 | [
100 | {
101 | "Name": "John Doe",
102 | "Email": "johndoe@example.com",
103 | "Phone": "+1-234-567-8901",
104 | "FavoriteIceCream": "Vanilla"
105 | },
106 | {
107 | "Name": "Jane Smith",
108 | "Email": "janesmith@example.com",
109 | "Phone": "+1-234-567-8902",
110 | "FavoriteIceCream": "Coffee with Hazelnut"
111 | },
112 | {
113 | "Name": "Alice Johnson",
114 | "Email": "alicejohnson@example.com",
115 | "Phone": "+1-234-567-8903",
116 | "FavoriteIceCream": "Chocolate"
117 | },
118 | {
119 | "Name": "Bob Miller",
120 | "Email": "bobmiller@example.com",
121 | "Phone": "+1-234-567-8904",
122 | "FavoriteIceCream": "Chocolate"
123 | },
124 | {
125 | "Name": "Charlie Brown",
126 | "Email": "charliebrown@example.com",
127 | "Phone": "+1-234-567-8905",
128 | "FavoriteIceCream": "Chocolate"
129 | }
130 | ]
131 | \`\`\`
132 | `
133 | }
134 | ]
135 |
136 | export function EmptyScreen({ setInput }: Pick) {
137 | return (
138 |
139 |
140 |
141 | Welcome to Secure ChatGPT!
142 |
143 |
144 | This is an open source AI chatbot app template built with{' '}
145 | Next.js and{' '}
146 |
147 | Pangea
148 |
149 | .
150 |
151 |
152 | You can safely communicate securely with the chat bot:
153 |
154 |
155 | {exampleMessages.map((message, index) => (
156 | setInput(message.message)}
161 | >
162 |
163 | {message.heading}
164 |
165 | ))}
166 |
167 |
168 |
169 | )
170 | }
171 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Secure ChatGPT
2 | ## Overview
3 | This app simulates the chatGPT experience with additional security features. It uses Pangea's robust set of security APIs such as:
4 | - [PII redaction](https://pangea.cloud/services/redact?utm_source=github&utm_medium=secure-chagpt) of the user prompts
5 | - [Audit logging](https://pangea.cloud/services/secure-audit-log?utm_source=github&utm_medium=secure-chagpt) the user prompts in their redacted form
6 | - [Authentication](https://pangea.cloud/services/authn/?utm_source=github&utm_medium=secure-chagpt) to access the ChatGPT dashboard to prevent malicious actors
7 |
8 | We also use OpenAI's advanced ChatGPT API to create a more secure version of the familiar chatGPT experience.
9 |
10 | This codebase was built using [NextJS's ChatBot template](https://github.com/vercel/ai-chatbot?ref=pangea.cloud), [Pangea's APIs](https://pangea.cloud/?utm_source=github&utm_medium=secure-chagpt), and [Open AI's GPT API](https://openai.com/product?ref=pangea.cloud).
11 |
12 | ## Getting Started
13 |
14 | * [Pre Reqs Check](#pre-reqs-check)
15 | * [OpenAI API Key](#openai-api-key)
16 | * [Enable Pangea Services](#enable-pangea-services)
17 | * [First Run](#first-run)
18 | * [Migrate Secrets to Pangea CLI](#migrate-secrets-to-pangea-cli)
19 | * [Learn More](#learn-more)
20 | * [Deploy on Vercel](#deploy-on-vercel)
21 |
22 |
23 | ## Pre Reqs Check
24 | In order to run this application you are going to need:
25 | - Node
26 | - OpenAI
27 | - Pangea Account
28 |
29 | ## OpenAI API Key
30 | If you are comfortable using a credit card, follow these instructions below.
31 |
32 | 1. Create an account to sign in to [OpenAI](https://openai.com)
33 |
34 |
35 | 2. Once signed in, [this link](https://platform.openai.com/account/api-keys) will begin the flow to create an API Token.
36 |
37 | 3. Name your token.
38 | 4. Copy the API Key and save it somewhere. We will not be able to access it again.
39 |
40 |
41 | ## Enable Pangea Services
42 | 1. Create and sign into your [Pangea account](https://l.pangea.cloud/SecurechatgptRepo)
43 |
44 |
45 | 2. Once you land on the Pangea User Console, You can see AuthN, Secure Audit Log, Redact, and Vault on the left.
46 |
47 | 3. Select **AuthN** to enable and begin the token creation process. While creating the token, you can enable it for all the services we are going to require for this application: AuthN, Redact, Secure Audit Log, and Vault.
48 |
49 |
50 | 4. Landing on the **AuthN Service Overview** page you'll see all the token information you will need from Pangea to run the application. Copy these values into a note pad or keep this page open.
51 |
52 | 5. Go to the Redirects tab and add the necessary redirect. If running this in codespace, it's the url of your codespace running instance.
53 | If running this app locally, add http://localhost:3000 to the redirect list. This is also go to a good time to go to General Settings and decide what methods of Login or MFA you need for your application. On first run it is recommended to do this in a bare bones way.
54 |
55 |
56 |
57 | > NOTE: By going to **Customize > View project branding**, you'll be able to customize your login page
58 |
59 | 6. Go to back to the **Main Menu** and then navigate to **Redact > Rulesets**. This is where you will be able to configure what gets redacted and how. For the demo, it's recommended that we enable redaction for:
60 | - PII: email address and phone number
61 | - US Identification Numbers: US Social Security Number
62 |
63 | 7. Enable **Secure Audit Log** with the following configuration
64 | - session_id: Short or Long String
65 | - timestamp: Timestamp
66 | - actor: Short or Long String
67 | - user_id: Long String
68 | - message: Long String
69 |
70 | > NOTE: You find find this configured in src>lib>auditLog.ts
71 |
72 | ## First Run
73 |
74 | 1. Copy the appropriate values into the .env file.
75 | - OPEN AI API Token
76 | - Pangea Service Token
77 | - Pangea Domain
78 | - Pangea AuthN Client Token
79 | - Pangea Hosted Login URL
80 |
81 | 2. Let's run the development server:
82 |
83 | ```bash
84 | npm run dev
85 | # or
86 | yarn dev
87 | # or
88 | pnpm dev
89 | ```
90 |
91 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
92 |
93 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
94 |
95 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
96 |
97 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
98 |
99 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
100 |
101 | ## Migrate Secrets to Pangea CLI
102 | If you wanted to get rid of the .env code, we can leverage the Pangea CLI to migrate the secrets to the Pangea Vault. Then when we run the application, the we will pull down the appropriate secrets from the Pangea Vault. Remember to **enable Vault** in your Pangea User Console.
103 |
104 | For the full documentation please go the the [Pangea CLI github](https://github.com/pangeacyber/pangea-cli)
105 |
106 | Login to the Pangea CLI
107 |
108 | ```
109 | pangea login –no-browser
110 | ```
111 |
112 | Next we create a folder within the Vault to hold our application's secrets
113 | ```
114 | pangea create
115 | ```
116 |
117 | Then we migrate the secrets in our .env file to the new folder in the Pangea Vault
118 | ```
119 | pangea migrate -f .env
120 | ```
121 |
122 | Now in order to run the application, we add 'pangea run -c' before the command in order to pull down the secret values.
123 | ```
124 | pangea run -c npm run dev
125 | ```
126 |
127 |
128 | ## Learn More
129 |
130 | To learn more about Next.js, take a look at the following resources:
131 |
132 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
133 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
134 |
135 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
136 |
137 | ## Deploy on Vercel
138 |
139 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
140 |
141 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
142 |
--------------------------------------------------------------------------------
/src/components/sidebar-actions.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { useRouter } from 'next/navigation'
5 | import { toast } from 'react-hot-toast'
6 |
7 | import { type Chat, ServerActionResult } from '@/lib/types'
8 | // import { cn, formatDate } from '@/lib/utils'
9 | import {
10 | AlertDialog,
11 | AlertDialogAction,
12 | AlertDialogCancel,
13 | AlertDialogContent,
14 | AlertDialogDescription,
15 | AlertDialogFooter,
16 | AlertDialogHeader,
17 | AlertDialogTitle
18 | } from '@/components/ui/alert-dialog'
19 | import { Button } from '@/components/ui/button'
20 | import {
21 | Dialog,
22 | DialogContent,
23 | DialogDescription,
24 | DialogFooter,
25 | DialogHeader,
26 | DialogTitle
27 | } from '@/components/ui/dialog'
28 | import {
29 | IconShare,
30 | IconSpinner,
31 | IconTrash,
32 | IconUsers
33 | } from '@/components/ui/icons'
34 | import Link from 'next/link'
35 | import { badgeVariants } from '@/components/ui/badge'
36 | import {
37 | Tooltip,
38 | TooltipContent,
39 | TooltipTrigger
40 | } from '@/components/ui/tooltip'
41 |
42 | interface SidebarActionsProps {
43 | chat: Chat
44 | removeChat: (args: { id: string; path: string }) => ServerActionResult
45 | shareChat: (chat: Chat) => ServerActionResult
46 | }
47 |
48 | export function SidebarActions({
49 | chat,
50 | removeChat,
51 | shareChat
52 | }: SidebarActionsProps) {
53 | const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false)
54 | const [shareDialogOpen, setShareDialogOpen] = React.useState(false)
55 | const [isRemovePending, startRemoveTransition] = React.useTransition()
56 | const [isSharePending, startShareTransition] = React.useTransition()
57 | const router = useRouter()
58 |
59 | const copyShareLink = React.useCallback(async (chat: Chat) => {
60 | if (!chat.sharePath) {
61 | return toast.error('Could not copy share link to clipboard')
62 | }
63 |
64 | const url = new URL(window.location.href)
65 | url.pathname = chat.sharePath
66 | navigator.clipboard.writeText(url.toString())
67 | setShareDialogOpen(false)
68 | toast.success('Share link copied to clipboard', {
69 | style: {
70 | borderRadius: '10px',
71 | background: '#333',
72 | color: '#fff',
73 | fontSize: '14px'
74 | },
75 | iconTheme: {
76 | primary: 'white',
77 | secondary: 'black'
78 | }
79 | })
80 | }, [])
81 |
82 | return (
83 | <>
84 |
85 |
86 |
87 | setShareDialogOpen(true)}
91 | >
92 |
93 | Share
94 |
95 |
96 | Share chat
97 |
98 |
99 |
100 | setDeleteDialogOpen(true)}
105 | >
106 |
107 | Delete
108 |
109 |
110 | Delete chat
111 |
112 |
113 |
114 |
115 |
116 | Share link to chat
117 |
118 | Anyone with the URL will be able to view the shared chat.
119 |
120 |
121 |
122 |
{chat.title}
123 |
124 | {/* {formatDate(chat.createdAt)} · {chat.messages.length} messages */}
125 |
126 |
127 |
128 | {chat.sharePath && (
129 |
137 |
138 | {chat.sharePath}
139 |
140 | )}
141 | {
144 | startShareTransition(async () => {
145 | if (chat.sharePath) {
146 | await new Promise(resolve => setTimeout(resolve, 500))
147 | copyShareLink(chat)
148 | return
149 | }
150 |
151 | const result = await shareChat(chat)
152 |
153 | if (result && 'error' in result) {
154 | toast.error(result.error)
155 | return
156 | }
157 |
158 | copyShareLink(result)
159 | })
160 | }}
161 | >
162 | {isSharePending ? (
163 | <>
164 |
165 | Copying...
166 | >
167 | ) : (
168 | <>Copy link>
169 | )}
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 | Are you absolutely sure?
178 |
179 | This will permanently delete your chat message and remove your
180 | data from our servers.
181 |
182 |
183 |
184 |
185 | Cancel
186 |
187 | {
190 | event.preventDefault()
191 | startRemoveTransition(async () => {
192 | const result = await removeChat({
193 | id: chat.id,
194 | path: chat.path
195 | })
196 |
197 | if (result && 'error' in result) {
198 | toast.error(result.error)
199 | return
200 | }
201 |
202 | setDeleteDialogOpen(false)
203 | router.refresh()
204 | router.push('/')
205 | toast.success('Chat deleted')
206 | })
207 | }}
208 | >
209 | {isRemovePending && }
210 | Delete
211 |
212 |
213 |
214 |
215 | >
216 | )
217 | }
218 |
--------------------------------------------------------------------------------
/src/components/ui/icons.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 |
5 | import { cn } from '@/lib/utils'
6 |
7 | function IconNextChat({
8 | className,
9 | inverted,
10 | ...props
11 | }: React.ComponentProps<'svg'> & { inverted?: boolean }) {
12 | const id = React.useId()
13 |
14 | return (
15 |
22 |
23 |
31 |
32 |
37 |
38 |
46 |
47 |
52 |
53 |
54 |
62 |
71 |
72 |
73 |
74 |
75 |
79 |
86 |
87 |
88 | )
89 | }
90 |
91 | function IconPangeaOpenAI({ className, ...props }: React.ComponentProps<'svg'>) {
92 | return (
93 |
96 | )
97 | }
98 |
99 | function IconOpenAI({ className, ...props }: React.ComponentProps<'svg'>) {
100 | return (
101 |
109 | OpenAI icon
110 |
111 |
112 | )
113 | }
114 |
115 | function IconVercel({ className, ...props }: React.ComponentProps<'svg'>) {
116 | return (
117 |
124 |
128 |
129 | )
130 | }
131 |
132 | function IconGitHub({ className, ...props }: React.ComponentProps<'svg'>) {
133 | return (
134 |
142 | GitHub
143 |
144 |
145 | )
146 | }
147 |
148 | function IconSeparator({ className, ...props }: React.ComponentProps<'svg'>) {
149 | return (
150 |
162 |
163 |
164 | )
165 | }
166 |
167 | function IconArrowDown({ className, ...props }: React.ComponentProps<'svg'>) {
168 | return (
169 |
176 |
177 |
178 | )
179 | }
180 |
181 | function IconArrowRight({ className, ...props }: React.ComponentProps<'svg'>) {
182 | return (
183 |
190 |
191 |
192 | )
193 | }
194 |
195 | function IconUser({ className, ...props }: React.ComponentProps<'svg'>) {
196 | return (
197 |
204 |
205 |
206 | )
207 | }
208 |
209 | function IconPlus({ className, ...props }: React.ComponentProps<'svg'>) {
210 | return (
211 |
218 |
219 |
220 | )
221 | }
222 |
223 | function IconArrowElbow({ className, ...props }: React.ComponentProps<'svg'>) {
224 | return (
225 |
232 |
233 |
234 | )
235 | }
236 |
237 | function IconSpinner({ className, ...props }: React.ComponentProps<'svg'>) {
238 | return (
239 |
246 |
247 |
248 | )
249 | }
250 |
251 | function IconMessage({ className, ...props }: React.ComponentProps<'svg'>) {
252 | return (
253 |
260 |
261 |
262 | )
263 | }
264 |
265 | function IconTrash({ className, ...props }: React.ComponentProps<'svg'>) {
266 | return (
267 |
274 |
275 |
276 | )
277 | }
278 |
279 | function IconRefresh({ className, ...props }: React.ComponentProps<'svg'>) {
280 | return (
281 |
288 |
289 |
290 | )
291 | }
292 |
293 | function IconStop({ className, ...props }: React.ComponentProps<'svg'>) {
294 | return (
295 |
302 |
303 |
304 | )
305 | }
306 |
307 | function IconSidebar({ className, ...props }: React.ComponentProps<'svg'>) {
308 | return (
309 |
316 |
317 |
318 | )
319 | }
320 |
321 | function IconMoon({ className, ...props }: React.ComponentProps<'svg'>) {
322 | return (
323 |
330 |
331 |
332 | )
333 | }
334 |
335 | function IconSun({ className, ...props }: React.ComponentProps<'svg'>) {
336 | return (
337 |
344 |
345 |
346 | )
347 | }
348 |
349 | function IconCopy({ className, ...props }: React.ComponentProps<'svg'>) {
350 | return (
351 |
358 |
359 |
360 | )
361 | }
362 |
363 | function IconCheck({ className, ...props }: React.ComponentProps<'svg'>) {
364 | return (
365 |
372 |
373 |
374 | )
375 | }
376 |
377 | function IconDownload({ className, ...props }: React.ComponentProps<'svg'>) {
378 | return (
379 |
386 |
387 |
388 | )
389 | }
390 |
391 | function IconClose({ className, ...props }: React.ComponentProps<'svg'>) {
392 | return (
393 |
400 |
401 |
402 | )
403 | }
404 |
405 | function IconEdit({ className, ...props }: React.ComponentProps<'svg'>) {
406 | return (
407 |
416 |
421 |
422 | )
423 | }
424 |
425 | function IconShare({ className, ...props }: React.ComponentProps<'svg'>) {
426 | return (
427 |
434 |
435 |
436 | )
437 | }
438 |
439 | function IconUsers({ className, ...props }: React.ComponentProps<'svg'>) {
440 | return (
441 |
448 |
449 |
450 | )
451 | }
452 |
453 | function IconExternalLink({
454 | className,
455 | ...props
456 | }: React.ComponentProps<'svg'>) {
457 | return (
458 |
465 |
466 |
467 | )
468 | }
469 |
470 | function IconChevronUpDown({
471 | className,
472 | ...props
473 | }: React.ComponentProps<'svg'>) {
474 | return (
475 |
482 |
483 |
484 | )
485 | }
486 |
487 | export {
488 | IconEdit,
489 | IconNextChat,
490 | IconOpenAI,
491 | IconVercel,
492 | IconGitHub,
493 | IconSeparator,
494 | IconArrowDown,
495 | IconArrowRight,
496 | IconPangeaOpenAI,
497 | IconUser,
498 | IconPlus,
499 | IconArrowElbow,
500 | IconSpinner,
501 | IconMessage,
502 | IconTrash,
503 | IconRefresh,
504 | IconStop,
505 | IconSidebar,
506 | IconMoon,
507 | IconSun,
508 | IconCopy,
509 | IconCheck,
510 | IconDownload,
511 | IconClose,
512 | IconShare,
513 | IconUsers,
514 | IconExternalLink,
515 | IconChevronUpDown
516 | }
517 |
--------------------------------------------------------------------------------
/public/pangea-openai.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------