diff --git a/MIGRATION.md b/MIGRATION.md deleted file mode 100644 index c11b494..0000000 --- a/MIGRATION.md +++ /dev/null @@ -1,236 +0,0 @@ -# Component Migration Guide - -## Overview - -All custom components have been migrated to use **shadcn/ui** while maintaining backward compatibility with existing code. - -## Migrated Components - -### Button Component - -**Location:** `src/components/Button.tsx` - -The Button component now wraps the shadcn/ui Button, providing: - -- ✅ All shadcn/ui features (accessibility, variants, Radix UI) -- ✅ Backward compatibility with original variant names -- ✅ Dark mode support -- ✅ Enhanced functionality (destructive, ghost, link variants) - -**Variant Mapping:** - -```tsx -// Original variants still work -'primary' → 'default' (shadcn) -'secondary' → 'secondary' (shadcn) -'outline' → 'outline' (shadcn) - -// New variants available -'ghost' → 'ghost' (shadcn) -'destructive' → 'destructive' (shadcn) -'link' → 'link' (shadcn) -``` - -**Size Mapping:** - -```tsx -'sm' → 'sm' (shadcn) -'md' → 'default' (shadcn) -'lg' → 'lg' (shadcn) -``` - -**Usage Examples:** - -Old code continues to work: - -```tsx - -``` - -New variants available: - -```tsx - - - - - -``` - -### Container Component - -**Location:** `src/components/Container.tsx` - -Updated to use the `cn()` utility function for better class name merging. - -**Changes:** - -- Uses `cn()` from `@/lib/utils` for proper Tailwind class merging -- Maintains same functionality and API -- Better handling of conditional classes - -**Usage (unchanged):** - -```tsx - - - -``` - -## Component Exports - -**Location:** `src/components/index.ts` - -All components are exported for easy imports: - -```tsx -// Custom wrapped components -import { Button, Container } from '@/components'; -// Direct shadcn/ui components -import { Card, CardContent, CardHeader, CardTitle } from '@/components'; -// Or import shadcn Button directly if needed -import { Button as ShadcnButton } from '@/components'; -``` - -## Benefits of Migration - -### 1. Enhanced Accessibility - -- Built on Radix UI primitives -- Proper ARIA attributes -- Keyboard navigation support - -### 2. Consistent Design System - -- Uses CSS variables for theming -- Automatic dark mode support -- Consistent spacing and sizing - -### 3. Better Developer Experience - -- TypeScript support with proper types -- Intellisense for all props -- Comprehensive variant system - -### 4. Backward Compatibility - -- Existing code continues to work -- No breaking changes to API -- Gradual migration path - -### 5. Future-Ready - -- Easy to add new shadcn components -- Maintained component library -- Active community support - -## Adding New shadcn Components - -1. Install required Radix UI dependencies: - - ```bash - npm install @radix-ui/[component-name] --legacy-peer-deps - ``` - -2. Create component file in `src/components/ui/[name].tsx` - -3. Copy code from [ui.shadcn.com](https://ui.shadcn.com/) - -4. Export in `src/components/index.ts`: - - ```tsx - export * from './ui/[name]'; - ``` - -5. Use directly or create custom wrapper for compatibility - -## Migration Checklist - -- [x] Button component migrated and wrapped -- [x] Container component updated with cn() -- [x] Component exports organized -- [x] Backward compatibility maintained -- [x] TypeScript types updated -- [x] Code formatted and linted -- [x] No errors in build -- [x] Documentation updated - -## Testing - -All existing code using Button and Container should work without changes: - -```tsx -// This still works ✅ - - -// This also works ✅ - - - -``` - -New features are available immediately: - -```tsx -// New variants ✨ - - - -// With icons (using lucide-react) ✨ -import { ArrowRight } from 'lucide-react'; - - -``` - -## Troubleshooting - -### Type Errors - -If you see TypeScript errors, ensure: - -- All imports are from `@/components` -- Variant names are correct -- Props match ButtonProps interface - -### Styling Issues - -If styles don't apply correctly: - -- Check that CSS variables are loaded (`src/styles/index.css`) -- Verify Tailwind config includes shadcn theme -- Use `cn()` for conditional classes - -### Dark Mode - -Dark mode is automatic via CSS variables. To customize: - -- Edit `src/styles/index.css` dark mode variables -- Or use the `useTheme` hook from `@/hooks` - -## Next Steps - -1. **Review the changes** in your dev server -2. **Test existing functionality** to ensure compatibility -3. **Explore new variants** available in Button -4. **Add more shadcn components** as needed -5. **Update other custom components** to use shadcn when beneficial - -## Resources - -- [shadcn/ui Documentation](https://ui.shadcn.com/) -- [Radix UI Components](https://www.radix-ui.com/) -- [Tailwind CSS](https://tailwindcss.com/) -- [Lucide Icons](https://lucide.dev/) diff --git a/index.html b/index.html index 74a95b0..ae310f8 100644 --- a/index.html +++ b/index.html @@ -187,7 +187,7 @@ "name": "What is stealth mode?", "acceptedAnswer": { "@type": "Answer", - "text": "Stealth mode allows you to operate the assistant discreetly during interviews. You can control everything via hotkeys, adjust window opacity, and position windows strategically—all without losing focus on your interview tab or application. Additionally, the window is not capturable in screenshots and remains invisible during full screen sharing, ensuring complete privacy during your interview." + "text": "Stealth mode allows you to operate the assistant discreetly during interviews. You can control everything via hotkeys, adjust window opacity, and position windows strategically-all without losing focus on your interview tab or application. Additionally, the window is not capturable in screenshots and remains invisible during full screen sharing, ensuring complete privacy during your interview." } }, { diff --git a/package-lock.json b/package-lock.json index ec7a1fd..8d12d9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,10 +12,13 @@ "@radix-ui/react-slot": "^1.2.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "github-markdown-css": "^5.2.0", "lucide-react": "^0.563.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-markdown": "^8.0.7", "react-router-dom": "^7.13.0", + "remark-gfm": "^3.0.1", "tailwind-merge": "^3.4.0" }, "devDependencies": { @@ -1723,6 +1726,15 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1730,6 +1742,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1737,6 +1758,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.33", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", @@ -1751,7 +1787,6 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, "license": "MIT" }, "node_modules/@types/react": { @@ -1775,6 +1810,12 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.55.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz", @@ -2222,6 +2263,16 @@ "postcss": "^8.1.0" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2351,6 +2402,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2368,6 +2429,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -2487,6 +2558,16 @@ "dev": true, "license": "MIT" }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -2563,7 +2644,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2577,6 +2657,19 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2584,6 +2677,15 @@ "dev": true, "license": "MIT" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -2591,6 +2693,15 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -2887,6 +2998,12 @@ "dev": true, "license": "MIT" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3044,6 +3161,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/github-markdown-css": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/github-markdown-css/-/github-markdown-css-5.9.0.tgz", + "integrity": "sha512-tmT5sY+zvg2302XLYEfH2mtkViIM1SWf2nvYoF5N1ZsO0V6B2qZTiw3GOzw4vpjLygK/KG35qRlPFweHqfzz5w==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3093,6 +3222,16 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz", + "integrity": "sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", @@ -3146,6 +3285,12 @@ "node": ">=0.8.19" } }, + "node_modules/inline-style-parser": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", + "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==", + "license": "MIT" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -3159,6 +3304,29 @@ "node": ">=8" } }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -3224,6 +3392,18 @@ "node": ">=0.12.0" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3314,6 +3494,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3451,6 +3640,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3482,145 +3681,953 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", "license": "MIT", - "engines": { - "node": ">= 8" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, + "node_modules/mdast-util-definitions": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz", + "integrity": "sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA==", "license": "MIT", "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "unist-util-visit": "^4.0.0" }, - "engines": { - "node": ">=8.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "dev": true, + "node_modules/mdast-util-find-and-replace": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz", + "integrity": "sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "license": "MIT", "engines": { - "node": ">=18" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", + "node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" }, - "engines": { - "node": "*" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, + "node_modules/mdast-util-gfm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz", + "integrity": "sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg==", "license": "MIT", "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-gfm-autolink-literal": "^1.0.0", + "mdast-util-gfm-footnote": "^1.0.0", + "mdast-util-gfm-strikethrough": "^1.0.0", + "mdast-util-gfm-table": "^1.0.0", + "mdast-util-gfm-task-list-item": "^1.0.0", + "mdast-util-to-markdown": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/nano-spawn": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", - "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==", - "dev": true, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz", + "integrity": "sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA==", "license": "MIT", - "engines": { - "node": ">=20.17" + "dependencies": { + "@types/mdast": "^3.0.0", + "ccount": "^2.0.0", + "mdast-util-find-and-replace": "^2.0.0", + "micromark-util-character": "^1.0.0" }, "funding": { - "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/mdast-util-gfm-footnote": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz", + "integrity": "sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ==", "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0", + "micromark-util-normalize-identifier": "^1.0.0" }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz", + "integrity": "sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ==", "license": "MIT", - "engines": { - "node": ">=0.10.0" + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, + "node_modules/mdast-util-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz", + "integrity": "sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg==", "license": "MIT", - "engines": { - "node": ">=0.10.0" + "dependencies": { + "@types/mdast": "^3.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "node_modules/mdast-util-gfm-task-list-item": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz", + "integrity": "sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-to-markdown": "^1.3.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz", + "integrity": "sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-definitions": "^5.0.0", + "micromark-util-sanitize-uri": "^1.1.0", + "trim-lines": "^3.0.0", + "unist-util-generated": "^2.0.0", + "unist-util-position": "^4.0.0", + "unist-util-visit": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz", + "integrity": "sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^3.0.0", + "mdast-util-to-string": "^3.0.0", + "micromark-util-decode-string": "^1.0.0", + "unist-util-visit": "^4.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz", + "integrity": "sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^1.0.0", + "micromark-extension-gfm-footnote": "^1.0.0", + "micromark-extension-gfm-strikethrough": "^1.0.0", + "micromark-extension-gfm-table": "^1.0.0", + "micromark-extension-gfm-tagfilter": "^1.0.0", + "micromark-extension-gfm-task-list-item": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz", + "integrity": "sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz", + "integrity": "sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q==", + "license": "MIT", + "dependencies": { + "micromark-core-commonmark": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz", + "integrity": "sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw==", + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz", + "integrity": "sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw==", + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz", + "integrity": "sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz", + "integrity": "sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ==", + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nano-spawn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", + "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "dev": true, "license": "MIT", "engines": { @@ -4062,6 +5069,33 @@ } } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4118,6 +5152,43 @@ "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-markdown": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.7.tgz", + "integrity": "sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/prop-types": "^15.0.0", + "@types/unist": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^2.0.0", + "prop-types": "^15.0.0", + "property-information": "^6.0.0", + "react-is": "^18.0.0", + "remark-parse": "^10.0.0", + "remark-rehype": "^10.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^0.4.0", + "unified": "^10.0.0", + "unist-util-visit": "^4.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -4189,6 +5260,53 @@ "node": ">=8.10.0" } }, + "node_modules/remark-gfm": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-3.0.1.tgz", + "integrity": "sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-gfm": "^2.0.0", + "micromark-extension-gfm": "^2.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.2.tgz", + "integrity": "sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^3.0.0", + "mdast-util-from-markdown": "^1.0.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-10.1.0.tgz", + "integrity": "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/mdast": "^3.0.0", + "mdast-util-to-hast": "^12.1.0", + "unified": "^10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -4324,6 +5442,18 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -4435,6 +5565,16 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -4491,6 +5631,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-object": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz", + "integrity": "sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.1.1" + } + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -4732,6 +5881,26 @@ "node": ">=8.0" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -4810,6 +5979,103 @@ "dev": true, "license": "MIT" }, + "node_modules/unified": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", + "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "bail": "^2.0.0", + "extend": "^3.0.0", + "is-buffer": "^2.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-generated": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.1.tgz", + "integrity": "sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", + "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.4.tgz", + "integrity": "sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", + "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0", + "unist-util-visit-parents": "^5.1.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", + "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-is": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -4858,6 +6124,54 @@ "dev": true, "license": "MIT" }, + "node_modules/uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + }, + "bin": { + "uvu": "bin.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/vfile": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", + "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "is-buffer": "^2.0.0", + "unist-util-stringify-position": "^3.0.0", + "vfile-message": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", + "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "unist-util-stringify-position": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", @@ -5074,6 +6388,16 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index 5a76467..94385ea 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,9 @@ "lucide-react": "^0.563.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-markdown": "^8.0.7", + "remark-gfm": "^3.0.1", + "github-markdown-css": "^5.2.0", "react-router-dom": "^7.13.0", "tailwind-merge": "^3.4.0" }, @@ -55,4 +58,4 @@ "typescript-eslint": "^8.55.0", "vite": "^6.0.5" } -} +} \ No newline at end of file diff --git a/public/media/code.test.mp4 b/public/media/code.test.mp4 index 7654177..d440321 100644 Binary files a/public/media/code.test.mp4 and b/public/media/code.test.mp4 differ diff --git a/public/media/docs/app-overview-stealth.jpg b/public/media/docs/app-overview-stealth.jpg new file mode 100644 index 0000000..60cfc44 Binary files /dev/null and b/public/media/docs/app-overview-stealth.jpg differ diff --git a/public/media/docs/app-overview.jpg b/public/media/docs/app-overview.jpg new file mode 100644 index 0000000..b979aa5 Binary files /dev/null and b/public/media/docs/app-overview.jpg differ diff --git a/public/media/docs/architecture-diagram.svg b/public/media/docs/architecture-diagram.svg new file mode 100644 index 0000000..23f12e1 --- /dev/null +++ b/public/media/docs/architecture-diagram.svg @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + Power Interview — Architecture Overview + + + + + Your Windows Machine + + + + Electron App + React + TypeScript + + + • Transcript panel + • Reply suggestions panel + • Code suggestions panel + • Video / camera panel + • Control panel & settings + • Stealth mode + • Global hotkeys + • Session management + • Payment / credits page + + + + ASR Agent (Python) + + • Mic + loopback audio capture + • WebSocket → ASR backend + • ZeroMQ → Electron app + + + + VCam Agent (Python) + + • Webcam frame capture + • WebRTC → GPU face swap + • → OBS Virtual Camera + + + + Audio Control Agent + + • Audio device enumeration + • Latency-sync delay buffer + • → VB-Cable Input + + + + + + ZeroMQ + + + + + ZeroMQ + + + + + ZeroMQ + + + + Cloud Backend + + + + Auth Service + login / signup / tokens + + + ASR Service + speech-to-text streaming + + + LLM Service + reply + code suggestions + + + Face Swap GPU Service + WebRTC video processing + + + Payment Service + credits / billing + + + + + WebSocket + + + + HTTPS / WebSocket + + + + WebRTC + + + + + HTTPS / WebSocket to cloud + + ZeroMQ (local IPC) + + ASR Agent + + VCam Agent + + Audio Agent + diff --git a/public/media/docs/audio-options.png b/public/media/docs/audio-options.png new file mode 100644 index 0000000..7ddb8a5 Binary files /dev/null and b/public/media/docs/audio-options.png differ diff --git a/public/media/docs/audio-sync-diagram.svg b/public/media/docs/audio-sync-diagram.svg new file mode 100644 index 0000000..dec94bf --- /dev/null +++ b/public/media/docs/audio-sync-diagram.svg @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + Audio / Video Synchronization — Face Swap Mode + + + + + + 🎤 Mic + + + + 📷 Webcam + + + + + + + Ring Buffer + depth = measured latency + + + + + + VB-Cable Input + (virtual audio cable) + + + + + + VB-Cable Output + → Meeting app mic + + + + + + VCam Agent + timestamps frame + + + + + + GPU Backend + face swap + + + + + + OBS Virtual Camera + → Meeting app cam + + + + + measured latency + (send_time → receive_time) + + + + ZeroMQ + latency signal + + + + Audio Control Agent + adjusts buffer depth dynamically + + + + + + + ✓ Synchronized + Video latency ≈ Audio delay + Lip-sync maintained + diff --git a/public/media/docs/code-suggestions.mp4 b/public/media/docs/code-suggestions.mp4 new file mode 100644 index 0000000..d440321 Binary files /dev/null and b/public/media/docs/code-suggestions.mp4 differ diff --git a/public/media/docs/configuration-dialog.png b/public/media/docs/configuration-dialog.png new file mode 100644 index 0000000..b094396 Binary files /dev/null and b/public/media/docs/configuration-dialog.png differ diff --git a/public/media/docs/export-example.docx b/public/media/docs/export-example.docx new file mode 100644 index 0000000..c0774ed Binary files /dev/null and b/public/media/docs/export-example.docx differ diff --git a/public/media/docs/export-example.png b/public/media/docs/export-example.png new file mode 100644 index 0000000..b37ee4f Binary files /dev/null and b/public/media/docs/export-example.png differ diff --git a/public/media/docs/export-interview.png b/public/media/docs/export-interview.png new file mode 100644 index 0000000..bcffa06 Binary files /dev/null and b/public/media/docs/export-interview.png differ diff --git a/public/media/docs/face-swap-example.png b/public/media/docs/face-swap-example.png new file mode 100644 index 0000000..3242ccf Binary files /dev/null and b/public/media/docs/face-swap-example.png differ diff --git a/public/media/docs/face-swap-options.png b/public/media/docs/face-swap-options.png new file mode 100644 index 0000000..6aeb7e5 Binary files /dev/null and b/public/media/docs/face-swap-options.png differ diff --git a/public/media/docs/face-swap-start.png b/public/media/docs/face-swap-start.png new file mode 100644 index 0000000..b5f106c Binary files /dev/null and b/public/media/docs/face-swap-start.png differ diff --git a/public/media/docs/face-swap-toggle.png b/public/media/docs/face-swap-toggle.png new file mode 100644 index 0000000..1b80442 Binary files /dev/null and b/public/media/docs/face-swap-toggle.png differ diff --git a/public/media/docs/meeting-audio-device.png b/public/media/docs/meeting-audio-device.png new file mode 100644 index 0000000..8dfafb8 Binary files /dev/null and b/public/media/docs/meeting-audio-device.png differ diff --git a/public/media/docs/meeting-video-device.png b/public/media/docs/meeting-video-device.png new file mode 100644 index 0000000..fd78802 Binary files /dev/null and b/public/media/docs/meeting-video-device.png differ diff --git a/public/media/docs/reference-photo-guide.svg b/public/media/docs/reference-photo-guide.svg new file mode 100644 index 0000000..46f6d85 --- /dev/null +++ b/public/media/docs/reference-photo-guide.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + Reference Photo Guidelines — Face Swap + + + + ✓ Good + + + + + + + + + + Front-facing + well-lit + + + + + + + + + + + Neutral bg + 512px+ res + + + + + + + + + Single subject + clear face + + + + + + ✗ Avoid + + + + + + + Sunglasses + + + + Blurry image + + + Blurry / low-res + + + + + + + + + Side profile + + + + Ideal photo requirements: + • Clear, front-facing, well-lit shot • No sunglasses, hats, or obstructions + • Neutral or calm expression • Minimum 512×512 pixels resolution + • Single subject in frame • No heavy makeup or filters + diff --git a/public/media/docs/reply-suggestions.mp4 b/public/media/docs/reply-suggestions.mp4 new file mode 100644 index 0000000..621ce2d Binary files /dev/null and b/public/media/docs/reply-suggestions.mp4 differ diff --git a/public/media/docs/stealth-mode.png b/public/media/docs/stealth-mode.png new file mode 100644 index 0000000..400bf24 Binary files /dev/null and b/public/media/docs/stealth-mode.png differ diff --git a/public/media/docs/window-positioning.svg b/public/media/docs/window-positioning.svg new file mode 100644 index 0000000..5162ec4 --- /dev/null +++ b/public/media/docs/window-positioning.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + Window Positioning — Numpad Layout + Ctrl+Shift + number key to snap window position + + + + + + + + + Top-Left + Ctrl+Shift+7 + + + Top-Center + Ctrl+Shift+8 + + + Top-Right + Ctrl+Shift+9 + + + + Mid-Left + Ctrl+Shift+4 + + + Center + Ctrl+Shift+5 + + + Mid-Right + Ctrl+Shift+6 + + + + Bottom-Left + Ctrl+Shift+1 + + + Bottom-Center + Ctrl+Shift+2 + + + + Bottom-Right + Ctrl+Shift+3 + ★ Recommended + + + + + + + + Fine Position: Ctrl+Alt+Shift+Arrow | Resize: Ctrl+Win+Shift+Arrow + Window is always hidden from screen capture — safe to place anywhere + diff --git a/public/sitemap.xml b/public/sitemap.xml index 245b136..d6015d8 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -31,10 +31,29 @@ 0.5 - https://www.powerinterviewai.com/how-to-use - 2026-02-18 + + https://www.powerinterviewai.com/docs + 2026-02-27 + weekly + 0.9 + + + https://www.powerinterviewai.com/docs/getting-started + 2026-02-27 monthly - 0.5 + 0.7 + + + https://www.powerinterviewai.com/docs/usage + 2026-02-27 + monthly + 0.6 + + + https://www.powerinterviewai.com/docs/api + 2026-02-27 + monthly + 0.6 https://www.powerinterviewai.com/why-choose diff --git a/src/App.tsx b/src/App.tsx index 22e2dbe..609f7ff 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,33 +2,38 @@ import React from 'react'; import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; +import ScrollToTop from '@/components/ScrollToTop'; + import Benefits from './pages/Benefits'; import Contact from './pages/Contact'; import FAQ from './pages/FAQ'; import Features from './pages/Features'; import Home from './pages/Home'; -import HowToUse from './pages/HowToUse'; import LegalNotice from './pages/LegalNotice'; import Pricing from './pages/Pricing'; import PrivacyPolicy from './pages/PrivacyPolicy'; import TermsOfService from './pages/TermsOfService'; import WhyChoose from './pages/WhyChoose'; +import DocsIndex from './pages/docs'; +import DocsPage from './pages/docs/[slug]'; const App: React.FC = () => { return ( + } /> } /> } /> } /> } /> - } /> } /> } /> } /> } /> } /> + } /> + } /> } /> diff --git a/src/components/custom/Container.tsx b/src/components/Container.tsx similarity index 100% rename from src/components/custom/Container.tsx rename to src/components/Container.tsx diff --git a/src/components/ScrollToTop.tsx b/src/components/ScrollToTop.tsx new file mode 100644 index 0000000..eec8559 --- /dev/null +++ b/src/components/ScrollToTop.tsx @@ -0,0 +1,26 @@ +import { useEffect } from 'react'; + +import { useLocation } from 'react-router-dom'; + +export default function ScrollToTop(): null { + const { pathname, hash } = useLocation(); + + useEffect(() => { + // If there's a hash, try to scroll to the element; otherwise scroll to top + if (hash) { + // Small timeout to allow elements to mount + setTimeout(() => { + const el = document.querySelector(hash); + if (el) { + (el as HTMLElement).scrollIntoView({ behavior: 'smooth' }); + return; + } + window.scrollTo({ top: 0, left: 0, behavior: 'auto' }); + }, 0); + } else { + window.scrollTo({ top: 0, left: 0, behavior: 'auto' }); + } + }, [pathname, hash]); + + return null; +} diff --git a/src/components/Seo.tsx b/src/components/Seo.tsx new file mode 100644 index 0000000..c13dd91 --- /dev/null +++ b/src/components/Seo.tsx @@ -0,0 +1,52 @@ +import { useEffect } from 'react'; + +interface SeoProps { + title?: string; + description?: string; + url?: string; + image?: string; +} + +const setMeta = (name: string, content: string | undefined, attr = 'name') => { + if (!content) return; + let el = document.querySelector(`meta[${attr}="${name}"]`) as HTMLMetaElement | null; + if (!el) { + el = document.createElement('meta'); + el.setAttribute(attr, name); + document.head.appendChild(el); + } + el.content = content; +}; + +export default function Seo({ title, description, url, image }: SeoProps) { + useEffect(() => { + const baseTitle = 'Power Interview AI'; + if (title) { + document.title = `${title} - ${baseTitle}`; + } else { + document.title = `${baseTitle} - Privacy-First AI Interview Assistant`; + } + + setMeta('description', description); + setMeta('og:title', title || baseTitle, 'property'); + setMeta('og:description', description, 'property'); + setMeta('og:url', url, 'property'); + setMeta('og:image', image, 'property'); + setMeta('twitter:title', title || baseTitle); + setMeta('twitter:description', description); + setMeta('twitter:image', image); + + // canonical link + if (url) { + let link = document.querySelector("link[rel='canonical']") as HTMLLinkElement | null; + if (!link) { + link = document.createElement('link'); + link.rel = 'canonical'; + document.head.appendChild(link); + } + link.href = url; + } + }, [title, description, url, image]); + + return null; +} diff --git a/src/components/custom/sections/HowToUseSection.tsx b/src/components/custom/sections/HowToUseSection.tsx deleted file mode 100644 index 419ad46..0000000 --- a/src/components/custom/sections/HowToUseSection.tsx +++ /dev/null @@ -1,208 +0,0 @@ -import React from 'react'; - -import Container from '@/components/custom/Container'; -import { Card, CardContent } from '@/components/ui/card'; - -export const HowToUseSection: React.FC = () => { - return ( -
- -
-

