├── .cursor └── rules │ └── convex_rules.mdc ├── .env.local.example ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── 1_bug_report.yml │ └── 2_feature_proposal.yml └── dependabot.yml ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── pre-push ├── .nvmrc ├── CONTRIBUTING.md ├── README.md ├── app ├── ConvexClientProvider.tsx ├── api │ ├── agent │ │ └── route.ts │ ├── chat │ │ ├── route.ts │ │ ├── schema.ts │ │ └── transcriptions │ │ │ └── route.ts │ ├── retrieval │ │ └── process │ │ │ ├── docx │ │ │ └── route.ts │ │ │ └── route.ts │ ├── stripe │ │ ├── restore │ │ │ └── route.ts │ │ └── webhook │ │ │ └── route.ts │ ├── subscription │ │ └── send-invite │ │ │ └── route.ts │ └── tasks │ │ └── route.ts ├── auth │ └── callback │ │ └── route.ts ├── c │ ├── [chatid] │ │ ├── layout.tsx │ │ └── page.tsx │ ├── layout.tsx │ └── page.tsx ├── chat │ └── page.tsx ├── download │ └── page.tsx ├── global-alert-dialog.tsx ├── globals.css ├── layout.tsx ├── loading.tsx ├── login │ ├── form.tsx │ ├── page.tsx │ ├── password │ │ └── page.tsx │ └── verify │ │ ├── mfa-verification.tsx │ │ └── page.tsx ├── page.tsx ├── posthog.js ├── privacy-policy │ └── page.tsx ├── providers.tsx ├── setup │ └── page.tsx ├── team │ └── new-team │ │ └── page.tsx ├── terms │ └── page.tsx └── upgrade │ └── page.tsx ├── biome.jsonc ├── commitlint.config.js ├── components.json ├── components ├── chat │ ├── chat-continue-button.tsx │ ├── chat-file-item.tsx │ ├── chat-files-display.tsx │ ├── chat-help.tsx │ ├── chat-helpers │ │ ├── create-messages.ts │ │ ├── create-temp-messages.ts │ │ ├── drag.ts │ │ ├── file-constants.ts │ │ ├── file-upload.ts │ │ ├── index.ts │ │ ├── stream-processor.ts │ │ └── validation.ts │ ├── chat-hooks │ │ ├── retrieval-logic.tsx │ │ ├── use-agent-sidebar.tsx │ │ ├── use-chat-handler.tsx │ │ ├── use-key-handler.tsx │ │ ├── use-message-handler.tsx │ │ ├── use-prompt-and-command.tsx │ │ ├── use-scroll.tsx │ │ ├── use-select-file-handler.tsx │ │ └── use-voice-recorder.ts │ ├── chat-input.tsx │ ├── chat-messages.tsx │ ├── chat-mic-button.tsx │ ├── chat-plugin-info.tsx │ ├── chat-retrieval-settings.tsx │ ├── chat-scroll-buttons.tsx │ ├── chat-secondary-buttons.tsx │ ├── chat-send-button.tsx │ ├── chat-settings.tsx │ ├── chat-share-button.tsx │ ├── chat-starters.tsx │ ├── chat-tools │ │ ├── tool-options.tsx │ │ └── upgrade-modal.tsx │ ├── chat-ui.tsx │ ├── dialog-portal.tsx │ ├── global-delete-chat-dialog.tsx │ ├── keyboard-shortcuts-popup.tsx │ ├── shared-message.tsx │ ├── temporary-chat-info.tsx │ └── temporary-chat-toggle.tsx ├── icons │ ├── google-icon.tsx │ ├── microsoft-icon.tsx │ ├── pentestgpt-svg.tsx │ └── pentestgpt-text-svg.tsx ├── image │ └── image-with-preview.tsx ├── messages │ ├── agent-status.tsx │ ├── citation-display.tsx │ ├── files-modal.tsx │ ├── loading-states.tsx │ ├── message-actions.tsx │ ├── message-attachments.tsx │ ├── message-citations.tsx │ ├── message-codeblock.tsx │ ├── message-detailed-feedback.tsx │ ├── message-markdown.tsx │ ├── message-quick-feedback.tsx │ ├── message-status.tsx │ ├── message-thinking.tsx │ ├── message-type-solver.tsx │ ├── message.tsx │ ├── reasoning-markdown.tsx │ └── terminal-messages │ │ ├── agent-sidebar.tsx │ │ ├── ask-terminal-command-block.tsx │ │ ├── content-parser.ts │ │ ├── file-content-block.tsx │ │ ├── info-search-web-block.tsx │ │ ├── message-terminal-block.tsx │ │ ├── message-terminal.tsx │ │ ├── shell-wait-block.tsx │ │ ├── show-more-button.tsx │ │ ├── terminal-block.tsx │ │ ├── types.ts │ │ └── use-auto-run-preference.ts ├── models │ ├── model-icon.tsx │ ├── model-option.tsx │ └── model-select.tsx ├── sidebar │ ├── items │ │ └── chat │ │ │ ├── chat-item.tsx │ │ │ ├── delete-chat.tsx │ │ │ └── update-chat.tsx │ ├── sidebar-content.tsx │ ├── sidebar-create-buttons.tsx │ ├── sidebar-data-list.tsx │ ├── sidebar-header.tsx │ ├── sidebar-invite-button.tsx │ ├── sidebar-switch-item.tsx │ ├── sidebar-switcher.tsx │ ├── sidebar-upgrade.tsx │ └── sidebar.tsx ├── ui │ ├── accordion.tsx │ ├── agent-codeblock.tsx │ ├── alert-dialog.tsx │ ├── alert.tsx │ ├── aspect-ratio.tsx │ ├── avatar.tsx │ ├── badge.tsx │ ├── brand.tsx │ ├── breadcrumb.tsx │ ├── button.tsx │ ├── card.tsx │ ├── carousel.tsx │ ├── chart.tsx │ ├── checkbox.tsx │ ├── collapsible.tsx │ ├── command.tsx │ ├── context-menu.tsx │ ├── copy-button.tsx │ ├── dashboard.tsx │ ├── dialog.tsx │ ├── download-csv-table.tsx │ ├── drawer.tsx │ ├── dropdown-menu.tsx │ ├── file-icon.tsx │ ├── file-preview.tsx │ ├── form.tsx │ ├── hover-card.tsx │ ├── input-otp.tsx │ ├── input.tsx │ ├── label.tsx │ ├── limit-display.tsx │ ├── link-with-tooltip.tsx │ ├── menubar.tsx │ ├── navigation-menu.tsx │ ├── pagination.tsx │ ├── password-input.tsx │ ├── popover.tsx │ ├── profile-button.tsx │ ├── progress.tsx │ ├── radio-group.tsx │ ├── resizable.tsx │ ├── screen-loader.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── skeleton.tsx │ ├── slider.tsx │ ├── sonner.tsx │ ├── switch-model.tsx │ ├── switch.tsx │ ├── table-components.tsx │ ├── table.tsx │ ├── tabs.tsx │ ├── textarea-autosize.tsx │ ├── textarea.tsx │ ├── toggle-group.tsx │ ├── toggle.tsx │ ├── tooltip.tsx │ ├── transitioned-dialog.tsx │ ├── voice-status-bar.tsx │ └── with-tooltip.tsx └── utility │ ├── accept-invitation-dialog.tsx │ ├── alerts.tsx │ ├── change-password.tsx │ ├── delete-all-chats-dialog.tsx │ ├── global-state.tsx │ ├── invite-members-dialog.tsx │ ├── mfa │ ├── mfa-disable-modal.tsx │ ├── mfa-enable-modal.tsx │ ├── mutil-step-deletion.tsx │ └── use-mfa.tsx │ ├── profile-tabs │ ├── data-controls-tab.tsx │ ├── general-tab.tsx │ ├── personalization-tab.tsx │ ├── security-tab.tsx │ ├── shared-chats-popup.tsx │ ├── subscription-tab.tsx │ └── team-tab.tsx │ ├── providers.tsx │ ├── remove-team-member-dialog.tsx │ ├── settings.tsx │ ├── theme-switcher.tsx │ ├── ui-state.tsx │ └── upgrade-plan.tsx ├── context ├── alert-context.tsx ├── context.tsx └── ui-context.tsx ├── convex ├── README.md ├── _generated │ ├── api.d.ts │ ├── api.js │ ├── dataModel.d.ts │ ├── server.d.ts │ └── server.js ├── chats.ts ├── chatsHttp.ts ├── crons.ts ├── feedback.ts ├── fileStorage.ts ├── fileStorageHttp.ts ├── file_items.ts ├── files.ts ├── http.ts ├── httpUtils.ts ├── invitations.ts ├── messages.ts ├── messagesHttp.ts ├── profileDeletion.ts ├── profiles.ts ├── profilesHttp.ts ├── sandboxes.ts ├── schema.ts ├── subscriptionAnalysis.ts ├── subscriptions.ts ├── subscriptionsHttp.ts ├── teams.ts ├── teamsHttp.ts └── tsconfig.json ├── db ├── chat-files.ts ├── chats.ts ├── files.ts ├── index.ts ├── limits.ts ├── message-file-items.ts ├── messages.ts ├── profiles.ts ├── storage │ ├── admin-files.ts │ ├── files.ts │ └── message-images.ts ├── subscriptions.ts └── teams.ts ├── e2e └── tests │ ├── .env.test.example │ └── auth.spec.ts ├── jest.config.ts ├── lib ├── ai-helper.ts ├── ai │ ├── actions.ts │ ├── actions │ │ ├── chat-actions.ts │ │ ├── chat-validation.ts │ │ └── message-actions.ts │ ├── image-processing.ts │ ├── message-utils.ts │ ├── prompts.ts │ ├── providers.ts │ ├── terminal-utils.ts │ ├── tool-handler.ts │ └── tools │ │ ├── agent │ │ ├── ask-shell-exec-tool.ts │ │ ├── deploy-expose-port-tool.ts │ │ ├── file-read-tool.ts │ │ ├── file-str-replace-tool.ts │ │ ├── file-write-tool.ts │ │ ├── idle-tool.ts │ │ ├── index.ts │ │ ├── message-ask-tool.ts │ │ ├── message-notify-tool.ts │ │ ├── shell-background-tool.ts │ │ ├── shell-exec-tool.ts │ │ ├── shell-wait-tool.ts │ │ ├── terminal-command-executor.ts │ │ ├── terminal-executor.ts │ │ ├── types.ts │ │ ├── utils │ │ │ ├── file-db-utils.ts │ │ │ ├── sandbox-manager.ts │ │ │ ├── sandbox-utils.ts │ │ │ └── sandbox.ts │ │ └── web-search-tool.ts │ │ ├── browser.ts │ │ ├── deep-research.ts │ │ ├── pentest-agent.ts │ │ ├── reason-llm.ts │ │ ├── toolSchemas.ts │ │ └── web-search.ts ├── api │ └── convex.ts ├── available-tools.ts ├── blob-to-b64.ts ├── build-prompt-backend.ts ├── build-prompt.ts ├── consume-stream.ts ├── errors.ts ├── hooks │ ├── use-copy-to-clipboard.tsx │ ├── use-hotkey.tsx │ └── use-local-storage-state.ts ├── models │ ├── agent-prompts.ts │ ├── api-error.ts │ ├── hackerai-llm-list.ts │ ├── llm-config.ts │ ├── llm-list.ts │ └── llm-prompting.ts ├── result.ts ├── retrieval │ └── processing │ │ ├── convert.ts │ │ ├── csv.ts │ │ ├── docx.ts │ │ ├── index.ts │ │ ├── json.ts │ │ ├── md.ts │ │ ├── pdf.ts │ │ └── txt.ts ├── sentinel.ts ├── server │ ├── check-auth-ratelimit.ts │ ├── moderation.ts │ ├── ratelimiter.ts │ ├── redis.ts │ ├── server-chat-helpers.ts │ ├── server-utils.ts │ ├── stripe-url.ts │ ├── stripe.ts │ └── subscription-utils.ts ├── shiki │ └── shared.ts ├── supabase │ ├── browser-client.ts │ ├── client.ts │ ├── middleware.ts │ └── server.ts ├── team-utils.ts ├── utils.test.ts ├── utils.ts └── utils │ ├── safe-wait-until.ts │ └── type-converters.ts ├── license ├── middleware.ts ├── next.config.ts ├── package.json ├── playwright.config.ts ├── pnpm-lock.yaml ├── postcss.config.js ├── prettier.config.cjs ├── public ├── apple-touch-icon.png ├── favicon.ico ├── icon-192x192.png ├── icon-256x256.png ├── icon-512x512.png ├── locales │ ├── de │ │ └── translation.json │ └── en │ │ └── translation.json └── manifest.json ├── supabase ├── .gitignore ├── config.toml ├── functions │ └── _shared │ │ └── cors.ts ├── migrations │ ├── 20240108234540_setup.sql │ ├── 20240108234541_add_profiles.sql │ ├── 20240108234542_add_workspaces.sql │ ├── 20240108234544_add_files.sql │ ├── 20240108234545_add_file_items.sql │ ├── 20240108234548_add_chats.sql │ ├── 20240108234549_add_messages.sql │ ├── 20240125192042_upgrade_openai_models.sql │ ├── 20240131140938_add_stripe_tables.sql │ ├── 20240319095700_add_finish_reason_to_chat.sql │ ├── 20240418235300_create_feedback_table.sql │ ├── 20240420225900_change_feedback_to_keep_deleted_messages.sql │ ├── 20240425022300_add_plugin_to_messages.sql │ ├── 20240430131658_remote_schema.sql │ ├── 20240618153844_add_voice_assistant_events.sql │ ├── 20240622132758_remove_columns_from_profiles.sql │ ├── 20240623213350_remove_models_tables_and_fields.sql │ ├── 20240624185827_remove_prompt_and_temperature_from_chats.sql │ ├── 20240625202323_remove_workspace_instructions.sql │ ├── 20240706053845_add_rag_data_to_message_and_feedback.sql │ ├── 20240707045329_update_feedback_table.sql │ ├── 20240905171856_remove_profile_columns.sql │ ├── 20240905180551_remove_profiles_and_workspaces_columns.sql │ ├── 20240905202529_add_shared_chats.sql │ ├── 20240906203119_remove_folders_and_update_chats.sql │ ├── 20240926130750_fix_access_to_shared_message_images.sql │ ├── 20240929193053_add_plan_type_team_name_quantity_to_subscriptions.sql │ ├── 20241003083828_adding_teams_and_other_security_changes.sql │ ├── 20241101201750_delete_user_function.sql │ ├── 20241102204057_remove_workspace_images.sql │ ├── 20241124141109_add_citations_to_messages.sql │ ├── 20241212091225_add_fragment_to_messages.sql │ ├── 20250106015643_add_mfa_policies.sql │ ├── 20250113165607_update_delete-user-with-mfa.sql │ ├── 20250116182657_add_e2b_sandboxes.sql │ ├── 20250125210351_add_thinking_to_messages.sql │ ├── 20250209000616_update_file_items.sql │ ├── 20250209090845_hardening_shared_chats.sql │ ├── 20250215113707_add_messages_id_and_chat_id_to_files.sql │ ├── 20250219085711_migrate_data.sql │ ├── 20250220093600_drop_chat_and_workspaces_files_table.sql │ ├── 20250223095606_add_sequence_to_file_items.sql │ ├── 20250224092523_add_temp_file_clean_up.sql │ ├── 20250309000339_remove_workspace_id_from_chats.sql │ ├── 20250421085801_local_db_sync_fields.sql │ └── 20250427221032_add_attachments_to_table.sql └── types.ts ├── tailwind.config.ts ├── tsconfig.json ├── types ├── agent.ts ├── chat-message.ts ├── chat.ts ├── content-type.ts ├── error-response.ts ├── file-item-chunk.ts ├── files.ts ├── images │ └── message-image.ts ├── index.ts ├── llms.ts ├── models.ts ├── plugins.ts ├── sharing.ts ├── sidebar-data.ts └── stream.ts └── worker └── index.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "plugin:import/recommended", 5 | "plugin:import/typescript", 6 | "prettier" 7 | ], 8 | "settings": { 9 | "import/resolver": { 10 | "typescript": { 11 | "alwaysTryTypes": true 12 | } 13 | } 14 | }, 15 | "ignorePatterns": ["**/components/ui/**"] 16 | } 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1_bug_report.yml: -------------------------------------------------------------------------------- 1 | name: '🐛 Bug Report' 2 | description: 'Report an bug' 3 | title: '[Bug] ' 4 | labels: ['🐛 Bug'] 5 | body: 6 | - type: dropdown 7 | attributes: 8 | label: '💻 Operating System' 9 | options: 10 | - Windows 11 | - macOS 12 | - Ubuntu 13 | - Other Linux 14 | - iOS 15 | - Android 16 | - Other 17 | validations: 18 | required: true 19 | 20 | - type: dropdown 21 | attributes: 22 | label: '🌐 Browser' 23 | options: 24 | - Chrome 25 | - Edge 26 | - Safari 27 | - Firefox 28 | - Other 29 | validations: 30 | required: true 31 | - type: textarea 32 | attributes: 33 | label: '🐛 Bug Description' 34 | description: Provide a detailed and specific description of the bug, highlighting the main issue and its impact. 35 | validations: 36 | required: true 37 | - type: textarea 38 | attributes: 39 | label: '🚦 Expected Behavior' 40 | description: Describe in detail what the expected behavior or outcome should have been under normal circumstances. 41 | - type: textarea 42 | attributes: 43 | label: '📷 Steps to Reproduce' 44 | description: Outline a clear and precise step-by-step process to reproduce the bug, ensuring reproducibility. 45 | - type: textarea 46 | attributes: 47 | label: '📝 Additional Context' 48 | description: Offer any extra information that might help in understanding the issue better. This includes non-reproducible cases or any unique scenarios pertaining to your problem. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2_feature_proposal.yml: -------------------------------------------------------------------------------- 1 | name: '🌠 Feature Proposal' 2 | description: 'Propose a new feature or enhancement' 3 | title: '[Feature Proposal] ' 4 | labels: ['🌠 Feature Proposal'] 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: '🥰 Feature Overview' 9 | description: Provide a detailed and articulate description of the problem or need that this feature will address. 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: '🧐 Suggested Solution' 15 | description: Clearly outline your proposed solution or the feature you envision, including how it would function and its intended benefits. 16 | validations: 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: '📝 Additional Information' 21 | description: Include any additional information or context that could help in understanding the feature better, such as potential impacts, use cases, or examples. -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | /e2e/results/ 12 | /playwright-report/ 13 | /playwright/.cache/ 14 | /test-results/ 15 | /e2e/tests/.env.test 16 | 17 | # next.js 18 | /.next/ 19 | /out/ 20 | 21 | # production 22 | /build 23 | 24 | # misc 25 | .DS_Store 26 | *.pem 27 | 28 | # debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | 33 | # local env files 34 | .env 35 | .env*.local 36 | 37 | # vercel 38 | .vercel 39 | 40 | # typescript 41 | *.tsbuildinfo 42 | next-env.d.ts 43 | 44 | seed.sql 45 | .VSCodeCounter 46 | tool-schemas 47 | 48 | sw.js 49 | sw.js.map 50 | workbox-*.js 51 | workbox-*.js.map 52 | worker-*.js 53 | 54 | # editor 55 | .vscode 56 | .idea 57 | 58 | # Sentry Config File 59 | .sentryclirc 60 | 61 | supabase/config.toml 62 | public/worker-development.js 63 | 64 | # Sensitive files 65 | *.zip 66 | traze.zip 67 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no -- commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | # Format code first 5 | pnpm run format 6 | 7 | # Run type checking 8 | pnpm run type-check 9 | 10 | # # Run linting 11 | # pnpm run lint 12 | 13 | # Run tests 14 | pnpm run test -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | # Run type checking and linting 5 | npm run type-check 6 | 7 | # Run E2E tests 8 | npm run test:e2e -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.11.0 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | **Welcome to PentestGPT!** 4 | 5 | We appreciate your interest in contributing to our project. 6 | 7 | Before you get started, please read our guidelines for contributing. 8 | 9 | ## Types of Contributions 10 | 11 | We welcome the following types of contributions: 12 | 13 | - Bug fixes 14 | - New features 15 | - Documentation improvements 16 | - Code optimizations 17 | - Translations 18 | - Tests 19 | 20 | ## Getting Started 21 | 22 | To get started, fork the project on GitHub and clone it locally on your machine. Then, create a new branch to work on your changes. 23 | 24 | ```bash 25 | git clone https://github.com/hackerai-tech/PentestGPT 26 | cd PentestGPT 27 | git checkout -b my-branch-name 28 | 29 | ``` 30 | 31 | Before submitting your pull request, please make sure your changes pass our automated tests and adhere to our code style guidelines. 32 | 33 | ## Pull Request Process 34 | 35 | 1. Fork the project on GitHub. 36 | 2. Clone your forked repository locally on your machine. 37 | 3. Create a new branch from the main branch. 38 | 4. Make your changes on the new branch. 39 | 5. Ensure that your changes adhere to our code style guidelines and pass our automated tests. 40 | 6. Commit your changes and push them to your forked repository. 41 | 7. Submit a pull request to the main branch of the main repository. 42 | 43 | **Format Code** 44 | 45 | Format the code with Prettier before submitting a pull request. 46 | 47 | ```bash 48 | npm run clean 49 | ``` 50 | 51 | ## Contact 52 | 53 | You can get in touch with us through email at [contact@hackerai.co](mailto:contact@hackerai.co) or connect with us on [X](https://x.com/PentestGPT). -------------------------------------------------------------------------------- /app/ConvexClientProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ConvexProvider, ConvexReactClient } from 'convex/react'; 4 | import type { ReactNode } from 'react'; 5 | 6 | const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!); 7 | 8 | export function ConvexClientProvider({ children }: { children: ReactNode }) { 9 | return {children}; 10 | } 11 | -------------------------------------------------------------------------------- /app/api/chat/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { PluginID } from '@/types/plugins'; 3 | import { VALID_MODEL_IDS } from '@/types/llms'; 4 | 5 | const imageUrlSchema = z.object({ 6 | url: z.string(), 7 | isPath: z.boolean(), 8 | }); 9 | 10 | const contentPartSchema = z.discriminatedUnion('type', [ 11 | z.object({ 12 | type: z.literal('text'), 13 | text: z.string(), 14 | }), 15 | z.object({ 16 | type: z.literal('image_url'), 17 | image_url: imageUrlSchema, 18 | }), 19 | ]); 20 | 21 | const messageSchema = z.object({ 22 | role: z.enum(['user', 'assistant']), 23 | content: z.union([z.string(), z.array(contentPartSchema)]), 24 | attachments: z.array(z.any()).default([]), 25 | }); 26 | 27 | const modelParamsSchema = z.object({ 28 | isContinuation: z.boolean(), 29 | isTerminalContinuation: z.boolean(), 30 | selectedPlugin: z.nativeEnum(PluginID), 31 | agentMode: z.enum(['auto-run', 'ask-every-time'] as const), 32 | confirmTerminalCommand: z.boolean(), 33 | isTemporaryChat: z.boolean(), 34 | isRegeneration: z.boolean(), 35 | editSequenceNumber: z.number().optional(), 36 | }); 37 | 38 | const chatMetadataSchema = z.object({ 39 | id: z.string().uuid().optional(), 40 | newChat: z.boolean().optional(), 41 | retrievedFileItems: z.array(z.any()).default([]), 42 | }); 43 | 44 | export const postRequestBodySchema = z.object({ 45 | messages: z.array(messageSchema), 46 | model: z.enum(VALID_MODEL_IDS), 47 | modelParams: modelParamsSchema, 48 | chatMetadata: chatMetadataSchema, 49 | }); 50 | 51 | export type PostRequestBody = z.infer; 52 | -------------------------------------------------------------------------------- /app/c/[chatid]/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { PentestGPTContext } from '@/context/context'; 4 | import { LargeModel } from '@/lib/models/hackerai-llm-list'; 5 | import { useContext, useEffect, useState } from 'react'; 6 | 7 | const MAX_TITLE_LENGTH = 50; 8 | 9 | function truncateChatName(name: string): string { 10 | if (!name) return 'PentestGPT'; 11 | return name.length > MAX_TITLE_LENGTH 12 | ? `${name.slice(0, MAX_TITLE_LENGTH)}...` 13 | : name; 14 | } 15 | 16 | export default function ChatLayout({ 17 | children, 18 | }: { 19 | children: React.ReactNode; 20 | }) { 21 | const { selectedChat, setSelectedChat } = useContext(PentestGPTContext); 22 | const [mounted, setMounted] = useState(false); 23 | 24 | // Add useEffect to check and change model on load 25 | // TODO: Remove in future 26 | useEffect(() => { 27 | if (selectedChat?.model === 'gpt-4-turbo-preview') { 28 | setSelectedChat({ ...selectedChat, model: LargeModel.modelId }); 29 | } 30 | }, [selectedChat, setSelectedChat]); 31 | 32 | useEffect(() => { 33 | setMounted(true); 34 | }, []); 35 | 36 | useEffect(() => { 37 | if (mounted && selectedChat) { 38 | const truncatedName = truncateChatName(selectedChat.name); 39 | document.title = `${truncatedName}`; 40 | } 41 | }, [selectedChat, mounted]); 42 | 43 | return children; 44 | } 45 | -------------------------------------------------------------------------------- /app/c/[chatid]/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ChatUI } from '@/components/chat/chat-ui'; 4 | 5 | export default function ChatIDPage() { 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /app/chat/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | 3 | export default function ChatRedirect() { 4 | redirect('/'); 5 | } 6 | -------------------------------------------------------------------------------- /app/global-alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { 4 | Dialog, 5 | DialogContent, 6 | DialogHeader, 7 | DialogTitle, 8 | } from '@/components/ui/dialog'; 9 | import { useAlertContext } from '@/context/alert-context'; 10 | import { X } from 'lucide-react'; 11 | import { Button } from '@/components/ui/button'; 12 | 13 | export const GlobalAlertDialog = () => { 14 | const { state, dispatch } = useAlertContext(); 15 | 16 | const handleOpenChange = () => { 17 | dispatch({ type: 'HIDE' }); 18 | }; 19 | 20 | return ( 21 | 22 | 26 | 27 |
28 | {state.title || 'Alert'} 29 | handleOpenChange()} 33 | /> 34 |
35 |
36 | 37 |
38 |