- How to Use Power Interview AI -

-

- Follow these simple steps to ace your next interview -

-
- -
-
- {/* Step 1 */} - - -
- 1 -
-
-

Run Power Interview AI App

-

- Download and launch the Power Interview AI desktop application on your computer. -

-
-
-
- - {/* Step 2 */} - - -
- 2 -
-
-

Login or Create Account

-

- Sign in to your account. New users get 30 free credits to start! If you don't - have an account yet, create one in just a few clicks. -

-
-
-
- - {/* Step 3 */} - - -
- 3 -
-
-

Configure Interview Profile

-

Set up your interview profile with:

-
    -
  • - - - Personal Info: Upload photo (will be used for{' '} - Face Swap), set name - -
  • -
  • - - - Profile Documents: Add your CV, resume, bio, and work - experience - -
  • -
  • - - - Interview Context: Include job description, role details, - and any previous dialogue - -
  • -
-
-
-
- - {/* Step 4 */} - - -
- 4 -
-
-

Configure Audio & Video Devices

-

- Select your microphone, speakers, and camera. Test your audio levels and video - quality to ensure everything works perfectly. -

-
-
-
- - {/* Step 5 */} - - -
- 5 -
-
-

Start Assistant

-

- IMPORTANT: Click "Start Assistant" - to activate AI transcription and suggestions. Credits will be consumed at 10 - credits per minute while the assistant is running. -

-
-
-
- - {/* Step 6 */} - - -
- 6 -
-
-

Begin Your Interview

-

- Only after starting the assistant, - join your interview call. The AI will provide real-time transcription, smart - suggestions, and code assistance throughout your interview. -

-
-
-
- - {/* Step 7 */} - - -
- 7 -
-
-

Stop Assistant After Interview

-

- When your interview ends, click "Stop Assistant" to immediately stop credit - consumption. Review your session summary and AI feedback. -

-
-
-
- - {/* Step 8 */} - - -
- 8 -
-
-

Export Interview Data

-

- Export your interview transcript, AI suggestions, and performance insights for - future reference and improvement. -

-
-
-
- - {/* Step 9 */} - - -
- 9 -
-
-

Close the App

-

- Safely close the application. All your data is stored locally on your device for - maximum privacy. -

-
-
-
-
- -
-

💡 Pro Tip: Credit Management

-

- Credits are only consumed while the assistant is actively running. Start it right - before your interview and stop it immediately after to maximize your credit usage. At - 10 credits per minute, you can have up to{' '} - 60 minutes of AI assistance with the - Starter plan! -