39 | {state.message} 40 |

41 | 42 | {state.action && ( 43 |
44 | 47 |
48 | )} 49 |
50 |
51 |
52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /app/loading.tsx: -------------------------------------------------------------------------------- 1 | import { LoaderCircle } from 'lucide-react'; 2 | import type { JSX } from 'react'; 3 | interface LoadingProps { 4 | size?: number; 5 | } 6 | 7 | export default function Loading({ size = 12 }: LoadingProps): JSX.Element { 8 | const sizeClass = `size-${size}`; 9 | return ( 10 |
11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /app/login/password/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ChangePassword } from '@/components/utility/change-password'; 4 | import { PentestGPTContext } from '@/context/context'; 5 | import { useRouter, useSearchParams } from 'next/navigation'; 6 | import { useContext, useEffect } from 'react'; 7 | 8 | export default function ChangePasswordPage() { 9 | const { user } = useContext(PentestGPTContext); 10 | const router = useRouter(); 11 | const searchParams = useSearchParams(); 12 | 13 | useEffect(() => { 14 | (async () => { 15 | // Check for error parameters in URL 16 | const error = searchParams.get('error'); 17 | 18 | if (error) { 19 | router.push('/login?message=code_expired'); 20 | return; 21 | } 22 | 23 | if (!user) { 24 | router.push('/login'); 25 | } 26 | })(); 27 | }, [user, searchParams]); 28 | 29 | return ; 30 | } 31 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Brand } from '@/components/ui/brand'; 4 | import { ArrowRight } from 'lucide-react'; 5 | import Link from 'next/link'; 6 | import { useEffect, useState } from 'react'; 7 | 8 | export default function HomePage() { 9 | const [mounted, setMounted] = useState(false); 10 | 11 | useEffect(() => { 12 | setMounted(true); 13 | }, []); 14 | 15 | if (!mounted) { 16 | return null; 17 | } 18 | 19 | return ( 20 |
21 | 22 | 23 | 27 | Start Chatting 28 | 29 | 30 | 31 |
32 | 36 | Privacy Policy 37 | 38 | 39 | Terms of Use 40 | 41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /app/posthog.js: -------------------------------------------------------------------------------- 1 | import { PostHog } from 'posthog-node'; 2 | import { isProductionEnvironment } from '@/lib/utils'; 3 | 4 | export default function PostHogClient() { 5 | if (!process.env.NEXT_PUBLIC_POSTHOG_KEY || !isProductionEnvironment) { 6 | return null; 7 | } 8 | 9 | const posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY, { 10 | host: process.env.NEXT_PUBLIC_POSTHOG_HOST, 11 | flushAt: 1, 12 | flushInterval: 0, 13 | }); 14 | return posthogClient; 15 | } 16 | -------------------------------------------------------------------------------- /app/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import posthog from 'posthog-js'; 4 | import { PostHogProvider as PHProvider } from 'posthog-js/react'; 5 | import { useEffect, useContext } from 'react'; 6 | import { PentestGPTContext } from '@/context/context'; 7 | 8 | export function PostHogProvider({ children }: { children: React.ReactNode }) { 9 | const { isPremiumSubscription } = useContext(PentestGPTContext); 10 | 11 | useEffect(() => { 12 | if (!process.env.NEXT_PUBLIC_POSTHOG_KEY) return; 13 | 14 | posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, { 15 | api_host: `${process.env.NEXT_PUBLIC_APP_URL}/ingest`, 16 | ui_host: `${process.env.NEXT_PUBLIC_POSTHOG_HOST}`, 17 | capture_pageview: false, // Disable automatic pageview capture, as we capture manually 18 | autocapture: false, // Disable automatic event capture, as we capture manually 19 | disable_session_recording: true, // Disable session recording by default 20 | }); 21 | 22 | // Only start session recording for premium users 23 | if (isPremiumSubscription) { 24 | posthog.startSessionRecording(); 25 | } 26 | }, [isPremiumSubscription]); 27 | 28 | return {children}; 29 | } 30 | -------------------------------------------------------------------------------- /app/setup/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { PentestGPTContext } from '@/context/context'; 4 | import { getProfileByUserId } from '@/db/profiles'; 5 | import { useRouter } from 'next/navigation'; 6 | import { useContext, useEffect } from 'react'; 7 | 8 | export default function SetupPage() { 9 | const { setProfile, fetchStartingData, user } = useContext(PentestGPTContext); 10 | const router = useRouter(); 11 | 12 | useEffect(() => { 13 | (async () => { 14 | if (!user) { 15 | router.push('/login'); 16 | return; 17 | } 18 | 19 | const profile = await getProfileByUserId(); 20 | setProfile(profile); 21 | 22 | if (!profile) { 23 | throw new Error('Profile not found'); 24 | } 25 | 26 | await fetchStartingData(); 27 | 28 | router.push(`/c`); 29 | })(); 30 | }, []); 31 | 32 | return null; 33 | } 34 | -------------------------------------------------------------------------------- /app/upgrade/page.tsx: -------------------------------------------------------------------------------- 1 | import { UpgradePlan } from '@/components/utility/upgrade-plan'; 2 | 3 | export default function UpgradePage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "gray", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/lib/hooks" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /components/chat/chat-continue-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui/button'; 2 | import { IconPlayerTrackNext } from '@tabler/icons-react'; 3 | import type { FC } from 'react'; 4 | 5 | interface ChatContinueButtonProps { 6 | isGenerating: boolean; 7 | finishReason?: string | null; 8 | onTerminalContinue: () => void; 9 | onContinue: () => void; 10 | } 11 | 12 | export const ChatContinueButton: FC = ({ 13 | isGenerating, 14 | finishReason, 15 | onTerminalContinue, 16 | onContinue, 17 | }) => { 18 | if ( 19 | !isGenerating && 20 | (finishReason === 'length' || finishReason === 'terminal-calls') 21 | ) { 22 | const getButtonText = () => { 23 | switch (finishReason) { 24 | case 'terminal-calls': 25 | return 'Continue'; 26 | case 'length': 27 | return 'Continue generating'; 28 | } 29 | }; 30 | 31 | return ( 32 |
33 | 43 |
44 | ); 45 | } 46 | 47 | return null; 48 | }; 49 | -------------------------------------------------------------------------------- /components/chat/chat-helpers/drag.ts: -------------------------------------------------------------------------------- 1 | export function dragHelper(e: { 2 | preventDefault: () => void; 3 | currentTarget: any; 4 | clientX: any; 5 | }) { 6 | e.preventDefault(); // Prevents the click event after dragging 7 | const el = e.currentTarget; 8 | let isDragging = false; 9 | const posX = e.clientX; 10 | const scrollLeft = el.scrollLeft; 11 | function onMouseMove(e: { clientX: number }) { 12 | isDragging = true; 13 | const dx = e.clientX - posX; 14 | el.scrollLeft = scrollLeft - dx; 15 | } 16 | function onMouseUp() { 17 | document.removeEventListener('mousemove', onMouseMove); 18 | el.style.cursor = 'grab'; 19 | if (isDragging) { 20 | el.addEventListener('click', preventClick, { once: true }); 21 | } 22 | isDragging = false; 23 | } 24 | function preventClick(e: { stopPropagation: () => void }) { 25 | e.stopPropagation(); // Prevents the click event from firing after dragging 26 | } 27 | document.addEventListener('mousemove', onMouseMove); 28 | document.addEventListener('mouseup', onMouseUp, { once: true }); 29 | el.style.cursor = 'grabbing'; 30 | } 31 | -------------------------------------------------------------------------------- /components/chat/chat-helpers/file-upload.ts: -------------------------------------------------------------------------------- 1 | import type { MessageImage } from '@/types/images/message-image'; 2 | import type { Doc } from '@/convex/_generated/dataModel'; 3 | import { toast } from 'sonner'; 4 | import { 5 | MAX_TOTAL_FILES, 6 | calculateTotalFileCount, 7 | validateFileUpload, 8 | } from './file-constants'; 9 | 10 | export const handleFileUpload = ( 11 | files: File[], 12 | handleSelectDeviceFile: (file: File) => void, 13 | newMessageImages: MessageImage[] = [], 14 | newMessageFiles: Doc<'files'>[] = [], 15 | ) => { 16 | let currentTotalFiles = calculateTotalFileCount( 17 | newMessageImages, 18 | newMessageFiles, 19 | ); 20 | 21 | for (const file of files) { 22 | if (currentTotalFiles >= MAX_TOTAL_FILES) { 23 | toast.error( 24 | `Maximum of ${MAX_TOTAL_FILES} files (including images) allowed.`, 25 | ); 26 | break; 27 | } 28 | 29 | // Use unified validation function 30 | if (!validateFileUpload(file, newMessageImages, newMessageFiles)) { 31 | break; 32 | } 33 | 34 | handleSelectDeviceFile(file); 35 | currentTotalFiles++; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /components/chat/chat-helpers/validation.ts: -------------------------------------------------------------------------------- 1 | import type { ChatSettings, LLM } from '@/types'; 2 | import type { Doc } from '@/convex/_generated/dataModel'; 3 | 4 | export const validateChatSettings = ( 5 | chatSettings: ChatSettings | null, 6 | modelData: LLM | undefined, 7 | profile: Doc<'profiles'> | null, 8 | isContinuation: boolean, 9 | messageContent: string | null, 10 | ) => { 11 | if (!chatSettings) throw new Error('Chat settings not found'); 12 | if (!modelData) throw new Error('Model not found'); 13 | if (!profile) throw new Error('Profile not found'); 14 | if (!isContinuation && !messageContent) 15 | throw new Error('Message content not found'); 16 | }; 17 | -------------------------------------------------------------------------------- /components/chat/chat-hooks/use-agent-sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | createContext, 3 | useContext, 4 | useState, 5 | type ReactNode, 6 | } from 'react'; 7 | import type { AgentSidebarState } from '@/types/agent'; 8 | 9 | interface AgentSidebarContextType { 10 | agentSidebar: AgentSidebarState; 11 | setAgentSidebar: React.Dispatch>; 12 | resetAgentSidebar: () => void; 13 | } 14 | 15 | const AgentSidebarContext = createContext( 16 | undefined, 17 | ); 18 | 19 | export const AgentSidebarProvider = ({ children }: { children: ReactNode }) => { 20 | const [agentSidebar, setAgentSidebar] = useState({ 21 | isOpen: false, 22 | item: null, 23 | }); 24 | 25 | const resetAgentSidebar = () => { 26 | setAgentSidebar({ isOpen: false, item: null }); 27 | }; 28 | 29 | return ( 30 | 33 | {children} 34 | 35 | ); 36 | }; 37 | 38 | export const useAgentSidebar = () => { 39 | const context = useContext(AgentSidebarContext); 40 | if (!context) { 41 | throw new Error( 42 | 'useAgentSidebar must be used within an AgentSidebarProvider', 43 | ); 44 | } 45 | return context; 46 | }; 47 | -------------------------------------------------------------------------------- /components/chat/chat-hooks/use-message-handler.tsx: -------------------------------------------------------------------------------- 1 | import type { ChatMessage } from '@/types'; 2 | import { useContext } from 'react'; 3 | import { PentestGPTContext } from '@/context/context'; 4 | 5 | interface UseMessageHandlerProps { 6 | isGenerating: boolean; 7 | userInput: string; 8 | chatMessages: ChatMessage[]; 9 | handleSendMessage: ( 10 | message: string, 11 | chatMessages: ChatMessage[], 12 | isRegeneration: boolean, 13 | shouldAddMessage?: boolean, 14 | ) => void; 15 | handleStopMessage: () => void; 16 | } 17 | 18 | export const useMessageHandler = ({ 19 | isGenerating, 20 | userInput, 21 | chatMessages, 22 | handleSendMessage, 23 | handleStopMessage, 24 | }: UseMessageHandlerProps) => { 25 | const { newMessageFiles, newMessageImages } = useContext(PentestGPTContext); 26 | 27 | const sendMessage = () => { 28 | if (!userInput || isGenerating) return; 29 | handleSendMessage(userInput, chatMessages, false, false); 30 | }; 31 | 32 | const stopMessage = () => { 33 | if (!isGenerating) return; 34 | handleStopMessage(); 35 | }; 36 | 37 | const fileLoading = newMessageFiles.some((file) => 38 | file._id.startsWith('loading'), 39 | ); 40 | 41 | const imageLoading = newMessageImages.some((image) => image.isLoading); 42 | 43 | return { 44 | sendMessage, 45 | stopMessage, 46 | canSend: !!userInput && !isGenerating && !fileLoading && !imageLoading, 47 | isFileLoading: fileLoading || imageLoading, 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /components/chat/chat-hooks/use-prompt-and-command.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { PentestGPTContext } from '@/context/context'; 3 | 4 | export const usePromptAndCommand = () => { 5 | const { setUserInput } = useContext(PentestGPTContext); 6 | 7 | const handleInputChange = (value: string) => { 8 | setUserInput(value); 9 | }; 10 | 11 | return { 12 | handleInputChange, 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /components/chat/chat-hooks/use-scroll.tsx: -------------------------------------------------------------------------------- 1 | import { useStickToBottom } from 'use-stick-to-bottom'; 2 | import { useEffect, useCallback } from 'react'; 3 | import { useUIContext } from '@/context/ui-context'; 4 | 5 | export const useScroll = () => { 6 | const { isGenerating } = useUIContext(); 7 | 8 | const stickToBottom = useStickToBottom({ 9 | resize: 'smooth', 10 | initial: 'instant', 11 | }); 12 | 13 | const scrollToBottom = useCallback( 14 | (options?: { 15 | force?: boolean; 16 | instant?: boolean; 17 | }): boolean | Promise => { 18 | if (options?.instant) { 19 | const scrollContainer = stickToBottom.scrollRef.current; 20 | if (scrollContainer) { 21 | scrollContainer.scrollTop = scrollContainer.scrollHeight; 22 | } 23 | return true; 24 | } 25 | 26 | return stickToBottom.scrollToBottom({ 27 | animation: 'smooth', 28 | preserveScrollPosition: !options?.force, 29 | }); 30 | }, 31 | [stickToBottom.scrollToBottom, stickToBottom.scrollRef], 32 | ); 33 | 34 | useEffect(() => { 35 | if (isGenerating) { 36 | void scrollToBottom(); 37 | } 38 | }, [isGenerating, scrollToBottom]); 39 | 40 | return { 41 | scrollRef: stickToBottom.scrollRef, 42 | contentRef: stickToBottom.contentRef, 43 | isAtBottom: stickToBottom.isAtBottom, 44 | scrollToBottom, 45 | stopScroll: stickToBottom.stopScroll, 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /components/chat/chat-plugin-info.tsx: -------------------------------------------------------------------------------- 1 | import type { PluginSummary } from '@/types/plugins'; 2 | import React from 'react'; 3 | 4 | interface ChatPluginInfoProps { 5 | pluginInfo: PluginSummary | undefined; 6 | } 7 | 8 | export const ChatPluginInfo: React.FC = ({ 9 | pluginInfo, 10 | }) => { 11 | if (!pluginInfo) return null; 12 | 13 | return ( 14 |
15 | {pluginInfo.name} 24 |

{pluginInfo.name}

25 |

{pluginInfo.description}

26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /components/chat/chat-scroll-buttons.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowDown } from 'lucide-react'; 2 | import type { FC } from 'react'; 3 | 4 | interface ChatScrollButtonsProps { 5 | isAtBottom: boolean; 6 | scrollToBottom: (options?: { force?: boolean }) => Promise | boolean; 7 | } 8 | 9 | export const ChatScrollButtons: FC = ({ 10 | isAtBottom, 11 | scrollToBottom, 12 | }) => { 13 | return ( 14 | <> 15 | {!isAtBottom && ( 16 |
{ 19 | void scrollToBottom({ force: true }); 20 | }} 21 | > 22 | 23 |
24 | )} 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /components/chat/chat-secondary-buttons.tsx: -------------------------------------------------------------------------------- 1 | import { useChatHandler } from '@/components/chat/chat-hooks/use-chat-handler'; 2 | import { IconMessagePlus } from '@tabler/icons-react'; 3 | import type { FC } from 'react'; 4 | import { WithTooltip } from '../ui/with-tooltip'; 5 | 6 | export const ChatSecondaryButtons: FC = () => { 7 | const { handleNewChat } = useChatHandler(); 8 | 9 | return ( 10 |
11 | New chat
} 13 | trigger={ 14 | 19 | } 20 | side="bottom" 21 | /> 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /components/chat/chat-send-button.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import type { FC } from 'react'; 3 | import { WithTooltip } from '../ui/with-tooltip'; 4 | import { ArrowUp, Square } from 'lucide-react'; 5 | import { Button } from '@/components/ui/button'; 6 | 7 | interface ChatSendButtonProps { 8 | isGenerating: boolean; 9 | canSend: boolean; 10 | onSend: () => void; 11 | onStop: () => void; 12 | isFileLoading?: boolean; 13 | } 14 | 15 | export const ChatSendButton: FC = ({ 16 | isGenerating, 17 | canSend, 18 | onSend, 19 | onStop, 20 | isFileLoading, 21 | }) => { 22 | if (isGenerating) { 23 | return ( 24 |
25 | 33 |
34 | ); 35 | } 36 | 37 | return ( 38 | 42 | 54 | 55 | } 56 | /> 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /components/chat/dialog-portal.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactNode } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | interface ModalProps { 5 | children: ReactNode; 6 | isOpen: boolean; 7 | } 8 | 9 | const Modal: React.FC = ({ children, isOpen }) => { 10 | if (!isOpen) return null; 11 | 12 | const portalRoot = document.getElementsByTagName('body')[0] as HTMLElement; 13 | if (!portalRoot) return null; 14 | 15 | return ReactDOM.createPortal( 16 |
{children}
, 17 | portalRoot, 18 | ); 19 | }; 20 | 21 | export default Modal; 22 | -------------------------------------------------------------------------------- /components/chat/shared-message.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { MessageTypeResolver } from '@/components/messages/message-type-solver'; 5 | import { ImageIcon } from 'lucide-react'; 6 | import type { Doc } from '@/convex/_generated/dataModel'; 7 | 8 | interface SharedMessageProps { 9 | message: Doc<'messages'>; 10 | isLastMessage: boolean; 11 | } 12 | 13 | export const SharedMessage: React.FC = ({ 14 | message, 15 | isLastMessage, 16 | }) => { 17 | return ( 18 |
19 |
20 |
21 |
24 |
25 | {message.image_paths.length > 0 && ( 26 |
29 | 30 | Uploaded an image 31 |
32 | )} 33 | 38 |
39 |
40 |
41 |
42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /components/chat/temporary-chat-info.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrandLarge } from '@/components/ui/brand'; 3 | 4 | export const TemporaryChatInfo: React.FC = () => { 5 | return ( 6 |
7 |
8 | 9 |
10 |

Temporary Chat

11 |

12 | This chat won't appear in your history, and no data from these 13 | conversations will be stored or retained. To clear the chat, simply 14 | reload the page or click the "Clear chat" button. 15 |

16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /components/icons/google-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface GoogleIconProps extends React.SVGProps { 4 | width?: number; 5 | height?: number; 6 | } 7 | 8 | export const GoogleIcon: React.FC = ({ 9 | width = 20, 10 | height = 20, 11 | ...props 12 | }) => { 13 | return ( 14 | 21 | 25 | 29 | 33 | 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /components/icons/microsoft-icon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface MicrosoftIconProps extends React.SVGProps { 4 | width?: number; 5 | height?: number; 6 | } 7 | 8 | export const MicrosoftIcon: React.FC = ({ 9 | width = 20, 10 | height = 20, 11 | ...props 12 | }) => { 13 | return ( 14 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /components/image/image-with-preview.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic'; 2 | import { forwardRef, type ImgHTMLAttributes, useState } from 'react'; 3 | 4 | const DynamicFilePreview = dynamic(() => import('../ui/file-preview'), { 5 | ssr: false, 6 | }); 7 | 8 | const ImageWithPreview = forwardRef< 9 | HTMLImageElement, 10 | ImgHTMLAttributes 11 | >(({ src, ...props }, ref) => { 12 | const [showImagePreview, setShowImagePreview] = useState(false); 13 | 14 | return ( 15 | <> 16 | setShowImagePreview(true)} 19 | className="w-1/2 rounded-md" 20 | src={src} 21 | {...props} 22 | /> 23 | {showImagePreview && ( 24 | { 35 | setShowImagePreview(isOpen); 36 | }} 37 | /> 38 | )} 39 | 40 | ); 41 | }); 42 | 43 | ImageWithPreview.displayName = 'ImageWithPreview'; 44 | 45 | export { ImageWithPreview }; 46 | -------------------------------------------------------------------------------- /components/messages/loading-states.tsx: -------------------------------------------------------------------------------- 1 | import { PluginID } from '@/types/plugins'; 2 | import { useUIContext } from '@/context/ui-context'; 3 | import { 4 | FileText, 5 | Puzzle, 6 | Globe, 7 | Atom, 8 | SquareTerminal, 9 | Search, 10 | Circle, 11 | } from 'lucide-react'; 12 | 13 | export const loadingStates = { 14 | none: { 15 | icon: , 16 | text: '', 17 | }, 18 | retrieval: { 19 | icon: , 20 | text: 'Reading documents...', 21 | }, 22 | thinking: { 23 | icon: , 24 | text: 'Thinking...', 25 | }, 26 | [PluginID.WEB_SEARCH]: { 27 | icon: , 28 | text: 'Searching the web...', 29 | }, 30 | [PluginID.BROWSER]: { 31 | icon: , 32 | text: 'Browsing the web...', 33 | }, 34 | [PluginID.DEEP_RESEARCH]: { 35 | icon: , 36 | text: 'Researching... (takes 1-5 minutes)', 37 | }, 38 | 39 | [PluginID.PENTEST_AGENT]: { 40 | icon: , 41 | text: 'Using pentest agent...', 42 | }, 43 | }; 44 | 45 | export const LoadingState = ({ 46 | isLastMessage, 47 | isAssistant, 48 | }: { isLastMessage: boolean; isAssistant: boolean }) => { 49 | const { firstTokenReceived, isGenerating, toolInUse } = useUIContext(); 50 | 51 | if (!isLastMessage || !isAssistant || firstTokenReceived || !isGenerating) 52 | return null; 53 | 54 | const { icon, text } = loadingStates[ 55 | toolInUse as keyof typeof loadingStates 56 | ] || { 57 | icon: , 58 | text: `Using ${toolInUse}...`, 59 | }; 60 | 61 | if (!text && toolInUse === 'none') { 62 | return icon; 63 | } 64 | 65 | return ( 66 |
67 | {icon} 68 |
{text}
69 |
70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /components/messages/message-citations.tsx: -------------------------------------------------------------------------------- 1 | import React, { type FC, useMemo } from 'react'; 2 | import { MessageMarkdown } from './message-markdown'; 3 | import { ReasoningMarkdown } from './reasoning-markdown'; 4 | import { CitationDisplay } from './citation-display'; 5 | 6 | interface MessageCitationsProps { 7 | content: string; 8 | citations?: string[]; 9 | isAssistant: boolean; 10 | reasoningWithCitations?: boolean; 11 | } 12 | 13 | export const MessageCitations: FC = ({ 14 | content, 15 | citations = [], 16 | isAssistant, 17 | reasoningWithCitations, 18 | }) => { 19 | const processedContent = useMemo( 20 | () => 21 | citations.length > 0 22 | ? content.replace(/\[(\d+)\]/g, (match, num) => { 23 | const index = Number.parseInt(num) - 1; 24 | return citations[index] ? `[${num}](${citations[index]})` : match; 25 | }) 26 | : content, 27 | [content, citations], 28 | ); 29 | 30 | return ( 31 |
32 | {reasoningWithCitations ? ( 33 | 34 | ) : ( 35 | 36 | )} 37 | 38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /components/messages/message-quick-feedback.tsx: -------------------------------------------------------------------------------- 1 | import type { Doc } from '@/convex/_generated/dataModel'; 2 | import { Button } from '../ui/button'; 3 | 4 | interface QuickFeedbackProps { 5 | handleBadResponseReason: (reason: string) => void; 6 | feedback?: Doc<'feedback'>; 7 | } 8 | 9 | export const MessageQuickFeedback: React.FC = ({ 10 | handleBadResponseReason, 11 | feedback, 12 | }) => { 13 | const feedbackOptions = [ 14 | "Don't like the style", 15 | 'Not factually correct', 16 | "Didn't fully follow instructions", 17 | "Refused when it shouldn't have", 18 | 'Being lazy', 19 | 'Other', 20 | ]; 21 | return ( 22 |
23 |

What was wrong?

24 |
25 | {feedbackOptions.map((option) => ( 26 | 34 | ))} 35 |
36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /components/messages/reasoning-markdown.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactMarkdown, { type Components } from 'react-markdown'; 3 | 4 | const components: Partial = { 5 | a({ children, href }) { 6 | return typeof children === 'string' && /^\d+$/.test(children) ? ( 7 | 14 | {children} 15 | 16 | ) : ( 17 | 18 | {children} 19 | 20 | ); 21 | }, 22 | p: ({ children }) => ( 23 |

{children}

24 | ), 25 | }; 26 | 27 | export const ReasoningMarkdown: React.FC<{ content: string }> = ({ 28 | content, 29 | }) => { 30 | return ( 31 |
32 | {content} 33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /components/messages/terminal-messages/message-terminal-block.tsx: -------------------------------------------------------------------------------- 1 | import { type FC, memo, useMemo } from 'react'; 2 | import chalk from 'chalk'; 3 | import AnsiToHtml from 'ansi-to-html'; 4 | import DOMPurify from 'isomorphic-dompurify'; 5 | 6 | interface MessageTerminalBlockProps { 7 | value: string; 8 | } 9 | 10 | const customColors = { 11 | 0: '#000000', 12 | 1: '#FF5555', 13 | 2: '#50FA7B', 14 | 3: '#F1FA8C', 15 | 4: '#BD93F9', 16 | 5: '#FF79C6', 17 | 6: '#8BE9FD', 18 | 7: '#F8F8F2', 19 | }; 20 | 21 | const converter = new AnsiToHtml({ 22 | fg: '#F8F8F2', 23 | bg: '#282A36', 24 | colors: customColors, 25 | newline: true, 26 | }); 27 | 28 | export const MessageTerminalBlock: FC = memo( 29 | ({ value }) => { 30 | const formattedValue = useMemo(() => { 31 | const styledValue = value 32 | .replace(/\[(\w+)\]/g, (_, word) => chalk.blue.bold(`[${word}]`)) 33 | .replace(/\b(error|warning)\b/gi, (match) => 34 | match.toLowerCase() === 'error' 35 | ? chalk.red.bold(match) 36 | : chalk.yellow.bold(match), 37 | ); 38 | 39 | const htmlWithColors = converter.toHtml(styledValue); 40 | 41 | let sanitizedHtml = htmlWithColors; 42 | sanitizedHtml = DOMPurify.sanitize(htmlWithColors, { 43 | ALLOWED_TAGS: ['span', 'br'], 44 | ALLOWED_ATTR: ['style'], 45 | ADD_ATTR: ['target'], 46 | KEEP_CONTENT: true, 47 | ALLOW_DATA_ATTR: false, 48 | }); 49 | 50 | return sanitizedHtml; 51 | }, [value]); 52 | 53 | return ( 54 |
55 |
60 |
61 | ); 62 | }, 63 | ); 64 | 65 | MessageTerminalBlock.displayName = 'MessageTerminalBlock'; 66 | -------------------------------------------------------------------------------- /components/messages/terminal-messages/shell-wait-block.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Clock } from 'lucide-react'; 3 | import { cn } from '@/lib/utils'; 4 | 5 | interface ShellWaitBlockProps { 6 | block: { 7 | seconds: string; 8 | }; 9 | } 10 | 11 | export const ShellWaitBlockComponent: React.FC = ({ 12 | block, 13 | }) => { 14 | return ( 15 |
16 |
17 |
18 |
19 | 20 | Waiting for terminal 21 |
22 |
23 | 24 | {block.seconds} 25 | 26 |
27 |
28 |
29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /components/messages/terminal-messages/show-more-button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '@/components/ui/button'; 3 | import { ArrowDown, ArrowUp } from 'lucide-react'; 4 | 5 | interface ShowMoreButtonProps { 6 | isExpanded: boolean; 7 | onClick: () => void; 8 | remainingCount: number; 9 | type?: 'lines' | 'results'; 10 | icon?: React.ReactNode; 11 | } 12 | 13 | export const ShowMoreButton: React.FC = ({ 14 | isExpanded, 15 | onClick, 16 | remainingCount, 17 | type = 'lines', 18 | icon, 19 | }) => ( 20 |
21 | 39 |
40 | ); 41 | -------------------------------------------------------------------------------- /components/messages/terminal-messages/types.ts: -------------------------------------------------------------------------------- 1 | export interface MessageTerminalProps { 2 | content: string; 3 | isAssistant: boolean; 4 | isLastMessage: boolean; 5 | } 6 | 7 | export interface TerminalBlock { 8 | command: string; 9 | stdout: string; 10 | error?: string; 11 | exec_dir?: string; 12 | } 13 | 14 | export interface ShellWaitBlock { 15 | seconds: string; 16 | } 17 | 18 | export interface FileContentBlock { 19 | path: string; 20 | content: string; 21 | mode?: 'read' | 'create' | 'append' | 'overwrite'; 22 | } 23 | 24 | export interface InfoSearchWebBlock { 25 | query: string; 26 | results: Array<{ 27 | title: string; 28 | url: string; 29 | description: string; 30 | }>; 31 | } 32 | 33 | export type ContentBlock = 34 | | { 35 | type: 'text'; 36 | content: string; 37 | } 38 | | { 39 | type: 'terminal'; 40 | content: TerminalBlock; 41 | } 42 | | { 43 | type: 'shell-wait'; 44 | content: ShellWaitBlock; 45 | } 46 | | { 47 | type: 'file-content'; 48 | content: FileContentBlock; 49 | } 50 | | { 51 | type: 'info-search-web'; 52 | content: InfoSearchWebBlock; 53 | }; 54 | 55 | export interface ShowMoreButtonProps { 56 | isExpanded: boolean; 57 | onClick: () => void; 58 | remainingLines: number; 59 | icon?: React.ReactNode; 60 | } 61 | 62 | export const MAX_VISIBLE_LINES = 12; 63 | export const COMMAND_LENGTH_THRESHOLD = 40; // Threshold for when to switch to full terminal view 64 | -------------------------------------------------------------------------------- /components/messages/terminal-messages/use-auto-run-preference.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import type { AgentMode } from '@/types/llms'; 3 | 4 | const agentModeKey = 'agentMode'; 5 | 6 | export const useAgentModePreference = () => { 7 | const [agentMode, setAgentMode] = useState('ask-every-time'); 8 | 9 | useEffect(() => { 10 | // Only run on client side 11 | if (typeof window !== 'undefined') { 12 | const savedPreference = localStorage.getItem(agentModeKey) as AgentMode; 13 | if (savedPreference) { 14 | setAgentMode(savedPreference); 15 | } 16 | } 17 | }, []); 18 | 19 | const updateAgentMode = (value: AgentMode) => { 20 | setAgentMode(value); 21 | localStorage.setItem(agentModeKey, value); 22 | }; 23 | 24 | return { 25 | agentMode, 26 | setAgentMode: updateAgentMode, 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /components/models/model-icon.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import type { LLMID } from '@/types'; 3 | import { Sparkles, Sparkle, Zap } from 'lucide-react'; 4 | import { useTheme } from 'next-themes'; 5 | import type { FC, HTMLAttributes } from 'react'; 6 | import { SmallModel, LargeModel } from '@/lib/models/hackerai-llm-list'; 7 | 8 | interface ModelIconProps extends HTMLAttributes { 9 | modelId: LLMID | 'custom'; 10 | size: number; 11 | } 12 | 13 | export const iconMap = { 14 | [SmallModel.modelId]: Zap, 15 | [LargeModel.modelId]: Sparkle, 16 | default: Sparkles, 17 | }; 18 | 19 | export const ModelIcon: FC = ({ modelId, size, ...props }) => { 20 | const { theme } = useTheme(); 21 | const IconComponent = iconMap[modelId] || iconMap.default; 22 | const className = cn( 23 | 'rounded-sm bg-white p-0.5 text-black', 24 | props.className, 25 | theme === 'dark' ? 'bg-white' : 'border border-black', 26 | ); 27 | 28 | return ; 29 | }; 30 | -------------------------------------------------------------------------------- /components/models/model-option.tsx: -------------------------------------------------------------------------------- 1 | import type { LLM } from '@/types'; 2 | import type { FC } from 'react'; 3 | import { ModelIcon } from './model-icon'; 4 | 5 | interface ModelOptionProps { 6 | model: LLM; 7 | onSelect: () => void; 8 | } 9 | 10 | export const ModelOption: FC = ({ model, onSelect }) => { 11 | return ( 12 |
16 |
17 | 18 | 19 |
{model.shortModelName}
20 |
21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /components/sidebar/sidebar-header.tsx: -------------------------------------------------------------------------------- 1 | import { IconLayoutSidebar } from '@tabler/icons-react'; 2 | import type { FC } from 'react'; 3 | import type { ContentType } from '@/types'; 4 | import { SidebarCreateButtons } from './sidebar-create-buttons'; 5 | import { SIDEBAR_ICON_SIZE } from './sidebar-content'; 6 | import { Button } from '../ui/button'; 7 | import { WithTooltip } from '../ui/with-tooltip'; 8 | 9 | interface SidebarHeaderProps { 10 | handleToggleSidebar: () => void; 11 | contentType: ContentType; 12 | handleSidebarVisibility: () => void; 13 | } 14 | 15 | export const SidebarHeader: FC = ({ 16 | handleToggleSidebar, 17 | contentType, 18 | handleSidebarVisibility, 19 | }) => { 20 | return ( 21 | <> 22 |
23 | 31 | 32 | 33 | } 34 | side="right" 35 | /> 36 | 37 |
38 | 42 |
43 |
44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /components/sidebar/sidebar-invite-button.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import { UserPlus } from 'lucide-react'; 3 | 4 | interface SidebarInviteButtonProps { 5 | onInvite: () => void; 6 | title?: string; 7 | subtitle?: string; 8 | } 9 | 10 | export const SidebarInviteButton: FC = ({ 11 | onInvite, 12 | title = 'Invite Members', 13 | subtitle = 'Add team members to your team', 14 | }) => { 15 | return ( 16 |
17 |
21 |
22 | 23 |
24 |
{title}
25 |
{subtitle}
26 |
27 |
28 |
29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /components/sidebar/sidebar-switch-item.tsx: -------------------------------------------------------------------------------- 1 | import type { ContentType } from '@/types'; 2 | import type { FC } from 'react'; 3 | import { TabsTrigger } from '../ui/tabs'; 4 | import { WithTooltip } from '../ui/with-tooltip'; 5 | 6 | interface SidebarSwitchItemProps { 7 | contentType: ContentType; 8 | icon: React.ReactNode; 9 | onContentTypeChange: (contentType: ContentType) => void; 10 | display?: string; 11 | } 12 | 13 | export const SidebarSwitchItem: FC = ({ 14 | contentType, 15 | icon, 16 | onContentTypeChange, 17 | display, 18 | }) => { 19 | return ( 20 | 23 | {display 24 | ? display 25 | : contentType[0].toUpperCase() + contentType.substring(1)} 26 |
27 | } 28 | trigger={ 29 | onContentTypeChange(contentType as ContentType)} 33 | > 34 | {icon} 35 | 36 | } 37 | /> 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /components/sidebar/sidebar-upgrade.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import { useRouter } from 'next/navigation'; 3 | import { LockOpen } from 'lucide-react'; 4 | 5 | export const SidebarUpgrade: FC = () => { 6 | const router = useRouter(); 7 | 8 | const handleUpgradeClick = () => { 9 | router.push('/upgrade'); 10 | }; 11 | 12 | return ( 13 |
14 |
18 |
19 | 20 |
21 |
Upgrade plan
22 |
23 | Upgrade for file upload, smarter AI, and more 24 |
25 |
26 |
27 |
28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /components/sidebar/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { PentestGPTContext } from '@/context/context'; 2 | import type { ContentType } from '@/types'; 3 | import { type FC, useContext } from 'react'; 4 | import { SIDEBAR_WIDTH } from '../ui/dashboard'; 5 | import { TabsContent } from '../ui/tabs'; 6 | import { SidebarContent } from './sidebar-content'; 7 | 8 | interface SidebarProps { 9 | contentType: ContentType; 10 | showSidebar: boolean; 11 | } 12 | 13 | export const Sidebar: FC = ({ contentType, showSidebar }) => { 14 | const { chats } = useContext(PentestGPTContext); 15 | 16 | const renderSidebarContent = (contentType: ContentType, data: any[]) => { 17 | return ; 18 | }; 19 | 20 | return ( 21 | 31 |
32 | {(() => { 33 | switch (contentType) { 34 | case 'chats': 35 | return renderSidebarContent('chats', chats); 36 | 37 | default: 38 | return null; 39 | } 40 | })()} 41 |
42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /components/ui/agent-codeblock.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { type JSX, useLayoutEffect, useState } from 'react'; 4 | import { highlight, CODE_THEMES } from '@/lib/shiki/shared'; 5 | import { cn } from '@/lib/utils'; 6 | import type { AgentCodeBlockLang } from '@/types'; 7 | import { useTheme } from 'next-themes'; 8 | 9 | interface AgentCodeBlockProps { 10 | code: string; 11 | lang: AgentCodeBlockLang; 12 | } 13 | 14 | export function AgentCodeBlock({ code, lang }: AgentCodeBlockProps) { 15 | const [nodes, setNodes] = useState(null); 16 | const [isLoading, setIsLoading] = useState(true); 17 | const { resolvedTheme } = useTheme(); 18 | 19 | useLayoutEffect(() => { 20 | setIsLoading(true); 21 | 22 | const theme = 23 | resolvedTheme === 'dark' ? CODE_THEMES.dark : CODE_THEMES.light; 24 | 25 | void highlight(code, { 26 | lang, 27 | theme, 28 | customComponents: { 29 | pre: (props) => ( 30 |
36 |         ),
37 |         code: (props) => (
38 |           
42 |         ),
43 |       },
44 |     })
45 |       .then(setNodes)
46 |       .finally(() => setIsLoading(false));
47 |   }, [code, lang, resolvedTheme]);
48 | 
49 |   if (isLoading) {
50 |     return (
51 |       
52 |
53 |
54 |
55 | ); 56 | } 57 | 58 | return
{nodes}
; 59 | } 60 | -------------------------------------------------------------------------------- /components/ui/alert.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 alertVariants = cva( 7 | '[&>svg]:text-foreground relative w-full rounded-lg border p-4 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg~*]:pl-7', 8 | { 9 | variants: { 10 | variant: { 11 | default: 'bg-background text-foreground', 12 | destructive: 13 | 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: 'default', 18 | }, 19 | }, 20 | ); 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )); 33 | Alert.displayName = 'Alert'; 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )); 45 | AlertTitle.displayName = 'AlertTitle'; 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )); 57 | AlertDescription.displayName = 'AlertDescription'; 58 | 59 | export { Alert, AlertTitle, AlertDescription }; 60 | -------------------------------------------------------------------------------- /components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'; 4 | 5 | function AspectRatio({ 6 | ...props 7 | }: React.ComponentProps) { 8 | return ; 9 | } 10 | 11 | export { AspectRatio }; 12 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as AvatarPrimitive from '@radix-ui/react-avatar'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | function Avatar({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ); 22 | } 23 | 24 | function AvatarImage({ 25 | className, 26 | ...props 27 | }: React.ComponentProps) { 28 | return ( 29 | 34 | ); 35 | } 36 | 37 | function AvatarFallback({ 38 | className, 39 | ...props 40 | }: React.ComponentProps) { 41 | return ( 42 | 50 | ); 51 | } 52 | 53 | export { Avatar, AvatarImage, AvatarFallback }; 54 | -------------------------------------------------------------------------------- /components/ui/badge.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 badgeVariants = cva( 8 | 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-4 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden', 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90', 14 | secondary: 15 | 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', 16 | destructive: 17 | 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', 18 | outline: 19 | 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', 20 | }, 21 | }, 22 | defaultVariants: { 23 | variant: 'default', 24 | }, 25 | }, 26 | ); 27 | 28 | function Badge({ 29 | className, 30 | variant, 31 | asChild = false, 32 | ...props 33 | }: React.ComponentProps<'span'> & 34 | VariantProps & { asChild?: boolean }) { 35 | const Comp = asChild ? Slot : 'span'; 36 | 37 | return ( 38 | 43 | ); 44 | } 45 | 46 | export { Badge, badgeVariants }; 47 | -------------------------------------------------------------------------------- /components/ui/brand.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { type FC, useEffect, useState, memo } from 'react'; 4 | import { PentestGPTSVG } from '../icons/pentestgpt-svg'; 5 | import { useTheme } from 'next-themes'; 6 | 7 | interface BrandProps { 8 | forceTheme?: 'dark' | 'light'; 9 | scale?: number; 10 | } 11 | 12 | const BrandBase: FC = memo(({ forceTheme, scale = 0.4 }) => { 13 | const { theme, systemTheme } = useTheme(); 14 | const [mounted, setMounted] = useState(false); 15 | 16 | useEffect(() => { 17 | setMounted(true); 18 | }, []); 19 | 20 | const currentTheme = mounted 21 | ? theme === 'system' 22 | ? systemTheme 23 | : theme 24 | : 'dark'; 25 | const brandTheme = forceTheme || (currentTheme === 'dark' ? 'dark' : 'light'); 26 | 27 | return ( 28 |
29 |
30 | 31 |
32 | {scale === 0.4 && ( 33 |
PentestGPT
34 | )} 35 |
36 | ); 37 | }); 38 | 39 | BrandBase.displayName = 'BrandBase'; 40 | 41 | export const Brand: FC = (props) => ( 42 | 43 | ); 44 | export const BrandSmall: FC = (props) => ( 45 | 46 | ); 47 | export const BrandLarge: FC = (props) => ( 48 | 49 | ); 50 | -------------------------------------------------------------------------------- /components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; 5 | import { Check } from 'lucide-react'; 6 | 7 | import { cn } from '@/lib/utils'; 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )); 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 29 | 30 | export { Checkbox }; 31 | -------------------------------------------------------------------------------- /components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'; 4 | 5 | const Collapsible = CollapsiblePrimitive.Root; 6 | 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; 8 | 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; 10 | 11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }; 12 | -------------------------------------------------------------------------------- /components/ui/copy-button.tsx: -------------------------------------------------------------------------------- 1 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'; 2 | import { Button } from '@/components/ui/button'; 3 | import { cn } from '@/lib/utils'; 4 | import { Check, Copy } from 'lucide-react'; 5 | 6 | export function CopyButton({ 7 | value, 8 | variant = 'link', 9 | className, 10 | }: { 11 | value: string; 12 | variant?: 'link' | 'outline'; 13 | className?: string; 14 | }) { 15 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }); 16 | return ( 17 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /components/ui/file-icon.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | IconFileTypeCsv, 3 | IconFileTypeDocx, 4 | IconFileTypePdf, 5 | IconMarkdown, 6 | IconPhoto, 7 | } from '@tabler/icons-react'; 8 | import { File, FileJson, FileText } from 'lucide-react'; 9 | import type { FC } from 'react'; 10 | 11 | interface FileIconProps { 12 | type: string; 13 | size?: number; 14 | } 15 | 16 | export const FileIcon: FC = ({ type, size = 32 }) => { 17 | if (type.includes('image')) { 18 | return ; 19 | } else if (type.includes('pdf')) { 20 | return ; 21 | } else if (type.includes('csv')) { 22 | return ; 23 | } else if (type.includes('docx')) { 24 | return ; 25 | } else if (type.includes('plain')) { 26 | return ; 27 | } else if (type.includes('json')) { 28 | return ; 29 | } else if (type.includes('markdown')) { 30 | return ; 31 | } else { 32 | return ; 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /components/ui/hover-card.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as HoverCardPrimitive from '@radix-ui/react-hover-card'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | function HoverCard({ 9 | ...props 10 | }: React.ComponentProps) { 11 | return ; 12 | } 13 | 14 | function HoverCardTrigger({ 15 | ...props 16 | }: React.ComponentProps) { 17 | return ( 18 | 19 | ); 20 | } 21 | 22 | function HoverCardContent({ 23 | className, 24 | align = 'center', 25 | sideOffset = 4, 26 | ...props 27 | }: React.ComponentProps) { 28 | return ( 29 | 30 | 40 | 41 | ); 42 | } 43 | 44 | export { HoverCard, HoverCardTrigger, HoverCardContent }; 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as LabelPrimitive from '@radix-ui/react-label'; 4 | import { cva, type VariantProps } from 'class-variance-authority'; 5 | import * as React from 'react'; 6 | 7 | import { cn } from '@/lib/utils'; 8 | 9 | const labelVariants = cva( 10 | 'text-sm font-semibold 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 | -------------------------------------------------------------------------------- /components/ui/limit-display.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | 3 | interface LimitDisplayProps { 4 | used: number; 5 | limit: number; 6 | isOverLimit?: boolean; 7 | } 8 | 9 | export const LimitDisplay: FC = ({ 10 | used, 11 | limit, 12 | isOverLimit = false, 13 | }) => { 14 | return ( 15 |
16 | {used}/{limit} 17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /components/ui/link-with-tooltip.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { WithTooltip } from './with-tooltip'; 3 | import { WebsiteCard } from '../messages/citation-display'; 4 | 5 | interface LinkWithTooltipProps { 6 | href: string; 7 | title?: string; 8 | children: React.ReactNode; 9 | } 10 | 11 | export const LinkWithTooltip: React.FC = ({ 12 | href, 13 | children, 14 | }) => { 15 | let domain = ''; 16 | try { 17 | const urlObj = new URL(href); 18 | domain = urlObj.hostname.replace(/^www\./, ''); 19 | } catch { 20 | domain = href; 21 | } 22 | 23 | return ( 24 | } 26 | trigger={ 27 | 36 | {children} 37 | 38 | } 39 | side="top" 40 | delayDuration={300} 41 | /> 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /components/ui/password-input.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | import { Eye, EyeOff } from 'lucide-react'; 5 | import { 6 | TooltipProvider, 7 | Tooltip, 8 | TooltipTrigger, 9 | TooltipContent, 10 | } from '@/components/ui/tooltip'; 11 | import { Button } from '@/components/ui/button'; 12 | import { Input } from '@/components/ui/input'; 13 | 14 | export function PasswordInput() { 15 | const [showPassword, setShowPassword] = useState(false); 16 | 17 | return ( 18 |
19 | 25 | 26 | 27 | 28 | 41 | 42 | 43 |

{showPassword ? 'Hide password' : 'Show password'}

44 |
45 |
46 |
47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as PopoverPrimitive from '@radix-ui/react-popover'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | const Popover = PopoverPrimitive.Root; 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger; 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )); 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 30 | 31 | export { Popover, PopoverTrigger, PopoverContent }; 32 | -------------------------------------------------------------------------------- /components/ui/profile-button.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import type { FC } from 'react'; 3 | 4 | interface ProfileButtonProps { 5 | imageUrl?: string; 6 | onClick: () => void; 7 | userEmail?: string; 8 | showEmail?: boolean; 9 | iconSize?: number; 10 | } 11 | 12 | export const ProfileButton: FC = ({ 13 | imageUrl, 14 | onClick, 15 | userEmail, 16 | showEmail = false, 17 | iconSize = 32, 18 | }) => { 19 | return ( 20 | 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as ProgressPrimitive from '@radix-ui/react-progress'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | function Progress({ 9 | className, 10 | value, 11 | ...props 12 | }: React.ComponentProps) { 13 | return ( 14 | 22 | 27 | 28 | ); 29 | } 30 | 31 | export { Progress }; 32 | -------------------------------------------------------------------------------- /components/ui/radio-group.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'; 5 | import { CircleIcon } from 'lucide-react'; 6 | 7 | import { cn } from '@/lib/utils'; 8 | 9 | function RadioGroup({ 10 | className, 11 | ...props 12 | }: React.ComponentProps) { 13 | return ( 14 | 19 | ); 20 | } 21 | 22 | function RadioGroupItem({ 23 | className, 24 | ...props 25 | }: React.ComponentProps) { 26 | return ( 27 | 35 | 39 | 40 | 41 | 42 | ); 43 | } 44 | 45 | export { RadioGroup, RadioGroupItem }; 46 | -------------------------------------------------------------------------------- /components/ui/screen-loader.tsx: -------------------------------------------------------------------------------- 1 | import { LoaderCircle } from 'lucide-react'; 2 | import type { FC } from 'react'; 3 | 4 | export const ScreenLoader: FC = () => { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | function ScrollArea({ 9 | className, 10 | children, 11 | ...props 12 | }: React.ComponentProps) { 13 | return ( 14 | 19 | 23 | {children} 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | function ScrollBar({ 32 | className, 33 | orientation = 'vertical', 34 | ...props 35 | }: React.ComponentProps) { 36 | return ( 37 | 50 | 54 | 55 | ); 56 | } 57 | 58 | export { ScrollArea, ScrollBar }; 59 | -------------------------------------------------------------------------------- /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 | function Separator({ 9 | className, 10 | orientation = 'horizontal', 11 | decorative = true, 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 25 | ); 26 | } 27 | 28 | export { Separator }; 29 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<'div'>) { 4 | return ( 5 |
10 | ); 11 | } 12 | 13 | export { Skeleton }; 14 | -------------------------------------------------------------------------------- /components/ui/slider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as SliderPrimitive from '@radix-ui/react-slider'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | const Slider = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 21 | 22 | 23 | 24 | 25 | )); 26 | Slider.displayName = SliderPrimitive.Root.displayName; 27 | 28 | export { Slider }; 29 | -------------------------------------------------------------------------------- /components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useTheme } from 'next-themes'; 4 | import { Toaster as Sonner, type ToasterProps } from 'sonner'; 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = 'system' } = useTheme(); 8 | 9 | return ( 10 | 22 | ); 23 | }; 24 | 25 | export { Toaster }; 26 | -------------------------------------------------------------------------------- /components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as SwitchPrimitive from '@radix-ui/react-switch'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | function Switch({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | 27 | 28 | ); 29 | } 30 | 31 | export { Switch }; 32 | -------------------------------------------------------------------------------- /components/ui/table-components.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Table: React.FC = ({ children }) => ( 4 |
5 | 6 | {children} 7 |
8 |
9 | ); 10 | 11 | export const Th: React.FC = ({ children }) => ( 12 | 13 | {children} 14 | 15 | ); 16 | 17 | export const Td: React.FC = ({ children }) => ( 18 | 19 | {children} 20 | 21 | ); 22 | -------------------------------------------------------------------------------- /components/ui/textarea-autosize.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | import type { FC } from 'react'; 3 | import ReactTextareaAutosize from 'react-textarea-autosize'; 4 | 5 | interface TextareaAutosizeProps { 6 | value: string; 7 | onValueChange: (value: string) => void; 8 | onClick?: () => void; 9 | 10 | textareaRef?: React.RefObject; 11 | className?: string; 12 | disabled?: boolean; 13 | 14 | placeholder?: string; 15 | minRows?: number; 16 | maxRows?: number; 17 | maxLength?: number; 18 | onKeyDown?: (event: React.KeyboardEvent) => void; 19 | onPaste?: (event: React.ClipboardEvent) => void; 20 | onCompositionStart?: (event: React.CompositionEvent) => void; 21 | onCompositionEnd?: (event: React.CompositionEvent) => void; 22 | } 23 | 24 | export const TextareaAutosize: FC = ({ 25 | value, 26 | onValueChange, 27 | onClick, 28 | disabled, 29 | textareaRef, 30 | className, 31 | placeholder = '', 32 | minRows = 1, 33 | maxRows = 6, 34 | maxLength, 35 | onKeyDown = () => {}, 36 | onPaste = () => {}, 37 | onCompositionStart = () => {}, 38 | onCompositionEnd = () => {}, 39 | }) => { 40 | return ( 41 | maxRows ? minRows : maxRows} 50 | placeholder={placeholder} 51 | value={value} 52 | disabled={disabled} 53 | onChange={(event) => onValueChange(event.target.value)} 54 | onKeyDown={onKeyDown} 55 | onPaste={onPaste} 56 | maxLength={maxLength} 57 | onCompositionStart={onCompositionStart} 58 | onCompositionEnd={onCompositionEnd} 59 | /> 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /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 |