-
-
-
-
- ); -}; diff --git a/src/components/custom/sections/index.ts b/src/components/custom/sections/index.ts deleted file mode 100644 index b4e4a5f..0000000 --- a/src/components/custom/sections/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export { Header } from './Header'; -export { HeroSection } from './HeroSection'; -export { FeaturesSection } from './FeaturesSection'; -export { BenefitsSection } from './BenefitsSection'; -export { WhyChooseSection } from './WhyChooseSection'; -export { HowToUseSection } from './HowToUseSection'; -export { PricingSection } from './PricingSection'; -export { FAQSection } from './FAQSection'; -export { ContactSection } from './ContactSection'; -export { LegalNoticeSection } from './LegalNoticeSection'; -export { FooterSection } from './FooterSection'; diff --git a/src/components/custom/sections/BenefitsSection.tsx b/src/components/sections/BenefitsSection.tsx similarity index 99% rename from src/components/custom/sections/BenefitsSection.tsx rename to src/components/sections/BenefitsSection.tsx index 8bc68aa..3623004 100644 --- a/src/components/custom/sections/BenefitsSection.tsx +++ b/src/components/sections/BenefitsSection.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { ArrowRight } from 'lucide-react'; -import Container from '@/components/custom/Container'; +import Container from '@/components/Container'; import { Button } from '@/components/ui/button'; import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; @@ -199,3 +199,5 @@ export const BenefitsSection: React.FC = () => { ); }; + +export default BenefitsSection; diff --git a/src/components/custom/sections/ContactSection.tsx b/src/components/sections/ContactSection.tsx similarity index 98% rename from src/components/custom/sections/ContactSection.tsx rename to src/components/sections/ContactSection.tsx index 3d04d66..916aade 100644 --- a/src/components/custom/sections/ContactSection.tsx +++ b/src/components/sections/ContactSection.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { SiDiscord, SiGithub, SiProtonmail, SiTelegram, SiX } from '@icons-pack/react-simple-icons'; -import Container from '@/components/custom/Container'; +import Container from '@/components/Container'; import { Card, CardContent } from '@/components/ui/card'; export const ContactSection: React.FC = () => { @@ -148,3 +148,5 @@ export const ContactSection: React.FC = () => { ); }; + +export default ContactSection; diff --git a/src/components/custom/sections/FAQSection.tsx b/src/components/sections/FAQSection.tsx similarity index 88% rename from src/components/custom/sections/FAQSection.tsx rename to src/components/sections/FAQSection.tsx index 96c81da..be4242f 100644 --- a/src/components/custom/sections/FAQSection.tsx +++ b/src/components/sections/FAQSection.tsx @@ -2,9 +2,8 @@ import React from 'react'; import { SiProtonmail } from '@icons-pack/react-simple-icons'; -import Container from '@/components/custom/Container'; +import Container from '@/components/Container'; import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; interface FAQSectionProps { openFaqIndex: number | null; @@ -51,7 +50,7 @@ const faqData = [ { question: 'What is stealth mode?', answer: - 'Stealth mode allows you to operate the assistant discreetly during interviews. You can control everything via hotkeys, adjust window opacity, and position windows strategically—all without losing focus on your interview tab or application. Additionally, the window is not capturable in screenshots and remains invisible during full screen sharing, ensuring complete privacy during your interview.', + 'Stealth mode allows you to operate the assistant discreetly during interviews. You can control everything via hotkeys, adjust window opacity, and position windows strategically-all without losing focus on your interview tab or application. Additionally, the window is not capturable in screenshots and remains invisible during full screen sharing, ensuring complete privacy during your interview.', }, { question: 'How do credits work?', @@ -97,13 +96,13 @@ export const FAQSection: React.FC = ({
{faqData.map((faq, index) => ( - +
{openFaqIndex === index && ( - -

{faq.answer}

-
+
{faq.answer}
)} - +
))}
@@ -141,3 +138,5 @@ export const FAQSection: React.FC = ({ ); }; + +export default FAQSection; diff --git a/src/components/custom/sections/FeaturesSection.tsx b/src/components/sections/FeaturesSection.tsx similarity index 98% rename from src/components/custom/sections/FeaturesSection.tsx rename to src/components/sections/FeaturesSection.tsx index 2190a71..4a13a00 100644 --- a/src/components/custom/sections/FeaturesSection.tsx +++ b/src/components/sections/FeaturesSection.tsx @@ -10,7 +10,7 @@ import { UserLock, } from 'lucide-react'; -import Container from '@/components/custom/Container'; +import Container from '@/components/Container'; import { Button } from '@/components/ui/button'; import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; @@ -151,3 +151,5 @@ export const FeaturesSection: React.FC = () => { ); }; + +export default FeaturesSection; diff --git a/src/components/custom/sections/FooterSection.tsx b/src/components/sections/FooterSection.tsx similarity index 97% rename from src/components/custom/sections/FooterSection.tsx rename to src/components/sections/FooterSection.tsx index b358711..994f624 100644 --- a/src/components/custom/sections/FooterSection.tsx +++ b/src/components/sections/FooterSection.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { SiDiscord, SiGithub, SiProtonmail, SiTelegram, SiX } from '@icons-pack/react-simple-icons'; import { Link, useLocation } from 'react-router-dom'; -import Container from '@/components/custom/Container'; +import Container from '@/components/Container'; interface FooterSectionProps { scrollToSection?: (sectionId: string) => void; // optional; if provided we still support in-page scroll on home @@ -123,9 +123,7 @@ export const FooterSection: React.FC = ({ scrollToSection })
  • Documentation @@ -262,3 +260,5 @@ export const FooterSection: React.FC = ({ scrollToSection }) ); }; + +export default FooterSection; diff --git a/src/components/custom/sections/Header.tsx b/src/components/sections/Header.tsx similarity index 89% rename from src/components/custom/sections/Header.tsx rename to src/components/sections/Header.tsx index a0561f9..e1f5623 100644 --- a/src/components/custom/sections/Header.tsx +++ b/src/components/sections/Header.tsx @@ -2,9 +2,9 @@ import React from 'react'; import { SiGithub } from '@icons-pack/react-simple-icons'; import { X } from 'lucide-react'; -import { Link, useLocation } from 'react-router-dom'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; -import Container from '@/components/custom/Container'; +import Container from '@/components/Container'; import { Button } from '@/components/ui/button'; interface HeaderProps { @@ -23,6 +23,15 @@ export const Header: React.FC = ({ setMobileMenuOpen, }) => { const location = useLocation(); + const navigate = useNavigate(); + + const handleGetStarted = () => { + if (location.pathname === '/') { + window.scrollTo({ top: 0, behavior: 'smooth' }); + } else { + navigate('/'); + } + }; return (
    = ({ )} - {location.pathname === '/' && scrollToSection ? ( - - ) : ( - - How to Use - - )} + {/* 'How to Use' moved into Docs; link removed */} {location.pathname === '/' && scrollToSection ? ( - @@ -283,21 +282,7 @@ export const Header: React.FC = ({ )} - {location.pathname === '/' && scrollToSection ? ( - - ) : ( - - How to Use - - )} + {/* 'How to Use' moved into Docs; link removed */} {location.pathname === '/' && scrollToSection ? ( - @@ -370,3 +359,5 @@ export const Header: React.FC = ({
    ); }; + +export default Header; diff --git a/src/components/custom/sections/HeroSection.tsx b/src/components/sections/HeroSection.tsx similarity index 98% rename from src/components/custom/sections/HeroSection.tsx rename to src/components/sections/HeroSection.tsx index 4ba61e6..f2f1015 100644 --- a/src/components/custom/sections/HeroSection.tsx +++ b/src/components/sections/HeroSection.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { Check, ChevronLeft, ChevronRight, Copy, Pause, Play } from 'lucide-react'; -import Container from '@/components/custom/Container'; +import Container from '@/components/Container'; import { Button } from '@/components/ui/button'; // Media carousel data @@ -193,7 +193,7 @@ export const HeroSection: React.FC = ({ scrollToSection }) =>

    Ace technical and behavioral interviews with real-time transcription, intelligent - suggestions, and cutting-edge face swap technology—all while maintaining your privacy. + suggestions, and cutting-edge face swap technology-all while maintaining your privacy.

    {/* Installation Command */} @@ -341,3 +341,5 @@ export const HeroSection: React.FC = ({ scrollToSection }) => ); }; + +export default HeroSection; diff --git a/src/components/custom/sections/LegalNoticeSection.tsx b/src/components/sections/LegalNoticeSection.tsx similarity index 98% rename from src/components/custom/sections/LegalNoticeSection.tsx rename to src/components/sections/LegalNoticeSection.tsx index a34ca34..2798655 100644 --- a/src/components/custom/sections/LegalNoticeSection.tsx +++ b/src/components/sections/LegalNoticeSection.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import Container from '@/components/custom/Container'; +import Container from '@/components/Container'; import { Card, CardContent } from '@/components/ui/card'; export const LegalNoticeSection: React.FC = () => { @@ -121,3 +121,5 @@ export const LegalNoticeSection: React.FC = () => { ); }; + +export default LegalNoticeSection; diff --git a/src/components/custom/sections/PricingSection.tsx b/src/components/sections/PricingSection.tsx similarity index 95% rename from src/components/custom/sections/PricingSection.tsx rename to src/components/sections/PricingSection.tsx index 4a610aa..28a9f65 100644 --- a/src/components/custom/sections/PricingSection.tsx +++ b/src/components/sections/PricingSection.tsx @@ -1,8 +1,9 @@ import React, { useEffect, useState } from 'react'; import { SiCheckmarx } from '@icons-pack/react-simple-icons'; +import { useLocation, useNavigate } from 'react-router-dom'; -import Container from '@/components/custom/Container'; +import Container from '@/components/Container'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Plan } from '@/types'; @@ -47,10 +48,20 @@ const calculateDiscount = (plan: Plan, starterPricePerCredit: number): number => }; export const PricingSection: React.FC = () => { + const location = useLocation(); + const navigate = useNavigate(); const [plans, setPlans] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const handleGetStarted = () => { + if (location.pathname === '/') { + window.scrollTo({ top: 0, behavior: 'smooth' }); + } else { + navigate('/'); + } + }; + useEffect(() => { const fetchPlans = async () => { try { @@ -203,7 +214,7 @@ export const PricingSection: React.FC = () => { @@ -216,3 +227,5 @@ export const PricingSection: React.FC = () => { ); }; + +export default PricingSection; diff --git a/src/components/custom/sections/WhyChooseSection.tsx b/src/components/sections/WhyChooseSection.tsx similarity index 98% rename from src/components/custom/sections/WhyChooseSection.tsx rename to src/components/sections/WhyChooseSection.tsx index 2f8dbb8..00777b9 100644 --- a/src/components/custom/sections/WhyChooseSection.tsx +++ b/src/components/sections/WhyChooseSection.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { SiCheckmarx } from '@icons-pack/react-simple-icons'; import { ArrowRight } from 'lucide-react'; -import Container from '@/components/custom/Container'; +import Container from '@/components/Container'; import { Button } from '@/components/ui/button'; import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; @@ -161,3 +161,5 @@ export const WhyChooseSection: React.FC = () => { ); }; + +export default WhyChooseSection; diff --git a/src/components/sections/index.ts b/src/components/sections/index.ts new file mode 100644 index 0000000..ea03112 --- /dev/null +++ b/src/components/sections/index.ts @@ -0,0 +1,10 @@ +export * from './BenefitsSection'; +export * from './ContactSection'; +export * from './FAQSection'; +export * from './FeaturesSection'; +export * from './FooterSection'; +export * from './Header'; +export * from './HeroSection'; +export * from './LegalNoticeSection'; +export * from './PricingSection'; +export * from './WhyChooseSection'; diff --git a/src/content/docs/best-practices.md b/src/content/docs/best-practices.md new file mode 100644 index 0000000..d7507ac --- /dev/null +++ b/src/content/docs/best-practices.md @@ -0,0 +1,171 @@ +# Best Practices + +Getting the most from Power Interview requires more than just turning it on. The following practices will help you achieve accurate transcription, relevant AI suggestions, and a seamless interview experience. + +--- + +## Profile & Context Setup + +### Write a Detailed CV / Profile + +The quality of AI reply suggestions depends directly on the detail in your profile. A one-line summary produces generic answers. A thorough profile produces relevant, personalized ones. + +**Do:** + +- Include your job titles, years of experience, and key responsibilities +- List specific technologies, frameworks, and languages you know +- Mention notable projects with measurable outcomes +- Include soft skills and working preferences when relevant + +**Avoid:** + +- Pasting a raw PDF-extracted text with garbled formatting +- Leaving the profile field empty or with placeholder text + +### Tailor the Context to Each Interview + +The **Context** field (job description / role requirements) directly shapes the tone and subject matter of reply suggestions. Before each interview: + +1. Click your profile name in the control panel and select **Configuration**. +2. Replace the previous job description with the one from the current role. +3. Highlight the key skills the role requires - the AI will weight those more heavily. + +The more aligned your context is to the actual interview, the more targeted and useful the suggestions will be. + +--- + +## Audio Setup + +### Use a Dedicated Microphone + +The built-in laptop microphone often picks up keyboard sounds, fan noise, and room echo, all of which reduce transcription accuracy. A USB headset or desktop microphone with noise cancellation produces significantly better results. + +### How Interviewer Audio is Captured + +Power Interview captures the interviewer's voice via Windows WASAPI audio loopback - it reads whatever audio is playing through your system speakers automatically. No extra routing software is needed for transcription to work for both channels. + +### Test Audio Before the Interview + +Run a short test session a few minutes before the interview starts: + +- Speak a few sentences and verify your voice appears in the transcript. +- Play a short audio clip through your speakers and verify the loopback channel picks it up. +- If the microphone channel is missing, re-check that your physical microphone is selected in **Audio Options**. + +--- + +## Face Swap Setup + +> **Critical: Start Power Interview assistant before you join the meeting.** +> Video call platforms (Zoom, Teams, Google Meet) detect available camera and audio devices only at launch time. If Power Interview, OBS Virtual Camera, or VB-Cable are not running when you join, the meeting app will not see those virtual devices and face swap cannot be activated. Always start the assistant and confirm the virtual devices are visible in your meeting app settings **before** clicking Join. + +### Use OBS Virtual Camera Correctly + +Make sure **OBSStudio** is is installed **before** your video call app opens. Many video call platforms (Zoom, Teams) only detect camera devices at launch time. If you start OBS after joining the call, you may need to leave and rejoin. + +### Select the Correct Devices in Your Meeting App + +When face swap is enabled, your meeting platform must use the virtual devices, not your physical ones: + +- **Camera**: select **OBS Virtual Camera** +- **Microphone**: select **CABLE Output (VB-Audio Virtual Cable)** + +![Select OBS Virtual Camera as camera in your meeting app](/media/docs/meeting-video-device.png) + +![Select CABLE Output as microphone in your meeting app](/media/docs/meeting-audio-device.png) + +Do not select your physical camera or physical microphone in the meeting platform when using the face swap feature. + +### Use a Good Reference Photo + +The quality of the face swap result is highly sensitive to the reference photo you upload. + +**Ideal photo characteristics:** + +- Clear, well-lit, front-facing shot +- Neutral or calm expression +- No sunglasses, heavy makeup, hats, or occlusions +- Resolution of at least 512×512 pixels +- Single subject (no other people in the frame) + +Avoid blurry photos, side profiles, or images where the face is partially obscured. + +![Reference photo guidelines — good vs bad examples](/media/docs/reference-photo-guide.svg) + +### Keep Credits Topped Up + +Credits are consumed while the assistant is running. Check your balance in the app (shown in the video panel and the stealth status bar) before your interview. Topping up in advance avoids the session being automatically stopped mid-interview due to depleted credits. + +--- + +## Code Suggestion Workflow + +### Capture Multiple Angles of the Problem + +A single screenshot of a long problem statement often misses important details. Use up to four screenshots to cover: + +- The full problem description +- Sample inputs and expected outputs +- Any relevant constraints or examples below the fold + +Press `Ctrl+Alt+Shift+P` for each capture, then `Ctrl+Alt+Shift+Enter` to submit. + +--- + +## Stealth Mode & Window Placement + +The Power Interview window is always excluded from screen capture and screen share — interviewers cannot see it regardless of where it is placed or whether stealth mode is on. + +### Set Up Your Window Position Before the Interview + +Decide where the Power Interview window will sit on your screen before the interview begins. Use `Ctrl+Shift+1–9` to snap it to your preferred screen zone. The bottom-right or top-right corners are often least intrusive. + +Position it so it is: + +- Visible to you at a glance +- Not overlapping the content you need to look at (your IDE, the interviewer's video) + +### Enable Stealth Mode During Coding Challenges + +Turn on stealth mode (`Ctrl+Shift+M`) whenever you need to type or interact with your IDE without the Power Interview window accidentally capturing focus. This keeps your keystrokes going to the right place. + +### Use a Dual-Monitor Setup + +If you have two monitors, run your video call on one screen and Power Interview on the other for maximum comfort — the assistant is always on a separate display. + +### Practice Hotkeys Beforehand + +The hotkey shortcuts are the core interface during an interview. Practice them before your interview so they feel natural: + +- Scroll through suggestions without looking at the keyboard +- Capture and submit a screenshot from memory +- Toggle stealth mode and opacity without hesitation + +Any fumbling with shortcuts during an interview will be distracting and costly. + +--- + +## Pre-Interview Checklist + +Before every interview, run through this checklist: + +- [ ] Profile (name, CV) is up to date +- [ ] Context field has the correct job description for this role +- [ ] **Power Interview is started before joining the meeting** +- [ ] Microphone is selected and producing transcript output (test session run) +- [ ] Camera and face swap photo are configured (if using face swap) +- [ ] **Power Interview assistant is running before joining the meeting** (if using face swap — critical) +- [ ] OBS Virtual Camera is started and selected in the video call app (if using face swap) +- [ ] CABLE Output is selected as microphone in the video call app (if using face swap) +- [ ] Credits balance is sufficient for the session (if using face swap) +- [ ] Power Interview window is positioned and hotkeys tested + +--- + +## Ethical Use + +Use it responsibly: + +- The face swap feature is intended for privacy and personal presentation. Do not use it to impersonate another person. +- AI suggestions are a starting point - adapt them in your own words. Parroting a suggestion verbatim can appear unnatural. +- Ensure your use complies with the terms of service of the platform hosting the interview and with any applicable laws in your jurisdiction. diff --git a/src/content/docs/how-it-works.md b/src/content/docs/how-it-works.md new file mode 100644 index 0000000..b07708b --- /dev/null +++ b/src/content/docs/how-it-works.md @@ -0,0 +1,278 @@ +# How It Works + +This page explains the internal architecture of Power Interview: the components that run on your machine, how they communicate with each other and with backend services, and how each feature is delivered. + +--- + +## Overview + +Power Interview is composed of three layers: + +1. **Electron Desktop App** - the user interface, window control, and configuration +2. **Python Agents** - lightweight local processes for audio capture and virtual camera streaming +3. **Cloud Backend** - AI inference, ASR transcription, and face swap GPU processing + +These layers communicate in real time using ZeroMQ (local inter-process), WebSocket (streaming), and HTTPS REST (configuration and auth). + +--- + +## Component Diagram + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Your Windows Machine │ +│ │ +│ ┌─────────────────────┐ ┌────────────────────────┐ │ +│ │ Electron App │◄──────►│ ASR Agent (Python) │ │ +│ │ (React + Node.js) │ ZeroMQ │ Audio capture + │ │ +│ │ │ │ WebSocket to backend │ │ +│ │ • UI & settings │ └────────────────────────┘ │ +│ │ • Session state │ │ +│ │ • IPC handlers │ ┌────────────────────────┐ │ +│ │ │◄──────►│ VCam Agent (Python) │ │ +│ │ │ ZeroMQ │ Webcam → WebRTC → │ │ +│ │ │ │ face swap → OBS VCam │ │ +│ │ │ └────────────────────────┘ │ +│ │ │ │ +│ │ │ ┌────────────────────────┐ │ +│ │ │◄──────►│ Audio Control Agent │ │ +│ │ │ ZeroMQ │ Voice sync with │ │ +│ └─────────────────────┘ │ face-swapped stream │ │ +│ │ └────────────────────────┘ │ +│ │ HTTPS / WebSocket │ +└───────────┼─────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Cloud Backend │ +│ │ +│ • Auth service (login / session) │ +│ • ASR service (speech-to-text streaming) │ +│ • LLM service (reply + code suggestions) │ +│ • Face swap GPU service (WebRTC video + image output) │ +│ • Payment service (credits) │ +└─────────────────────────────────────────────────────────────┘ +``` + +![Architecture overview — components and connections](/media/docs/architecture-diagram.svg) + +--- + +## Desktop App (Electron) + +The desktop app runs as an Electron process with two main parts: + +### Main Process + +The main process (Node.js) is responsible for: + +- **Authentication** - login, signup, token refresh, change password +- **Configuration store** - reads and writes user settings (profile, device names, face swap toggle, resolution) using Electron Store (local encrypted storage) +- **Session management** - starts and stops the Python agents as child processes +- **IPC handlers** - receives calls from the renderer (UI) and relays them to backend services or local agents +- **App state** - tracks `runningState` (Idle / Starting / Running / Stopping), transcript list, reply suggestions, code suggestions, and credits +- **Health checks** - polls backend API and GPU server every 5 seconds; also refreshes credit balance on each client ping +- **Auto-updater** - checks for new releases using `electron-updater` and notifies the UI + +### Renderer Process + +The renderer (React + TypeScript) renders the UI and communicates with the main process over Electron IPC: + +- **Main page** - shows the transcript panel, reply suggestions panel, code suggestions panel, and video panel side by side; layout adapts based on which panels have content +- **Control panel** - audio device selector, face swap toggle + options, start/stop button, export and clear tools, profile dropdown +- **Configuration dialog** - profile name, CV text, job context, reference photo upload +- **Payment page** - buy credits, payment history, payment status tabs +- **Stealth mode** - when active, the window stops stealing focus so your keyboard and mouse stay on your coding challenge or video call; the main panel collapses to a minimal status bar showing running state, credit balance, and hotkey reference + +--- + +## ASR Agent (Python) + +The ASR (Automatic Speech Recognition) agent is a compiled Python process that runs locally and coordinates all audio capture and transcription. + +### Audio Capture - Two Channels + +The agent opens two simultaneous audio streams: + +| Channel | Source | Purpose | +| ---------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| Loopback | Windows WASAPI default loopback device | Captures whatever audio is playing through the system speakers - the interviewer's voice from the video call | +| Microphone | Physical microphone selected by the user | Captures the user's own voice | + +Both streams are sampled at 16 kHz mono (resampled from the native device rate using `scipy`). + +### ASR Streaming + +Both audio channels are forwarded to the backend ASR service over a single WebSocket connection. The backend processes each channel independently and returns: + +- **Partial transcripts** - low-latency intermediate results shown immediately in the UI +- **Final transcripts** - confirmed results replacing the partial ones + +### ZeroMQ Publishing + +Finalized transcripts are published to the Electron main process over a ZeroMQ PUB socket. The main process subscribes to this socket, aggregates transcript messages into the app state, and pushes updates to the renderer via IPC. + +--- + +## VCam Agent (Python) + +The VCam (Virtual Camera) agent handles face swap video streaming. It is only active when face swap is toggled on. + +### Frame Capture + +The agent reads frames from the user's selected physical webcam using OpenCV at the configured resolution (640×360, 640×480, or 1280×720). + +### WebRTC Streaming + +Captured frames are sent to the backend face swap GPU server over WebRTC (low-latency peer-to-peer video streaming). The GPU server: + +1. Receives the raw webcam frames +2. Detects and replaces the user's face with the reference photo face using deep learning +3. Optionally applies face enhancement post-processing +4. Streams the processed frames back to the VCam agent + +### OBS Virtual Camera Output + +The processed frames received from the backend are pushed to OBS Virtual Camera using `pyvirtualcam`. OBS Virtual Camera makes the output available as a camera device that any application (Zoom, Teams, Google Meet, etc.) can select. + +--- + +## Audio Control Agent (Python) + +The audio control agent is responsible for two things: enumerating audio devices so the user can select the correct microphone, and — critically — keeping the user's real voice synchronized with the face-swapped video stream. + +### The Synchronization Problem + +When face swap is active, the VCam agent sends raw webcam frames to the cloud GPU server and receives processed frames back. This round-trip introduces a variable network delay (typically 80–250 ms) before the face-swapped video is available for output. + +If the user's microphone audio is routed directly and without compensation, the interviewer will see the processed face move out of sync with the voice — the lips on screen will appear to lag behind the words being spoken. + +### How Synchronization Works + +The Audio Control Agent solves this with a **timestamped delay buffer**: + +1. **Frame timestamp tagging** — the VCam agent stamps each outgoing frame with a monotonic clock timestamp before sending it to the GPU server +2. **Round-trip measurement** — when the processed frame arrives back, the agent computes the actual frame latency: `latency = receive_time − send_timestamp` +3. **Audio delay buffer** — the Audio Control Agent receives the current measured latency over ZeroMQ from the VCam agent and holds incoming microphone audio in a rolling ring buffer for exactly that duration before releasing it to the virtual audio output +4. **Dynamic adjustment** — latency is re-measured on every frame and the buffer depth is smoothed with an exponential moving average to avoid abrupt audio jumps from transient network spikes + +![Audio / video synchronization diagram](/media/docs/audio-sync-diagram.svg) + +``` +Microphone ──► Ring Buffer (depth = measured video latency) + │ + ▼ (delayed by N ms) + VB-Cable Input ──► VB-Cable Output ◄── interviewer's app selects this + +Webcam ──► VCam Agent ──► GPU Server ──► OBS Virtual Camera ◄── interviewer's app selects this + │ │ + └── timestamp ─┘ (latency measured here) + │ + ▼ + Audio Control Agent adjusts buffer depth +``` + +### VB-Cable Virtual Audio Output + +The agent writes the delay-compensated audio stream to **VB-Cable Input**, a virtual audio cable driver ([VB-Audio VB-Cable](https://vb-audio.com/Cable/)). VB-Cable works as a loopback: anything written to the **VB-Cable Input** device is instantly readable from the **VB-Cable Output** device. + +Meeting apps (Zoom, Teams, Google Meet) expose VB-Cable Output in their microphone selector. The user selects it once in the meeting app settings; from that point on, the app always receives the synchronized, delay-compensated voice stream. + +> **When face swap is active, select both of the following in your meeting app:** +> +> - **Camera / Video** → `OBS Virtual Camera` (the face-swapped video) +> - **Microphone / Audio input** → `CABLE Output (VB-Audio Virtual Cable)` (the synced voice) +> +> Selecting either device alone will result in mismatched audio or video. + +--- + +## AI Suggestion Flow + +### Reply Suggestions + +When the ASR backend delivers a final transcript segment, the Electron main process forwards the latest transcript context, combined with the user's profile (name, CV) and job context, to the LLM suggestion API. The API streams back a text response which is displayed word-by-word in the reply suggestions panel. + +### Code Suggestions + +When the user presses `Ctrl+Alt+Shift+P`, the main process takes a screenshot of the entire screen using `screenshot-desktop` and stores it temporarily. Up to 4 screenshots can be queued. When the user presses `Ctrl+Alt+Shift+Enter`, all queued screenshots are sent as image attachments to the LLM code suggestion API. The API analyzes the images and streams back a code solution which is rendered with syntax highlighting. + +--- + +## Inter-Process Communication (IPC) + +All communication between the renderer and main process goes through Electron's IPC bridge exposed via the `preload.cjs` script. The main categories of IPC channels are: + +| Category | Examples | +| ---------------- | ------------------------------------------------------- | +| Auth | login, logout, signup, change-password | +| Config | get-config, update-config | +| App State | get-app-state, state-update events | +| Tools | export-transcript, clear-all | +| Code Suggestions | capture-screenshot, clear-images, start-generate | +| Health Check | triggered automatically on startup | +| Auto-Updater | get-version, check-for-updates | +| Payment | get-currencies, create-payment, get-payment-history | +| Window | toggle-stealth, set-opacity, move-window, resize-window | + +--- + +## Data Storage + +All persistent data is stored locally on your machine: + +| Data | Storage | Location | +| --------------------------------------------------- | -------------- | --------------------------- | +| Login token, session | Electron Store | `%AppData%\power-interview` | +| Profile name, CV, job context, reference photo | Electron Store | `%AppData%\power-interview` | +| Device preferences (microphone, camera, resolution) | Electron Store | `%AppData%\power-interview` | +| Feature toggles (face swap, face enhance) | Electron Store | `%AppData%\power-interview` | + +No transcripts or suggestions are persisted to disk unless you explicitly export them. Nothing is stored on external servers after a session ends. + +--- + +## Session Lifecycle + +``` +User clicks Start + │ + ▼ +Main process validates config (profile, microphone, face swap photo if needed) + │ + ▼ +Spawns ASR Agent → ASR Agent opens microphone + loopback streams → WebSocket to backend + │ + ▼ (if face swap enabled) +Spawns VCam Agent → VCam Agent opens webcam → WebRTC to GPU backend → pushes to OBS VCam + │ + ▼ +Spawns Audio Control Agent → measures VCam round-trip latency → buffers microphone audio → writes to VB-Cable Input → readable as VB-Cable Output in meeting app + │ + ▼ +Running state set to Running; Renderer shows active panels + │ + │ (during session) + │ ┌─ Audio loopback → ASR → ZeroMQ → Transcript panel + │ ├─ Microphone → ASR → ZeroMQ → Transcript panel + │ ├─ Transcript final → LLM API → Reply suggestion panel (streaming) + │ ├─ Screenshots → LLM API → Code suggestion panel (streaming) + │ ├─ VCam frame latency → ZeroMQ → Audio Control Agent buffer depth + │ └─ Microphone → Audio Control Agent delay buffer → VB-Cable Input → VB-Cable Output + │ +User clicks Stop (or presses Ctrl+Shift+Q) + │ + ▼ +Main process sends stop signal to agents → agents shut down audio/video streams + │ + ▼ +Running state set to Idle; Panels retain last session data until cleared +``` + +--- + +## Credits System + +Credits are consumed while the assistant is running, covering AI inference (reply and code suggestions), transcription, and face swap GPU processing. The credit balance is fetched from the backend on every health-check client ping (every 5 seconds while running). If the balance reaches zero during a session, the assistant stops and a notification is shown in the UI. + +Credits are purchased from the **Buy Credits** page inside the app, which calls the backend payment API. Payments are processed externally; the credit balance updates automatically once a payment is confirmed. diff --git a/src/content/docs/installation.md b/src/content/docs/installation.md new file mode 100644 index 0000000..2fd595d --- /dev/null +++ b/src/content/docs/installation.md @@ -0,0 +1,197 @@ +# Installation + +This page covers everything you need to install and run Power Interview, whether you are using the prebuilt release or building from source. + +--- + +## Option A - Use Install Commandline (Recommended) + +On the [Home page](https://powerinterviewai.com/) you can see full command line to install latest release version like this: + +```bash +curl -L -o PowerInterview-Setup-x.x.x.exe https://github.com/PowerInterviewAI/client/releases/latest/download/PowerInterview-Setup-x.x.x.exe && start "" "PowerInterview-Setup-x.x.x.exe" +``` + +Just open a terminal, copy & paste the command, and run it. + +This will download the latest installer and launch it immediately. Follow the installer prompts to complete installation, then launch Power Interview from your Start Menu or desktop shortcut. + +--- + +## Option B - Prebuilt Release + +Download the latest installer from the [Releases page](https://github.com/PowerInterviewAI/power-interview-assistant/releases/latest) and run it. The installer packages the Electron app and all compiled Python agents together - no additional setup is required. + +After installation, launch **Power Interview** from your Start Menu or desktop shortcut, sign in with your account, and proceed to [First-Run Setup](#first-run-setup). + +--- + +## Option C - Build from Source + +Use this path if you want to run or modify the development version. + +### System Requirements + +| Requirement | Minimum Version | +| ---------------- | --------------- | +| Operating System | Windows 10 / 11 | +| Node.js | 18 or higher | +| Python | 3.12 | +| npm | 8 or higher | + +### External Software (Optional, for specific features) + +| Software | Purpose | +| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| [OBS Studio with Virtual Camera](https://obsproject.com/) | Required for the face swap / virtual camera feature | +| [VB-Audio Virtual Cable](https://vb-audio.com/Cable/) | Required for face swap: routes your microphone audio through the virtual camera output so the meeting platform receives your voice | + +These are only needed if you intend to use the **face swap** feature. Transcription of both your voice and the interviewer's voice works without them. + +--- + +### Step 1 - Clone the Repository + +```bash +git clone https://github.com/PowerInterviewAI/power-interview-assistant +cd power-interview-assistant +``` + +--- + +### Step 2 - Install Node.js Dependencies + +```bash +cd app +npm install +cd .. +``` + +--- + +### Step 3 - Install Python Dependencies + +It is recommended to use a virtual environment: + +```bash +python -m venv venv +venv\Scripts\activate # Windows +pip install -r requirements.txt +``` + +The key Python packages installed are: + +| Package | Purpose | +| ----------------- | ----------------------------------------------- | +| `pyaudiowpatch` | Audio capture including system (loopback) audio | +| `pyvirtualcam` | Virtual camera output | +| `opencv-python` | Video frame processing | +| `pyzmq` | Inter-process communication between agents | +| `websockets` | Real-time streaming to backend services | +| `sounddevice` | Audio device enumeration and routing | +| `scipy` / `numpy` | Signal processing | +| `loguru` | Logging | + +--- + +### Step 4 - Build the Python Agents + +> **Important:** This step must be run inside a **Visual Studio Developer Command Prompt** (not a regular terminal or PowerShell). Nuitka uses the MSVC compiler toolchain which is only available in that environment. +> +> Open it from the Start Menu: **Visual Studio → Developer Command Prompt for VS 20xx**. Then activate your virtual environment inside it before running the commands below. + +The Python agents are compiled into standalone executables using Nuitka. Build each agent separately or all at once: + +**Build a specific agent:** + +```bat +:: ASR (transcription) agent +python -m scripts.build_asr_agent + +:: Audio control agent +python -m scripts.build_audio_control_agent + +:: Virtual camera agent +python -m scripts.build_vcam_agent +``` + +**Build all agents at once:** + +```bat +python -m scripts.build_all +``` + +Compiled agent executables will be placed in the `build/agents/` directory. + +--- + +### Step 5 - Run the Application (Development Mode) + +```bash +cd app +npm run start +``` + +This starts both the Vite dev server and the Electron window. The app opens in development mode with hot reload enabled. + +Alternatively, to run with the browser content visible in a separate Chrome window (useful for debugging the renderer): + +```bash +npm run electron:dev-show +``` + +--- + +### Step 6 - Build a Production Installer (Optional) + +To produce a self-contained Windows installer: + +```bash +# From the repo root +python -m scripts.build_electron_app +``` + +Or build everything (agents + app) in one command: + +```bash +python -m scripts.build_all +``` + +The packaged installer will be output to the `build/` directory. + +--- + +## First-Run Setup + +After launching the app for the first time (whether from the installer or source): + +1. **Sign in** with your Power Interview account credentials. If you don't have an account, sign up using the application. + +2. **Open Configuration** from the profile dropdown (bottom-left of the control panel). + +3. **Set up your profile:** + - Enter your **name** (required) + - Paste your **CV / resume** or bio summary (required) + - Paste the **job description** or role context for your upcoming interview (recommended) + + ![Configuration dialog — Profile tab](/media/docs/configuration-dialog.png) + +4. **Select your microphone:** + - Click the microphone icon in the control panel to open **Audio Options** + - Choose your physical microphone from the dropdown + - The interviewer's voice is captured automatically via Windows system audio loopback - no extra device configuration needed + + ![Audio Options — microphone selector](/media/docs/audio-options.png) + +5. **Face swap setup** (optional - requires OBS Virtual Camera and VB-Audio Virtual Cable): + - Set a face swap reference photo in the **Configuration** dialog + - Click the face icon in the control panel to open **Face Swap Options** + - Select your physical webcam and preferred resolution + - Toggle **Face Swap** on from the control panel + - In your video call app, select **OBS Virtual Camera** as the camera and **CABLE Output (VB-Audio Virtual Cable)** as the microphone + + | Camera | Microphone | + | :------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------: | + | ![Select OBS Virtual Camera in your meeting app](/media/docs/meeting-video-device.png) | ![Select CABLE Output as microphone in your meeting app](/media/docs/meeting-audio-device.png) | + +After completing setup, click **Start** to begin a session. diff --git a/src/content/docs/introduction.md b/src/content/docs/introduction.md new file mode 100644 index 0000000..5065f4d --- /dev/null +++ b/src/content/docs/introduction.md @@ -0,0 +1,122 @@ +# Introduction + +Power Interview is a privacy-first AI assistant that helps you perform confidently during live technical and behavioral interviews. It runs as a desktop application on your machine and combines real-time transcription, intelligent AI suggestions, and optional face swap technology - all designed to keep your data under your control. + +| Normal Mode | Stealth Mode | +| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| ![Power Interview — Main Application Interface](/media/docs/app-overview.jpg) | ![Power Interview — Main Application Interface](/media/docs/app-overview-stealth.jpg) | + +--- + +## What It Does + +Power Interview listens to your interview conversation, transcribes it in real time, and surfaces contextual suggestions so you can respond with clarity. For technical roles, it can analyze coding problems on your screen and generate solution. For behavioral rounds, it draws on your profile - your name, CV, and the job description - to generate personalized, natural-sounding responses. + +The face swap feature lets you replace your camera feed with a chosen photo of your preferred appearance, routed through a virtual camera that any video-conferencing app can use. + +--- + +## Core Features + +### Real-Time Transcription + +- **Dual-Channel**: Your microphone captures your voice; system audio loopback captures the interviewer automatically +- Streams transcription live via WebSocket with low latency +- Each line is labeled by speaker (your name or "Interviewer") with a timestamp +- Auto-scroll keeps up with new lines; a toggle lets you pause and scroll back + +### AI Reply Suggestions + +- Generates interview answers personalized to your CV and job description +- Streams suggestions in real time as the conversation progresses +- Considers the full conversation context, not just the last question +- Produces human-like responses based on your profile + +### AI Code Suggestions + +- Captures one or multiple screenshots of your screen (up to 4) to read the problem +- Sends the screenshots to an AI model that generates a suggested solution +- Displays the result with syntax highlighting inside the app +- All triggered without ever switching focus away from your interview window + +### Face Swap (Virtual Camera) + +- Replaces your webcam feed with a face-swapped version using a chosen reference photo +- Output is delivered through OBS Virtual Camera - works with Zoom, Google Meet, Teams, and others +- Optional AI-powered face enhancement for more natural results +- Configurable video resolution (640×360, 640×480, 1280×720) +- Requires an active credits balance; balance is shown live in the app + +![Face Swap Options dialog](/media/docs/face-swap-options.png) + +### Stealth Mode + +- The Power Interview window is **always hidden from screen capture and screen share** — interviewers can never see it regardless of mode +- Stealth mode keeps the window from receiving focus, so your keyboard and mouse stay locked on your coding challenge or video call +- A minimal status bar shows running state, credit balance, and active hotkeys +- A toggleable low-opacity overlay lets you glance at suggestions without switching focus +- Entire workflow controllable by keyboard shortcuts - no mouse required, no lost focus + +![Stealth mode status bar](/media/docs/stealth-mode.png) + +### Interview Export + +- After a session, export a complete interview report as a Word document (`.docx`) +- Report includes an AI-generated summary, full timestamped transcript with speaker labels, and all reply suggestions + +### Credits & Payments + +- Credits are consumed while the assistant is running (AI suggestions, transcription, and face swap) +- Credit balance is displayed live inside the app +- Buy credits directly from the **Buy Credits** page within the app, with payment history and status tracking + +### Global Hotkeys + +Every function in Power Interview is accessible from any window via keyboard shortcuts: + +| Action | Shortcut | +| -------------------------------- | ------------------------------- | +| Stop all & exit stealth | `Ctrl+Shift+Q` | +| Toggle stealth mode | `Ctrl+Shift+M` | +| Toggle window opacity | `Ctrl+Shift+N` | +| Position window (numpad layout) | `Ctrl+Shift+1` – `Ctrl+Shift+9` | +| Move window | `Ctrl+Alt+Shift+Arrow` | +| Resize window | `Ctrl+Win+Shift+Arrow` | +| Scroll reply suggestions down/up | `Ctrl+Shift+J` / `Ctrl+Shift+K` | +| Scroll code suggestions down/up | `Ctrl+Shift+U` / `Ctrl+Shift+I` | +| Capture screenshot | `Ctrl+Alt+Shift+P` | +| Submit screenshots | `Ctrl+Alt+Shift+Enter` | +| Clear screenshots | `Ctrl+Alt+Shift+X` | + +--- + +## How It Is Built + +Power Interview consists of three layers running together on your machine: + +| Layer | Technology | Purpose | +| ---------------- | ----------------------------- | -------------------------------------------- | +| Desktop UI | Electron + React + TypeScript | User interface and configuration | +| Local Agents | Python 3.12 | Audio capture, virtual camera, audio routing | +| Backend Services | Cloud (separate) | AI/LLM inference, face swap, ASR processing | + +The desktop client communicates with the local Python agents over ZeroMQ and with cloud services over WebSocket and HTTPS. Transcripts, screenshots, and profile data are sent to the backend only when you actively request a suggestion. Nothing is stored remotely in a persistent way. + +--- + +## Privacy at a Glance + +- All credentials, session tokens, and profile data are stored locally using Electron Store +- Your CV, job descriptions, and configurations never leave your device unless you explicitly trigger a suggestion +- Transcripts are not stored on external servers after a session ends + +--- + +## What You Need + +Before getting started, ensure you have the following: + +- A Power Interview account (sign up at using the application) +- A Windows machine +- A working microphone and (optionally) webcam +- **For face swap only**: [OBS Studio](https://obsproject.com/) and [VB-Audio Virtual Cable](https://vb-audio.com/Cable/) installed diff --git a/src/content/docs/troubleshooting.md b/src/content/docs/troubleshooting.md new file mode 100644 index 0000000..a8ace8a --- /dev/null +++ b/src/content/docs/troubleshooting.md @@ -0,0 +1,227 @@ +# Troubleshooting + +This page covers the most common issues users encounter with Power Interview and how to resolve them. + +--- + +## Installation & Startup + +### The app does not launch after installation + +**Possible causes and fixes:** + +- **Missing runtime**: Ensure you are on Windows 10 or Windows 11. Older Windows versions are not supported. +- **Antivirus blocking**: Some antivirus programs quarantine Electron apps or Nuitka-compiled executables. Add the Power Interview installation folder to your antivirus exclusion list and try launching again. +- **Corrupted install**: Uninstall the app, delete any leftover files from `%AppData%\power-interview`, and reinstall. + +### The app opens but shows a blank white screen + +This typically means the renderer process failed to load. + +- Restart the app. +- If running from source, ensure the Vite dev server started correctly on port 5173 before Electron attempted to connect to it. +- Check for Node.js version compatibility - Node.js 18 or higher is required. + +--- + +## Authentication + +### Cannot log in - "Invalid credentials" error + +- Verify your email and password are correct at [powerinterviewai.com](https://www.powerinterviewai.com/). +- If you recently reset your password, use the new password. Old sessions may be cached; log out completely and log in again. +- Check that your internet connection is active. + +### Session expires immediately after login + +- Your system clock may be out of sync. JWT tokens are time-sensitive. Open Windows settings and enable **Set time automatically**. + +--- + +## Backend & Service Connectivity + +### Cannot connect to the Backend or GPU service + +The app checks connectivity to backend services on startup and periodically during a session. If a service is unreachable, the relevant feature (transcription, AI suggestions, face swap) will not work. + +**Steps to diagnose:** + +1. Check your internet connection. +2. Visit [powerinterviewai.com](https://www.powerinterviewai.com/) in a browser to confirm the service is not under maintenance. +3. Check if a firewall or VPN is blocking outbound WebSocket connections (ports 443 or 8080 are typically used). +4. Disable your VPN temporarily and retry. +5. If the GPU service is unreachable but the backend is reachable, the face swap feature will not be available, but transcription and AI suggestions will still work. + +--- + +## Transcription + +### No transcription appearing at all + +1. Confirm the session is **started** (the Start button should show as active / Stop should be visible). +2. Open **Audio Options** from the control panel (microphone icon) and verify your physical microphone is selected. + + ![Audio Options — verify the correct microphone is selected](/media/docs/audio-options.png) + +3. Speak and check if your operating-system volume indicator shows sound input from the selected device (Windows Sound settings → Recording). +4. Ensure the backend is reachable - transcription is processed on the backend. Try visiting [powerinterviewai.com](https://www.powerinterviewai.com/) to rule out a service outage. +5. Restart the session. + +### Only your voice is being transcribed (not the interviewer's) + +The interviewer's voice is captured via Windows WASAPI audio loopback - whatever is playing through your system speakers is automatically picked up. If the interviewer's channel is silent: + +1. Make sure your video call is actually playing audio through your Windows default playback device (e.g., not routing audio only to a Bluetooth device that isn't the system default). +2. Check your Windows Volume Mixer: the video call app's audio level should not be muted or at zero. +3. Restart the session after verifying audio is coming through the speakers. + +### Transcription is inaccurate or garbled + +- Move to a quieter environment or use a better microphone. +- Avoid speaking too quickly; natural pacing improves ASR accuracy significantly. +- Check whether background noise sources (fans, AC, keyboard) are being picked up by your microphone. Use a directional or noise-cancelling microphone. + +### Transcription has significant delay + +- A brief delay (1–3 seconds) is normal for streaming ASR. Longer delays indicate a network issue. +- Check your upload bandwidth. The ASR agent streams audio data continuously to the backend. +- If using a VPN, try disabling it or switching to a VPN server with lower latency. + +--- + +## AI Suggestions + +### No reply suggestions are generated + +- Verify your **Profile** (CV / resume) and **Context** (job description) fields are filled in. The AI requires this information to generate useful responses. +- Confirm the backend is reachable (see [Cannot connect to the Backend or GPU service](#cannot-connect-to-the-backend-or-gpu-service)). +- Ensure the session is active and transcription is running. Suggestions require conversation data. + +### Suggestions are generic and not relevant to the interview + +- Your **Profile** field may be too sparse. Add more detail about your experience, role history, and skills. +- Your **Context** field may not match the current interview. Update it with the actual job description before the interview starts. +- If you are interviewing for a technical role, include the specific tech stack in the context field. + +### Suggestions are cut off mid-sentence + +- This can happen when the session is stopped during streaming. Restart the session and request a new suggestion. +- Poor network conditions can interrupt streaming. Check your connection. + +--- + +## Code Suggestions + +### Screenshot capture produces a blank or black image + +- Some applications use hardware acceleration that prevents software-level screen capture. + - In Chrome/Edge: launch with `--disable-gpu-sandbox` or use the built-in window capture workaround. + - For other apps: try taking a screenshot using the Windows Snip & Sketch tool first to confirm the issue is application-specific. +- Ensure the target window is not minimized when you press `Ctrl+Alt+Shift+P`. + +### Code suggestion is incorrect or does not address the problem + +- Capture additional screenshots that include edge cases, constraints, or sample I/O (`Ctrl+Alt+Shift+P` for each). +- Clear the previous set of screenshots (`Ctrl+Alt+Shift+X`) before capturing images for a new problem. +- Zoom into the relevant portion of the problem before capturing so the model receives higher-resolution text. + +--- + +## Face Swap & Virtual Camera + +### OBS Virtual Camera is not appearing in the video call app + +- Start OBS and enable **Virtual Camera** in OBS **before** opening the video call app. Most video call apps detect cameras only at startup. +- Ensure OBS Virtual Camera is installed (it is bundled with OBS Studio 26+, but must be explicitly installed from the OBS Tools menu on some older versions). +- In the video call app, look for a camera source named **OBS Virtual Camera**. If it does not appear, restart both OBS and the video call app. + + ![Select OBS Virtual Camera as camera in your meeting app](/media/docs/meeting-video-device.png) + + ![Select CABLE Output as microphone in your meeting app](/media/docs/meeting-audio-device.png) + +### The face swap output is misaligned, jittery, or flickering + +- Use a better reference photo: clear, front-facing, well-lit, with no obstructions. +- Enable **Face Enhancement** in the Face Swap Options dialog for additional post-processing. +- Check your GPU usage. If the GPU is overloaded with other tasks, processing frames will stutter. Close GPU-intensive applications running in the background. +- Reduce the output resolution in face swap settings if performance is the bottleneck. + +### The face swap video is out of sync with audio + +Video processing introduces a small amount of latency. If the face swap video and your audio are noticeably out of sync in the video call: + +- Check your network connection. High latency or packet loss between your machine and the GPU backend increases processing delay. +- Reduce the video resolution in Face Swap Options (try 640×360 instead of 1280×720) to decrease processing time. +- Close other GPU-intensive applications on your machine to free GPU resources for the backend. + +### Face swap starts but the output shows the original face (no swap) + +- Confirm a face swap **photo is uploaded** in the Configuration dialog (profile dropdown → Configuration). Without a reference photo, the feature cannot swap faces. +- Confirm the GPU service is reachable. Face swap processing runs on the backend GPU server — if it is unavailable, stop and restart the assistant once connectivity is restored. +- Stop and restart the face swap feature from the control panel. + + ![Face Swap Options dialog — verify reference photo is uploaded](/media/docs/face-swap-options.png) + +--- + +## Hotkeys + +### Hotkeys are not working + +- Power Interview registers global hotkeys through Electron. Another application may already be using the same key combination. + - Check for conflicting applications: screen recorders, game overlays, other productivity tools. + - Quit potential conflicting apps and test whether the hotkeys register. +- On some systems, `Ctrl+Shift+1–9` may conflict with browser tab management shortcuts. Ensure your browser is not in focus trapping these keys. +- Restart Power Interview to re-register the hotkeys. + +### Pressing `Ctrl+Shift+Q` closes the browser tab instead of the assistant + +- This shortcut only fires correctly when Power Interview has successfully registered its global hotkey. If it is not registered (due to a conflict), the OS or foreground app may interpret the key combination instead. +- Use the Stop button in the UI as a fallback. + +--- + +## Window Management + +### The app window went off-screen and cannot be found + +Use the window positioning shortcuts to bring it back: + +- `Ctrl+Shift+5` - Center of the screen +- `Ctrl+Shift+1` - Bottom-left corner + +If the window is still not visible, right-click the Power Interview icon in the taskbar and select **Move**, then use arrow keys to drag it into view. + +--- + +## Data & Privacy + +### I want to clear session data (transcripts and suggestions) + +Click the **Clear** button (trash icon) in the control panel toolbar. This removes all transcripts, reply suggestions, and code suggestions from the current session. Your profile, credentials, and configuration are not affected. + +### I want to sign out and remove my account data + +Click your profile name in the control panel and select **Sign Out**. Local data such as tokens and configuration are managed by Electron Store in your user profile folder (`%AppData%\power-interview`). You can delete this folder manually after signing out to remove all locally stored data. + +### The app is logging me out automatically + +Your session token may have expired. Log in again. If this happens repeatedly, check your system clock is synchronized correctly as session tokens rely on accurate timestamps. + +--- + +## Getting Further Help + +If none of the above resolves your issue: + +- **Email**: [power-interview@protonmail.com](mailto:power-interview@protonmail.com) +- **Discord**: [discord.gg/TJJp5azK7Z](https://discord.gg/TJJp5azK7Z) +- **Telegram**: [t.me/+uQuuBdrsIYBjY2Qx](https://t.me/+uQuuBdrsIYBjY2Qx) +- **GitHub Issues**: Open an issue at [github.com/PowerInterviewAI/power-interview-assistant](https://github.com/PowerInterviewAI/power-interview-assistant) + +When reporting a bug, include: + +- Your Windows version +- Power Interview version (shown in the app title bar or About screen) +- A description of what you expected vs. what happened +- Relevant logs from `%AppData%\power-interview\logs\` if available diff --git a/src/content/docs/usage.md b/src/content/docs/usage.md new file mode 100644 index 0000000..c00405e --- /dev/null +++ b/src/content/docs/usage.md @@ -0,0 +1,242 @@ +# Usage + +This page describes how to use Power Interview during a live interview session, including transcription, AI suggestions, code assistance, face swap, stealth mode, and window management. + +--- + +## Starting a Session + +1. Launch Power Interview. +2. Review configuration and settings to ensure your profile is complete and devices are set up correctly. +3. Click the **Start** button to activate transcription and AI assistance. + +Once started, the app begins capturing audio from your configured microphone (your voice) and the system audio loopback (the interviewer's voice). Both channels are streamed to the ASR backend in real time and displayed in the transcript panel. + +To stop the session at any time, click **Stop** in the UI or press `Ctrl+Shift+Q` from any window (this also exits stealth mode if active). + +![Power Interview — main UI during an active session](/media/docs/app-overview.png) + +--- + +## Transcription + +### How It Works + +- Your voice is captured through your selected physical microphone. +- The interviewer's voice is captured automatically via Windows system audio loopback (whatever is playing through your speakers from the video call). No additional device configuration is needed. +- Both streams are sent to the ASR backend over WebSocket and returned as live transcription. +- Each line is labeled by speaker name: your configured name or "Interviewer", with a timestamp. + +--- + +## AI Reply Suggestions + +Reply suggestions are generated based on the current transcript, your profile (name, CV), and the job context you configured. + +### Getting a Suggestion + +Suggestions are triggered automatically as the AI determines a response is useful, or you can request one manually through the UI. The suggestion streams into the reply panel word by word in real time. + +![Reply Suggestions panel — streamed in real time](/media/docs/reply-suggestions.mp4) + +### Scrolling Suggestions + +You can use the keyboard to scroll through reply suggestion content without leaving your video call window: + +| Action | Shortcut | +| ----------- | -------------- | +| Scroll up | `Ctrl+Shift+K` | +| Scroll down | `Ctrl+Shift+J` | + +--- + +## AI Code Suggestions + +For technical interviews with coding problems, Power Interview can analyze your screen and suggest solutions. + +### Workflow + +1. When a coding problem appears on screen (in your browser, IDE, or shared screen), press `Ctrl+Alt+Shift+P` to take a screenshot. +2. You can capture up to **4 screenshots** to provide more context (e.g., multiple parts of the problem due overflowed content or different tabs). +3. Press `Ctrl+Alt+Shift+Enter` to submit the screenshots for analysis. +4. The AI processes the images and streams a suggested solution with syntax-highlighted code into the code suggestion panel. +5. To clear the captured screenshots (e.g., mistakes), press `Ctrl+Alt+Shift+X` to clear all captured screenshots. + +![Code Suggestions panel — syntax-highlighted solution streamed in real time](/media/docs/code-suggestions.mp4) + +### Scrolling Code Suggestions + +| Action | Shortcut | +| ----------- | -------------- | +| Scroll up | `Ctrl+Shift+I` | +| Scroll down | `Ctrl+Shift+U` | + +--- + +## Face Swap + +The face swap feature replaces your webcam output with a face-swapped stream - your physical appearance is replaced with the face from the photo you configured in settings. + +### Setting It Up + +1. Ensure **OBS Studio** is installed before opening your video call. +2. In your video call app (Zoom, Google Meet, Teams, etc.), select: + - **OBS Virtual Camera** as your camera source + - **CABLE Output (VB-Audio Virtual Cable)** as your microphone + + | Camera | Microphone | + | :----------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------: | + | ![Select OBS Virtual Camera as camera in your meeting app](/media/docs/meeting-video-device.png) | ![Select CABLE Output as microphone in your meeting app](/media/docs/meeting-audio-device.png) | + +3. In Power Interview, open **Face Swap Options** from the control panel (face scan icon), select your webcam and resolution, then close the dialog. + + ![Face Swap Options dialog](/media/docs/face-swap-options.png) + +4. Toggle **Face Swap on** using the face icon button in the control panel. + + | Face Swap ON | Start assistant with face swap active | + | :-------------------------------------------------------------------------------: | :-----------------------------------------------------------------------: | + | ![Face swap toggle button in the control panel](/media/docs/face-swap-toggle.png) | ![Start assistant with face swap active](/media/docs/face-swap-start.png) | + +5. Click start button to begin the session with face swap active. + + ![Face swap in action — webcam feed replaced with face-swapped output](/media/docs/face-swap-example.png) + +The VCam Agent captures frames from your webcam, streams them to the face swap backend, and pushes the processed output to OBS Virtual Camera. + +### Face Swap Settings + +| Setting | Description | +| ---------------- | -------------------------------------------------- | +| Camera Device | Physical webcam used as the video input | +| Resolution | Output resolution: 640×360, 640×480, or 1280×720 | +| Face Enhancement | Toggle AI post-processing for more natural results | + +--- + +## Stealth Mode + +The Power Interview window is **always hidden from screen capture and screen share** — interviewers cannot see it at any time, regardless of whether stealth mode is on or off. + +Stealth mode is about **focus control**. When active, the window will not steal focus from your coding challenge, IDE, or video call. You stay in full control of your keyboard and mouse at all times. + +### Activating Stealth Mode + +Press `Ctrl+Shift+M` to toggle stealth mode on or off. + +When active, the window collapses to a minimal status bar showing running state, credit balance, and active hotkeys. + +![Stealth mode status bar](/media/docs/stealth-mode.png) + +### Opacity Toggle + +Press `Ctrl+Shift+N` to toggle a low-opacity overlay, letting you glance at suggestions through a semi-transparent window without switching focus away from your active window. + +--- + +## Window Management + +Because you cannot use the mouse to interact with Power Interview while focused on your interview, all window controls are available via keyboard shortcuts. + +### Positioning the Window + +Use a numpad-style layout to snap the window to any screen position: + +![Window positioning grid — numpad layout for screen zones](/media/docs/window-positioning.svg) + +### Fine-Tuning Position and Size + +| Action | Shortcut | +| ------------- | ---------------------- | +| Move window | `Ctrl+Alt+Shift+Arrow` | +| Resize window | `Ctrl+Win+Shift+Arrow` | + +--- + +## Profile & Settings + +Access your settings by clicking the **profile icon** in the app's control panel. + +### Profile Settings + +| Field | Description | +| ------------ | -------------------------------------------------------------------- | +| Name | Your name (required) - used as the speaker label and in AI responses | +| Profile / CV | Your resume, bio, or portfolio content (required) | +| Context | Job description or role requirements for the interview (recommended) | +| Face Photo | Reference photo used for face swap | + +### Device Settings + +| Setting | Where to configure | Description | +| ---------- | ----------------------------- | ---------------------------------------- | +| Microphone | Audio Options (mic icon) | Physical microphone for your voice | +| Camera | Face Swap Options (face icon) | Webcam used as video input for face swap | + +### Account Menu + +Click your name/avatar in the bottom-left of the control panel to access: + +- **Configuration** - open the profile/settings dialog +- **Change Password** - update your account password +- **Buy Credits** - go to the payments page +- **Sign Out** - log out of your account + +--- + +## Session Tools + +The control panel toolbar includes two utility buttons (at the right edge): + +### Export Interview + +The **Export** button generates a Word document (`.docx`) containing: + +- An AI-generated summary of the interview +- Full timestamped transcript with speaker labels +- All reply suggestions with the questions that triggered them + +A save dialog appears so you can choose where to save the file. + +| ![Export Interview dialog — save generated report as a .docx file](/media/docs/export-interview.png) | ![Export Example — generated interview report in Word](/media/docs/export-example.png) | +| ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | + +[Export Example — generated interview report in Word](/media/docs/export-example.docx) + +### Clear Session + +The **Clear** button removes all transcripts, reply suggestions, and code suggestions from the current session. This does not affect your saved profile, credentials, or settings. + +--- + +## Credits & Payments + +Credits are consumed while the assistant is running — covering AI reply suggestions, transcription, and face swap processing. Your remaining credit balance is shown live in the video panel and in the status bar (stealth mode). + +To buy credits: + +1. Click your name/avatar in the control panel. +2. Select **Buy Credits**. +3. Choose a credit package, complete payment, and your balance updates automatically. + +You can also view your payment history and check pending payment status from the same page. + +--- + +## Complete Hotkey Reference + +| Action | Shortcut | +| ----------------------------- | ------------------------------- | +| Stop all & exit stealth | `Ctrl+Shift+Q` | +| Toggle stealth mode | `Ctrl+Shift+M` | +| Toggle window opacity | `Ctrl+Shift+N` | +| Position window (numpad) | `Ctrl+Shift+1` – `Ctrl+Shift+9` | +| Move window | `Ctrl+Alt+Shift+Arrow` | +| Resize window | `Ctrl+Win+Shift+Arrow` | +| Scroll reply suggestions down | `Ctrl+Shift+J` | +| Scroll reply suggestions up | `Ctrl+Shift+K` | +| Scroll code suggestions down | `Ctrl+Shift+U` | +| Scroll code suggestions up | `Ctrl+Shift+I` | +| Capture screenshot | `Ctrl+Alt+Shift+P` | +| Submit screenshots | `Ctrl+Alt+Shift+Enter` | +| Clear screenshots | `Ctrl+Alt+Shift+X` | diff --git a/src/layouts/DocsLayout.tsx b/src/layouts/DocsLayout.tsx new file mode 100644 index 0000000..22d3f18 --- /dev/null +++ b/src/layouts/DocsLayout.tsx @@ -0,0 +1,68 @@ +import React, { useState } from 'react'; + +import Container from '@/components/Container'; +import { FooterSection } from '@/components/sections/FooterSection'; +import { Header } from '@/components/sections/Header'; +import { useTheme } from '@/hooks/useTheme'; + +import DocsSidebar from './DocsSidebar'; + +const DocsLayout: React.FC<{ children?: React.ReactNode }> = ({ children }) => { + const { theme, setTheme } = useTheme(); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const [sidebarOpen, setSidebarOpen] = useState(false); + + // lock body scroll when sidebar is open on mobile + React.useEffect(() => { + document.body.style.overflow = sidebarOpen ? 'hidden' : ''; + return () => { + document.body.style.overflow = ''; + }; + }, [sidebarOpen]); + + const toggleTheme = () => setTheme(theme === 'dark' ? 'light' : 'dark'); + + return ( +
    +
    + + +
    + {/* mobile toggle for docs sidebar */} + + +
    + {/* desktop sidebar, hidden on small screens */} + setSidebarOpen(false)} /> + +
    {children}
    +
    +
    +
    + + {/* overlay sidebar for mobile */} + {sidebarOpen && ( +
    +
    + setSidebarOpen(false)} /> +
    +
    setSidebarOpen(false)} /> +
    + )} + + +
    + ); +}; + +export default DocsLayout; diff --git a/src/layouts/DocsSidebar.tsx b/src/layouts/DocsSidebar.tsx new file mode 100644 index 0000000..9ac43da --- /dev/null +++ b/src/layouts/DocsSidebar.tsx @@ -0,0 +1,63 @@ +import React from 'react'; + +import { Link, useLocation } from 'react-router-dom'; + +const ORDER = [ + 'introduction', + 'installation', + 'usage', + 'how-it-works', + 'best-practices', + 'troubleshooting', +]; + +const docPaths = Object.keys(import.meta.glob('/src/content/docs/*.md')); + +const list = docPaths + .map((p) => { + const name = p.split('/').pop() || ''; + const slug = name.replace(/\.md$/, ''); + const title = slug.replace(/-/g, ' '); + return { slug, title }; + }) + .sort((a, b) => { + const ai = ORDER.indexOf(a.slug); + const bi = ORDER.indexOf(b.slug); + return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi); + }); + +interface DocsSidebarProps { + className?: string; + onLinkClick?: () => void; +} + +export const DocsSidebar: React.FC = ({ className = '', onLinkClick }) => { + const location = useLocation(); + + return ( + + ); +}; + +export default DocsSidebar; diff --git a/src/main.tsx b/src/main.tsx index 4ba42af..27d5858 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,5 +1,6 @@ import React from 'react'; +import 'github-markdown-css/github-markdown.css'; import ReactDOM from 'react-dom/client'; import App from './App'; diff --git a/src/pages/Benefits.tsx b/src/pages/Benefits.tsx index eb38d50..f9c9613 100644 --- a/src/pages/Benefits.tsx +++ b/src/pages/Benefits.tsx @@ -1,6 +1,7 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; -import { BenefitsSection, FooterSection, Header } from '@/components/custom/sections'; +import Seo from '@/components/Seo'; +import { BenefitsSection, FooterSection, Header } from '@/components/sections'; import { useTheme } from '@/hooks/useTheme'; const BenefitsPage: React.FC = () => { @@ -8,29 +9,16 @@ const BenefitsPage: React.FC = () => { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const toggleTheme = () => setTheme(theme === 'dark' ? 'light' : 'dark'); - useEffect(() => { - window.scrollTo(0, 0); - document.title = 'Benefits - Power Interview AI'; - - const metaDescription = document.querySelector('meta[name="description"]'); - if (metaDescription) { - metaDescription.setAttribute( - 'content', - 'Benefits of using Power Interview AI for interview practice, confidence building, and coding prep.' - ); - } - - let canonical = document.querySelector('link[rel="canonical"]') as HTMLLinkElement | null; - if (!canonical) { - canonical = document.createElement('link'); - canonical.setAttribute('rel', 'canonical'); - document.head.appendChild(canonical); - } - canonical.setAttribute('href', 'https://www.powerinterviewai.com/benefits'); - }, []); + const description = + 'Benefits of using Power Interview AI for interview practice, confidence building, and coding prep.'; return (
    +
    { @@ -8,29 +9,16 @@ const ContactPage: React.FC = () => { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const toggleTheme = () => setTheme(theme === 'dark' ? 'light' : 'dark'); - useEffect(() => { - window.scrollTo(0, 0); - document.title = 'Contact - Power Interview AI'; - - const metaDescription = document.querySelector('meta[name="description"]'); - if (metaDescription) { - metaDescription.setAttribute( - 'content', - 'Contact Power Interview AI support or sales — reach out for help, partnership, or product inquiries.' - ); - } - - let canonical = document.querySelector('link[rel="canonical"]') as HTMLLinkElement | null; - if (!canonical) { - canonical = document.createElement('link'); - canonical.setAttribute('rel', 'canonical'); - document.head.appendChild(canonical); - } - canonical.setAttribute('href', 'https://www.powerinterviewai.com/contact'); - }, []); + const description = + 'Contact Power Interview AI support or sales - reach out for help, partnership, or product inquiries.'; return (
    +
    { @@ -18,29 +19,12 @@ const FAQPage: React.FC = () => { } }; - useEffect(() => { - window.scrollTo(0, 0); - document.title = 'FAQ - Power Interview AI'; - - const metaDescription = document.querySelector('meta[name="description"]'); - if (metaDescription) { - metaDescription.setAttribute( - 'content', - 'Frequently asked questions about Power Interview AI — features, privacy, payments, and usage.' - ); - } - - let canonical = document.querySelector('link[rel="canonical"]') as HTMLLinkElement | null; - if (!canonical) { - canonical = document.createElement('link'); - canonical.setAttribute('rel', 'canonical'); - document.head.appendChild(canonical); - } - canonical.setAttribute('href', 'https://www.powerinterviewai.com/faq'); - }, []); + const description = + 'Frequently asked questions about Power Interview AI - features, privacy, payments, and usage.'; return (
    +
    { @@ -8,50 +9,16 @@ const FeaturesPage: React.FC = () => { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const toggleTheme = () => setTheme(theme === 'dark' ? 'light' : 'dark'); - useEffect(() => { - window.scrollTo(0, 0); - document.title = 'Features - Power Interview AI'; - - const metaDescription = document.querySelector('meta[name="description"]'); - if (metaDescription) { - metaDescription.setAttribute( - 'content', - 'Explore Power Interview AI features: real-time transcription, face-swap privacy, live coding assistance, intelligent reply suggestions, and secure crypto payments.' - ); - } - - const ogTitle = document.querySelector('meta[property="og:title"]'); - if (ogTitle) ogTitle.setAttribute('content', 'Features - Power Interview AI'); - - const ogDescription = document.querySelector('meta[property="og:description"]'); - if (ogDescription) - ogDescription.setAttribute( - 'content', - 'Explore Power Interview AI features: real-time transcription, face-swap privacy, live coding assistance, intelligent reply suggestions, and secure crypto payments.' - ); - - const twitterTitle = document.querySelector('meta[name="twitter:title"]'); - if (twitterTitle) twitterTitle.setAttribute('content', 'Features - Power Interview AI'); - - const twitterDescription = document.querySelector('meta[name="twitter:description"]'); - if (twitterDescription) - twitterDescription.setAttribute( - 'content', - 'Explore Power Interview AI features: real-time transcription, face-swap privacy, live coding assistance, intelligent reply suggestions, and secure crypto payments.' - ); - - // set canonical for this page - let canonical = document.querySelector('link[rel="canonical"]') as HTMLLinkElement | null; - if (!canonical) { - canonical = document.createElement('link'); - canonical.setAttribute('rel', 'canonical'); - document.head.appendChild(canonical); - } - canonical.setAttribute('href', 'https://www.powerinterviewai.com/features'); - }, []); + const description = + 'Explore Power Interview AI features: real-time transcription, face-swap privacy, live coding assistance, intelligent reply suggestions, and secure crypto payments.'; return (
    +
    { @@ -32,51 +32,12 @@ const Home: React.FC = () => { } }; - // Reset meta tags to default when returning to home - useEffect(() => { - document.title = - 'Power Interview AI - Privacy-First AI Interview Assistant | Ace Your Interviews'; - - const metaDescription = document.querySelector('meta[name="description"]'); - if (metaDescription) { - metaDescription.setAttribute( - 'content', - 'Transform your interview performance with Power Interview AI - the ultimate AI-powered interview assistant featuring advanced face-swap technology for privacy, live coding challenge assistance with real-time transcription and intelligent code suggestions, smart export functionality to save your sessions, and flexible cryptocurrency payment options. Get 30 free credits to start! No credit card, PayPal, or bank account required - pay securely with crypto. Perfect for technical interviews, coding assessments, and behavioral interviews with AI-driven real-time guidance.' - ); - } - - const ogTitle = document.querySelector('meta[property="og:title"]'); - if (ogTitle) { - ogTitle.setAttribute('content', 'Power Interview AI - Privacy-First AI Interview Assistant'); - } - - const ogDescription = document.querySelector('meta[property="og:description"]'); - if (ogDescription) { - ogDescription.setAttribute( - 'content', - 'Transform your interview performance with Power Interview AI - the ultimate AI-powered interview assistant featuring advanced face-swap technology for privacy, live coding challenge assistance with real-time transcription and intelligent code suggestions, smart export functionality to save your sessions, and flexible cryptocurrency payment options. Get 30 free credits to start! No credit card, PayPal, or bank account required - pay securely with crypto. Perfect for technical interviews, coding assessments, and behavioral interviews with AI-driven real-time guidance.' - ); - } - - const twitterTitle = document.querySelector('meta[name="twitter:title"]'); - if (twitterTitle) { - twitterTitle.setAttribute( - 'content', - 'Power Interview AI - Privacy-First AI Interview Assistant' - ); - } - - const twitterDescription = document.querySelector('meta[name="twitter:description"]'); - if (twitterDescription) { - twitterDescription.setAttribute( - 'content', - 'Transform your interview performance with Power Interview AI - the ultimate AI-powered interview assistant featuring advanced face-swap technology for privacy, live coding challenge assistance with real-time transcription and intelligent code suggestions, smart export functionality to save your sessions, and flexible cryptocurrency payment options. Get 30 free credits to start! No credit card, PayPal, or bank account required - pay securely with crypto. Perfect for technical interviews, coding assessments, and behavioral interviews with AI-driven real-time guidance.' - ); - } - }, []); + const longDescription = + 'Transform your interview performance with Power Interview AI - the ultimate AI-powered interview assistant featuring advanced face-swap technology for privacy, live coding challenge assistance with real-time transcription and intelligent code suggestions, smart export functionality to save your sessions, and flexible cryptocurrency payment options. Get 30 free credits to start! No credit card, PayPal, or bank account required - pay securely with crypto. Perfect for technical interviews, coding assessments, and behavioral interviews with AI-driven real-time guidance.'; return (
    + {/* Header */}
    { {/* Why Choose Power Interview AI Section */} - {/* How to Use Section */} - - {/* Pricing Section */} diff --git a/src/pages/HowToUse.tsx b/src/pages/HowToUse.tsx deleted file mode 100644 index 1990cc1..0000000 --- a/src/pages/HowToUse.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { useEffect, useState } from 'react'; - -import { FooterSection, Header, HowToUseSection } from '@/components/custom/sections'; -import { useTheme } from '@/hooks/useTheme'; - -const HowToUsePage: React.FC = () => { - const { theme, setTheme } = useTheme(); - const [mobileMenuOpen, setMobileMenuOpen] = useState(false); - const toggleTheme = () => setTheme(theme === 'dark' ? 'light' : 'dark'); - - useEffect(() => { - window.scrollTo(0, 0); - document.title = 'How to Use - Power Interview AI'; - - const metaDescription = document.querySelector('meta[name="description"]'); - if (metaDescription) { - metaDescription.setAttribute( - 'content', - 'Learn how to use Power Interview AI: setup face swap, run live transcription, use AI suggestions and download sessions.' - ); - } - - let canonical = document.querySelector('link[rel="canonical"]') as HTMLLinkElement | null; - if (!canonical) { - canonical = document.createElement('link'); - canonical.setAttribute('rel', 'canonical'); - document.head.appendChild(canonical); - } - canonical.setAttribute('href', 'https://www.powerinterviewai.com/how-to-use'); - }, []); - - return ( -
    -
    -
    - -
    - -
    - ); -}; - -export default HowToUsePage; diff --git a/src/pages/LegalNotice.tsx b/src/pages/LegalNotice.tsx index f74b352..66440bc 100644 --- a/src/pages/LegalNotice.tsx +++ b/src/pages/LegalNotice.tsx @@ -1,6 +1,7 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; -import { FooterSection, Header, LegalNoticeSection } from '@/components/custom/sections'; +import Seo from '@/components/Seo'; +import { FooterSection, Header, LegalNoticeSection } from '@/components/sections'; import { useTheme } from '@/hooks/useTheme'; const LegalNoticePage: React.FC = () => { @@ -8,29 +9,15 @@ const LegalNoticePage: React.FC = () => { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const toggleTheme = () => setTheme(theme === 'dark' ? 'light' : 'dark'); - useEffect(() => { - window.scrollTo(0, 0); - document.title = 'Legal Notice - Power Interview AI'; - - const metaDescription = document.querySelector('meta[name="description"]'); - if (metaDescription) { - metaDescription.setAttribute( - 'content', - 'Legal notices and important information for Power Interview AI users.' - ); - } - - let canonical = document.querySelector('link[rel="canonical"]') as HTMLLinkElement | null; - if (!canonical) { - canonical = document.createElement('link'); - canonical.setAttribute('rel', 'canonical'); - document.head.appendChild(canonical); - } - canonical.setAttribute('href', 'https://www.powerinterviewai.com/legal-notice'); - }, []); + const description = 'Legal notices and important information for Power Interview AI users.'; return (
    +
    { @@ -8,29 +9,16 @@ const PricingPage: React.FC = () => { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const toggleTheme = () => setTheme(theme === 'dark' ? 'light' : 'dark'); - useEffect(() => { - window.scrollTo(0, 0); - document.title = 'Pricing - Power Interview AI'; - - const metaDescription = document.querySelector('meta[name="description"]'); - if (metaDescription) { - metaDescription.setAttribute( - 'content', - 'See Power Interview AI pricing plans and credit packs — secure crypto payments, flexible options for interview practice and live assistance.' - ); - } - - let canonical = document.querySelector('link[rel="canonical"]') as HTMLLinkElement | null; - if (!canonical) { - canonical = document.createElement('link'); - canonical.setAttribute('rel', 'canonical'); - document.head.appendChild(canonical); - } - canonical.setAttribute('href', 'https://www.powerinterviewai.com/pricing'); - }, []); + const description = + 'See Power Interview AI pricing plans and credit packs - secure crypto payments, flexible options for interview practice and live assistance.'; return (
    +
    { - useEffect(() => { - window.scrollTo(0, 0); - - // Update meta tags for SEO - document.title = 'Privacy Policy - Power Interview AI'; - - const metaDescription = document.querySelector('meta[name="description"]'); - if (metaDescription) { - metaDescription.setAttribute( - 'content', - "Read Power Interview AI's Privacy Policy to understand how we protect your data, handle information, and ensure your privacy during interview preparation." - ); - } - - const ogTitle = document.querySelector('meta[property="og:title"]'); - if (ogTitle) { - ogTitle.setAttribute('content', 'Privacy Policy - Power Interview AI'); - } - - const ogDescription = document.querySelector('meta[property="og:description"]'); - if (ogDescription) { - ogDescription.setAttribute( - 'content', - "Read Power Interview AI's Privacy Policy to understand how we protect your data, handle information, and ensure your privacy during interview preparation." - ); - } - - const twitterTitle = document.querySelector('meta[name="twitter:title"]'); - if (twitterTitle) { - twitterTitle.setAttribute('content', 'Privacy Policy - Power Interview AI'); - } - - const twitterDescription = document.querySelector('meta[name="twitter:description"]'); - if (twitterDescription) { - twitterDescription.setAttribute( - 'content', - "Read Power Interview AI's Privacy Policy to understand how we protect your data, handle information, and ensure your privacy during interview preparation." - ); - } - }, []); + const description = + "Read Power Interview AI's Privacy Policy to understand how we protect your data, handle information, and ensure your privacy during interview preparation."; return (
    +
    diff --git a/src/pages/TermsOfService.tsx b/src/pages/TermsOfService.tsx index 49f7175..791e303 100644 --- a/src/pages/TermsOfService.tsx +++ b/src/pages/TermsOfService.tsx @@ -1,55 +1,23 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { ArrowLeft } from 'lucide-react'; import { Link } from 'react-router-dom'; -import Container from '@/components/custom/Container'; +import Container from '@/components/Container'; +import Seo from '@/components/Seo'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; const TermsOfService: React.FC = () => { - useEffect(() => { - window.scrollTo(0, 0); - - // Update meta tags for SEO - document.title = 'Terms of Service - Power Interview AI'; - - const metaDescription = document.querySelector('meta[name="description"]'); - if (metaDescription) { - metaDescription.setAttribute( - 'content', - "Review Power Interview AI's Terms of Service to understand your rights, responsibilities, and the rules for using our AI-powered interview assistance platform." - ); - } - - const ogTitle = document.querySelector('meta[property="og:title"]'); - if (ogTitle) { - ogTitle.setAttribute('content', 'Terms of Service - Power Interview AI'); - } - - const ogDescription = document.querySelector('meta[property="og:description"]'); - if (ogDescription) { - ogDescription.setAttribute( - 'content', - "Review Power Interview AI's Terms of Service to understand your rights, responsibilities, and the rules for using our AI-powered interview assistance platform." - ); - } - - const twitterTitle = document.querySelector('meta[name="twitter:title"]'); - if (twitterTitle) { - twitterTitle.setAttribute('content', 'Terms of Service - Power Interview AI'); - } - - const twitterDescription = document.querySelector('meta[name="twitter:description"]'); - if (twitterDescription) { - twitterDescription.setAttribute( - 'content', - "Review Power Interview AI's Terms of Service to understand your rights, responsibilities, and the rules for using our AI-powered interview assistance platform." - ); - } - }, []); + const description = + "Review Power Interview AI's Terms of Service to understand your rights, responsibilities, and the rules for using our AI-powered interview assistance platform."; return (
    +
    diff --git a/src/pages/WhyChoose.tsx b/src/pages/WhyChoose.tsx index 1c56f13..9f2d01a 100644 --- a/src/pages/WhyChoose.tsx +++ b/src/pages/WhyChoose.tsx @@ -1,6 +1,7 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; -import { FooterSection, Header, WhyChooseSection } from '@/components/custom/sections'; +import Seo from '@/components/Seo'; +import { FooterSection, Header, WhyChooseSection } from '@/components/sections'; import { useTheme } from '@/hooks/useTheme'; const WhyChoosePage: React.FC = () => { @@ -8,29 +9,16 @@ const WhyChoosePage: React.FC = () => { const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const toggleTheme = () => setTheme(theme === 'dark' ? 'light' : 'dark'); - useEffect(() => { - window.scrollTo(0, 0); - document.title = 'Why Choose Power Interview AI'; - - const metaDescription = document.querySelector('meta[name="description"]'); - if (metaDescription) { - metaDescription.setAttribute( - 'content', - 'Why choose Power Interview AI — privacy-first design, face-swap, real-time assistance and high accuracy for interview practice.' - ); - } - - let canonical = document.querySelector('link[rel="canonical"]') as HTMLLinkElement | null; - if (!canonical) { - canonical = document.createElement('link'); - canonical.setAttribute('rel', 'canonical'); - document.head.appendChild(canonical); - } - canonical.setAttribute('href', 'https://www.powerinterviewai.com/why-choose'); - }, []); + const description = + 'Why choose Power Interview AI - privacy-first design, face-swap, real-time assistance and high accuracy for interview practice.'; return (
    +
    + String(text) + .toLowerCase() + .replace(/[^\w\s-]/g, '') + .trim() + .replace(/\s+/g, '-'); + +const heading = + (Tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6') => + ({ children }: React.HTMLAttributes) => { + const id = slugify(children); + return ( + + {children} + + # + + + ); + }; + +// Dark-mode-aware table components +const mdComponents = { + h1: heading('h1'), + h2: heading('h2'), + h3: heading('h3'), + h4: heading('h4'), + h5: heading('h5'), + h6: heading('h6'), + table: ({ children }: React.HTMLAttributes) => ( +
    + {children}
    +
    + ), + thead: ({ children }: React.HTMLAttributes) => ( + {children} + ), + tbody: ({ children }: React.HTMLAttributes) => ( + {children} + ), + tr: ({ children }: React.HTMLAttributes) => ( + {children} + ), + th: ({ children }: React.HTMLAttributes) => ( + + {children} + + ), + td: ({ children }: React.HTMLAttributes) => ( + {children} + ), + img: ({ src, alt }: React.ImgHTMLAttributes) => + src?.endsWith('.mp4') ? ( +