chore: import upstream snapshot with attribution
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:24:08 +08:00
commit 0d3cb498a3
5438 changed files with 1316560 additions and 0 deletions
@@ -0,0 +1,33 @@
.audioContainer {
margin-top: 1rem;
padding: 1rem;
border-radius: var(--ifm-border-radius);
background: var(--ifm-background-surface-color);
box-shadow: var(--ifm-box-shadow);
}
.audioPlayer {
width: 100%;
height: 40px;
}
.transcript {
margin-top: 0.5rem;
font-size: 0.9rem;
color: var(--ifm-color-emphasis-700);
line-height: 1.4;
}
.transcriptLabel {
font-weight: var(--ifm-font-weight-bold);
margin-right: 0.5rem;
}
[data-theme='dark'] .audioContainer {
background: var(--ifm-background-color);
border: 1px solid var(--ifm-color-emphasis-300);
}
[data-theme='dark'] .transcript {
color: var(--ifm-color-emphasis-600);
}
+41
View File
@@ -0,0 +1,41 @@
import React, { useEffect, useRef } from 'react';
import styles from './AudioPlayer.module.css';
interface AudioPlayerProps {
audioData: string; // base64 encoded audio data
format?: string;
transcript?: string;
}
export default function AudioPlayer({
audioData,
format = 'wav',
transcript,
}: AudioPlayerProps): React.ReactElement {
const audioRef = useRef<HTMLAudioElement>(null);
useEffect(() => {
if (audioRef.current) {
const blob = new Blob([Buffer.from(audioData, 'base64')], { type: `audio/${format}` });
const url = URL.createObjectURL(blob);
audioRef.current.src = url;
return () => {
URL.revokeObjectURL(url);
};
}
}, [audioData, format]);
return (
<div className={styles.audioContainer}>
<audio ref={audioRef} controls className={styles.audioPlayer} />
{transcript && (
<div className={styles.transcript}>
<span className={styles.transcriptLabel}>Transcript:</span>
{transcript}
</div>
)}
</div>
);
}
@@ -0,0 +1,233 @@
.blogPostCard {
background-color: var(--ifm-card-background-color);
border-radius: 10px;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
transition:
box-shadow 0.2s ease,
transform 0.2s ease;
height: 100%;
display: flex;
flex-direction: column;
text-decoration: none;
color: inherit;
border: 1px solid var(--ifm-color-emphasis-200);
}
.blogPostCard:hover {
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12);
transform: translateY(-4px);
text-decoration: none;
color: inherit;
border-color: var(--ifm-color-emphasis-300);
}
.blogPostImage {
aspect-ratio: 16 / 9;
overflow: hidden;
background-color: var(--ifm-color-emphasis-100);
}
.blogPostImage img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.blogPostCard:hover .blogPostImage img {
transform: scale(1.03);
}
.blogPostContent {
padding: 1.25rem;
flex-grow: 1;
display: flex;
flex-direction: column;
}
.tag {
display: inline-block;
background-color: var(--ifm-color-primary-lightest);
color: var(--ifm-color-primary-dark);
padding: 0.25rem 0.625rem;
border-radius: 4px;
font-size: 0.7rem;
font-weight: 600;
margin-bottom: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
align-self: flex-start;
}
.title {
font-size: 1.125rem;
font-weight: 600;
margin: 0;
color: var(--ifm-heading-color);
line-height: 1.4;
transition: color 0.2s ease;
}
.blogPostCard:hover .title {
color: var(--ifm-color-primary);
}
.title a {
color: inherit;
text-decoration: none;
}
.preview {
font-size: 0.875rem;
color: var(--ifm-color-secondary-darkest);
margin: 0.5rem 0 0 0;
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.blogPostMeta {
margin-top: auto;
padding-top: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8rem;
color: var(--ifm-color-secondary-darkest);
}
.author {
font-weight: 600;
}
.author::after {
content: '·';
margin-left: 0.5rem;
}
.date {
font-weight: 500;
}
/* Tablet */
@media (max-width: 900px) {
.blogPostContent {
padding: 1rem;
}
.title {
font-size: 1rem;
}
.tag {
font-size: 0.65rem;
padding: 0.2rem 0.5rem;
margin-bottom: 0.5rem;
}
.preview {
font-size: 0.8rem;
-webkit-line-clamp: 2;
}
.blogPostMeta {
padding-top: 0.75rem;
font-size: 0.75rem;
}
}
/* Small tablet - compact 2-column cards (600-700px) */
@media (max-width: 700px) {
.blogPostCard {
border-radius: 8px;
}
.blogPostContent {
padding: 0.875rem;
}
.title {
font-size: 0.9rem;
line-height: 1.35;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.tag {
font-size: 0.6rem;
padding: 0.15rem 0.4rem;
margin-bottom: 0.4rem;
}
.preview {
display: none;
}
.blogPostMeta {
padding-top: 0.5rem;
font-size: 0.7rem;
}
}
/* Mobile - single column */
@media (max-width: 600px) {
.blogPostCard {
border-radius: 10px;
}
.blogPostCard:hover {
transform: none;
}
.blogPostContent {
padding: 1.25rem;
}
.title {
font-size: 1.1rem;
line-height: 1.4;
-webkit-line-clamp: 3;
}
.tag {
font-size: 0.7rem;
padding: 0.25rem 0.625rem;
margin-bottom: 0.75rem;
}
.preview {
display: -webkit-box;
font-size: 0.875rem;
-webkit-line-clamp: 2;
}
.blogPostMeta {
padding-top: 0.75rem;
font-size: 0.8rem;
}
}
/* Small mobile */
@media (max-width: 400px) {
.blogPostContent {
padding: 1rem;
}
.title {
font-size: 1rem;
}
.preview {
font-size: 0.8rem;
}
.blogPostMeta {
font-size: 0.75rem;
}
}
+64
View File
@@ -0,0 +1,64 @@
import React from 'react';
import Link from '@docusaurus/Link';
import styles from './BlogPostCard.module.css';
import type { PropBlogPostContent } from '@docusaurus/plugin-content-blog';
// Format tag label: "red-teaming" → "Red Teaming", "ai-security" → "AI Security"
function formatTagLabel(label: string): string {
const acronyms = ['ai', 'llm', 'owasp', 'mcp', 'rag', 'agi', 'a2a', 'eu'];
return label
.split('-')
.map((word) => {
if (acronyms.includes(word.toLowerCase())) {
return word.toUpperCase();
}
return word.charAt(0).toUpperCase() + word.slice(1);
})
.join(' ');
}
interface BlogPostCardProps {
post: PropBlogPostContent;
}
export default function BlogPostCard({ post }: BlogPostCardProps): React.ReactElement {
const { metadata } = post;
const { title, date, permalink, tags, description } = metadata;
const { authors } = metadata;
// Format date as "Dec 12, 2025" style
const formattedDate = new Date(date).toLocaleDateString('en-US', {
timeZone: 'UTC',
month: 'short',
day: 'numeric',
year: 'numeric',
});
// Get the first tag if available
const primaryTag = tags && tags.length > 0 ? tags[0] : null;
// Get first sentence of description for preview
const preview = description ? description.split('. ')[0] + '.' : null;
const authorNames = authors.map((a) => a.name).join(' and ');
return (
<Link to={permalink} className={styles.blogPostCard}>
{metadata.frontMatter.image && (
<div className={styles.blogPostImage}>
<img src={metadata.frontMatter.image} alt={title} loading="lazy" />
</div>
)}
<div className={styles.blogPostContent}>
{primaryTag && <span className={styles.tag}>{formatTagLabel(primaryTag.label)}</span>}
<h3 className={styles.title}>{title}</h3>
{preview && <p className={styles.preview}>{preview}</p>}
<div className={styles.blogPostMeta}>
{authorNames && <span className={styles.author}>{authorNames}</span>}
<span className={styles.date}>{formattedDate}</span>
</div>
</div>
</Link>
);
}
@@ -0,0 +1,164 @@
.blogPostGridContainer {
margin-top: 4rem;
}
.blogPostGridTitle {
font-size: 2rem;
font-weight: 500;
margin-bottom: 2rem;
color: var(--ifm-heading-color);
position: relative;
display: inline-block;
}
.blogPostGridTitle::after {
content: '';
position: absolute;
width: 60px;
height: 3px;
bottom: -10px;
left: 0;
background-color: var(--ifm-color-primary);
}
/* Style for paginated pages (page 2+) */
.blogPostGridTitle[data-is-paginated='true'] {
display: flex;
align-items: center;
gap: 0.75rem;
}
.blogPostGridTitle[data-is-paginated='true']::after {
width: 40px;
}
/* Create styling for the page number in the title */
.pageNumber {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2rem;
height: 2rem;
padding: 0 0.5rem;
margin-left: 0.5rem;
font-size: 1.25rem;
font-weight: 600;
color: white;
background-color: var(--ifm-color-primary);
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.blogPostGrid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.75rem;
}
/* Responsive breakpoints */
/* Tablet landscape */
@media (max-width: 1100px) {
.blogPostGridContainer {
margin-top: 3rem;
}
.blogPostGrid {
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
.blogPostGridTitle {
font-size: 1.75rem;
margin-bottom: 1.5rem;
}
.blogPostGridTitle::after {
width: 50px;
height: 2px;
bottom: -8px;
}
.pageNumber {
font-size: 1.1rem;
min-width: 1.75rem;
height: 1.75rem;
}
}
/* Tablet portrait */
@media (max-width: 900px) {
.blogPostGridContainer {
margin-top: 2.5rem;
}
.blogPostGridTitle {
font-size: 1.5rem;
margin-bottom: 1.25rem;
}
.blogPostGrid {
gap: 1.25rem;
}
.pageNumber {
font-size: 1rem;
min-width: 1.5rem;
height: 1.5rem;
padding: 0 0.4rem;
margin-left: 0.4rem;
}
}
/* Small tablet / large phone - keep 2 columns */
@media (max-width: 700px) {
.blogPostGridContainer {
margin-top: 2rem;
}
.blogPostGrid {
gap: 1rem;
}
.blogPostGridTitle {
font-size: 1.35rem;
margin-bottom: 1rem;
}
.blogPostGridTitle::after {
width: 40px;
bottom: -6px;
}
.pageNumber {
font-size: 0.9rem;
min-width: 1.35rem;
height: 1.35rem;
padding: 0 0.35rem;
margin-left: 0.35rem;
border-radius: 6px;
}
}
/* Mobile - single column for phones and small tablets */
@media (max-width: 600px) {
.blogPostGrid {
grid-template-columns: 1fr;
gap: 1rem;
}
}
/* Small mobile */
@media (max-width: 400px) {
.blogPostGridContainer {
margin-top: 1.5rem;
}
.blogPostGridTitle {
font-size: 1.25rem;
}
.blogPostGrid {
gap: 0.875rem;
}
}
@@ -0,0 +1,23 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import BlogPostGrid from './BlogPostGrid';
vi.mock('./BlogPostCard', () => ({
default: () => null,
}));
describe('BlogPostGrid', () => {
it('renders the latest posts heading on the first page', () => {
render(<BlogPostGrid posts={[]} />);
expect(screen.getByRole('heading', { level: 2, name: 'Latest Posts' })).toBeInTheDocument();
});
it('separates the older posts heading from the page number', () => {
render(<BlogPostGrid posts={[]} title="Older Posts • Page 2" />);
expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent('Older Posts 2');
});
});
+49
View File
@@ -0,0 +1,49 @@
import React from 'react';
import BlogPostCard from './BlogPostCard';
import styles from './BlogPostGrid.module.css';
import type { PropBlogPostContent } from '@docusaurus/plugin-content-blog';
interface BlogPostGridProps {
posts: PropBlogPostContent[];
title?: string;
}
export default function BlogPostGrid({
posts,
title = 'Latest Posts',
}: BlogPostGridProps): React.ReactElement {
// Check if this is a paginated page (not the first page)
const isPaginatedPage = title.includes('Older Posts');
// If it's a paginated page, split the title to style the page number separately
let mainTitle = title;
let pageNumber = null;
if (isPaginatedPage) {
const titleParts = title.split('•');
mainTitle = titleParts[0].trim();
// Extract page number from the second part
if (titleParts.length > 1) {
const pageText = titleParts[1].trim();
const pageNum = pageText.replace('Page ', '');
pageNumber = <span className={styles.pageNumber}>{pageNum}</span>;
}
}
return (
<div className={styles.blogPostGridContainer}>
<h2 className={styles.blogPostGridTitle} data-is-paginated={isPaginatedPage}>
{pageNumber ? `${mainTitle} ` : mainTitle}
{pageNumber}
</h2>
<div className={styles.blogPostGrid}>
{posts.map((post, idx) => (
<BlogPostCard key={idx} post={post} />
))}
</div>
</div>
);
}
@@ -0,0 +1,34 @@
import React from 'react';
import Link from '@docusaurus/Link';
import styles from './styles.module.css';
export default function EnterpriseBanner(): React.ReactElement {
return (
<div className={styles.banner}>
<div className={styles.iconWrapper}>
<svg
className={styles.icon}
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
<path d="m9 12 2 2 4-4" />
</svg>
</div>
<span className={styles.text}>
This feature requires{' '}
<Link to="/docs/enterprise/" className={styles.link}>
Promptfoo Enterprise
</Link>
.
</span>
</div>
);
}
@@ -0,0 +1,76 @@
.banner {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 16px;
margin-bottom: 1.5rem;
border: 1px solid rgba(229, 58, 58, 0.15);
border-left: 3px solid var(--pf-red-base);
border-radius: 8px;
background: linear-gradient(135deg, rgba(229, 58, 58, 0.03) 0%, rgba(229, 58, 58, 0.06) 100%);
font-size: 0.8125rem;
line-height: 1.5;
}
[data-theme='dark'] .banner {
border-color: rgba(255, 122, 122, 0.15);
border-left-color: var(--pf-red-light);
background: linear-gradient(135deg, rgba(255, 122, 122, 0.04) 0%, rgba(255, 122, 122, 0.08) 100%);
}
.iconWrapper {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--pf-red-base);
}
[data-theme='dark'] .iconWrapper {
color: var(--pf-red-light);
}
.icon {
display: block;
}
.text {
color: var(--pf-red-darker);
font-weight: 500;
}
[data-theme='dark'] .text {
color: var(--pf-red-lighter);
}
.link {
color: var(--pf-red-darker);
font-weight: 600;
text-decoration: underline;
text-decoration-color: rgba(179, 46, 46, 0.3);
text-underline-offset: 2px;
transition: text-decoration-color 0.15s;
}
.link:hover {
text-decoration-color: rgba(179, 46, 46, 0.8);
color: var(--pf-red-darker);
}
[data-theme='dark'] .link {
color: var(--pf-red-lighter);
text-decoration-color: rgba(255, 160, 160, 0.3);
}
[data-theme='dark'] .link:hover {
text-decoration-color: rgba(255, 160, 160, 0.8);
color: var(--pf-red-lighter);
}
@media (max-width: 480px) {
.banner {
padding: 8px 12px;
gap: 8px;
font-size: 0.75rem;
}
}
@@ -0,0 +1,385 @@
.card {
position: relative;
display: flex;
flex-direction: column;
background: var(--ifm-card-background-color);
border-radius: 16px;
overflow: hidden;
text-decoration: none;
color: inherit;
height: 100%;
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.04),
0 4px 12px rgba(0, 0, 0, 0.04);
transition:
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.card::before {
content: '';
position: absolute;
inset: 0;
border-radius: 16px;
padding: 1px;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%);
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: exclude;
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease;
}
.card:hover {
transform: translateY(-8px);
box-shadow:
0 12px 40px rgba(0, 0, 0, 0.12),
0 4px 12px rgba(0, 0, 0, 0.08);
text-decoration: none;
color: inherit;
}
.card:hover::before {
opacity: 1;
background: linear-gradient(135deg, var(--ifm-color-primary) 0%, rgba(37, 99, 235, 0.3) 100%);
}
[data-theme='dark'] .card {
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.2),
0 4px 12px rgba(0, 0, 0, 0.15);
}
[data-theme='dark'] .card:hover {
box-shadow:
0 20px 60px rgba(0, 0, 0, 0.4),
0 8px 24px rgba(37, 99, 235, 0.15);
}
/* Image Container */
.imageContainer {
position: relative;
aspect-ratio: 16 / 9;
overflow: hidden;
background: linear-gradient(
135deg,
var(--ifm-color-emphasis-100) 0%,
var(--ifm-color-emphasis-200) 100%
);
}
.imageContainer::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(to bottom, transparent 0%, transparent 50%, rgba(0, 0, 0, 0.03) 100%);
pointer-events: none;
transition: opacity 0.3s ease;
}
.card:hover .imageContainer::after {
opacity: 0;
}
.image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.card:hover .image {
transform: scale(1.08);
}
.placeholderImage {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--ifm-color-primary) 0%, #1d4ed8 50%, #1e40af 100%);
position: relative;
overflow: hidden;
}
.placeholderImage::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle at center, rgba(255, 255, 255, 0.1) 0%, transparent 50%);
animation: shimmer 3s ease-in-out infinite;
}
@keyframes shimmer {
0%,
100% {
transform: translate(-10%, -10%) rotate(0deg);
opacity: 0.5;
}
50% {
transform: translate(10%, 10%) rotate(180deg);
opacity: 1;
}
}
.placeholderIcon {
font-size: 2.5rem;
position: relative;
z-index: 1;
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.2));
}
/* Image Overlay (for events like BSides SF 2026) */
.imageOverlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-end;
background: linear-gradient(
135deg,
rgba(10, 15, 26, 0.6) 0%,
rgba(10, 15, 26, 0.2) 50%,
transparent 70%
);
z-index: 1;
text-align: left;
padding: 1rem;
}
.overlayTitle {
font-size: 1.5rem;
font-weight: 800;
color: #f97316;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
margin-bottom: 0.125rem;
}
.overlayMeta {
font-size: 0.75rem;
font-weight: 600;
color: #ffffff;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.7);
}
/* Status Badge */
.badge {
position: absolute;
top: 14px;
left: 14px;
padding: 6px 14px;
border-radius: 100px;
font-size: 0.6875rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
z-index: 2;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.upcoming {
background: rgba(37, 99, 235, 0.9);
color: white;
box-shadow:
0 2px 8px rgba(37, 99, 235, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
.past {
background: rgba(30, 30, 30, 0.75);
color: rgba(255, 255, 255, 0.9);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
/* Content */
.content {
display: flex;
flex-direction: column;
padding: 1.5rem;
flex: 1;
background: var(--ifm-card-background-color);
}
.name {
font-size: 1.125rem;
font-weight: 650;
margin: 0 0 0.875rem 0;
line-height: 1.35;
color: var(--ifm-heading-color);
letter-spacing: -0.01em;
}
.meta {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}
.metaItem {
display: flex;
align-items: center;
gap: 0.625rem;
font-size: 0.8125rem;
color: var(--ifm-color-emphasis-600);
font-weight: 450;
}
.icon {
width: 0.9375rem;
height: 0.9375rem;
flex-shrink: 0;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0.7;
}
.description {
font-size: 0.875rem;
color: var(--ifm-color-emphasis-600);
line-height: 1.6;
margin: 0 0 1.25rem 0;
flex: 1;
}
.cta {
display: inline-flex;
align-items: center;
gap: 0.375rem;
font-size: 0.8125rem;
font-weight: 600;
color: var(--ifm-color-primary);
margin-top: auto;
transition: gap 0.2s ease;
}
.card:hover .cta {
gap: 0.5rem;
}
.ctaIcon {
width: 0.875rem;
height: 0.875rem;
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
stroke-linecap: round;
stroke-linejoin: round;
}
.card:hover .ctaIcon {
transform: translateX(3px);
}
/* Past event muted state */
.card[data-event-status='past'] {
opacity: 0.85;
}
.card[data-event-status='past']:hover {
opacity: 1;
}
.card[data-event-status='past'] .imageContainer::after {
background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.05) 100%);
}
/* Dark mode adjustments */
[data-theme='dark'] .content {
background: var(--ifm-card-background-color);
}
[data-theme='dark'] .badge.past {
background: rgba(60, 60, 60, 0.85);
}
[data-theme='dark'] .description {
color: var(--ifm-color-emphasis-500);
}
[data-theme='dark'] .imageContainer {
background: linear-gradient(
135deg,
var(--ifm-color-emphasis-200) 0%,
var(--ifm-color-emphasis-100) 100%
);
}
/* Mobile optimizations */
@media (max-width: 768px) {
.card {
border-radius: 12px;
}
.card::before {
border-radius: 12px;
}
.card:hover {
transform: translateY(-4px);
}
.content {
padding: 1.25rem;
}
.name {
font-size: 1rem;
}
.metaItem {
font-size: 0.75rem;
}
.description {
font-size: 0.8125rem;
margin-bottom: 1rem;
}
.placeholderIcon {
font-size: 2rem;
}
.badge {
top: 12px;
left: 12px;
padding: 5px 12px;
font-size: 0.625rem;
}
.overlayTitle {
font-size: 1.25rem;
}
.overlayMeta {
font-size: 0.75rem;
}
}
/* Hover state enhancement for touch devices */
@media (hover: none) {
.card:hover {
transform: none;
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.04),
0 4px 12px rgba(0, 0, 0, 0.04);
}
.card:hover::before {
opacity: 0;
}
.card:active {
transform: scale(0.98);
}
}
+91
View File
@@ -0,0 +1,91 @@
import React from 'react';
import Link from '@docusaurus/Link';
import { formatEventDate, formatEventLocation } from '../../data/events';
import styles from './EventCard.module.css';
import type { Event } from '../../data/events';
interface EventCardProps {
event: Event;
}
export default function EventCard({ event }: EventCardProps): React.ReactElement {
// All events are linkable - either to their custom page or the events listing with hash
const eventUrl = event.customPageUrl || `/events#${event.id}`;
const CardContent = () => (
<>
{/* Image */}
<div className={styles.imageContainer}>
{event.cardImage ? (
<img src={event.cardImage} alt={event.name} className={styles.image} loading="lazy" />
) : (
<div className={styles.placeholderImage}>
<span className={styles.placeholderIcon}>
{event.type === 'conference' ? '🎪' : event.type === 'party' ? '🎉' : '📅'}
</span>
</div>
)}
<span className={`${styles.badge} ${styles[event.status]}`}>
{event.status === 'upcoming' ? 'Upcoming' : 'Past'}
</span>
</div>
{/* Content */}
<div className={styles.content}>
<h3 className={styles.name}>{event.shortName}</h3>
<div className={styles.meta}>
<div className={styles.metaItem}>
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
>
<path d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span>{formatEventDate(event.startDate, event.endDate)}</span>
</div>
<div className={styles.metaItem}>
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
>
<path d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span>{formatEventLocation(event.location)}</span>
</div>
</div>
<p className={styles.description}>{event.description}</p>
<span className={styles.cta}>
View event
<svg
className={styles.ctaIcon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
>
<path d="M9 5l7 7-7 7" />
</svg>
</span>
</div>
</>
);
return (
<Link to={eventUrl} className={styles.card}>
<CardContent />
</Link>
);
}
@@ -0,0 +1,279 @@
.filters {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1.5rem;
padding: 1rem 1.5rem;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.8) 0%, rgba(248, 250, 252, 0.9) 100%);
border-radius: 14px;
margin-bottom: 2.5rem;
position: relative;
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.02),
0 4px 12px rgba(0, 0, 0, 0.03);
}
.filters::before {
content: '';
position: absolute;
inset: 0;
border-radius: 14px;
padding: 1px;
background: linear-gradient(
135deg,
rgba(37, 99, 235, 0.1) 0%,
rgba(148, 163, 184, 0.1) 50%,
rgba(37, 99, 235, 0.05) 100%
);
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: exclude;
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
pointer-events: none;
}
[data-theme='dark'] .filters {
background: linear-gradient(135deg, rgba(30, 41, 59, 0.6) 0%, rgba(15, 23, 42, 0.8) 100%);
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.1),
0 4px 12px rgba(0, 0, 0, 0.15);
}
[data-theme='dark'] .filters::before {
background: linear-gradient(
135deg,
rgba(37, 99, 235, 0.2) 0%,
rgba(99, 102, 241, 0.1) 50%,
rgba(37, 99, 235, 0.15) 100%
);
}
.filterGroup {
display: flex;
align-items: center;
gap: 0.75rem;
}
.filterLabel {
font-size: 0.8125rem;
font-weight: 600;
color: var(--ifm-color-emphasis-500);
text-transform: uppercase;
letter-spacing: 0.05em;
white-space: nowrap;
}
.filterButtons {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.filterButton {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(148, 163, 184, 0.2);
border-radius: 100px;
font-size: 0.8125rem;
font-weight: 550;
color: var(--ifm-color-emphasis-700);
cursor: pointer;
transition:
background 0.2s cubic-bezier(0.4, 0, 0.2, 1),
border-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.15s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1);
white-space: nowrap;
position: relative;
}
.filterButton:hover {
background: rgba(255, 255, 255, 0.95);
border-color: rgba(37, 99, 235, 0.3);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(37, 99, 235, 0.1);
}
.filterButton:active {
transform: translateY(0);
}
.filterButton.active {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
border-color: transparent;
color: white;
box-shadow:
0 2px 8px rgba(37, 99, 235, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
}
.filterButton.active:hover {
background: linear-gradient(135deg, #1d4ed8 0%, #1e40af 100%);
transform: translateY(-1px);
box-shadow:
0 4px 12px rgba(37, 99, 235, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
}
.count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.375rem;
height: 1.375rem;
padding: 0 0.4rem;
background: rgba(148, 163, 184, 0.15);
border-radius: 100px;
font-size: 0.6875rem;
font-weight: 700;
letter-spacing: 0.02em;
}
.filterButton.active .count {
background: rgba(255, 255, 255, 0.25);
}
.clearButton {
padding: 0.5rem 1rem;
background: transparent;
border: none;
font-size: 0.8125rem;
font-weight: 550;
color: var(--ifm-color-primary);
cursor: pointer;
transition:
color 0.15s ease,
transform 0.15s ease;
position: relative;
}
.clearButton::after {
content: '';
position: absolute;
bottom: 0.375rem;
left: 1rem;
right: 1rem;
height: 1px;
background: currentColor;
opacity: 0;
transform: scaleX(0);
transition:
opacity 0.15s ease,
transform 0.15s ease;
}
.clearButton:hover {
color: var(--ifm-color-primary-dark);
}
.clearButton:hover::after {
opacity: 0.5;
transform: scaleX(1);
}
/* Dark mode adjustments */
[data-theme='dark'] .filterButton {
background: rgba(30, 41, 59, 0.6);
border-color: rgba(148, 163, 184, 0.15);
color: var(--ifm-color-emphasis-600);
}
[data-theme='dark'] .filterButton:hover {
background: rgba(30, 41, 59, 0.9);
border-color: rgba(37, 99, 235, 0.4);
}
[data-theme='dark'] .count {
background: rgba(148, 163, 184, 0.2);
}
/* Mobile optimizations */
@media (max-width: 768px) {
.filters {
flex-direction: column;
align-items: flex-start;
gap: 1.25rem;
padding: 1.25rem;
border-radius: 12px;
}
.filterGroup {
flex-direction: column;
align-items: flex-start;
gap: 0.625rem;
width: 100%;
}
.filterLabel {
font-size: 0.75rem;
}
.filterButtons {
width: 100%;
overflow-x: auto;
flex-wrap: nowrap;
padding-bottom: 0.375rem;
margin-bottom: -0.375rem;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.filterButtons::-webkit-scrollbar {
display: none;
}
.filterButton {
flex-shrink: 0;
padding: 0.5rem 0.875rem;
font-size: 0.8125rem;
}
/* Disable hover transforms on mobile */
.filterButton:hover {
transform: none;
}
.filterButton:active {
transform: scale(0.97);
}
.clearButton {
align-self: flex-end;
padding: 0.375rem 0;
font-size: 0.8125rem;
}
}
/* Touch device optimization */
@media (hover: none) {
.filterButton:hover {
transform: none;
box-shadow: none;
background: rgba(255, 255, 255, 0.7);
border-color: rgba(148, 163, 184, 0.2);
}
.filterButton.active:hover {
transform: none;
box-shadow:
0 2px 8px rgba(37, 99, 235, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
}
.filterButton:active {
transform: scale(0.97);
}
[data-theme='dark'] .filterButton:hover {
background: rgba(30, 41, 59, 0.6);
border-color: rgba(148, 163, 184, 0.15);
}
}
+103
View File
@@ -0,0 +1,103 @@
import React from 'react';
import styles from './EventFilters.module.css';
import type { EventStatus } from '../../data/events';
export type FilterStatus = EventStatus | 'all';
export type FilterYear = number | 'all';
interface EventFiltersProps {
selectedStatus: FilterStatus;
selectedYear: FilterYear;
availableYears: number[];
onStatusChange: (status: FilterStatus) => void;
onYearChange: (year: FilterYear) => void;
eventCounts: {
all: number;
upcoming: number;
past: number;
};
}
export default function EventFilters({
selectedStatus,
selectedYear,
availableYears,
onStatusChange,
onYearChange,
eventCounts,
}: EventFiltersProps): React.ReactElement {
return (
<div className={styles.filters}>
{/* Status Filter */}
<div className={styles.filterGroup}>
<span className={styles.filterLabel}>Status</span>
<div className={styles.filterButtons}>
<button
className={`${styles.filterButton} ${selectedStatus === 'all' ? styles.active : ''}`}
onClick={() => onStatusChange('all')}
type="button"
>
All
<span className={styles.count}>{eventCounts.all}</span>
</button>
<button
className={`${styles.filterButton} ${selectedStatus === 'upcoming' ? styles.active : ''}`}
onClick={() => onStatusChange('upcoming')}
type="button"
>
Upcoming
<span className={styles.count}>{eventCounts.upcoming}</span>
</button>
<button
className={`${styles.filterButton} ${selectedStatus === 'past' ? styles.active : ''}`}
onClick={() => onStatusChange('past')}
type="button"
>
Past
<span className={styles.count}>{eventCounts.past}</span>
</button>
</div>
</div>
{/* Year Filter */}
<div className={styles.filterGroup}>
<span className={styles.filterLabel}>Year</span>
<div className={styles.filterButtons}>
<button
className={`${styles.filterButton} ${selectedYear === 'all' ? styles.active : ''}`}
onClick={() => onYearChange('all')}
type="button"
>
All Years
</button>
{availableYears.map((year) => (
<button
key={year}
className={`${styles.filterButton} ${selectedYear === year ? styles.active : ''}`}
onClick={() => onYearChange(year)}
type="button"
>
{year}
</button>
))}
</div>
</div>
{/* Clear Filters */}
{(selectedStatus !== 'all' || selectedYear !== 'all') && (
<button
className={styles.clearButton}
onClick={() => {
onStatusChange('all');
onYearChange('all');
}}
type="button"
>
Clear filters
</button>
)}
</div>
);
}
@@ -0,0 +1,457 @@
.featured {
position: relative;
border-radius: 20px;
padding: 3rem;
margin-bottom: 3.5rem;
overflow: hidden;
background: linear-gradient(135deg, #f0f7ff 0%, #e0edff 50%, #f5f9ff 100%);
}
.featured::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(ellipse 80% 50% at 20% 40%, rgba(37, 99, 235, 0.12) 0%, transparent 50%),
radial-gradient(ellipse 60% 40% at 80% 60%, rgba(99, 102, 241, 0.08) 0%, transparent 50%);
pointer-events: none;
}
.featured::after {
content: '';
position: absolute;
top: -1px;
left: -1px;
right: -1px;
bottom: -1px;
border-radius: 20px;
padding: 1px;
background: linear-gradient(
135deg,
rgba(37, 99, 235, 0.3) 0%,
rgba(99, 102, 241, 0.15) 50%,
rgba(37, 99, 235, 0.2) 100%
);
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: exclude;
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
pointer-events: none;
}
[data-theme='dark'] .featured {
background: linear-gradient(
135deg,
rgba(37, 99, 235, 0.15) 0%,
rgba(30, 41, 59, 0.8) 50%,
rgba(15, 23, 42, 0.9) 100%
);
}
[data-theme='dark'] .featured::before {
background:
radial-gradient(ellipse 80% 50% at 20% 40%, rgba(37, 99, 235, 0.2) 0%, transparent 50%),
radial-gradient(ellipse 60% 40% at 80% 60%, rgba(99, 102, 241, 0.15) 0%, transparent 50%);
}
[data-theme='dark'] .featured::after {
background: linear-gradient(
135deg,
rgba(37, 99, 235, 0.5) 0%,
rgba(99, 102, 241, 0.2) 50%,
rgba(37, 99, 235, 0.3) 100%
);
}
.container {
position: relative;
display: grid;
grid-template-columns: 1.1fr 0.9fr;
gap: 3.5rem;
align-items: center;
z-index: 1;
}
.content {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.badgeRow {
display: flex;
align-items: center;
gap: 1rem;
}
.badge {
display: inline-flex;
align-items: center;
align-self: flex-start;
padding: 0.5rem 1rem;
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
color: white;
border-radius: 100px;
font-size: 0.6875rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
box-shadow:
0 2px 8px rgba(37, 99, 235, 0.35),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
}
.badge::before {
content: '';
width: 6px;
height: 6px;
background: #34d399;
border-radius: 50%;
margin-right: 0.5rem;
animation: pulse 2s ease-in-out infinite;
box-shadow: 0 0 8px rgba(52, 211, 153, 0.6);
}
@keyframes pulse {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.6;
transform: scale(0.9);
}
}
.title {
font-size: 2.25rem;
font-weight: 750;
margin: 0;
line-height: 1.15;
color: var(--ifm-heading-color);
letter-spacing: -0.025em;
}
.meta {
display: flex;
flex-direction: column;
gap: 0.625rem;
}
.metaItem {
display: flex;
align-items: center;
gap: 0.625rem;
font-size: 0.9375rem;
color: var(--ifm-color-emphasis-700);
font-weight: 450;
}
.icon {
width: 1.125rem;
height: 1.125rem;
flex-shrink: 0;
stroke-linecap: round;
stroke-linejoin: round;
color: var(--ifm-color-primary);
opacity: 0.8;
}
.description {
font-size: 1.0625rem;
color: var(--ifm-color-emphasis-700);
line-height: 1.65;
margin: 0.25rem 0 0 0;
}
[data-theme='dark'] .description {
color: var(--ifm-color-emphasis-600);
}
.buttons {
display: flex;
gap: 1rem;
margin-top: 0.75rem;
flex-wrap: wrap;
}
.primaryButton {
display: inline-flex;
align-items: center;
gap: 0.625rem;
padding: 0.875rem 1.75rem;
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
color: white;
border-radius: 10px;
font-size: 0.9375rem;
font-weight: 600;
text-decoration: none;
box-shadow:
0 4px 14px rgba(37, 99, 235, 0.35),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
transition:
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.2s ease;
}
.primaryButton:hover {
transform: translateY(-2px);
box-shadow:
0 8px 20px rgba(37, 99, 235, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
color: white;
text-decoration: none;
}
.primaryButton:active {
transform: translateY(0);
}
.buttonIcon {
width: 1rem;
height: 1rem;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform 0.2s ease;
}
.primaryButton:hover .buttonIcon {
transform: translateX(3px);
}
/* Image */
.imageWrapper {
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.imageWrapper::before {
content: '';
position: absolute;
inset: -20px;
background: radial-gradient(circle at center, rgba(37, 99, 235, 0.1) 0%, transparent 70%);
pointer-events: none;
}
.imageContainer {
position: relative;
width: 100%;
max-width: 380px;
border-radius: 16px;
overflow: hidden;
z-index: 1;
}
.image {
width: 100%;
height: auto;
display: block;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.12),
0 2px 8px rgba(0, 0, 0, 0.08);
}
/* Image Overlay (for BSides SF 2026) */
.imageOverlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-end;
background: linear-gradient(
135deg,
rgba(10, 15, 26, 0.7) 0%,
rgba(10, 15, 26, 0.3) 40%,
transparent 60%
);
padding: 1.25rem;
}
.overlayTitle {
font-size: 1.75rem;
font-weight: 800;
color: #f97316;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
margin-bottom: 0.125rem;
}
.overlayMeta {
font-size: 0.9375rem;
font-weight: 600;
color: #ffffff;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.7);
}
.overlayVenue {
font-size: 0.8125rem;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.7);
}
[data-theme='dark'] .imageContainer {
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.5),
0 0 0 1px rgba(255, 255, 255, 0.05);
}
[data-theme='dark'] .image {
box-shadow: none;
}
.placeholderImage {
width: 100%;
max-width: 380px;
aspect-ratio: 16 / 9;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 50%, #1e40af 100%);
border-radius: 16px;
position: relative;
overflow: hidden;
z-index: 1;
box-shadow:
0 8px 32px rgba(37, 99, 235, 0.25),
0 2px 8px rgba(0, 0, 0, 0.1);
}
.placeholderImage::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle at center, rgba(255, 255, 255, 0.15) 0%, transparent 40%);
animation: rotateGlow 8s linear infinite;
}
@keyframes rotateGlow {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.placeholderIcon {
font-size: 4rem;
position: relative;
z-index: 1;
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.2));
}
/* Mobile optimizations */
@media (max-width: 996px) {
.container {
grid-template-columns: 1fr;
gap: 2.5rem;
}
.imageWrapper {
order: -1;
}
.imageWrapper::before {
display: none;
}
.imageContainer,
.placeholderImage {
max-width: 340px;
}
}
@media (max-width: 768px) {
.featured {
padding: 2rem 1.5rem;
margin-bottom: 2.5rem;
border-radius: 16px;
}
.featured::after {
border-radius: 16px;
}
.container {
gap: 2rem;
}
.content {
gap: 1rem;
}
.title {
font-size: 1.625rem;
letter-spacing: -0.02em;
}
.metaItem {
font-size: 0.875rem;
}
.description {
font-size: 0.9375rem;
}
.buttons {
flex-direction: column;
margin-top: 0.5rem;
}
.primaryButton {
width: 100%;
justify-content: center;
padding: 1rem 1.5rem;
}
.imageContainer,
.placeholderImage {
max-width: 100%;
}
.overlayTitle {
font-size: 1.5rem;
}
.overlayMeta {
font-size: 0.875rem;
}
.overlayVenue {
font-size: 0.75rem;
}
.placeholderIcon {
font-size: 3rem;
}
}
/* Small mobile */
@media (max-width: 480px) {
.featured {
padding: 1.5rem 1.25rem;
}
.title {
font-size: 1.375rem;
}
.badge {
padding: 0.4rem 0.75rem;
font-size: 0.625rem;
}
}
@@ -0,0 +1,154 @@
import React from 'react';
import Link from '@docusaurus/Link';
import { formatEventDate, formatEventLocation } from '../../data/events';
import styles from './FeaturedEvent.module.css';
import type { Event } from '../../data/events';
interface FeaturedEventProps {
event: Event;
}
export default function FeaturedEvent({ event }: FeaturedEventProps): React.ReactElement {
const hasExistingPage = Boolean(event.customPageUrl);
const eventUrl = event.customPageUrl || `/events/${event.slug}`;
return (
<section className={styles.featured}>
<div className={styles.container}>
<div className={styles.content}>
<span className={styles.badge}>Upcoming Event</span>
<h2 className={styles.title}>{event.name}</h2>
<div className={styles.meta}>
<div className={styles.metaItem}>
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
>
<path d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span>{formatEventDate(event.startDate, event.endDate)}</span>
</div>
<div className={styles.metaItem}>
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
>
<path d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span>{formatEventLocation(event.location)}</span>
</div>
{event.booth && (
<div className={styles.metaItem}>
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
>
<path d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
<span>{event.booth}</span>
</div>
)}
</div>
<p className={styles.description}>{event.fullDescription || event.description}</p>
<div className={styles.buttons}>
{hasExistingPage ? (
<Link to={eventUrl} className={styles.primaryButton}>
View event details
<svg
className={styles.buttonIcon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
>
<path d="M9 5l7 7-7 7" />
</svg>
</Link>
) : event.registrationUrl ? (
<a
href={event.registrationUrl}
className={styles.primaryButton}
target="_blank"
rel="noopener noreferrer"
>
Register now
<svg
className={styles.buttonIcon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
>
<path d="M9 5l7 7-7 7" />
</svg>
</a>
) : event.meetingUrl ? (
<a
href={event.meetingUrl}
className={styles.primaryButton}
target="_blank"
rel="noopener noreferrer"
>
Book a meeting
<svg
className={styles.buttonIcon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
>
<path d="M9 5l7 7-7 7" />
</svg>
</a>
) : (
<Link to="/contact" className={styles.primaryButton}>
Get in touch
<svg
className={styles.buttonIcon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
>
<path d="M9 5l7 7-7 7" />
</svg>
</Link>
)}
</div>
</div>
<div className={styles.imageWrapper}>
<div className={styles.imageContainer}>
{event.heroImage ? (
<img src={event.heroImage} alt={event.name} className={styles.image} />
) : (
<div className={styles.placeholderImage}>
<span className={styles.placeholderIcon}>
{event.type === 'conference' ? '🎪' : event.type === 'party' ? '🎉' : '📅'}
</span>
</div>
)}
</div>
</div>
</div>
</section>
);
}
+5
View File
@@ -0,0 +1,5 @@
export { default as EventCard } from './EventCard';
export { default as EventFilters } from './EventFilters';
export { default as FeaturedEvent } from './FeaturedEvent';
export type { FilterStatus, FilterYear } from './EventFilters';
@@ -0,0 +1,96 @@
import React, { useEffect, useState } from 'react';
import Link from '@docusaurus/Link';
import { useLocation } from '@docusaurus/router';
import styles from './styles.module.css';
type EventBannerItem = {
badge: string;
cta: string;
date: string;
href: string;
location: string;
};
const ACTIVE_EVENTS: EventBannerItem[] = [];
function getActiveEvents(): EventBannerItem[] {
return ACTIVE_EVENTS;
}
export default function EventsBanner(): React.ReactElement | null {
const [isVisible, setIsVisible] = useState(true);
const location = useLocation();
const activeEvents = getActiveEvents();
const shouldShowBanner =
activeEvents.length > 0 &&
!activeEvents.some((event) => location.pathname.includes(event.href));
useEffect(() => {
if (!shouldShowBanner) {
return;
}
const dismissed = sessionStorage.getItem('eventsBannerDismissed');
if (dismissed === 'true') {
setIsVisible(false);
}
}, [shouldShowBanner]);
const handleDismiss = () => {
setIsVisible(false);
sessionStorage.setItem('eventsBannerDismissed', 'true');
};
if (!shouldShowBanner || !isVisible) {
return null;
}
return (
<div className={styles.banner}>
<div className={styles.backgroundAnimation}>
<div className={styles.particle1} />
<div className={styles.particle2} />
<div className={styles.particle3} />
</div>
<div className={styles.content}>
<div className={styles.leftSection}>
<span className={styles.megaphone} aria-hidden="true">
!
</span>
<span className={styles.heading}>JOIN US AT</span>
</div>
<div className={styles.eventsContainer}>
{activeEvents.map((event, index) => (
<React.Fragment key={event.href}>
{index > 0 && (
<div className={styles.divider}>
<span className={styles.plus}>+</span>
</div>
)}
<Link to={event.href} className={styles.eventCard}>
<div className={styles.eventBadge}>{event.badge}</div>
<div className={styles.eventDetails}>
<span className={styles.eventDate}>{event.date}</span>
<span className={styles.eventLocation}>{event.location}</span>
</div>
<span className={styles.eventCta}>{event.cta}</span>
</Link>
</React.Fragment>
))}
</div>
<button className={styles.closeButton} onClick={handleDismiss} aria-label="Dismiss banner">
x
</button>
</div>
<div className={styles.progressBar}>
<div className={styles.progressFill} />
</div>
</div>
);
}
@@ -0,0 +1,385 @@
.banner {
position: relative;
background:
radial-gradient(ellipse at top left, rgba(179, 46, 46, 0.2) 0%, transparent 45%),
radial-gradient(ellipse at bottom right, rgba(203, 52, 52, 0.15) 0%, transparent 45%),
linear-gradient(135deg, #10191c 0%, #17252b 35%, #2e3c46 70%, #17252b 100%);
overflow: hidden;
animation: bannerGlow 4s ease-in-out infinite alternate;
border-bottom: 2px solid rgba(179, 46, 46, 0.3);
}
.banner::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 200%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(179, 46, 46, 0.08), transparent);
animation: sweep 8s linear infinite;
}
@keyframes sweep {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
@keyframes bannerGlow {
0% {
box-shadow:
0 1px 15px rgba(179, 46, 46, 0.25),
inset 0 1px 0 rgba(229, 58, 58, 0.1);
}
100% {
box-shadow:
0 1px 25px rgba(179, 46, 46, 0.35),
inset 0 1px 0 rgba(229, 58, 58, 0.15);
}
}
.backgroundAnimation {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
}
.particle1,
.particle2,
.particle3 {
position: absolute;
width: 3px;
height: 3px;
background: linear-gradient(135deg, #e53a3a, #cb3434);
border-radius: 50%;
opacity: 0.3;
box-shadow: 0 0 4px rgba(229, 58, 58, 0.4);
}
.particle1 {
animation: float1 15s infinite linear;
}
.particle2 {
animation: float2 20s infinite linear;
}
.particle3 {
animation: float3 25s infinite linear;
}
@keyframes float1 {
0% {
transform: translateX(-100px) translateY(0);
}
100% {
transform: translateX(calc(100vw + 100px)) translateY(-30px);
}
}
@keyframes float2 {
0% {
transform: translateX(-100px) translateY(20px);
}
100% {
transform: translateX(calc(100vw + 100px)) translateY(10px);
}
}
@keyframes float3 {
0% {
transform: translateX(-100px) translateY(40px);
}
100% {
transform: translateX(calc(100vw + 100px)) translateY(50px);
}
}
.content {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 8px 20px;
gap: 16px;
z-index: 1;
}
.leftSection {
display: flex;
align-items: center;
gap: 8px;
}
.megaphone {
font-size: 18px;
animation: pulse 2s ease-in-out infinite;
filter: drop-shadow(0 0 4px rgba(229, 58, 58, 0.5));
}
@keyframes pulse {
0%,
100% {
transform: scale(1);
filter: drop-shadow(0 0 4px rgba(229, 58, 58, 0.5));
}
50% {
transform: scale(1.15);
filter: drop-shadow(0 0 8px rgba(229, 58, 58, 0.8));
}
}
.heading {
color: #ffffff;
font-size: 11px;
font-weight: 700;
letter-spacing: 1.5px;
text-transform: uppercase;
opacity: 0.9;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.eventsContainer {
display: flex;
align-items: center;
gap: 12px;
}
.eventCard {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 16px;
background: rgba(255, 255, 255, 0.03);
border: 2px solid rgba(229, 58, 58, 0.2);
border-radius: 10px;
text-decoration: none !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(10px);
position: relative;
overflow: hidden;
}
.eventCard::before {
content: '';
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
background: linear-gradient(45deg, #991515, #b32e2e, #cb3434, #e53a3a);
border-radius: 10px;
opacity: 0;
transition: opacity 0.3s ease;
z-index: -1;
filter: blur(4px);
}
.eventCard:hover {
transform: translateY(-3px);
background: rgba(229, 58, 58, 0.1);
border-color: rgba(229, 58, 58, 0.4);
box-shadow: 0 8px 20px rgba(229, 58, 58, 0.3);
text-decoration: none !important;
}
.eventCard:hover::before {
opacity: 0.7;
}
.eventBadge {
color: #ffffff;
font-size: 11px;
font-weight: 900;
letter-spacing: 0.5px;
text-transform: uppercase;
white-space: nowrap;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
.eventDetails {
display: flex;
flex-direction: column;
gap: 2px;
}
.eventDate {
color: #ff7a7a;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
text-shadow: 0 0 4px rgba(229, 58, 58, 0.5);
}
.eventLocation {
color: rgba(255, 255, 255, 0.7);
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.eventCta {
color: #ffffff;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
white-space: nowrap;
opacity: 0.8;
transition: all 0.3s ease;
text-decoration: none;
}
.eventCard:hover .eventCta {
opacity: 1;
color: #ff5555;
text-shadow: none;
text-decoration: none;
}
.divider {
display: flex;
align-items: center;
justify-content: center;
}
.plus {
color: rgba(255, 255, 255, 0.5);
font-size: 16px;
font-weight: 400;
animation: rotate 10s linear infinite;
text-shadow: 0 0 4px rgba(229, 58, 58, 0.3);
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.closeButton {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
background: none;
border: 1px solid transparent;
color: rgba(255, 255, 255, 0.6);
font-size: 20px;
cursor: pointer;
padding: 4px 8px;
line-height: 1;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 6px;
}
.closeButton:hover {
color: #ffffff;
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-50%) scale(1.1);
box-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
}
.progressBar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: rgba(255, 255, 255, 0.05);
}
.progressFill {
height: 100%;
background: linear-gradient(90deg, #991515, #b32e2e, #cb3434, #e53a3a, #cb3434, #b32e2e, #991515);
background-size: 200% 100%;
animation:
progress 30s linear infinite,
shimmer 3s ease-in-out infinite;
box-shadow: 0 0 6px rgba(179, 46, 46, 0.5);
}
@keyframes shimmer {
0%,
100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
@keyframes progress {
0% {
width: 0%;
}
100% {
width: 100%;
}
}
/* Responsive design */
@media (max-width: 968px) {
.content {
flex-direction: column;
padding: 8px 16px;
gap: 8px;
}
.leftSection {
margin-bottom: 0;
}
.eventsContainer {
flex-direction: column;
width: 100%;
gap: 12px;
}
.eventCard {
width: 100%;
justify-content: center;
}
.divider {
display: none;
}
.closeButton {
top: 12px;
right: 12px;
transform: none;
}
}
@media (max-width: 640px) {
.megaphone {
font-size: 16px;
}
.heading {
font-size: 10px;
}
.eventCard {
flex-direction: column;
gap: 4px;
padding: 6px 12px;
text-align: center;
}
.eventDetails {
align-items: center;
}
}
@@ -0,0 +1,28 @@
import { useEffect } from 'react';
/**
* Forces light theme via DOM attribute without touching Docusaurus state or
* localStorage, so the user's stored preference (dark/system) survives
* navigation through non-docs pages.
*/
export default function ForceLightTheme(): null {
useEffect(() => {
const root = document.documentElement;
const forceLight = () => {
if (root.getAttribute('data-theme') !== 'light') {
root.setAttribute('data-theme', 'light');
}
};
forceLight();
// Re-enforce if Docusaurus reacts to system preference or stored value
const observer = new MutationObserver(forceLight);
observer.observe(root, { attributes: true, attributeFilter: ['data-theme'] });
return () => observer.disconnect();
}, []);
return null;
}
+105
View File
@@ -0,0 +1,105 @@
import React, { useEffect, useState } from 'react';
import { SITE_CONSTANTS } from '../../constants';
import styles from './styles.module.css';
const CACHE_KEY = 'github_stars_cache_promptfoo';
const CACHE_DURATION = 1000 * 60 * 60 * 24; // 1 day
const FETCH_TIMEOUT = 5000; // 5 seconds
interface CachedData {
stars: string;
timestamp: number;
}
function formatStarCount(count: number): string {
if (count < 1000) {
return count.toString();
}
const formatted = (count / 1000).toFixed(1);
const trimmed = formatted.endsWith('.0') ? formatted.slice(0, -2) : formatted;
return `${trimmed}k`;
}
function getCachedStars(): string | null {
try {
const cached = localStorage.getItem(CACHE_KEY);
if (cached) {
const data: CachedData = JSON.parse(cached);
if (Date.now() - data.timestamp < CACHE_DURATION) {
return data.stars;
}
}
} catch {
// Ignore localStorage errors
}
return null;
}
function setCachedStars(stars: string): void {
try {
localStorage.setItem(CACHE_KEY, JSON.stringify({ stars, timestamp: Date.now() }));
} catch {
// Ignore localStorage errors
}
}
export default function GitHubStars(): React.ReactElement {
const [stars, setStars] = useState<string>(
() => getCachedStars() || SITE_CONSTANTS.GITHUB_STARS_DISPLAY,
);
useEffect(() => {
// Skip fetch if we have cached data
if (getCachedStars()) {
return;
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
fetch('https://api.github.com/repos/promptfoo/promptfoo', {
signal: controller.signal,
})
.then((res) => res.json())
.then((data) => {
if (typeof data.stargazers_count === 'number') {
const formatted = formatStarCount(data.stargazers_count);
setStars(formatted);
setCachedStars(formatted);
}
})
.catch((error) => {
if (error.name !== 'AbortError') {
console.error('Error fetching GitHub stars:', error);
}
})
.finally(() => clearTimeout(timeoutId));
return () => {
clearTimeout(timeoutId);
controller.abort();
};
}, []);
return (
<a
href="https://github.com/promptfoo/promptfoo#readme"
target="_blank"
rel="noopener noreferrer"
className={styles.githubStars}
aria-label={`${stars} stars on GitHub`}
>
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className={styles.githubIcon}
aria-hidden="true"
>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
<span className={styles.starCount}>{stars}</span>
</a>
);
}
@@ -0,0 +1,40 @@
.githubStars {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
text-decoration: none;
color: inherit;
font-size: 14px;
font-weight: 500;
transition: opacity 0.2s ease;
white-space: nowrap;
}
.githubStars:hover {
opacity: 0.6;
text-decoration: none;
color: inherit;
}
.githubIcon {
width: 24px;
height: 24px;
fill: currentColor;
}
.starCount {
font-variant-numeric: tabular-nums;
}
@media screen and (max-width: 768px) {
.githubStars {
display: none;
}
}
@media screen and (max-width: 1280px) {
:global(.docs-wrapper) .githubStars {
display: none;
}
}
@@ -0,0 +1,124 @@
import React from 'react';
import Link from '@docusaurus/Link';
import { SITE_CONSTANTS } from '../../constants';
import styles from './styles.module.css';
export default function HomepageFeatures() {
const features = [
{
title: 'Find vulnerabilities you actually care about',
description: (
<>
<p>
Our <strong>specialized language models</strong> generate attacks specific to your
industry, company, and application.
</p>
<p>No generic canned attacks - every attack is created on-the-fly.</p>
</>
),
image: '/img/security-coverage.png',
image2x: '/img/security-coverage.png',
alt: 'promptfoo security coverage examples',
link: '/docs/red-team/llm-vulnerability-types/',
cta: 'Learn More',
},
{
title: 'Battle-tested at enterprise scale',
description: (
<>
<p>
Adopted by <strong>{SITE_CONSTANTS.FORTUNE_500_COUNT} Fortune 500 companies</strong>{' '}
shipping apps to hundreds of millions of users.
</p>
<p>
Embraced by an open-source community of{' '}
<strong>over {SITE_CONSTANTS.USER_COUNT_DISPLAY} developers</strong> worldwide.
</p>
</>
),
image: '/img/f500-usage.svg',
image2x: '/img/f500-usage.svg',
alt: 'promptfoo quickstart',
link: '/docs/red-team/quickstart/',
cta: 'Get Started',
className: 'f500',
},
/*
{
title: 'Set your standards',
description: (
<>
<p>Map to OWASP, NIST, MITRE, EU AI Act, and more.</p>
<p>
Customizable to your specific policies and industry regulations, with detailed
reporting.
</p>
</>
),
image: '/img/compliance-frameworks.png',
image2x: '/img/compliance-frameworks.png',
alt: 'promptfoo compliance reporting',
link: '/red-teaming/',
cta: 'Learn More',
},
*/
{
title: 'Security-first, developer-friendly',
image:
'https://user-images.githubusercontent.com/310310/244891726-480e1114-d049-40b9-bd5f-f81c15060284.gif',
image2x:
'https://user-images.githubusercontent.com/310310/244891726-480e1114-d049-40b9-bd5f-f81c15060284.gif',
description:
'Move quickly with a command-line interface, live reloads, and caching. No SDKs, cloud dependencies, or logins.',
link: '/docs/red-team/quickstart/',
cta: 'Get Started',
},
{
title: 'Deploy your way',
description:
'Get started in minutes with our CLI tool, or choose our managed cloud or on-premises enterprise solutions for advanced features and support.',
image: '/img/deploy-options.svg',
image2x: '/img/deploy-options.svg',
alt: 'promptfoo deployment options',
link: '/contact/',
cta: 'Get Started',
className: 'noBorder',
},
];
return (
<section className={styles.features}>
<div className="container">
<h2 className={styles.featuresTitle}>Why Promptfoo?</h2>
<div className={styles.featuresList}>
{features.map((feature, idx) => (
<div
key={idx}
className={`${styles.featureItem} ${feature.className ? styles[feature.className] : ''}`}
>
<div className={styles.featureContent}>
<h3>{feature.title}</h3>
<div>{feature.description}</div>
<Link to={feature.link} className="button button--secondary">
{feature.cta}
</Link>
</div>
<div className={styles.featureImageWrapper}>
<Link to={feature.link}>
<img
loading="lazy"
src={feature.image}
srcSet={`${feature.image} 1x, ${feature.image2x} 2x`}
alt={feature.alt}
className={`${styles.featureImage} ${feature.className ? styles[feature.className + 'Image'] : ''}`}
/>
</Link>
</div>
</div>
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,85 @@
.features {
padding: 4rem 0;
}
.featuresTitle {
text-align: center;
font-size: 2.5rem;
margin-bottom: 3rem;
}
.featuresList {
display: flex;
flex-direction: column;
gap: 4rem;
}
.featureItem {
display: flex;
align-items: center;
gap: 2rem;
}
.featureItem:nth-child(even) {
flex-direction: row-reverse;
}
.featureContent {
flex: 1;
}
.featureContent h3 {
font-size: 1.8rem;
margin-bottom: 1rem;
}
.featureContent p {
font-size: 1.1rem;
margin-bottom: 1.5rem;
}
.featureImageWrapper {
flex: 1;
}
.featureImage {
max-width: 100%;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
}
.featureImage:hover {
transform: scale(1.02);
}
.f500 img {
width: min(500px, 100%);
box-shadow: none;
}
/* No border or shadow styles */
.noBorder {
/* Custom styles for the container if needed */
}
.noBorderImage {
box-shadow: none;
border-radius: 0;
}
.noBorderImage:hover {
transform: none;
}
@media screen and (max-width: 768px) {
.featureItem,
.featureItem:nth-child(even) {
flex-direction: column;
}
.featureContent,
.featureImageWrapper {
width: 100%;
}
}
+112
View File
@@ -0,0 +1,112 @@
import React, { useState } from 'react';
import AssessmentIcon from '@mui/icons-material/Assessment';
import SettingsIcon from '@mui/icons-material/Settings';
import CodeBlock from '@theme/CodeBlock';
import styles from './styles.module.css';
export default function HomepageInfo(): React.ReactElement {
type TabId = 'evaluations' | 'security';
const [activeTab, setActiveTab] = useState<TabId>('security');
const tabs: Array<{ id: TabId; label: string; mobileLabel: string }> = [
{ id: 'evaluations', label: 'Quality evaluations', mobileLabel: 'Evaluations' },
{ id: 'security', label: 'Pre-deployment security scans', mobileLabel: 'Security' },
];
const config: Array<{ id: TabId; code: string; image: string }> = [
{
id: 'evaluations',
code: `
# Compare prompts...
prompts:
- "Summarize this in {{language}}: {{document}}"
- "Summarize this in {{language}}, concisely and professionally: {{document}}"
# And models...
providers:
- openai:gpt-5
- anthropic:claude-3.5-sonnet
# ... using these tests
tests:
- vars:
language: French
document: "file://docs/*.txt"
assert:
- type: contains
value: "foo bar"
- type: llm-rubric
value: "does not apologize"
- type: cost
threshold: 0.01
- type: latency
threshold: 3000
- # ...
`,
image:
'https://user-images.githubusercontent.com/310310/261666627-ce5a7817-da82-4484-b26d-32474f1cabc5.png',
},
{
id: 'security',
code: `
# Test cases are generated to specifically target the system's use case
purpose: 'Budget travel agent'
# Define where payloads are sent
targets:
- id: 'https://example.com/generate'
config:
method: 'POST'
headers:
'Content-Type': 'application/json'
body:
userInput: '{{prompt}}'
`,
image: '/img/risk-category-drawer@2x.png',
},
];
const activeConfig = config.find((item) => item.id === activeTab) ?? config[0];
return (
<div className={styles.infoContainer}>
<section className={styles.section}>
<h2>Easy abstractions for complex LLM testing</h2>
<div className={styles.exampleTabs}>
{tabs.map((tab) => (
<button
key={tab.id}
className={`${styles.exampleTab} ${activeTab === tab.id ? styles.exampleTabActive : ''}`}
onClick={() => setActiveTab(tab.id)}
>
<span className={styles.tabLabelDesktop}>{tab.label}</span>
<span className={styles.tabLabelMobile}>{tab.mobileLabel}</span>
</button>
))}
</div>
<div className={styles.configToWebview}>
<div className={styles.codeExample}>
<div className={styles.featureTitle}>
<SettingsIcon /> Simple declarative config
</div>
<CodeBlock language="yaml" className={styles.codeBlock}>
{activeConfig.code}
</CodeBlock>
</div>
<div className={styles.webviewExample}>
<div className={styles.featureTitle}>
<AssessmentIcon /> Detailed, actionable results
</div>
<img
loading="lazy"
src={activeConfig.image}
className={styles.resultImage}
alt={`${activeTab === 'evaluations' ? 'Evaluation' : 'Security'} Results`}
/>
</div>
</div>
</section>
</div>
);
}
@@ -0,0 +1,327 @@
:root {
--transition-timing: cubic-bezier(0.4, 0, 0.2, 1);
--transition-duration: 0.2s;
--border-radius-sm: 6px;
--border-radius-md: 8px;
--border-radius-lg: 12px;
--border-radius-pill: 100px;
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.2);
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.25);
--blue-primary: #1890ff;
--blue-hover: #40a9ff;
}
.infoContainer {
display: flex;
flex-direction: column;
gap: 2rem;
margin: 4rem 0 6rem 0;
}
.infoContainer h2 {
font-size: 2rem;
}
.section {
text-align: center;
}
.blurb {
font-size: 2.5rem;
margin-bottom: 2rem;
color: #333;
}
.codeExample,
.webviewExample {
flex: 1;
max-width: 48%;
background: white;
border-radius: var(--border-radius-lg);
overflow: hidden;
transition: all var(--transition-duration) var(--transition-timing);
}
.codeExample {
padding: 1.5rem;
}
.webviewExample {
padding: 1.5rem;
}
.codeBlock {
text-align: left;
font-size: 0.95rem;
line-height: 1.5;
border-radius: 8px;
}
.configToWebview {
display: flex;
justify-content: center;
align-items: stretch;
margin-top: 3rem;
gap: 2rem;
padding: 0 1rem;
}
.featureTitle {
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1.5rem;
color: #2c3e50;
letter-spacing: -0.02em;
}
.featureTitle svg {
margin-right: 0.75rem;
font-size: 1.5rem;
}
.resultImage {
max-width: 100%;
border-radius: var(--border-radius-md);
box-shadow: var(--shadow-sm);
transition: all var(--transition-duration) var(--transition-timing);
}
@media (max-width: 768px) {
.configToWebview {
flex-direction: column;
gap: 2rem;
}
.codeExample,
.webviewExample {
max-width: 100%;
padding: 1.25rem;
}
[data-theme='dark'] .codeExample,
[data-theme='dark'] .webviewExample {
backdrop-filter: none;
background: rgba(22, 27, 34, 0.95);
}
[data-theme='dark'] .configToWebview {
padding: 1rem;
}
.exampleTabs {
width: 100%;
max-width: 100%;
padding: 0.25rem;
gap: 0.25rem;
}
.exampleTab {
padding: 0.5rem 0.75rem;
font-size: 0.9rem;
flex: 1;
text-align: center;
}
.tabLabelDesktop {
display: none;
}
.tabLabelMobile {
display: block;
}
}
@media (max-width: 480px) {
.exampleTab {
padding: 0.5rem;
font-size: 0.85rem;
}
}
.exampleTabs {
display: flex;
justify-content: center;
gap: 0.5rem;
margin: 2.5rem 0 2rem;
padding: 0.375rem;
background: rgba(0, 0, 0, 0.03);
border-radius: var(--border-radius-pill);
width: fit-content;
margin-inline: auto;
max-width: 100%;
overflow-x: hidden;
}
.exampleTab {
padding: 0.75rem 1.25rem;
border: none;
background: none;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-duration) var(--transition-timing);
border-radius: var(--border-radius-pill);
color: #666;
position: relative;
}
.tabLabelDesktop {
display: block;
}
.tabLabelMobile {
display: none;
}
.exampleTabActive {
background: white;
color: var(--blue-primary);
box-shadow: var(--shadow-sm);
}
[data-theme='dark'] {
--shadow-sm: 0 2px 8px rgba(24, 144, 255, 0.2);
--shadow-md: 0 4px 24px rgba(0, 0, 0, 0.2);
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.25);
}
[data-theme='dark'] .exampleTabs {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(8px);
}
[data-theme='dark'] .exampleTab {
color: rgba(255, 255, 255, 0.65);
}
[data-theme='dark'] .exampleTabActive {
background: rgba(24, 144, 255, 0.1);
color: var(--blue-hover);
box-shadow:
var(--shadow-sm),
inset 0 0 0 1px rgba(64, 169, 255, 0.2);
}
[data-theme='dark'] .codeExample,
[data-theme='dark'] .webviewExample {
background: rgba(22, 27, 34, 0.8);
border: 1px solid rgba(99, 99, 99, 0.15);
backdrop-filter: blur(10px);
box-shadow: var(--shadow-md);
}
[data-theme='dark'] .codeExample {
background: rgba(13, 17, 23, 0.8);
}
[data-theme='dark'] .codeExample:hover,
[data-theme='dark'] .webviewExample:hover {
transform: translateY(-2px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
border-color: rgba(99, 99, 99, 0.25);
}
[data-theme='dark'] .featureTitle {
color: rgba(255, 255, 255, 0.95);
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
[data-theme='dark'] .featureTitle svg {
color: #40a9ff;
filter: drop-shadow(0 2px 8px rgba(64, 169, 255, 0.3));
}
[data-theme='dark'] .resultImage {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(99, 99, 99, 0.2);
filter: brightness(0.95) contrast(1.05);
transition: all 0.3s ease;
}
[data-theme='dark'] .resultImage:hover {
filter: brightness(1) contrast(1.05);
transform: scale(1.01);
}
[data-theme='dark'] .codeBlock {
background: rgba(13, 17, 23, 0.6) !important;
border: 1px solid rgba(99, 99, 99, 0.15);
}
[data-theme='dark'] .exampleTabs {
border-bottom-color: rgba(99, 99, 99, 0.2);
background: rgba(30, 30, 30, 0.3);
border-radius: 8px 8px 0 0;
padding: 0.5rem;
}
[data-theme='dark'] .exampleTab {
color: #a0a0a0;
border-radius: 6px;
padding: 0.75rem 1.25rem;
transition: all 0.2s ease;
}
[data-theme='dark'] .exampleTabActive {
background: rgba(64, 169, 255, 0.1);
border-bottom: 2px solid #40a9ff;
color: #40a9ff;
font-weight: 500;
}
[data-theme='dark'] .exampleTab:hover:not(.exampleTabActive) {
color: #40a9ff;
background: rgba(64, 169, 255, 0.05);
}
@media (max-width: 768px) {
[data-theme='dark'] .codeExample,
[data-theme='dark'] .webviewExample {
backdrop-filter: none;
background: rgba(30, 30, 30, 0.9);
}
}
/* ... rest of the existing styles ... */
/* Responsive improvements */
@media (min-width: 769px) and (max-width: 1024px) {
.configToWebview {
gap: 1.5rem;
}
.codeExample,
.webviewExample {
max-width: 49%;
}
.featureTitle {
font-size: 1.1rem;
}
}
/* Performance optimizations */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* Remove backdrop-filter on lower-end devices */
@media (max-width: 768px) and (prefers-reduced-transparency: reduce) {
[data-theme='dark'] .exampleTabs,
[data-theme='dark'] .codeExample,
[data-theme='dark'] .webviewExample {
backdrop-filter: none;
background: rgba(22, 27, 34, 0.95);
}
}
@@ -0,0 +1,53 @@
.container {
position: relative;
cursor: pointer;
margin-top: 4rem;
transition: transform 0.3s ease-in-out;
}
.container:not(.flipped):hover {
transform: scale(1.02);
}
.imageWrapper {
position: relative;
}
.image {
width: 100%;
height: auto;
transition: filter 0.5s linear;
}
.container:not(.flipped) .image {
filter: blur(20px);
}
.title {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 2rem;
color: white;
text-align: center;
transition: opacity 0.5s ease-in-out;
z-index: 1;
}
@media (max-width: 600px) {
.title {
font-size: 1rem;
}
}
.flipped .title {
opacity: 0;
}
.caption {
background-color: #000;
color: white;
padding: 2rem;
margin-top: -1rem;
}
@@ -0,0 +1,54 @@
import React, { useState } from 'react';
import styles from './ImageJailbreakPreview.module.css';
interface ImageJailbreakPreviewProps {
title: string;
images: { src: string; caption: string }[];
}
const ImageJailbreakPreview: React.FC<ImageJailbreakPreviewProps> = ({ title, images }) => {
const [flipped, setFlipped] = useState(false);
const handleClick = () => {
setFlipped(!flipped);
};
return (
<div className={`${styles.container} ${flipped ? styles.flipped : ''}`} onClick={handleClick}>
{!flipped && (
<>
<h3 className={styles.title}>{title}</h3>
{images.length > 0 && (
<div className={styles.imageWrapper}>
<img
src={images[0].src}
alt={images[0].caption}
className={`${styles.image} no-zoom`}
/>
</div>
)}
</>
)}
{flipped &&
images.map((image, index) => (
<div key={index} className={styles.imageWrapper}>
<img
loading="lazy"
src={image.src}
alt={image.caption}
className={`${styles.image} no-zoom`}
/>
<p className={styles.caption}>
<strong>Dall-E refused prompt:</strong> {title}
<br />
<br />
<strong>Jailbreak prompt:</strong> {image.caption}
</p>
</div>
))}
</div>
);
};
export default ImageJailbreakPreview;
@@ -0,0 +1,108 @@
.logoContainer {
display: flex;
flex-wrap: nowrap;
gap: 4rem;
align-items: center;
justify-content: center;
padding: 2rem;
background: rgba(255, 255, 255, 0.03);
border-radius: 8px;
}
.noBackground {
background: none !important;
}
.noBorder {
box-shadow: none !important;
}
.logoContainer img {
max-width: 150px;
flex-shrink: 0;
transition: all 0.3s ease;
opacity: 0.9;
}
.logoContainer img:hover {
opacity: 1;
transform: translateY(-2px);
}
[data-theme='dark'] .logoContainer {
background: rgba(255, 255, 255, 0.02);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05);
}
[data-theme='dark'] .logoContainer img {
opacity: 0.85;
}
[data-theme='dark'] .logoContainer img:hover {
opacity: 1;
}
/* Apply invert filter to all logos in dark mode for better visibility */
[data-theme='dark'] .logoContainer img {
filter: brightness(0) invert(0.9);
}
/* Mobile responsive - progressively hide logos from the end */
@media (max-width: 1200px) {
.logoContainer {
gap: 3rem;
}
/* Show first 5 logos */
.logoContainer img:nth-child(n + 6) {
display: none;
}
}
@media (max-width: 968px) {
.logoContainer {
gap: 2.5rem;
padding: 1.5rem;
}
.logoContainer img {
max-width: 120px;
}
/* Show first 4 logos */
.logoContainer img:nth-child(n + 5) {
display: none;
}
}
@media (max-width: 768px) {
.logoContainer {
gap: 2rem;
padding: 1rem;
}
.logoContainer img {
max-width: 100px;
}
/* Show first 3 logos */
.logoContainer img:nth-child(n + 4) {
display: none;
}
}
@media (max-width: 480px) {
.logoContainer {
gap: 1.5rem;
padding: 1rem;
}
.logoContainer img {
max-width: 90px;
}
/* Show first 2 logos */
.logoContainer img:nth-child(n + 3) {
display: none;
}
}
+44
View File
@@ -0,0 +1,44 @@
import React from 'react';
import clsx from 'clsx';
import styles from './LogoContainer.module.css';
interface LogoContainerProps {
className?: string;
noBackground?: boolean;
noBorder?: boolean;
}
export default function LogoContainer({
className,
noBackground = false,
noBorder = false,
}: LogoContainerProps) {
return (
<div
className={clsx(
styles.logoContainer,
noBackground && styles.noBackground,
noBorder && styles.noBorder,
className,
)}
>
<img
style={{ maxWidth: '100px' }}
className={styles.shopify}
src="/img/brands/shopify-logo.svg"
alt="Shopify"
/>
<img src="/img/brands/discord-logo-blue.svg" alt="Discord" />
<img
style={{ maxWidth: '110px' }}
className={styles.anthropic}
src="/img/brands/anthropic-logo.svg"
alt="Anthropic"
/>
<img style={{ maxWidth: '100px' }} src="/img/brands/microsoft-logo.svg" alt="Microsoft" />
<img style={{ maxHeight: '50px' }} src="/img/brands/doordash-logo.svg" alt="Doordash" />
<img style={{ maxHeight: '120px' }} src="/img/brands/carvana-logo.svg" alt="Carvana" />
</div>
);
}
+154
View File
@@ -0,0 +1,154 @@
import React from 'react';
import AppsIcon from '@mui/icons-material/Apps';
import CloudDoneIcon from '@mui/icons-material/CloudDone';
import CloudOffIcon from '@mui/icons-material/CloudOff';
import GroupIcon from '@mui/icons-material/Group';
import SecurityIcon from '@mui/icons-material/Security';
import VisibilityIcon from '@mui/icons-material/Visibility';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
export default function MCPProxyDiagram() {
const AV = 80; // avatar diameter
const PROXY_D = 200; // proxy circle diameter
return (
<Box
sx={{
width: '100%',
maxWidth: 900,
mx: 'auto',
p: 4,
bgcolor: 'var(--ifm-background-surface-color)',
border: '1px solid var(--ifm-color-emphasis-300)',
borderRadius: 4,
boxShadow: 3,
}}
>
<Grid
container
spacing={0}
sx={{
alignItems: 'center',
justifyContent: 'center',
}}
>
{/* Left column: Users & Apps */}
<Grid size={{ xs: 12, sm: 3 }}>
<Stack
spacing={8}
sx={{
justifyContent: 'center',
alignItems: 'center',
height: PROXY_D + 100,
}}
>
<Stack
spacing={1}
sx={{
alignItems: 'center',
}}
>
<Avatar sx={{ bgcolor: '#1976d2', width: AV, height: AV }}>
<GroupIcon sx={{ fontSize: 36, color: '#fff' }} />
</Avatar>
<Typography variant="h6">Users</Typography>
</Stack>
<Stack
spacing={1}
sx={{
alignItems: 'center',
}}
>
<Avatar sx={{ bgcolor: '#7b1fa2', width: AV, height: AV }}>
<AppsIcon sx={{ fontSize: 36, color: '#fff' }} />
</Avatar>
<Typography variant="h6">Apps</Typography>
</Stack>
</Stack>
</Grid>
{/* Center column: Proxy */}
<Grid size={{ xs: 12, sm: 6 }}>
<Stack
spacing={2}
sx={{
alignItems: 'center',
}}
>
<Box
sx={{
position: 'relative',
width: PROXY_D,
height: PROXY_D,
border: '6px solid #43a047',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bgcolor: 'var(--ifm-background-color)',
}}
>
<SecurityIcon sx={{ fontSize: 80, color: '#43a047' }} />
<Box
sx={{
position: 'absolute',
top: 12,
right: 12,
bgcolor: 'var(--ifm-background-color)',
border: '1px solid var(--ifm-color-emphasis-300)',
borderRadius: '50%',
p: 0.5,
}}
>
<VisibilityIcon sx={{ fontSize: 24, color: '#fbc02d' }} />
</Box>
</Box>
<Typography variant="h5" sx={{ fontWeight: 600 }}>
MCP Proxy
</Typography>
</Stack>
</Grid>
{/* Right column: Trusted & Unapproved */}
<Grid size={{ xs: 12, sm: 3 }}>
<Stack
spacing={8}
sx={{
justifyContent: 'center',
alignItems: 'center',
height: PROXY_D + 100,
}}
>
<Stack
spacing={1}
sx={{
alignItems: 'center',
}}
>
<Avatar sx={{ bgcolor: '#43a047', width: AV, height: AV }}>
<CloudDoneIcon sx={{ fontSize: 36, color: '#fff' }} />
</Avatar>
<Typography variant="h6">Trusted MCP</Typography>
</Stack>
<Stack
spacing={1}
sx={{
alignItems: 'center',
}}
>
<Avatar sx={{ bgcolor: '#f44336', width: AV, height: AV }}>
<CloudOffIcon sx={{ fontSize: 36, color: '#fff' }} />
</Avatar>
<Typography variant="h6">Unapproved MCP</Typography>
</Stack>
</Stack>
</Grid>
</Grid>
</Box>
);
}
+180
View File
@@ -0,0 +1,180 @@
import React, { useEffect, useId, useRef, useState } from 'react';
import Link from '@docusaurus/Link';
import styles from './styles.module.css';
export interface NavMenuCardItem {
to?: string;
href?: string;
label: string;
description?: string;
icon?: string;
type?: 'section-header' | 'link';
sectionTitle?: string;
}
interface NavMenuCardProps {
label: string;
items: NavMenuCardItem[];
position?: 'left' | 'right';
}
export default function NavMenuCard({
label,
items,
position = 'left',
}: NavMenuCardProps): React.ReactElement {
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const dropdownId = useId();
// Close dropdown when clicking outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}
function handleEscape(event: KeyboardEvent) {
if (event.key === 'Escape') {
setIsOpen(false);
}
}
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleEscape);
};
}
}, [isOpen]);
const canHover = () => window.matchMedia('(hover: hover) and (pointer: fine)').matches;
const handleToggle = () => {
if (canHover()) {
setIsOpen(true);
return;
}
setIsOpen(!isOpen);
};
// Group items by section
const sections: { title?: string; items: NavMenuCardItem[] }[] = [];
let currentSection: { title?: string; items: NavMenuCardItem[] } = { items: [] };
items.forEach((item) => {
if (item.type === 'section-header') {
if (currentSection.items.length > 0) {
sections.push(currentSection);
}
currentSection = { title: item.label, items: [] };
} else if (!item.type?.startsWith('html')) {
currentSection.items.push(item);
}
});
// Push the last section if it has items
if (currentSection.items.length > 0) {
sections.push(currentSection);
}
// If no sections were created, put all items in a single section
const displaySections =
sections.length > 0
? sections
: [
{
items: items.filter(
(item) => !item.type?.startsWith('html') && item.type !== 'section-header',
),
},
];
return (
<div
className={styles.navMenuCard}
ref={menuRef}
onMouseEnter={() => {
if (canHover()) {
setIsOpen(true);
}
}}
onMouseLeave={() => {
if (canHover()) {
setIsOpen(false);
}
}}
onBlurCapture={(event) => {
if (!menuRef.current?.contains(event.relatedTarget as Node)) {
setIsOpen(false);
}
}}
>
<button
type="button"
className={`${styles.navMenuCardButton} ${isOpen ? styles.navMenuCardButtonOpen : ''} navbar__link`}
onClick={handleToggle}
aria-controls={dropdownId}
aria-expanded={isOpen}
aria-haspopup="true"
>
{label}
<svg
className={styles.navMenuCardIcon}
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m6 9 6 6 6-6" />
</svg>
</button>
<div
id={dropdownId}
className={`${styles.navMenuCardDropdown} ${isOpen ? styles.navMenuCardDropdownOpen : ''}`}
>
<div className={styles.navMenuCardContainer}>
{displaySections.map((section, sectionIndex) => (
<div key={sectionIndex} className={styles.navMenuCardSection}>
{section.title && (
<div className={styles.navMenuCardSectionTitle}>{section.title}</div>
)}
<div className={styles.navMenuCardGrid}>
{section.items.map((item, itemIndex) => {
const isExternal =
item.href &&
(item.href.startsWith('http://') || item.href.startsWith('https://'));
const linkProps = {
to: item.to,
href: item.href,
className: styles.navMenuCardItem,
...(isExternal && { target: '_blank', rel: 'noopener noreferrer' }),
};
return (
<Link key={itemIndex} {...linkProps}>
<div className={styles.navMenuCardItemTitle}>{item.label}</div>
{item.description && (
<div className={styles.navMenuCardItemDescription}>{item.description}</div>
)}
</Link>
);
})}
</div>
</div>
))}
</div>
</div>
</div>
);
}
@@ -0,0 +1,237 @@
/* Container */
.navMenuCard {
position: relative;
display: inline-block;
}
/* Button */
.navMenuCardButton {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
padding: var(--ifm-navbar-item-padding-vertical) var(--ifm-navbar-item-padding-horizontal);
border: 0;
background: transparent;
color: inherit;
font: inherit;
}
/* Dropdown icon */
.navMenuCardIcon {
opacity: 0.4;
margin-left: 2px;
transition: transform 0.2s ease;
}
.navMenuCardButtonOpen .navMenuCardIcon {
transform: rotate(180deg);
}
/* Dropdown container - hidden by default */
.navMenuCardDropdown {
position: absolute;
top: 100%;
left: 0;
opacity: 0;
visibility: hidden;
pointer-events: none;
transition:
opacity 0.2s ease,
visibility 0.2s ease;
padding-top: 8px;
z-index: 1000;
}
/* Inner dropdown with background and border */
.navMenuCardContainer {
background: white;
border-radius: 12px;
box-shadow:
0 10px 25px -5px rgba(0, 0, 0, 0.1),
0 8px 10px -6px rgba(0, 0, 0, 0.1),
0 0 0 1px rgba(0, 0, 0, 0.05);
padding: 32px;
display: flex;
gap: 56px;
min-width: 700px;
}
/* Section container */
.navMenuCardSection {
flex: 1;
min-width: 0;
}
/* Section title */
.navMenuCardSectionTitle {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--ifm-color-emphasis-500);
margin-bottom: 20px;
padding-left: 0;
}
/* Grid of items within a section */
.navMenuCardGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 8px;
}
/* Individual menu items */
.navMenuCardItem {
display: block;
padding: 16px 18px;
border-radius: 8px;
text-decoration: none;
color: var(--ifm-font-color-base);
transition: all 0.2s ease;
border: 1px solid transparent;
}
.navMenuCardItem:hover {
background-color: rgba(0, 0, 0, 0.03);
border-color: rgba(0, 0, 0, 0.08);
text-decoration: none;
color: var(--ifm-font-color-base);
transform: translateY(-1px);
}
.navMenuCardItemTitle {
font-size: 15px;
font-weight: 600;
line-height: 1.5;
color: var(--ifm-heading-color);
margin-bottom: 6px;
}
.navMenuCardItemDescription {
font-size: 13px;
line-height: 1.6;
color: var(--ifm-color-emphasis-600);
}
/* Dark mode adjustments */
[data-theme='dark'] .navMenuCardContainer {
background: var(--ifm-background-surface-color);
box-shadow:
0 10px 25px -5px rgba(0, 0, 0, 0.4),
0 8px 10px -6px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(255, 255, 255, 0.1);
}
[data-theme='dark'] .navMenuCardItem:hover {
background-color: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.12);
}
@media (prefers-reduced-motion: reduce) {
.navMenuCardIcon,
.navMenuCardDropdown,
.navMenuCardItem {
transition: none;
}
.navMenuCardItem:hover {
transform: none;
}
}
/* Open state for mobile (when clicked) */
.navMenuCardDropdownOpen {
opacity: 1;
visibility: visible;
pointer-events: all;
}
/* Responsive */
@media (max-width: 1100px) {
/* Hide dropdown menus in the top navbar but keep them for the hamburger menu */
:global(.navbar__inner) .navMenuCard {
display: none;
}
/* Style for mobile sidebar/hamburger menu */
:global(.navbar-sidebar__items) .navMenuCard {
display: block;
width: 100%;
}
:global(.navbar-sidebar__items) .navMenuCardButton {
color: var(--ifm-menu-color);
}
:global(.navbar-sidebar__items) .navMenuCardButton:hover {
color: var(--ifm-menu-color-active);
}
.navMenuCardButton {
width: 100%;
justify-content: space-between;
font-weight: 600;
min-height: 44px;
}
.navMenuCardDropdown {
position: static;
padding: 0;
width: 100%;
}
/* Show dropdown inline when open, hide when closed */
.navMenuCardDropdown {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.navMenuCardDropdownOpen {
max-height: 2000px;
}
.navMenuCardContainer {
min-width: auto;
max-width: 100%;
flex-direction: column;
gap: 16px;
padding: 12px 0;
box-shadow: none;
background: transparent;
border-radius: 0;
}
.navMenuCardGrid {
grid-template-columns: 1fr;
gap: 4px;
}
.navMenuCardItem {
display: flex;
align-items: center;
min-height: 44px;
padding: 10px 16px;
font-size: 14px;
}
.navMenuCardItemTitle {
font-size: 14px;
}
.navMenuCardItemDescription {
display: none;
}
.navMenuCardSectionTitle {
font-size: 9px;
padding-left: 16px;
}
/* Dark mode adjustments for mobile */
[data-theme='dark'] .navMenuCardContainer {
box-shadow: none;
background: transparent;
}
}
@@ -0,0 +1,7 @@
.container {
padding-bottom: 4rem;
}
.container :global(.inline-container) {
margin: 0 auto;
}
+33
View File
@@ -0,0 +1,33 @@
import React, { useEffect } from 'react';
import styles from './NewsletterForm.module.css';
const NewsletterForm: React.FC = () => {
const containerRef = React.useRef<HTMLDivElement>(null);
useEffect(() => {
if (!containerRef.current) {
return;
}
const script = document.createElement('script');
script.src = 'https://eocampaign1.com/form/f3ae5b98-452e-11ef-bf73-e58938252fdd.js';
script.async = true;
script.setAttribute('data-form', 'f3ae5b98-452e-11ef-bf73-e58938252fdd');
containerRef.current.appendChild(script);
return () => {
if (containerRef.current && script.parentNode === containerRef.current) {
containerRef.current.removeChild(script);
}
};
}, []);
return (
<div className={styles.container} ref={containerRef}>
{/* The form will be injected here by the external script */}
</div>
);
};
export default NewsletterForm;
@@ -0,0 +1,50 @@
import React, { useState } from 'react';
import CodeBlock from '@theme/CodeBlock';
import styles from './styles.module.css';
interface FileDefinition {
name: string;
content: string;
description?: string;
}
interface PythonFileViewerProps {
files: FileDefinition[];
defaultOpen?: string;
}
export default function PythonFileViewer({
files,
defaultOpen,
}: PythonFileViewerProps): React.ReactElement {
const [activeFile, setActiveFile] = useState<string | null>(defaultOpen || null);
return (
<div className={styles.container}>
<div className={styles.header}>
<span className={styles.headerLabel}>View source</span>
<div className={styles.tabs}>
{files.map((file) => (
<button
key={file.name}
className={`${styles.tab} ${activeFile === file.name ? styles.active : ''}`}
onClick={() => setActiveFile(activeFile === file.name ? null : file.name)}
title={file.description}
type="button"
>
{file.name}
</button>
))}
</div>
</div>
{activeFile && (
<div className={styles.codeContainer}>
<CodeBlock language="python">
{files.find((f) => f.name === activeFile)?.content || ''}
</CodeBlock>
</div>
)}
</div>
);
}
@@ -0,0 +1,147 @@
.container {
margin: 1.25rem 0;
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: var(--ifm-code-border-radius);
overflow: hidden;
background: var(--ifm-background-surface-color);
}
.header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: var(--ifm-color-emphasis-100);
border-bottom: 1px solid var(--ifm-color-emphasis-200);
flex-wrap: wrap;
min-height: 42px;
}
.headerLabel {
font-size: 0.75rem;
color: var(--ifm-color-emphasis-700);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
white-space: nowrap;
}
.tabs {
display: flex;
gap: 0.375rem;
flex-wrap: wrap;
}
.tab {
display: inline-flex;
align-items: center;
padding: 0.375rem 0.625rem;
border: 1px solid transparent;
border-radius: 4px;
background: transparent;
cursor: pointer;
font-family: var(--ifm-font-family-monospace);
font-size: 0.8125rem;
font-weight: 500;
color: var(--ifm-color-emphasis-700);
transition: all 0.15s ease;
line-height: 1.2;
}
.tab:hover {
background: var(--ifm-color-emphasis-200);
color: var(--ifm-color-emphasis-900);
}
.tab.active {
background: var(--ifm-color-primary);
color: white;
border-color: var(--ifm-color-primary-dark);
}
.tab.active:hover {
background: var(--ifm-color-primary-dark);
}
.codeContainer {
animation: fadeIn 0.15s ease;
}
@keyframes fadeIn {
from {
opacity: 0.5;
}
to {
opacity: 1;
}
}
.codeContainer > div {
margin-bottom: 0 !important;
}
.codeContainer pre {
margin: 0 !important;
border-radius: 0 !important;
border: none !important;
}
.codeContainer pre code {
padding: 0.875rem 1rem !important;
}
/* Dark mode adjustments */
[data-theme='dark'] .container {
border-color: var(--ifm-color-emphasis-400);
}
[data-theme='dark'] .header {
background: rgba(255, 255, 255, 0.03);
border-bottom-color: var(--ifm-color-emphasis-400);
}
[data-theme='dark'] .headerLabel {
color: var(--ifm-color-emphasis-600);
}
[data-theme='dark'] .tab {
color: var(--ifm-color-emphasis-600);
}
[data-theme='dark'] .tab:hover {
background: var(--ifm-color-emphasis-300);
color: var(--ifm-color-emphasis-900);
}
[data-theme='dark'] .tab.active {
background: var(--ifm-color-primary);
color: white;
}
/* Mobile adjustments */
@media (max-width: 576px) {
.header {
padding: 0.5rem;
gap: 0.375rem;
}
.headerLabel {
font-size: 0.7rem;
width: 100%;
}
.tabs {
width: 100%;
gap: 0.25rem;
}
.tab {
font-size: 0.75rem;
padding: 0.3rem 0.5rem;
}
.codeContainer pre code {
padding: 0.75rem !important;
font-size: 0.8rem !important;
}
}
+379
View File
@@ -0,0 +1,379 @@
import React from 'react';
import AddIcon from '@mui/icons-material/Add';
import CloseIcon from '@mui/icons-material/Close';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutlineOutlined';
import RemoveIcon from '@mui/icons-material/Remove';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import Divider from '@mui/material/Divider';
import Drawer from '@mui/material/Drawer';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import { useCartContext } from './CartProvider';
import { useCopyToClipboard } from './useCopyToClipboard';
import { formatPrice, getAttributeName, getCheckoutUrl } from './useFourthwall';
export function CartDrawer() {
const { cart, isCartOpen, closeCart, removeFromCart, updateQuantity, isLoading, couponCode } =
useCartContext();
const { copied: copiedInDrawer, handleCopy: handleCopyInDrawer } = useCopyToClipboard(couponCode);
const handleCheckout = () => {
if (cart?.id) {
// Get currency from first item in cart, or default to USD
const currency = cart.items[0]?.variant?.unitPrice?.currency || 'USD';
window.location.href = getCheckoutUrl(cart.id, currency);
}
};
return (
<Drawer
anchor="right"
open={isCartOpen}
onClose={closeCart}
slotProps={{
paper: {
sx: {
width: { xs: '100%', sm: '400px' },
maxWidth: '100vw',
// Support safe area insets for notched phones
paddingTop: 'env(safe-area-inset-top)',
paddingBottom: 'env(safe-area-inset-bottom)',
},
},
}}
>
{/* Header */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: 2,
py: { xs: 1.5, sm: 2 },
borderBottom: '1px solid var(--ifm-color-emphasis-300)',
minHeight: { xs: 56, sm: 64 },
}}
>
<Typography
variant="h6"
sx={{ fontWeight: 600, fontSize: { xs: '1.1rem', sm: '1.25rem' } }}
>
Cart
</Typography>
<IconButton
onClick={closeCart}
aria-label="Close cart"
sx={{
minWidth: 44,
minHeight: 44,
}}
>
<CloseIcon />
</IconButton>
</Box>
{/* Cart items */}
<Box
sx={{
flex: 1,
overflow: 'auto',
p: 2,
WebkitOverflowScrolling: 'touch',
}}
>
{!cart || cart.items.length === 0 ? (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '260px',
gap: 1.5,
px: 3,
}}
>
<Typography
variant="body1"
sx={{ color: 'var(--ifm-color-emphasis-600)', fontWeight: 500 }}
>
Your cart is empty
</Typography>
<Button
variant="outlined"
size="small"
onClick={closeCart}
sx={{
borderColor: 'var(--ifm-color-emphasis-400)',
color: 'var(--ifm-font-color-base)',
borderRadius: '8px',
textTransform: 'none',
fontWeight: 500,
'&:hover': {
borderColor: 'var(--ifm-color-emphasis-600)',
backgroundColor: 'var(--ifm-background-surface-color)',
},
}}
>
Continue shopping
</Button>
</Box>
) : (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
opacity: isLoading ? 0.6 : 1,
transition: 'opacity 0.2s ease-in-out',
pointerEvents: isLoading ? 'none' : 'auto',
}}
>
{cart.items.map((item) => {
// Get image from variant first, then product, with fallbacks
const image = item.variant?.images?.[0] || item.variant?.product?.images?.[0] || null;
const productName = item.variant?.product?.name || item.variant?.name || 'Product';
return (
<Box
key={item.variant.id}
sx={{
display: 'flex',
gap: 2,
pb: 2,
borderBottom: '1px solid var(--ifm-color-emphasis-200)',
}}
>
{/* Product image */}
<Box
sx={{
width: 80,
height: 80,
flexShrink: 0,
backgroundColor: 'var(--ifm-background-surface-color)',
overflow: 'hidden',
}}
>
{image && (
<Box
component="img"
src={image.url}
alt={productName}
sx={{
width: '100%',
height: '100%',
objectFit: 'cover',
}}
/>
)}
</Box>
{/* Product details */}
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography
variant="body2"
sx={{
fontWeight: 600,
mb: 0.5,
}}
>
{productName}
</Typography>
{/* Variant attributes */}
{item.variant?.attributes &&
Object.entries(item.variant.attributes).length > 0 && (
<Typography
variant="caption"
sx={{ color: 'text.secondary', display: 'block' }}
>
{Object.entries(item.variant.attributes)
.filter(([key]) => key !== 'description')
.map(([_, value]) => getAttributeName(value))
.join(' / ')}
</Typography>
)}
{item.variant?.unitPrice && (
<Typography variant="body2" sx={{ mt: 0.5 }}>
{formatPrice(item.variant.unitPrice)}
</Typography>
)}
{/* Quantity controls */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
mt: 1,
}}
>
<IconButton
size="small"
onClick={() => {
if (item.quantity > 1) {
updateQuantity(item.variant.id, item.quantity - 1);
}
}}
disabled={isLoading || item.quantity <= 1}
sx={{
border: '1px solid var(--ifm-color-emphasis-300)',
borderRadius: '4px',
p: 0.5,
minWidth: 36,
minHeight: 36,
touchAction: 'manipulation',
}}
aria-label="Decrease quantity"
>
<RemoveIcon fontSize="small" />
</IconButton>
<Typography variant="body2" sx={{ minWidth: 28, textAlign: 'center' }}>
{item.quantity}
</Typography>
<IconButton
size="small"
onClick={() => updateQuantity(item.variant.id, item.quantity + 1)}
disabled={isLoading}
sx={{
border: '1px solid var(--ifm-color-emphasis-300)',
borderRadius: '4px',
p: 0.5,
minWidth: 36,
minHeight: 36,
touchAction: 'manipulation',
}}
aria-label="Increase quantity"
>
<AddIcon fontSize="small" />
</IconButton>
<IconButton
size="small"
onClick={() => removeFromCart(item.variant.id)}
disabled={isLoading}
sx={{
ml: 'auto',
minWidth: 36,
minHeight: 36,
touchAction: 'manipulation',
}}
aria-label="Remove item"
>
<DeleteOutlineIcon fontSize="small" />
</IconButton>
</Box>
</Box>
</Box>
);
})}
</Box>
)}
</Box>
{/* Footer with total and checkout */}
{cart && cart.items.length > 0 && (
<Box sx={{ p: 2, borderTop: '1px solid var(--ifm-color-emphasis-300)' }}>
{cart.subtotal && (
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
mb: 2,
}}
>
<Typography variant="body1" sx={{ fontWeight: 600 }}>
Subtotal
</Typography>
<Typography variant="body1" sx={{ fontWeight: 600 }}>
{formatPrice(cart.subtotal)}
</Typography>
</Box>
)}
<Typography variant="caption" sx={{ color: 'text.secondary', display: 'block', mb: 2 }}>
Shipping and taxes calculated at checkout
</Typography>
<Divider sx={{ mb: 2 }} />
{couponCode && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
mb: 2,
p: 1.5,
backgroundColor: 'var(--ifm-color-success-contrast-background)',
borderRadius: '8px',
border: '1px solid var(--ifm-color-success-dark)',
}}
>
<Typography
variant="caption"
sx={{ flex: 1, color: 'var(--ifm-color-success-darkest)' }}
>
Enter code{' '}
<Box component="span" sx={{ fontWeight: 700, fontFamily: 'monospace' }}>
{couponCode}
</Box>{' '}
at checkout
</Typography>
<IconButton
size="small"
onClick={handleCopyInDrawer}
aria-label="Copy promo code"
title={copiedInDrawer ? 'Copied!' : 'Copy code'}
sx={{ color: 'var(--ifm-color-success-darkest)', p: 0.5 }}
>
<ContentCopyIcon sx={{ fontSize: '0.9rem' }} />
</IconButton>
{copiedInDrawer && (
<Typography
variant="caption"
sx={{ color: 'var(--ifm-color-success-darkest)', fontWeight: 500 }}
>
Copied!
</Typography>
)}
</Box>
)}
<Button
variant="contained"
fullWidth
size="large"
onClick={handleCheckout}
disabled={isLoading}
sx={{
py: { xs: 1.75, sm: 1.5 },
minHeight: { xs: 48, sm: 44 },
backgroundColor: 'var(--ifm-color-primary-darker)',
borderRadius: '8px',
textTransform: 'uppercase',
letterSpacing: '0.05em',
touchAction: 'manipulation',
'&:hover': {
backgroundColor: 'var(--ifm-color-primary-darkest)',
},
}}
>
{isLoading ? (
<CircularProgress size={20} sx={{ color: 'var(--ifm-button-color, #fff)' }} />
) : (
'Checkout'
)}
</Button>
</Box>
)}
</Drawer>
);
}
+178
View File
@@ -0,0 +1,178 @@
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
import { useCart } from './useFourthwall';
import type { FourthwallCart, FourthwallProduct } from './types';
const COUPON_STORAGE_KEY = 'promptfoo_coupon_code';
interface CartContextValue {
// Cart state
cart: FourthwallCart | null;
isLoading: boolean;
error: string | null;
itemCount: number;
// Cart actions
addToCart: (variantId: string, quantity?: number) => Promise<FourthwallCart | undefined>;
removeFromCart: (itemId: string) => Promise<FourthwallCart | undefined>;
updateQuantity: (itemId: string, quantity: number) => Promise<FourthwallCart | undefined>;
clearCart: () => void;
// Cart drawer state
isCartOpen: boolean;
openCart: () => void;
closeCart: () => void;
// Product modal state
selectedProduct: FourthwallProduct | null;
openProductModal: (product: FourthwallProduct) => void;
closeProductModal: () => void;
// Coupon state
couponCode: string | null;
clearCoupon: () => void;
}
const CartContext = createContext<CartContextValue | null>(null);
export function CartProvider({ children }: { children: React.ReactNode }) {
const {
cart,
isLoading,
error,
itemCount,
addToCart: addToCartApi,
removeFromCart: removeFromCartApi,
updateQuantity: updateQuantityApi,
clearCart: clearCartApi,
} = useCart();
const [isCartOpen, setIsCartOpen] = useState(false);
const [selectedProduct, setSelectedProduct] = useState<FourthwallProduct | null>(null);
const [couponCode, setCouponCode] = useState<string | null>(null);
const ingestCouponFromUrl = useCallback(() => {
const params = new URLSearchParams(window.location.search);
const urlCoupon = params.get('coupon');
if (urlCoupon) {
const code = urlCoupon.trim().toUpperCase();
setCouponCode(code);
try {
localStorage.setItem(COUPON_STORAGE_KEY, code);
} catch {
// Private browsing
}
// Clean the URL without triggering a navigation
params.delete('coupon');
const newUrl = params.toString()
? `${window.location.pathname}?${params.toString()}`
: window.location.pathname;
window.history.replaceState(window.history.state, '', newUrl);
return true;
}
return false;
}, []);
// Read coupon from URL or localStorage on mount
useEffect(() => {
if (!ingestCouponFromUrl()) {
// Fall back to localStorage
try {
const stored = localStorage.getItem(COUPON_STORAGE_KEY);
if (stored) {
setCouponCode(stored);
}
} catch {
// Private browsing
}
}
}, [ingestCouponFromUrl]);
// Re-check URL on client-side navigations (Docusaurus SPA route changes)
useEffect(() => {
const handleRouteChange = () => ingestCouponFromUrl();
// Docusaurus uses pushState/replaceState for navigation
window.addEventListener('popstate', handleRouteChange);
// Patch pushState/replaceState to detect Docusaurus client-side navigation
const originalPushState = window.history.pushState;
const originalReplaceState = window.history.replaceState;
window.history.pushState = function (...args) {
originalPushState.apply(this, args);
handleRouteChange();
};
window.history.replaceState = function (...args) {
originalReplaceState.apply(this, args);
handleRouteChange();
};
return () => {
window.removeEventListener('popstate', handleRouteChange);
window.history.pushState = originalPushState;
window.history.replaceState = originalReplaceState;
};
}, [ingestCouponFromUrl]);
const clearCoupon = useCallback(() => {
setCouponCode(null);
try {
localStorage.removeItem(COUPON_STORAGE_KEY);
} catch {
// Private browsing
}
}, []);
const openCart = useCallback(() => setIsCartOpen(true), []);
const closeCart = useCallback(() => setIsCartOpen(false), []);
const openProductModal = useCallback((product: FourthwallProduct) => {
setSelectedProduct(product);
}, []);
const closeProductModal = useCallback(() => {
setSelectedProduct(null);
}, []);
// Wrap addToCart to automatically open cart drawer on success
const addToCart = useCallback(
async (variantId: string, quantity: number = 1) => {
const result = await addToCartApi(variantId, quantity);
if (result) {
setIsCartOpen(true);
}
return result;
},
[addToCartApi],
);
const value: CartContextValue = {
cart,
isLoading,
error,
itemCount,
addToCart,
removeFromCart: removeFromCartApi,
updateQuantity: updateQuantityApi,
clearCart: clearCartApi,
isCartOpen,
openCart,
closeCart,
selectedProduct,
openProductModal,
closeProductModal,
couponCode,
clearCoupon,
};
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}
export function useCartContext() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCartContext must be used within a CartProvider');
}
return context;
}
@@ -0,0 +1,140 @@
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';
import { ProductCard } from './ProductCard';
import type { FourthwallProduct } from './types';
const mockProduct: FourthwallProduct = {
id: 'prod-1',
slug: 'test-product',
name: 'Test Product',
description: '<p>A test product description</p>',
images: [
{ id: 'img-1', url: 'https://example.com/image1.jpg', width: 800, height: 800 },
{ id: 'img-2', url: 'https://example.com/image2.jpg', width: 800, height: 800 },
],
variants: [
{
id: 'var-1',
name: 'Default',
sku: 'TEST-001',
unitPrice: { value: 29.99, currency: 'USD' },
stock: { type: 'UNLIMITED' },
images: [],
attributes: {},
},
],
};
describe('ProductCard', () => {
it('renders product image', () => {
render(<ProductCard product={mockProduct} onClick={vi.fn()} />);
const image = screen.getByRole('img', { name: mockProduct.name });
expect(image).toBeInTheDocument();
expect(image).toHaveAttribute('src', mockProduct.images[0].url);
});
it('has accessible label', () => {
render(<ProductCard product={mockProduct} onClick={vi.fn()} />);
const button = screen.getByRole('button', { name: `View ${mockProduct.name}` });
expect(button).toBeInTheDocument();
});
it('calls onClick when clicked', async () => {
const handleClick = vi.fn();
const user = userEvent.setup();
render(<ProductCard product={mockProduct} onClick={handleClick} />);
await user.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledWith(mockProduct);
});
it('shows skeleton while image loads', () => {
render(<ProductCard product={mockProduct} onClick={vi.fn()} />);
// Primary image should have opacity 0 initially
const primaryImage = screen.getByRole('img', { name: mockProduct.name });
expect(primaryImage).toHaveStyle({ opacity: 0 });
});
it('shows image after loading', () => {
render(<ProductCard product={mockProduct} onClick={vi.fn()} />);
const primaryImage = screen.getByRole('img', { name: mockProduct.name });
fireEvent.load(primaryImage);
expect(primaryImage).toHaveStyle({ opacity: 1 });
});
it('crossfades to hover image on hover when both images loaded', async () => {
render(<ProductCard product={mockProduct} onClick={vi.fn()} />);
const button = screen.getByRole('button');
const primaryImage = screen.getByRole('img', { name: mockProduct.name });
const hoverImage = screen.getByRole('img', { name: `${mockProduct.name} alternate view` });
// Load both images
fireEvent.load(primaryImage);
fireEvent.load(hoverImage);
// Initially primary is visible, hover is hidden
expect(primaryImage).toHaveStyle({ opacity: 1 });
expect(hoverImage).toHaveStyle({ opacity: 0 });
// Hover shows second image, hides primary
fireEvent.mouseEnter(button);
expect(primaryImage).toHaveStyle({ opacity: 0 });
expect(hoverImage).toHaveStyle({ opacity: 1 });
// Mouse leave returns to primary
fireEvent.mouseLeave(button);
expect(primaryImage).toHaveStyle({ opacity: 1 });
expect(hoverImage).toHaveStyle({ opacity: 0 });
});
it('displays product name', () => {
render(<ProductCard product={mockProduct} onClick={vi.fn()} />);
expect(screen.getByText('Test Product')).toBeInTheDocument();
});
it('displays product price', () => {
render(<ProductCard product={mockProduct} onClick={vi.fn()} />);
expect(screen.getByText('$29.99')).toBeInTheDocument();
});
it('displays the lowest price when multiple variants exist', () => {
const multiVariantProduct = {
...mockProduct,
variants: [
{ ...mockProduct.variants[0], id: 'v1', unitPrice: { value: 49.99, currency: 'USD' } },
{ ...mockProduct.variants[0], id: 'v2', unitPrice: { value: 19.99, currency: 'USD' } },
{ ...mockProduct.variants[0], id: 'v3', unitPrice: { value: 39.99, currency: 'USD' } },
],
};
render(<ProductCard product={multiVariantProduct} onClick={vi.fn()} />);
expect(screen.getByText('$19.99')).toBeInTheDocument();
});
it('handles product with single image', () => {
const singleImageProduct = {
...mockProduct,
images: [mockProduct.images[0]],
};
render(<ProductCard product={singleImageProduct} onClick={vi.fn()} />);
const button = screen.getByRole('button');
const image = screen.getByRole('img');
// Hover should not change image
fireEvent.mouseEnter(button);
expect(image).toHaveAttribute('src', singleImageProduct.images[0].url);
});
});
+158
View File
@@ -0,0 +1,158 @@
import React, { useMemo, useState } from 'react';
import Box from '@mui/material/Box';
import ButtonBase from '@mui/material/ButtonBase';
import Skeleton from '@mui/material/Skeleton';
import Typography from '@mui/material/Typography';
import { formatPrice } from './useFourthwall';
import type { FourthwallProduct } from './types';
interface ProductCardProps {
product: FourthwallProduct;
onClick: (product: FourthwallProduct) => void;
}
export function ProductCard({ product, onClick }: ProductCardProps) {
const [primaryLoaded, setPrimaryLoaded] = useState(false);
const [hoverLoaded, setHoverLoaded] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const primaryImage = product.images[0];
const hoverImage = product.images[1];
const hasHoverImage = Boolean(hoverImage);
const lowestPrice = useMemo(
() =>
product.variants.reduce(
(min, v) => (v.unitPrice.value < min.value ? v.unitPrice : min),
product.variants[0]?.unitPrice ?? { value: 0, currency: 'USD' },
),
[product.variants],
);
return (
<ButtonBase
onClick={() => onClick(product)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
sx={{
display: 'block',
width: '100%',
position: 'relative',
backgroundColor: 'var(--ifm-background-surface-color)',
borderRadius: '8px',
overflow: 'hidden',
transition: 'transform 0.2s ease-out, box-shadow 0.2s ease-out, opacity 0.15s ease-out',
// Touch-friendly: prevent zoom and improve tap response
touchAction: 'manipulation',
WebkitTapHighlightColor: 'transparent',
// Only apply hover effect on devices that support hover
'@media (hover: hover)': {
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: 'var(--store-card-shadow-hover)',
},
},
// Active state for touch feedback
'&:active': {
opacity: 0.8,
transform: 'scale(0.98)',
},
'&:focus-visible': {
outline: '2px solid var(--ifm-color-primary)',
outlineOffset: '2px',
},
}}
aria-label={`View ${product.name}`}
>
<Box
sx={{
width: '100%',
aspectRatio: '1',
position: 'relative',
overflow: 'hidden',
}}
>
{!primaryLoaded && (
<Skeleton
variant="rectangular"
animation="wave"
sx={{
position: 'absolute',
inset: 0,
width: '100%',
height: '100%',
}}
/>
)}
{/* Primary image — always rendered */}
<Box
component="img"
src={primaryImage?.url}
alt={product.name}
onLoad={() => setPrimaryLoaded(true)}
sx={{
position: 'absolute',
inset: 0,
width: '100%',
height: '100%',
objectFit: 'cover',
opacity: primaryLoaded && !(isHovered && hasHoverImage && hoverLoaded) ? 1 : 0,
transition: 'opacity 0.3s ease-out',
}}
/>
{/* Hover image — rendered once to preload, shown on hover */}
{hasHoverImage && (
<Box
component="img"
src={hoverImage.url}
alt={`${product.name} alternate view`}
onLoad={() => setHoverLoaded(true)}
sx={{
position: 'absolute',
inset: 0,
width: '100%',
height: '100%',
objectFit: 'cover',
opacity: isHovered && hoverLoaded ? 1 : 0,
transition: 'opacity 0.3s ease-out',
}}
/>
)}
</Box>
<Box
sx={{
px: 1.5,
py: 1.25,
textAlign: 'left',
}}
>
<Typography
variant="body2"
sx={{
fontWeight: 500,
lineHeight: 1.3,
color: 'var(--ifm-font-color-base)',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
}}
>
{product.name}
</Typography>
<Typography
variant="body2"
sx={{
mt: 0.25,
color: 'var(--ifm-color-emphasis-700)',
fontSize: '0.8rem',
}}
>
{formatPrice(lowestPrice)}
</Typography>
</Box>
</ButtonBase>
);
}
+100
View File
@@ -0,0 +1,100 @@
import React from 'react';
import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';
import Typography from '@mui/material/Typography';
import { useCartContext } from './CartProvider';
import { ProductCard } from './ProductCard';
import { PromoCard } from './PromoCard';
import type { FourthwallProduct } from './types';
interface ProductGridProps {
products: FourthwallProduct[];
isLoading: boolean;
error: string | null;
}
export function ProductGrid({ products, isLoading, error }: ProductGridProps) {
const { openProductModal } = useCartContext();
if (isLoading) {
return (
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '50vh',
}}
>
<CircularProgress sx={{ color: 'var(--ifm-color-primary)' }} />
</Box>
);
}
if (error) {
return (
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '50vh',
px: 3,
}}
>
<Typography color="error" align="center">
Failed to load products. Please try again later.
</Typography>
</Box>
);
}
if (products.length === 0) {
return (
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '50vh',
px: 3,
}}
>
<Typography color="text.secondary" align="center">
No products available.
</Typography>
</Box>
);
}
return (
<Box
sx={{
display: 'grid',
gridTemplateColumns: {
xs: 'repeat(2, 1fr)',
sm: 'repeat(2, 1fr)',
md: 'repeat(3, 1fr)',
lg: 'repeat(4, 1fr)',
},
gap: { xs: 1.5, sm: 2.5 },
maxWidth: '1200px',
mx: 'auto',
px: { xs: 1.5, sm: 3, md: 4 },
py: { xs: 2, sm: 3 },
}}
>
{products.map((product, index) => (
<React.Fragment key={product.id}>
<ProductCard product={product} onClick={openProductModal} />
{/* Insert promo card after 3rd product */}
{index === 2 && <PromoCard />}
</React.Fragment>
))}
{/* If less than 3 products, still show promo card at end */}
{products.length > 0 && products.length <= 2 && <PromoCard />}
</Box>
);
}
+511
View File
@@ -0,0 +1,511 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import CloseIcon from '@mui/icons-material/Close';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import Dialog from '@mui/material/Dialog';
import FormControl from '@mui/material/FormControl';
import IconButton from '@mui/material/IconButton';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select';
import Typography from '@mui/material/Typography';
import { useCartContext } from './CartProvider';
import {
formatPrice,
getAttributeName,
getAttributeSwatch,
isInStock,
stripHtml,
} from './useFourthwall';
import type { FourthwallAttributeValue } from './types';
export function ProductModal() {
const { selectedProduct, closeProductModal, addToCart, isLoading } = useCartContext();
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const [selectedVariantId, setSelectedVariantId] = useState<string>('');
const [isAdding, setIsAdding] = useState(false);
// Touch swipe handling
const touchStartX = useRef<number | null>(null);
const touchEndX = useRef<number | null>(null);
const isOpen = selectedProduct !== null;
// Reset state when product changes
useEffect(() => {
if (selectedProduct) {
setCurrentImageIndex(0);
setSelectedVariantId(selectedProduct.variants[0]?.id || '');
}
}, [selectedProduct]);
// Get unique attribute options (e.g., sizes, colors)
// Skip 'description' as it's not a selectable attribute
const attributeOptions = useMemo(() => {
if (!selectedProduct) return {};
const options: Record<string, { name: string; swatch?: string }[]> = {};
selectedProduct.variants.forEach((variant) => {
Object.entries(variant.attributes).forEach(([key, value]) => {
// Skip description attribute - it's just a concatenation of other attrs
if (key === 'description') return;
if (!options[key]) {
options[key] = [];
}
const name = getAttributeName(value);
const swatch = getAttributeSwatch(value);
// Only add if not already present
if (!options[key].some((opt) => opt.name === name)) {
options[key].push({ name, swatch });
}
});
});
return options;
}, [selectedProduct]);
// Current selected variant
const selectedVariant = useMemo(() => {
return selectedProduct?.variants.find((v) => v.id === selectedVariantId);
}, [selectedProduct, selectedVariantId]);
// Images to show (variant-specific or product-level)
const images = useMemo(() => {
if (!selectedProduct) return [];
// If variant has images, use those; otherwise use product images
if (selectedVariant?.images && selectedVariant.images.length > 0) {
return selectedVariant.images;
}
return selectedProduct.images;
}, [selectedProduct, selectedVariant]);
const handlePrevImage = useCallback(() => {
setCurrentImageIndex((prev) => (prev > 0 ? prev - 1 : images.length - 1));
}, [images.length]);
const handleNextImage = useCallback(() => {
setCurrentImageIndex((prev) => (prev < images.length - 1 ? prev + 1 : 0));
}, [images.length]);
// Handle keyboard navigation
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'ArrowLeft') {
handlePrevImage();
} else if (e.key === 'ArrowRight') {
handleNextImage();
} else if (e.key === 'Escape') {
closeProductModal();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, handlePrevImage, handleNextImage, closeProductModal]);
// Touch swipe handlers for image carousel
const handleTouchStart = useCallback((e: React.TouchEvent) => {
touchStartX.current = e.touches[0].clientX;
touchEndX.current = null;
}, []);
const handleTouchMove = useCallback((e: React.TouchEvent) => {
touchEndX.current = e.touches[0].clientX;
}, []);
const handleTouchEnd = useCallback(() => {
if (touchStartX.current === null || touchEndX.current === null) return;
const swipeDistance = touchStartX.current - touchEndX.current;
const minSwipeDistance = 50;
if (Math.abs(swipeDistance) > minSwipeDistance) {
if (swipeDistance > 0) {
// Swiped left -> next image
handleNextImage();
} else {
// Swiped right -> previous image
handlePrevImage();
}
}
touchStartX.current = null;
touchEndX.current = null;
}, [handleNextImage, handlePrevImage]);
const handleAddToCart = async () => {
if (!selectedVariantId) return;
setIsAdding(true);
try {
await addToCart(selectedVariantId, 1);
closeProductModal();
} catch {
// Error is handled by the cart context
} finally {
setIsAdding(false);
}
};
// Find variant by selected attributes (comparing by name string)
const handleAttributeChange = (attributeName: string, newValue: string) => {
if (!selectedProduct) return;
// Build a map of current attribute names
const currentAttrNames: Record<string, string> = {};
if (selectedVariant) {
Object.entries(selectedVariant.attributes).forEach(([key, val]) => {
if (key !== 'description') {
currentAttrNames[key] = getAttributeName(val);
}
});
}
// Update with the new value
currentAttrNames[attributeName] = newValue;
// Find a variant that matches all current attribute names
const matchingVariant = selectedProduct.variants.find((v) =>
Object.entries(currentAttrNames).every(
([key, name]) => v.attributes[key] && getAttributeName(v.attributes[key]) === name,
),
);
if (matchingVariant) {
setSelectedVariantId(matchingVariant.id);
} else {
// Fallback: find any variant with the selected attribute name
const fallbackVariant = selectedProduct.variants.find(
(v) =>
v.attributes[attributeName] && getAttributeName(v.attributes[attributeName]) === newValue,
);
if (fallbackVariant) {
setSelectedVariantId(fallbackVariant.id);
}
}
};
if (!selectedProduct) return null;
const hasMultipleImages = images.length > 1;
const hasVariants = selectedProduct.variants.length > 1;
return (
<Dialog
open={isOpen}
onClose={closeProductModal}
maxWidth={false}
slotProps={{
paper: {
sx: {
width: { xs: '100vw', sm: '90vw' },
maxWidth: { xs: '100vw', sm: '1000px' },
height: { xs: '100vh', sm: '85vh' },
maxHeight: { xs: '100vh', sm: '800px' },
m: 0,
borderRadius: { xs: 0, sm: '8px' },
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
color: 'var(--ifm-font-color-base, #1c1e21)',
},
},
}}
>
{/* Header bar - OpenAI Supply style */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: { xs: 1.5, sm: 2 },
py: 1.5,
backgroundColor: 'var(--ifm-color-primary-darker)',
color: 'var(--ifm-button-color, #fff)',
flexShrink: 0,
gap: 1,
}}
>
<Typography
variant="subtitle1"
sx={{
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.05em',
fontSize: { xs: '0.75rem', sm: '0.875rem' },
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flex: 1,
minWidth: 0,
}}
>
{selectedProduct.name}
</Typography>
<IconButton
onClick={closeProductModal}
size="small"
sx={{
color: 'var(--ifm-button-color, #fff)',
'&:hover': {
backgroundColor: 'rgba(255, 255, 255, 0.15)',
},
}}
aria-label="Close"
>
<CloseIcon />
</IconButton>
</Box>
{/* Content area */}
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
flex: 1,
overflow: 'hidden',
backgroundColor: 'var(--ifm-background-color, #ffffff)',
}}
>
{/* Image carousel */}
<Box
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
sx={{
flex: { xs: '0 0 auto', md: '1 1 60%' },
position: 'relative',
backgroundColor: 'var(--ifm-background-surface-color)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: { xs: '250px', sm: '300px', md: 'auto' },
maxHeight: { xs: '40vh', md: 'none' },
// Improve touch scrolling
touchAction: 'pan-y pinch-zoom',
cursor: hasMultipleImages ? 'grab' : 'default',
}}
>
{images[currentImageIndex] && (
<Box
component="img"
src={images[currentImageIndex].url}
alt={`${selectedProduct.name} - Image ${currentImageIndex + 1}`}
sx={{
maxWidth: '100%',
maxHeight: '100%',
objectFit: 'contain',
}}
/>
)}
{/* Navigation arrows */}
{hasMultipleImages && (
<>
<IconButton
onClick={handlePrevImage}
sx={{
position: 'absolute',
left: 8,
top: '50%',
transform: 'translateY(-50%)',
backgroundColor: 'var(--ifm-background-color, #ffffff)',
border: '1px solid var(--ifm-color-emphasis-300)',
'&:hover': {
backgroundColor: 'var(--ifm-background-surface-color)',
},
}}
aria-label="Previous image"
>
<ChevronLeftIcon />
</IconButton>
<IconButton
onClick={handleNextImage}
sx={{
position: 'absolute',
right: 8,
top: '50%',
transform: 'translateY(-50%)',
backgroundColor: 'var(--ifm-background-color, #ffffff)',
border: '1px solid var(--ifm-color-emphasis-300)',
'&:hover': {
backgroundColor: 'var(--ifm-background-surface-color)',
},
}}
aria-label="Next image"
>
<ChevronRightIcon />
</IconButton>
{/* Image counter */}
<Typography
variant="caption"
aria-live="polite"
sx={{
position: 'absolute',
bottom: 12,
right: 12,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
color: '#fff',
px: 1,
py: 0.25,
borderRadius: '4px',
fontSize: '0.75rem',
fontWeight: 500,
fontVariantNumeric: 'tabular-nums',
userSelect: 'none',
}}
>
{currentImageIndex + 1} / {images.length}
</Typography>
</>
)}
</Box>
{/* Product details */}
<Box
sx={{
flex: { xs: '1 1 auto', md: '1 1 40%' },
p: { xs: 2, sm: 3 },
display: 'flex',
flexDirection: 'column',
overflow: 'auto',
WebkitOverflowScrolling: 'touch',
}}
>
{/* Description */}
{selectedProduct.description && (
<Typography
variant="body2"
sx={{
color: 'text.secondary',
mb: 3,
lineHeight: 1.6,
}}
>
{stripHtml(selectedProduct.description)}
</Typography>
)}
{/* Variant selectors */}
{hasVariants && Object.keys(attributeOptions).length > 0 && (
<Box sx={{ mb: 3 }}>
{Object.entries(attributeOptions).map(([attrName, options]) => {
// Get current selected value as string
const currentValue = selectedVariant?.attributes[attrName]
? getAttributeName(selectedVariant.attributes[attrName])
: '';
return (
<FormControl key={attrName} fullWidth sx={{ mb: 2 }} size="small">
<InputLabel id={`${attrName}-label`}>
{attrName.charAt(0).toUpperCase() + attrName.slice(1)}
</InputLabel>
<Select
labelId={`${attrName}-label`}
value={currentValue}
label={attrName.charAt(0).toUpperCase() + attrName.slice(1)}
onChange={(e) => handleAttributeChange(attrName, e.target.value)}
>
{options.map((option) => (
<MenuItem key={option.name} value={option.name}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{option.swatch && (
<Box
sx={{
width: 16,
height: 16,
borderRadius: '50%',
backgroundColor: option.swatch,
border: '1px solid var(--ifm-color-emphasis-300)',
}}
/>
)}
{option.name}
</Box>
</MenuItem>
))}
</Select>
</FormControl>
);
})}
</Box>
)}
{/* Price */}
<Box sx={{ mb: 3 }}>
<Typography variant="h5" sx={{ fontWeight: 600 }}>
{selectedVariant ? formatPrice(selectedVariant.unitPrice) : '—'}
</Typography>
{selectedVariant?.compareAtPrice && (
<Typography
variant="body2"
sx={{
textDecoration: 'line-through',
color: 'text.secondary',
}}
>
{formatPrice(selectedVariant.compareAtPrice)}
</Typography>
)}
</Box>
{/* Stock status */}
{selectedVariant && !isInStock(selectedVariant.stock) && (
<Typography variant="body2" sx={{ color: 'error.main', mb: 2 }}>
Out of stock
</Typography>
)}
{/* Add to cart button */}
<Button
variant="contained"
size="large"
onClick={handleAddToCart}
disabled={
!selectedVariant || !isInStock(selectedVariant.stock) || isAdding || isLoading
}
sx={{
mt: 'auto',
py: { xs: 1.75, sm: 1.5 },
minHeight: { xs: 48, sm: 44 },
fontSize: { xs: '0.875rem', sm: '1rem' },
backgroundColor: 'var(--ifm-color-primary-darker)',
borderRadius: '8px',
textTransform: 'uppercase',
letterSpacing: '0.05em',
// Prevent iOS zoom on input
touchAction: 'manipulation',
'&:hover': {
backgroundColor: 'var(--ifm-color-primary-darkest)',
},
'&:disabled': {
backgroundColor: 'var(--ifm-color-emphasis-300)',
},
}}
>
{isAdding ? (
<CircularProgress size={24} sx={{ color: 'var(--ifm-button-color, #fff)' }} />
) : selectedVariant && isInStock(selectedVariant.stock) ? (
'Add to Cart'
) : (
'Out of Stock'
)}
</Button>
</Box>
</Box>
</Dialog>
);
}
+108
View File
@@ -0,0 +1,108 @@
import React, { useState } from 'react';
import Box from '@mui/material/Box';
import ButtonBase from '@mui/material/ButtonBase';
import Typography from '@mui/material/Typography';
import { PromoModal } from './PromoModal';
export function PromoCard() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<>
<ButtonBase
onClick={() => setIsModalOpen(true)}
sx={{
display: 'block',
width: '100%',
position: 'relative',
overflow: 'hidden',
backgroundColor: 'var(--ifm-background-surface-color)',
borderRadius: '8px',
transition: 'transform 0.2s ease-out, box-shadow 0.2s ease-out',
touchAction: 'manipulation',
WebkitTapHighlightColor: 'transparent',
'@media (hover: hover)': {
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: 'var(--store-card-shadow-hover)',
},
},
'&:active': {
opacity: 0.9,
transform: 'scale(0.98)',
},
'&:focus-visible': {
outline: '2px solid var(--ifm-color-primary)',
outlineOffset: '2px',
},
}}
aria-label="Open source contributor perks - merge a PR to get a discount code"
>
{/* Gradient area — matches the product card image slot */}
<Box
sx={{
width: '100%',
aspectRatio: '1',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
background:
'linear-gradient(135deg, var(--ifm-color-primary-darkest) 0%, var(--ifm-color-primary-darker) 50%, var(--ifm-color-primary-dark) 100%)',
px: 2,
gap: 1,
}}
>
<Typography
sx={{
color: '#fff',
fontWeight: 800,
fontSize: { xs: '1.1rem', sm: '1.4rem' },
letterSpacing: '-0.02em',
lineHeight: 1.2,
textAlign: 'center',
}}
>
PRs Perks
</Typography>
<Typography
sx={{
color: 'rgba(255,255,255,0.7)',
fontSize: { xs: '0.7rem', sm: '0.8rem' },
textAlign: 'center',
}}
>
Ship code, get merch
</Typography>
</Box>
{/* Text area — matches the product card name/price slot */}
<Box sx={{ px: 1.5, py: 1.25, textAlign: 'left' }}>
<Typography
variant="body2"
sx={{
fontWeight: 500,
lineHeight: 1.3,
color: 'var(--ifm-font-color-base)',
}}
>
Contributor Discount
</Typography>
<Typography
variant="body2"
sx={{
mt: 0.25,
color: 'var(--ifm-color-emphasis-700)',
fontSize: '0.8rem',
}}
>
Learn more
</Typography>
</Box>
</ButtonBase>
<PromoModal open={isModalOpen} onClose={() => setIsModalOpen(false)} />
</>
);
}
+283
View File
@@ -0,0 +1,283 @@
import React from 'react';
import CloseIcon from '@mui/icons-material/Close';
import GitHubIcon from '@mui/icons-material/GitHub';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import IconButton from '@mui/material/IconButton';
import SvgIcon from '@mui/material/SvgIcon';
import Typography from '@mui/material/Typography';
const DISCORD_URL = 'https://discord.gg/promptfoo';
const GITHUB_URL = 'https://github.com/promptfoo/promptfoo';
function DiscordIcon(props: React.ComponentProps<typeof SvgIcon>) {
return (
<SvgIcon {...props} viewBox="0 0 24 24">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
</SvgIcon>
);
}
interface PromoModalProps {
open: boolean;
onClose: () => void;
}
export function PromoModal({ open, onClose }: PromoModalProps) {
const handleDiscordClick = () => {
window.open(DISCORD_URL, '_blank', 'noopener,noreferrer');
};
const handleGitHubClick = () => {
window.open(GITHUB_URL, '_blank', 'noopener,noreferrer');
};
return (
<Dialog
open={open}
onClose={onClose}
maxWidth={false}
slotProps={{
paper: {
sx: {
width: { xs: '100vw', sm: '90vw' },
maxWidth: { xs: '100vw', sm: '500px' },
height: { xs: '100vh', sm: 'auto' },
maxHeight: { xs: '100vh', sm: '90vh' },
m: 0,
borderRadius: { xs: 0, sm: '8px' },
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
color: 'var(--ifm-font-color-base, #1c1e21)',
},
},
}}
>
{/* Header */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: { xs: 1.5, sm: 2 },
py: 1.5,
backgroundColor: 'var(--ifm-color-primary-darker)',
color: '#fff',
flexShrink: 0,
}}
>
<Typography
variant="subtitle1"
sx={{
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.05em',
fontSize: { xs: '0.75rem', sm: '0.875rem' },
}}
>
Contributor Discount
</Typography>
<IconButton
onClick={onClose}
size="small"
sx={{
color: '#fff',
'&:hover': {
backgroundColor: 'rgba(255, 255, 255, 0.15)',
},
}}
aria-label="Close"
>
<CloseIcon />
</IconButton>
</Box>
{/* Content */}
<Box
sx={{
flex: 1,
overflow: 'auto',
WebkitOverflowScrolling: 'touch',
backgroundColor: 'var(--ifm-background-color, #ffffff)',
}}
>
{/* Headline */}
<Box sx={{ px: { xs: 2, sm: 3 }, pt: { xs: 2.5, sm: 3 }, pb: { xs: 1.5, sm: 2 } }}>
<Typography
variant="h5"
sx={{
fontWeight: 800,
letterSpacing: '-0.02em',
fontSize: { xs: '1.25rem', sm: '1.5rem' },
color: 'var(--ifm-heading-color)',
mb: 0.5,
}}
>
Merge a PR, get a discount code
</Typography>
<Typography
variant="body2"
sx={{
color: 'var(--ifm-color-emphasis-600)',
fontSize: { xs: '0.85rem', sm: '0.9rem' },
}}
>
We give back to open source contributors with exclusive store discounts.
</Typography>
</Box>
{/* Steps */}
<Box sx={{ px: { xs: 2, sm: 3 }, pb: { xs: 2, sm: 3 } }}>
<Typography
variant="subtitle2"
sx={{
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.1em',
color: 'var(--ifm-color-emphasis-500)',
mb: 2,
fontSize: '0.7rem',
}}
>
How it works
</Typography>
<Box component="ol" sx={{ listStyle: 'none', m: 0, p: 0 }}>
{[
{
title: 'Fork & Fix',
description: 'Bug fix, feature, docs — all contributions welcome.',
},
{
title: 'Get Merged',
description: 'Work with maintainers to land your PR.',
},
{
title: 'Claim Perks',
description: "Drop your merged PR link in Discord #general. We'll DM you a code.",
},
].map((item, index) => (
<Box
component="li"
key={item.title}
sx={{
display: 'flex',
gap: 1.5,
mb: index < 2 ? 2 : 0,
alignItems: 'flex-start',
}}
>
<Box
aria-hidden="true"
sx={{
width: 24,
height: 24,
borderRadius: '50%',
backgroundColor: 'var(--ifm-color-primary-darker)',
color: '#fff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 700,
fontSize: '0.7rem',
flexShrink: 0,
mt: '2px',
}}
>
{index + 1}
</Box>
<Box sx={{ flex: 1 }}>
<Typography
variant="body2"
sx={{
fontWeight: 600,
fontSize: '0.875rem',
lineHeight: 1.4,
mb: 0.25,
color: 'var(--ifm-heading-color)',
}}
>
{item.title}
</Typography>
<Typography
variant="body2"
sx={{
color: 'var(--ifm-color-emphasis-600)',
fontSize: '0.8rem',
lineHeight: 1.5,
}}
>
{item.description}
</Typography>
</Box>
</Box>
))}
</Box>
</Box>
</Box>
{/* Footer CTAs */}
<Box
sx={{
p: { xs: 2, sm: 3 },
borderTop: '1px solid var(--ifm-color-emphasis-300)',
backgroundColor: 'var(--ifm-background-color, #ffffff)',
display: 'flex',
flexDirection: { xs: 'column-reverse', sm: 'row' },
gap: 1.5,
}}
>
<Button
variant="outlined"
fullWidth
size="large"
onClick={handleGitHubClick}
startIcon={<GitHubIcon />}
sx={{
py: { xs: 1.5, sm: 1.25 },
minHeight: { xs: 48, sm: 44 },
borderColor: 'var(--ifm-color-emphasis-400)',
color: 'var(--ifm-font-color-base)',
borderRadius: '8px',
textTransform: 'none',
fontWeight: 600,
fontSize: { xs: '0.8rem', sm: '0.875rem' },
touchAction: 'manipulation',
'&:hover': {
borderColor: 'var(--ifm-color-emphasis-600)',
backgroundColor: 'var(--ifm-background-surface-color)',
},
}}
>
View Repository
</Button>
<Button
variant="contained"
fullWidth
size="large"
onClick={handleDiscordClick}
startIcon={<DiscordIcon />}
sx={{
py: { xs: 1.5, sm: 1.25 },
minHeight: { xs: 48, sm: 44 },
backgroundColor: 'var(--ifm-color-primary-darker)',
color: '#fff',
borderRadius: '8px',
textTransform: 'none',
fontWeight: 600,
fontSize: { xs: '0.8rem', sm: '0.875rem' },
touchAction: 'manipulation',
'&:hover': {
backgroundColor: 'var(--ifm-color-primary-darkest)',
},
}}
>
Join Discord
</Button>
</Box>
</Dialog>
);
}
@@ -0,0 +1,87 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { StoreErrorBoundary } from './StoreErrorBoundary';
// Component that throws an error
function ThrowingComponent({ shouldThrow }: { shouldThrow: boolean }) {
if (shouldThrow) {
throw new Error('Test error message');
}
return <div>Content rendered successfully</div>;
}
describe('StoreErrorBoundary', () => {
// Suppress console.error for error boundary tests
const originalError = console.error;
beforeEach(() => {
console.error = vi.fn();
});
afterEach(() => {
console.error = originalError;
});
it('renders children when there is no error', () => {
render(
<StoreErrorBoundary>
<div>Test content</div>
</StoreErrorBoundary>,
);
expect(screen.getByText('Test content')).toBeInTheDocument();
});
it('renders error UI when child throws', () => {
render(
<StoreErrorBoundary>
<ThrowingComponent shouldThrow={true} />
</StoreErrorBoundary>,
);
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
expect(screen.getByText(/couldn't load the store/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /try again/i })).toBeInTheDocument();
});
it('recovers when Try Again is clicked', async () => {
const user = userEvent.setup();
// Start with throwing state
const { rerender } = render(
<StoreErrorBoundary>
<ThrowingComponent shouldThrow={true} />
</StoreErrorBoundary>,
);
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
// Change to non-throwing state before clicking retry
rerender(
<StoreErrorBoundary>
<ThrowingComponent shouldThrow={false} />
</StoreErrorBoundary>,
);
// Click retry button
await user.click(screen.getByRole('button', { name: /try again/i }));
// Should now render the content
expect(screen.getByText('Content rendered successfully')).toBeInTheDocument();
});
it('logs error to console', () => {
render(
<StoreErrorBoundary>
<ThrowingComponent shouldThrow={true} />
</StoreErrorBoundary>,
);
expect(console.error).toHaveBeenCalledWith(
'[Store] Error caught by boundary:',
expect.any(Error),
expect.objectContaining({ componentStack: expect.any(String) }),
);
});
});
@@ -0,0 +1,99 @@
import React, { Component } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
interface Props {
children: React.ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
/**
* Error boundary for the store page.
* Catches rendering errors and displays a fallback UI instead of crashing.
*/
export class StoreErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
// Log error for debugging (could send to error tracking service)
console.error('[Store] Error caught by boundary:', error, errorInfo);
}
handleRetry = (): void => {
this.setState({ hasError: false, error: null });
};
render(): React.ReactNode {
if (this.state.hasError) {
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '50vh',
textAlign: 'center',
px: 3,
gap: 2,
}}
>
<Typography variant="h5" sx={{ fontWeight: 600, color: 'text.primary' }}>
Something went wrong
</Typography>
<Typography variant="body1" sx={{ color: 'text.secondary', maxWidth: 400 }}>
We couldn't load the store. This might be a temporary issue.
</Typography>
<Button
variant="contained"
onClick={this.handleRetry}
sx={{
mt: 2,
backgroundColor: 'var(--ifm-color-primary-darker)',
'&:hover': {
backgroundColor: 'var(--ifm-color-primary-darkest)',
},
}}
>
Try Again
</Button>
{process.env.NODE_ENV === 'development' && this.state.error && (
<Box
component="pre"
sx={{
mt: 3,
p: 2,
backgroundColor: 'var(--ifm-background-surface-color)',
border: '1px solid var(--ifm-color-emphasis-300)',
borderRadius: 1,
fontSize: '0.75rem',
maxWidth: '100%',
overflow: 'auto',
textAlign: 'left',
}}
>
{this.state.error.message}
{'\n\n'}
{this.state.error.stack}
</Box>
)}
</Box>
);
}
return this.props.children;
}
}
+37
View File
@@ -0,0 +1,37 @@
// Components
export { CartDrawer } from './CartDrawer';
export { CartProvider, useCartContext } from './CartProvider';
export { ProductCard } from './ProductCard';
export { ProductGrid } from './ProductGrid';
export { ProductModal } from './ProductModal';
export { PromoCard } from './PromoCard';
export { PromoModal } from './PromoModal';
export { StoreErrorBoundary } from './StoreErrorBoundary';
// Hooks
// Utilities
export {
formatPrice,
getAttributeName,
getAttributeSwatch,
getCheckoutUrl,
isInStock,
stripHtml,
useCart,
useCollections,
useProduct,
useProducts,
} from './useFourthwall';
// Types
export type {
FourthwallAttributeValue,
FourthwallCart,
FourthwallCartItem,
FourthwallCartVariant,
FourthwallCollection,
FourthwallImage,
FourthwallMoney,
FourthwallProduct,
FourthwallVariant,
PaginatedResponse,
} from './types';
+123
View File
@@ -0,0 +1,123 @@
// Fourthwall Storefront API Types
export interface FourthwallImage {
url: string;
width: number;
height: number;
}
export interface FourthwallMoney {
value: number;
currency: string;
}
// Attribute can be a string, or an object with name (and optionally swatch for colors)
export interface FourthwallAttribute {
name: string;
swatch?: string;
}
export type FourthwallAttributeValue = string | FourthwallAttribute;
export interface FourthwallVariant {
id: string;
name: string;
sku: string;
unitPrice: FourthwallMoney;
compareAtPrice?: FourthwallMoney;
attributes: Record<string, FourthwallAttributeValue>;
stock: {
type: 'LIMITED' | 'UNLIMITED';
quantity?: number;
};
images: FourthwallImage[];
weight?: {
value: number;
unit: string;
};
dimensions?: {
length: number;
width: number;
height: number;
unit: string;
};
}
export interface FourthwallProduct {
id: string;
name: string;
slug: string;
description: string;
status: 'AVAILABLE' | 'UNAVAILABLE';
access: 'PUBLIC' | 'PRIVATE';
images: FourthwallImage[];
variants: FourthwallVariant[];
}
export interface FourthwallCollection {
id: string;
name: string;
slug: string;
description: string;
}
// Cart item structure per OpenAPI spec - variant is nested object, no top-level variantId
export interface FourthwallCartItem {
variant: FourthwallCartVariant;
quantity: number;
}
// Variant info returned in cart responses (subset of full variant)
export interface FourthwallCartVariant {
id: string;
name: string;
sku?: string;
unitPrice: FourthwallMoney;
compareAtPrice?: FourthwallMoney;
attributes?: Record<string, FourthwallAttributeValue>;
stock?: {
type: 'LIMITED' | 'UNLIMITED';
quantity?: number;
};
images?: FourthwallImage[];
product?: {
id?: string;
name?: string;
slug?: string;
images?: FourthwallImage[];
};
}
export interface FourthwallCart {
id: string;
items: FourthwallCartItem[];
checkoutUrl?: string;
subtotal?: FourthwallMoney;
}
export interface PagingInfo {
pageNumber: number;
pageSize: number;
elementsSize: number;
elementsTotal: number;
totalPages: number;
hasNextPage: boolean;
}
export interface PaginatedResponse<T> {
results: T[];
paging: PagingInfo;
}
// Store UI State Types
export interface CartState {
cart: FourthwallCart | null;
isLoading: boolean;
isOpen: boolean;
error: string | null;
}
export interface ProductModalState {
product: FourthwallProduct | null;
selectedVariantId: string | null;
}
@@ -0,0 +1,31 @@
import { useCallback, useEffect, useRef, useState } from 'react';
export function useCopyToClipboard(text: string | null) {
const [copied, setCopied] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleCopy = useCallback(() => {
if (!text) return;
if (navigator.clipboard?.writeText) {
navigator.clipboard.writeText(text).then(
() => {
setCopied(true);
timerRef.current = setTimeout(() => setCopied(false), 2000);
},
() => {
// Clipboard write denied — ignore silently
},
);
}
}, [text]);
useEffect(() => {
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
}, []);
return { copied, handleCopy };
}
@@ -0,0 +1,160 @@
import { describe, expect, it } from 'vitest';
import {
formatPrice,
getAttributeName,
getAttributeSwatch,
getCheckoutUrl,
isInStock,
stripHtml,
} from './useFourthwall';
describe('formatPrice', () => {
it('formats USD prices correctly', () => {
expect(formatPrice({ value: 29.99, currency: 'USD' })).toBe('$29.99');
});
it('formats whole dollar amounts', () => {
expect(formatPrice({ value: 100, currency: 'USD' })).toBe('$100.00');
});
it('formats other currencies', () => {
expect(formatPrice({ value: 50, currency: 'EUR' })).toMatch(/50/);
});
it('handles zero values', () => {
expect(formatPrice({ value: 0, currency: 'USD' })).toBe('$0.00');
});
});
describe('stripHtml', () => {
it('returns empty string for falsy input', () => {
expect(stripHtml('')).toBe('');
expect(stripHtml(null as unknown as string)).toBe('');
});
it('strips basic HTML tags', () => {
expect(stripHtml('<p>Hello World</p>')).toBe('Hello World');
});
it('handles nested tags', () => {
expect(stripHtml('<div><p><strong>Bold</strong> text</p></div>')).toBe('Bold text');
});
it('removes script tags and content', () => {
expect(stripHtml('<p>Safe</p><script>alert("xss")</script><p>Text</p>')).toBe('SafeText');
});
it('removes script tags with attributes', () => {
expect(stripHtml('<script type="text/javascript">evil()</script>OK')).toBe('OK');
});
it('removes script tags with whitespace in closing tag', () => {
// Edge case: </script > with trailing space
expect(stripHtml('<script>bad()</script >Safe')).toBe('Safe');
});
it('removes style tags and content', () => {
expect(stripHtml('<style>.red{color:red}</style><p>Text</p>')).toBe('Text');
});
it('removes style tags with whitespace in closing tag', () => {
expect(stripHtml('<style>.x{}</style >OK')).toBe('OK');
});
it('decodes HTML entities', () => {
expect(stripHtml('Tom &amp; Jerry')).toBe('Tom & Jerry');
expect(stripHtml('&lt;not a tag&gt;')).toBe('<not a tag>');
expect(stripHtml('&quot;quoted&quot;')).toBe('"quoted"');
});
it('decodes numeric entities', () => {
expect(stripHtml('&#65;BC')).toBe('ABC');
expect(stripHtml('&#x41;BC')).toBe('ABC');
});
it('handles complex product descriptions', () => {
const html = `
<div class="description">
<p>Premium quality <strong>T-Shirt</strong> made from 100% cotton.</p>
<ul>
<li>Comfortable fit</li>
<li>Machine washable</li>
</ul>
</div>
`;
const result = stripHtml(html);
expect(result).toContain('Premium quality');
expect(result).toContain('T-Shirt');
expect(result).toContain('100% cotton');
expect(result).not.toContain('<');
expect(result).not.toContain('>');
});
});
describe('isInStock', () => {
it('returns true for unlimited stock', () => {
expect(isInStock({ type: 'UNLIMITED' })).toBe(true);
});
it('returns true for limited stock with quantity > 0', () => {
expect(isInStock({ type: 'LIMITED', quantity: 5 })).toBe(true);
expect(isInStock({ type: 'LIMITED', quantity: 1 })).toBe(true);
});
it('returns false for limited stock with quantity 0', () => {
expect(isInStock({ type: 'LIMITED', quantity: 0 })).toBe(false);
});
it('returns false for limited stock without quantity', () => {
expect(isInStock({ type: 'LIMITED' })).toBe(false);
});
it('returns false for unknown stock type', () => {
expect(isInStock({ type: 'UNKNOWN' })).toBe(false);
});
});
describe('getAttributeName', () => {
it('returns string attributes directly', () => {
expect(getAttributeName('Large')).toBe('Large');
expect(getAttributeName('Red')).toBe('Red');
});
it('extracts name from object attributes', () => {
expect(getAttributeName({ name: 'Blue', swatch: '#0000ff' })).toBe('Blue');
expect(getAttributeName({ name: 'Medium' })).toBe('Medium');
});
});
describe('getAttributeSwatch', () => {
it('returns undefined for string attributes', () => {
expect(getAttributeSwatch('Large')).toBeUndefined();
});
it('returns swatch color from object attributes', () => {
expect(getAttributeSwatch({ name: 'Blue', swatch: '#0000ff' })).toBe('#0000ff');
});
it('returns undefined for object without swatch', () => {
expect(getAttributeSwatch({ name: 'Medium' })).toBeUndefined();
});
});
describe('getCheckoutUrl', () => {
it('generates correct checkout URL', () => {
const url = getCheckoutUrl('cart-123', 'USD');
expect(url).toBe(
'https://promptfoo-shop.fourthwall.com/checkout/?cartCurrency=USD&cartId=cart-123',
);
});
it('defaults to USD currency', () => {
const url = getCheckoutUrl('cart-456');
expect(url).toContain('cartCurrency=USD');
});
it('handles different currencies', () => {
const url = getCheckoutUrl('cart-789', 'EUR');
expect(url).toContain('cartCurrency=EUR');
});
});
+457
View File
@@ -0,0 +1,457 @@
import { useCallback, useEffect, useState } from 'react';
import type {
FourthwallAttributeValue,
FourthwallCart,
FourthwallCollection,
FourthwallProduct,
PaginatedResponse,
} from './types';
// Public storefront token - this is INTENTIONALLY public and client-facing.
// Fourthwall storefront tokens are designed to be exposed in frontend code.
// Base64 encoded only to prevent false-positive secret scanner alerts.
// Decoded value: ptkn_1be7c822-bfc7-4167-83a6-95619ff7ed7f
const STOREFRONT_TOKEN = atob('cHRrbl8xYmU3YzgyMi1iZmM3LTQxNjctODNhNi05NTYxOWZmN2VkN2Y=');
const API_BASE = 'https://storefront-api.fourthwall.com/v1';
const CHECKOUT_DOMAIN = 'https://promptfoo-shop.fourthwall.com';
// Maximum pages to fetch (safety limit for pagination)
const MAX_PAGES = 50;
const PAGE_SIZE = 10;
// Safe localStorage wrapper (handles private browsing mode)
function safeLocalStorage() {
return {
getItem(key: string): string | null {
try {
return localStorage.getItem(key);
} catch {
return null;
}
},
setItem(key: string, value: string): void {
try {
localStorage.setItem(key, value);
} catch {
// Silently fail in private browsing mode
}
},
removeItem(key: string): void {
try {
localStorage.removeItem(key);
} catch {
// Silently fail in private browsing mode
}
},
};
}
// Input validation helpers
function validateVariantId(variantId: string): void {
if (!variantId || typeof variantId !== 'string' || variantId.trim() === '') {
throw new Error('Invalid variant ID');
}
}
function validateQuantity(quantity: number): void {
if (
typeof quantity !== 'number' ||
quantity < 1 ||
quantity > 99 ||
!Number.isInteger(quantity)
) {
throw new Error('Quantity must be an integer between 1 and 99');
}
}
async function apiFetch<T>(endpoint: string, options?: RequestInit): Promise<T> {
const url = new URL(`${API_BASE}${endpoint}`);
url.searchParams.set('storefront_token', STOREFRONT_TOKEN);
const response = await fetch(url.toString(), {
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
});
if (!response.ok) {
throw new Error(`API error: ${response.status} ${response.statusText}`);
}
return response.json();
}
// Fetch all collections
export function useCollections() {
const [collections, setCollections] = useState<FourthwallCollection[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
apiFetch<PaginatedResponse<FourthwallCollection>>('/collections')
.then((data) => setCollections(data.results))
.catch((err) => setError(err.message))
.finally(() => setIsLoading(false));
}, []);
return { collections, isLoading, error };
}
// Fetch all products from a collection (handles pagination)
export function useProducts(collectionSlug: string = 'all') {
const [products, setProducts] = useState<FourthwallProduct[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchAllProducts() {
setIsLoading(true);
setError(null);
try {
const allProducts: FourthwallProduct[] = [];
let page = 0;
// Fetch pages until we get an empty results array
while (true) {
const response = await apiFetch<{ results: FourthwallProduct[] }>(
`/collections/${collectionSlug}/products?page=${page}&size=${PAGE_SIZE}`,
);
if (!response.results || response.results.length === 0) {
break;
}
allProducts.push(...response.results);
page++;
// Safety limit to prevent infinite loops
if (page >= MAX_PAGES) {
console.warn(
`[Store] Pagination limit reached (${MAX_PAGES} pages, ${allProducts.length} products). ` +
'Some products may not be displayed. Consider increasing MAX_PAGES if the catalog has grown.',
);
break;
}
}
setProducts(allProducts);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch products');
} finally {
setIsLoading(false);
}
}
fetchAllProducts();
}, [collectionSlug]);
return { products, isLoading, error };
}
// Fetch a single product
export function useProduct(slug: string | null) {
const [product, setProduct] = useState<FourthwallProduct | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!slug) {
setProduct(null);
return;
}
setIsLoading(true);
apiFetch<FourthwallProduct>(`/products/${slug}`)
.then(setProduct)
.catch((err) => setError(err.message))
.finally(() => setIsLoading(false));
}, [slug]);
return { product, isLoading, error };
}
// Cart operations
const CART_STORAGE_KEY = 'promptfoo_cart_id';
const storage = safeLocalStorage();
export function useCart() {
const [cart, setCart] = useState<FourthwallCart | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Load existing cart on mount
useEffect(() => {
const cartId = storage.getItem(CART_STORAGE_KEY);
if (cartId) {
setIsLoading(true);
apiFetch<FourthwallCart>(`/carts/${cartId}`)
.then(setCart)
.catch(() => {
// Cart expired or invalid, clear it
storage.removeItem(CART_STORAGE_KEY);
})
.finally(() => setIsLoading(false));
}
}, []);
const createCart = useCallback(async (variantId: string, quantity: number = 1) => {
// Validate inputs
validateVariantId(variantId);
validateQuantity(quantity);
setIsLoading(true);
setError(null);
try {
const newCart = await apiFetch<FourthwallCart>('/carts', {
method: 'POST',
body: JSON.stringify({
items: [{ variantId, quantity }],
}),
});
storage.setItem(CART_STORAGE_KEY, newCart.id);
setCart(newCart);
return newCart;
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to create cart');
throw err;
} finally {
setIsLoading(false);
}
}, []);
const addToCart = useCallback(
async (variantId: string, quantity: number = 1) => {
// Validate inputs
validateVariantId(variantId);
validateQuantity(quantity);
setIsLoading(true);
setError(null);
try {
if (!cart) {
return createCart(variantId, quantity);
}
const updatedCart = await apiFetch<FourthwallCart>(`/carts/${cart.id}/add`, {
method: 'POST',
body: JSON.stringify({
items: [{ variantId, quantity }],
}),
});
setCart(updatedCart);
return updatedCart;
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to add to cart');
throw err;
} finally {
setIsLoading(false);
}
},
[cart, createCart],
);
const removeFromCart = useCallback(
async (variantId: string) => {
if (!cart) return;
// Validate input
validateVariantId(variantId);
setIsLoading(true);
setError(null);
try {
// API expects: { items: [{ variantId }] }
const updatedCart = await apiFetch<FourthwallCart>(`/carts/${cart.id}/remove`, {
method: 'POST',
body: JSON.stringify({
items: [{ variantId }],
}),
});
setCart(updatedCart);
return updatedCart;
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to remove from cart');
throw err;
} finally {
setIsLoading(false);
}
},
[cart],
);
const updateQuantity = useCallback(
async (variantId: string, quantity: number) => {
if (!cart) return;
// Validate inputs
validateVariantId(variantId);
validateQuantity(quantity);
setIsLoading(true);
setError(null);
try {
// API expects: { items: [{ variantId, quantity }] }
const updatedCart = await apiFetch<FourthwallCart>(`/carts/${cart.id}/change`, {
method: 'POST',
body: JSON.stringify({
items: [{ variantId, quantity }],
}),
});
setCart(updatedCart);
return updatedCart;
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to update quantity');
throw err;
} finally {
setIsLoading(false);
}
},
[cart],
);
const clearCart = useCallback(() => {
storage.removeItem(CART_STORAGE_KEY);
setCart(null);
}, []);
const itemCount = cart?.items.reduce((sum, item) => sum + item.quantity, 0) ?? 0;
return {
cart,
isLoading,
error,
itemCount,
addToCart,
removeFromCart,
updateQuantity,
clearCart,
};
}
// Format price for display (Fourthwall returns value in dollars, not cents)
export function formatPrice(money: { value: number; currency: string }): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: money.currency,
}).format(money.value);
}
// Common HTML entities for SSR decoding
const HTML_ENTITIES: Record<string, string> = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&#39;': "'",
'&apos;': "'",
'&nbsp;': ' ',
'&copy;': '©',
'&reg;': '®',
'&trade;': '™',
};
/**
* Strip HTML tags from a string using indexOf (no regex for tag stripping).
* Uses DOMParser in browser (safe), string-based fallback for SSR.
* SSR fallback processes trusted Fourthwall API content only.
*/
export function stripHtml(html: string): string {
if (!html) return '';
// Browser: Use DOMParser (safe, handles all edge cases)
if (typeof document !== 'undefined') {
const doc = new DOMParser().parseFromString(html, 'text/html');
// Remove script and style elements before getting textContent
doc.querySelectorAll('script, style').forEach((el) => el.remove());
return doc.body.textContent || '';
}
// SSR fallback: String-based stripping for trusted Fourthwall API content.
// Uses indexOf/substring instead of regex to avoid CodeQL js/bad-tag-filter alerts.
// The browser path (above) uses safe DOMParser for client-side rendering.
let result = html;
// Remove script/style tags and contents using indexOf (no regex)
for (const tag of ['script', 'style']) {
let safety = 0;
while (safety++ < 100) {
const openTag = result.toLowerCase().indexOf(`<${tag}`);
if (openTag === -1) break;
const closeTag = result.toLowerCase().indexOf(`</${tag}`, openTag);
if (closeTag === -1) break;
const closeEnd = result.indexOf('>', closeTag);
if (closeEnd === -1) break;
result = result.substring(0, openTag) + result.substring(closeEnd + 1);
}
}
// Remove remaining HTML tags using indexOf (no regex)
let safety = 0;
while (safety++ < 1000) {
const start = result.indexOf('<');
if (start === -1) break;
const end = result.indexOf('>', start);
if (end === -1) break;
result = result.substring(0, start) + result.substring(end + 1);
}
// Decode common HTML entities (using string split/join, not regex)
for (const [entity, char] of Object.entries(HTML_ENTITIES)) {
result = result.split(entity).join(char);
}
// Decode numeric entities (&#123; format) - simple parsing without regex
let numericSafety = 0;
while (numericSafety++ < 500) {
const start = result.indexOf('&#');
if (start === -1) break;
const end = result.indexOf(';', start);
if (end === -1 || end - start > 10) break;
const numStr = result.substring(start + 2, end);
const isHex = numStr.toLowerCase().startsWith('x');
const num = isHex ? Number.parseInt(numStr.substring(1), 16) : Number.parseInt(numStr, 10);
if (!Number.isNaN(num) && num > 0 && num < 0x10ffff) {
result = result.substring(0, start) + String.fromCodePoint(num) + result.substring(end + 1);
} else {
break; // Invalid entity, stop processing
}
}
return result.trim();
}
// Check if variant is in stock
export function isInStock(stock: { type: string; quantity?: number }): boolean {
if (stock.type === 'UNLIMITED') {
return true;
}
if (stock.type === 'LIMITED' && typeof stock.quantity === 'number') {
return stock.quantity > 0;
}
return false;
}
// Get display name from attribute value (handles both string and object formats)
export function getAttributeName(attr: FourthwallAttributeValue): string {
if (typeof attr === 'string') {
return attr;
}
return attr.name;
}
// Get swatch color if available (for color attributes)
export function getAttributeSwatch(attr: FourthwallAttributeValue): string | undefined {
if (typeof attr === 'object' && 'swatch' in attr) {
return attr.swatch;
}
return undefined;
}
// Generate checkout URL for a cart
export function getCheckoutUrl(cartId: string, currency: string = 'USD'): string {
return `${CHECKOUT_DOMAIN}/checkout/?cartCurrency=${currency}&cartId=${cartId}`;
}
+4
View File
@@ -0,0 +1,4 @@
import generatedStats from './.generated-stats.json';
import siteStats from './site-stats.json';
export const SITE_CONSTANTS = { ...siteStats, ...generatedStats };
+104
View File
@@ -0,0 +1,104 @@
/* Minimal text-link pagination */
.pagination-nav {
margin-top: 3rem;
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid var(--ifm-color-emphasis-200);
padding-top: 1.5rem;
}
.pagination-nav__item {
max-width: 48%;
}
/* Push "next" to the right when it's the only item */
.pagination-nav__link--next:first-child {
margin-left: auto;
}
/* Strip card styling — plain text links */
.pagination-nav__link {
border: none;
background: none;
box-shadow: none;
padding: 0;
display: flex;
flex-direction: column;
text-decoration: none;
color: var(--ifm-font-color-base);
transition: color 0.15s;
}
.pagination-nav__link:hover {
background: none;
box-shadow: none;
border: none;
text-decoration: none;
}
/* Sublabel: "Previous" / "Next" */
.pagination-nav__sublabel {
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--ifm-color-emphasis-600);
margin-bottom: 0.15rem;
}
/* Page title label */
.pagination-nav__label {
font-size: 0.95rem;
font-weight: 500;
color: var(--ifm-color-primary);
transition: color 0.15s;
}
.pagination-nav__link:hover .pagination-nav__label {
color: var(--ifm-color-primary-dark);
text-decoration: underline;
}
/* Alignment */
.pagination-nav__link--next {
align-items: flex-end;
text-align: right;
}
.pagination-nav__link--prev {
align-items: flex-start;
text-align: left;
}
/* Remove the arrow pseudo-elements */
.pagination-nav__link--next::after,
.pagination-nav__link--prev::before {
display: none;
}
/* Responsive */
@media (max-width: 768px) {
.pagination-nav {
margin-top: 2rem;
padding-top: 1rem;
}
.pagination-nav__item {
max-width: 49%;
}
.pagination-nav__label {
font-size: 0.875rem;
}
}
/* Dark mode */
[data-theme='dark'] .pagination-nav {
border-top-color: rgba(255, 255, 255, 0.1);
}
[data-theme='dark'] .pagination-nav__sublabel {
color: var(--ifm-color-emphasis-500);
}
+586
View File
@@ -0,0 +1,586 @@
/**
* Any CSS included here will be global. The classic template
* bundles Infima by default. Infima is a CSS framework designed to
* work well for content-centric websites.
*/
/* Self-hosted Inter variable font (GDPR: avoids Google Fonts CDN) */
@font-face {
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 100 900;
src: url('/fonts/Inter-Variable-Latin.woff2') format('woff2');
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* You can override the default Infima variables here. */
:root {
/* Primary red color palette */
--ifm-color-primary: #e53a3a;
--ifm-color-primary-dark: #cb3434;
--ifm-color-primary-darker: #b32e2e;
--ifm-color-primary-darkest: #991515;
--ifm-color-primary-light: #ff7a7a;
--ifm-color-primary-lighter: #ffa0a0;
--ifm-color-primary-lightest: #ffc6c6;
/* Custom palette */
--pf-red-base: #e53a3a;
--pf-red-dark: #cb3434;
--pf-red-darker: #b32e2e;
--pf-red-light: #ff7a7a;
--pf-red-lighter: #ffa0a0;
--pf-red-lightest: #ffc6c6;
--pf-red-ultra-light: #ffd9d9;
--pf-dark-base: #2e3c46;
--pf-dark-darker: #17252b;
--pf-dark-darkest: #10191c;
--ifm-code-font-size: 95%;
--ifm-navbar-collapse-breakpoint: 1100px;
--ifm-font-family-base:
'Inter', system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif,
BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol';
--docusaurus-highlighted-code-line-bg: #fffbdc;
/* Store page tokens */
--store-card-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
--store-card-shadow-hover: 0 8px 24px rgba(0, 0, 0, 0.1);
}
/* For readability concerns, you should choose a lighter palette in dark mode. */
:root[data-theme='dark'] {
--ifm-color-primary: #ff7a7a;
--ifm-color-primary-dark: #e53a3a;
--ifm-color-primary-darker: #cb3434;
--ifm-color-primary-darkest: #b32e2e;
--ifm-color-primary-light: #ffa0a0;
--ifm-color-primary-lighter: #ffc6c6;
--ifm-color-primary-lightest: #ffd9d9;
--ifm-button-color: #fff;
--docusaurus-highlighted-code-line-bg: rgba(255, 255, 255, 0.08);
/* Update background colors */
--ifm-background-color: #10191c;
--ifm-background-surface-color: #17252b;
/* Store page tokens */
--store-card-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
--store-card-shadow-hover: 0 8px 24px rgba(0, 0, 0, 0.3);
}
code p {
margin-bottom: 0;
}
.hero--primary {
--ifm-hero-background-color: var(--ifm-background-color);
--ifm-hero-text-color: var(--ifm-heading-color);
}
.hero__title {
font-size: 4rem;
}
.theme-doc-sidebar-item-category-level-1 > div {
font-weight: 600;
}
.navbar__brand {
font-family: var(--ifm-font-family-monospace);
flex-shrink: 0;
}
/* Hide search bar on non-docs pages */
html:not(.docs-wrapper) .navbar__search,
html:not(.docs-wrapper) .DocSearch {
display: none !important;
}
/* Scalar API Reference search modal - Safari fixes */
#app div.scalar-modal.scalar-modal-search.bg-b-1,
#app div.scalar-modal-body {
background-color: white !important;
background: white !important;
}
[data-theme='dark'] #app div.scalar-modal.scalar-modal-search.bg-b-1,
[data-theme='dark'] #app div.scalar-modal-body {
background-color: #1a1a1a !important;
background: #1a1a1a !important;
}
/* Mobile navbar adjustments */
@media (max-width: 1100px) {
/* Hide search box on mobile to prevent overlap */
.navbar__search {
display: none !important;
}
/* Keep hamburger top-level items consistent */
.navbar-sidebar__items .menu__list > .menu__list-item > .menu__link,
.navbar-sidebar__items .header-book-demo-link,
.navbar-sidebar__items .header-discord-link {
display: flex;
align-items: center;
min-height: 44px;
}
.navbar-sidebar__items .menu__list > .menu__list-item > .menu__link {
font-weight: 600;
}
.navbar-sidebar__items .header-discord-link {
gap: 12px;
}
.navbar-sidebar__items .header-discord-link::after {
content: 'Discord';
}
.docs-wrapper .DocSearch-Button {
width: 44px;
height: 44px;
min-width: 44px;
padding: 0;
justify-content: center;
}
.docs-wrapper .DocSearch-Button-Placeholder,
.docs-wrapper .DocSearch-Button-Keys {
display: none;
}
}
.navbar__logo img {
filter: invert(0);
}
.header-github-link:hover {
opacity: 0.6;
}
.header-github-link::before {
content: '';
width: 24px;
height: 24px;
display: flex;
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E")
no-repeat;
}
[data-theme='dark'] .header-github-link::before {
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='white' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E")
no-repeat;
}
.header-discord-link:hover {
opacity: 0.6;
}
.header-discord-link::before {
content: '';
width: 24px;
height: 24px;
display: flex;
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z'/%3E%3C/svg%3E")
no-repeat;
}
[data-theme='dark'] .header-discord-link::before {
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='white' d='M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z'/%3E%3C/svg%3E")
no-repeat;
}
.navbar__items--right > :nth-last-child(2) {
/* Equalize the spacing to the left of the color mode toggle */
margin-left: 4px;
}
@media (max-width: 1280px) {
.docs-wrapper .navbar__items--right .header-discord-link {
display: none;
}
}
.dropdown > .navbar__link:after {
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E")
no-repeat center;
border: none;
content: '';
width: 16px;
height: 16px;
position: relative;
top: 3px;
margin-left: 2px;
transform: none;
opacity: 0.4;
background-size: 16px;
}
/* Book a Demo button styling */
.header-book-demo-link {
background-color: var(--pf-red-base);
color: white !important;
padding: 0.5rem 1rem;
border-radius: 6px;
font-weight: 600;
transition: all 0.2s ease-in-out;
margin-right: 0.75rem;
}
.header-book-demo-link:hover {
background-color: var(--pf-red-dark);
color: white !important;
text-decoration: none;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(229, 58, 58, 0.3);
}
[data-theme='dark'] .header-book-demo-link {
background-color: var(--pf-red-light);
color: var(--pf-dark-darkest) !important;
}
[data-theme='dark'] .header-book-demo-link:hover {
background-color: var(--pf-red-lighter);
color: var(--pf-dark-darkest) !important;
box-shadow: 0 4px 12px rgba(255, 122, 122, 0.3);
}
/* Mobile adjustments for book demo button */
@media (max-width: 1100px) {
.header-book-demo-link {
margin-right: 0;
margin-bottom: 0.5rem;
}
}
/* ── Sidebar refinements ── */
/* Subtle off-white background for the sidebar */
.theme-doc-sidebar-container {
background: #f8f9fa;
border-right: 1px solid #e9ecef;
}
[data-theme='dark'] .theme-doc-sidebar-container {
background: var(--ifm-background-surface-color);
border-right-color: #1e2d33;
}
/* Tighter vertical spacing on sidebar items */
.menu__link {
padding: 0.3rem 0.75rem;
font-size: 0.9375rem;
border-radius: 6px;
transition:
background 0.15s,
color 0.15s;
}
/* Active item: colored left border accent */
.menu__link--active {
background: var(--pf-red-ultra-light);
border-left: 3px solid var(--pf-red-base);
padding-left: calc(0.75rem - 3px);
font-weight: 600;
}
[data-theme='dark'] .menu__link--active {
background: rgba(255, 122, 122, 0.1);
border-left-color: var(--pf-red-light);
}
/* When a child is also active, suppress the parent category accent */
.menu__list-item:has(> .menu__list .menu__link--active) > div > .menu__link--active,
.menu__list-item:has(> .menu__list .menu__link--active) > .menu__link--active {
background: none;
border-left: none;
padding-left: 0.75rem;
}
/* Hover state */
.menu__link:hover:not(.menu__link--active) {
background: #eef0f2;
}
[data-theme='dark'] .menu__link:hover:not(.menu__link--active) {
background: rgba(255, 255, 255, 0.05);
}
/* Smooth expand/collapse transition for categories */
.menu__list {
transition: height 0.2s ease;
}
/* ── Admonition refinements ── */
/* Base: strip default Docusaurus/Infima alert styling */
.theme-admonition {
border: none;
border-left: 3px solid;
border-radius: 0 8px 8px 0;
padding: 1rem 1.25rem;
margin-bottom: 1.5rem;
box-shadow: none;
--ifm-alert-padding-horizontal: 1.25rem;
--ifm-alert-padding-vertical: 1rem;
}
/* Heading: uppercase small label */
[class*='admonitionHeading'] {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
margin-bottom: 0.5rem;
}
/* Icon: slightly smaller */
[class*='admonitionIcon'] svg {
width: 1rem;
height: 1rem;
}
/* Content: tighter typography */
[class*='admonitionContent'] > :last-child {
margin-bottom: 0;
}
/* ── Per-type colors ── */
/* Note (gray) */
.theme-admonition-note {
border-left-color: #868e96;
background: #f8f9fa;
}
[data-theme='dark'] .theme-admonition-note {
border-left-color: #adb5bd;
background: rgba(173, 181, 189, 0.08);
}
/* Tip (green) */
.theme-admonition-tip {
border-left-color: #2f9e44;
background: #ebfbee;
}
[data-theme='dark'] .theme-admonition-tip {
border-left-color: #51cf66;
background: rgba(47, 158, 68, 0.08);
}
/* Info (blue) */
.theme-admonition-info,
.theme-admonition-important {
border-left-color: #1971c2;
background: #e7f5ff;
}
[data-theme='dark'] .theme-admonition-info,
[data-theme='dark'] .theme-admonition-important {
border-left-color: #4dabf7;
background: rgba(25, 113, 194, 0.08);
}
/* Warning / Caution (amber) */
.theme-admonition-warning,
.theme-admonition-caution {
border-left-color: #e8590c;
background: #fff4e6;
}
[data-theme='dark'] .theme-admonition-warning,
[data-theme='dark'] .theme-admonition-caution {
border-left-color: #ff922b;
background: rgba(232, 89, 12, 0.08);
}
/* Danger (red) */
.theme-admonition-danger {
border-left-color: #c92a2a;
background: #fff5f5;
}
[data-theme='dark'] .theme-admonition-danger {
border-left-color: #ff6b6b;
background: rgba(201, 42, 42, 0.08);
}
/* Add breathing room between content and TOC */
.theme-doc-toc-desktop {
padding-left: 1.5rem;
}
/* Constrain doc content area for readability */
[class*='docMainContainer'] > .container {
max-width: 1280px;
margin: 0 auto;
}
.llmJudgeFigure {
max-width: 1120px;
margin: 2rem auto;
}
.llmJudgeFigure img {
display: block;
width: 100%;
height: auto;
margin: 0 auto;
}
.redteamPolicyScreenshotLink {
display: block;
margin: 1rem 0 1.5rem;
border-radius: 8px;
line-height: 0;
}
.redteamPolicyScreenshotLink:hover,
.redteamPolicyScreenshotLink:focus {
text-decoration: none;
}
.redteamPolicyScreenshotLink:focus:not(:focus-visible) {
outline: none;
}
.redteamPolicyScreenshotLink:focus-visible {
outline: 2px solid var(--ifm-color-primary);
outline-offset: 3px;
}
.redteamPolicyScreenshot {
display: block;
width: 100%;
max-width: 100%;
height: auto;
box-sizing: border-box;
margin: 0;
border: 1px solid var(--ifm-color-emphasis-200);
border-radius: 8px;
background: #fff;
box-shadow: 0 1px 3px rgba(16, 24, 40, 0.08);
}
[data-theme='dark'] .redteamPolicyScreenshot {
border-color: rgba(255, 255, 255, 0.12);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.24);
}
.redteamPluginScreenshotFigure {
margin: 1.25rem 0 1.75rem;
}
.redteamPluginScreenshotFigure figcaption {
margin: 0.5rem 0 0;
color: var(--ifm-color-emphasis-700);
font-size: 0.875rem;
line-height: 1.5;
}
.redteamPluginScreenshotLink {
display: block;
border-radius: 8px;
line-height: 0;
}
.redteamPluginScreenshotLink:hover,
.redteamPluginScreenshotLink:focus {
text-decoration: none;
}
.redteamPluginScreenshotLink:focus:not(:focus-visible) {
outline: none;
}
.redteamPluginScreenshotLink:focus-visible {
outline: 2px solid var(--ifm-color-primary);
outline-offset: 3px;
}
.redteamPluginScreenshot {
display: block;
width: 100%;
max-width: 100%;
height: auto;
box-sizing: border-box;
margin: 0;
border: 1px solid var(--ifm-color-emphasis-200);
border-radius: 8px;
background: #fff;
box-shadow: 0 1px 3px rgba(16, 24, 40, 0.08);
}
[data-theme='dark'] .redteamPluginScreenshot {
border-color: rgba(255, 255, 255, 0.12);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.24);
}
@media (max-width: 768px) {
.redteamPolicyScreenshotLink {
margin: 0.75rem 0 1.25rem;
border-radius: 6px;
}
.redteamPolicyScreenshot {
border-radius: 6px;
}
.redteamPluginScreenshotFigure {
margin: 1rem 0 1.5rem;
}
.redteamPluginScreenshotLink {
border-radius: 6px;
}
.redteamPluginScreenshot {
border-radius: 6px;
}
}
/* Remove pill background from active breadcrumb item */
.breadcrumbs__item--active .breadcrumbs__link {
background: none;
padding: 0;
}
/* Prevent iOS zoom on input focus within MUI overlays */
[class*='MuiDrawer'] input,
[class*='MuiDrawer'] select,
[class*='MuiDrawer'] textarea,
[class*='MuiDialog'] input,
[class*='MuiDialog'] select,
[class*='MuiDialog'] textarea {
font-size: 16px !important;
}
/* Print styles — hide MUI overlays */
@media print {
.MuiDrawer-root,
.MuiDialog-root,
.MuiFab-root {
display: none !important;
}
}
/* Import custom pagination styles */
@import url('./custom-pagination.css');
/* Announcement bar link styling */
div[class*='announcementBar'] a {
color: #ffffff;
text-decoration: none;
text-underline-offset: 2px;
opacity: 0.85;
transition: opacity 0.15s;
}
div[class*='announcementBar'] a:hover {
color: #ffffff;
text-decoration: underline;
opacity: 1;
}
+742
View File
@@ -0,0 +1,742 @@
// Event Types
export type EventStatus = 'upcoming' | 'past';
export type EventType = 'conference' | 'webinar' | 'workshop' | 'party';
export interface EventSpeaker {
name: string;
title: string;
photo?: string;
linkedin?: string;
}
export interface EventSession {
title: string;
description: string;
date?: string;
time?: string;
location?: string;
speakers: EventSpeaker[];
recording?: string;
slides?: string;
}
export interface EventDemo {
title: string;
description: string;
schedule?: string;
}
export interface EventResource {
title: string;
description: string;
type: 'pdf' | 'video' | 'link' | 'report';
url: string;
}
export interface EventHighlight {
icon: string;
title: string;
description: string;
}
export interface EventLocation {
venue: string;
city: string;
state: string;
country: string;
}
export interface Event {
id: string;
slug: string;
name: string;
shortName: string;
status: EventStatus;
type: EventType;
startDate: string;
endDate: string;
location: EventLocation;
booth?: string;
description: string;
fullDescription?: string;
heroImage?: string;
cardImage?: string;
highlights?: EventHighlight[];
demos?: EventDemo[];
sessions?: EventSession[];
teamMembers?: EventSpeaker[];
registrationUrl?: string;
meetingUrl?: string;
photos?: string[];
resources?: EventResource[];
externalLinks?: { label: string; url: string }[];
customPageUrl?: string; // Custom dedicated page URL for special events
}
// Helper to determine event status based on date
function getEventStatus(endDate: string): EventStatus {
const today = new Date();
const eventEnd = new Date(endDate);
return eventEnd < today ? 'past' : 'upcoming';
}
// Event Data
export const events: Event[] = [
// Future Events (2026)
{
id: 'bsides-seattle-2026',
slug: 'bsides-seattle-2026',
name: 'Promptfoo at BSides Seattle 2026',
shortName: 'BSides Seattle 2026',
status: 'upcoming',
type: 'conference',
startDate: '2026-02-27T09:00:00-08:00', // PST
endDate: '2026-02-28T18:00:00-08:00', // PST
location: {
venue: 'Building 92 (Microsoft Campus)',
city: 'Redmond',
state: 'WA',
country: 'USA',
},
description:
'Meet the Promptfoo team at BSides Seattle for hands-on AI red teaming demos, hallway-track threat intel, and practical ways to harden LLM apps.',
fullDescription:
'Meet the Promptfoo team for live demos of AI red teaming: prompt injection, jailbreaks, and data exfiltration against real-world LLM apps. Bring your use case and leave with a testing plan you can run in CI.',
cardImage: '/img/events/bsides-seattle-2026.jpg',
heroImage: '/img/events/bsides-seattle-2026.jpg',
highlights: [
{
icon: '🌲',
title: 'PNW Community',
description: 'Connect with Seattle security pros',
},
{
icon: '🛠️',
title: 'Workshops',
description: 'Hands-on AI security training',
},
{
icon: '🤝',
title: 'Networking',
description: 'Meet security researchers',
},
],
customPageUrl: '/events/bsides-seattle-2026',
},
{
id: 'rsa-2026',
slug: 'rsa-2026',
name: 'Promptfoo at RSA Conference 2026',
shortName: 'RSA 2026',
status: 'upcoming',
type: 'conference',
startDate: '2026-03-23T09:00:00-07:00', // PDT
endDate: '2026-03-26T18:00:00-07:00', // PDT
location: {
venue: 'Moscone Center',
city: 'San Francisco',
state: 'CA',
country: 'USA',
},
description:
'Meet Promptfoo at RSAC for executive-ready AI security: red teaming, guardrails, and reporting your leadership can act on.',
fullDescription:
'Meet Promptfoo to see how security teams build an AI security program that scales: continuous red teaming, runtime guardrails, and reporting that tracks risk reduction over time.',
cardImage: '/img/events/rsa-2026.jpg',
heroImage: '/img/events/rsa-2026.jpg',
highlights: [
{
icon: '🎯',
title: 'Live Demos',
description: 'Watch AI red teaming attacks in real-time',
},
{
icon: '🔒',
title: 'Free Assessment',
description: 'Get a complimentary AI vulnerability assessment',
},
{
icon: '🎁',
title: 'Exclusive Swag',
description: 'Limited edition Promptfoo gear',
},
],
customPageUrl: '/events/rsa-2026',
},
{
id: 'bsides-sf-2026',
slug: 'bsides-sf-2026',
name: 'Promptfoo at BSides SF 2026',
shortName: 'BSides SF 2026',
status: 'upcoming',
type: 'conference',
startDate: '2026-03-21T09:00:00-07:00', // PDT
endDate: '2026-03-22T18:00:00-07:00', // PDT
location: {
venue: 'City View at the Metreon',
city: 'San Francisco',
state: 'CA',
country: 'USA',
},
description:
'Practitioner-led AI security during RSA week. Fast demos, deep conversations, and concrete red teaming takeaways.',
fullDescription:
'Stop by to compare notes on prompt injection, agent abuse, and the testing workflows security teams actually run. We show quick demos, then go deep on how to reproduce issues and prevent regressions.',
cardImage: '/img/events/bsides-sf-2026.jpg',
heroImage: '/img/events/bsides-sf-2026.jpg',
highlights: [
{
icon: '🛠️',
title: 'Workshops',
description: 'Hands-on AI security training',
},
{
icon: '🤝',
title: 'Community',
description: 'Connect with security researchers',
},
{
icon: '🏆',
title: 'Challenges',
description: 'AI red teaming CTF challenges',
},
],
customPageUrl: '/events/bsides-sf-2026',
},
{
id: 'humanx-2026',
slug: 'humanx-2026',
name: 'Promptfoo at HumanX 2026',
shortName: 'HumanX 2026',
status: 'upcoming',
type: 'conference',
startDate: '2026-04-06T09:00:00-07:00', // PDT
endDate: '2026-04-09T18:00:00-07:00', // PDT
location: {
venue: 'Moscone Center South',
city: 'San Francisco',
state: 'CA',
country: 'USA',
},
description:
'For AI leaders shipping real products: see how to evaluate and secure LLM apps and agents without slowing teams down.',
fullDescription:
'AI is moving fast. Security and evaluation need to keep up. Meet Promptfoo for live demos on testing and securing LLM features across copilots, RAG, and agents, before launch and continuously in production.',
cardImage: '/img/events/humanx-2026.jpg',
heroImage: '/img/events/humanx-2026.jpg',
highlights: [
{
icon: '🧠',
title: 'AI Leadership',
description: 'Connect with AI executives and innovators',
},
{
icon: '🎯',
title: 'Live Demos',
description: 'See AI security testing in action',
},
{
icon: '🤝',
title: 'Networking',
description: 'Meet enterprise AI teams',
},
],
customPageUrl: '/events/humanx-2026',
},
{
id: 'gartner-security-2026',
slug: 'gartner-security-2026',
name: 'Promptfoo at Gartner Security & Risk Management Summit 2026',
shortName: 'Gartner Security 2026',
status: 'upcoming',
type: 'conference',
startDate: '2026-06-01T09:00:00-04:00', // EDT
endDate: '2026-06-03T18:00:00-04:00', // EDT
location: {
venue: 'Gaylord National Resort & Convention Center',
city: 'National Harbor',
state: 'MD',
country: 'USA',
},
description:
'Turn AI risk into a measurable program. Meet Promptfoo for briefings on continuous red teaming, guardrails, and executive reporting.',
fullDescription:
'If you are building an AI security program, we can help you move from ad hoc testing to continuous coverage. Meet Promptfoo for demos of automated red teaming, runtime guardrails, and reporting security leadership can track.',
cardImage: '/img/events/gartner-security-2026.jpg',
heroImage: '/img/events/gartner-security-2026.jpg',
highlights: [
{
icon: '📊',
title: 'Analyst Briefings',
description: 'Meet with Gartner analysts',
},
{
icon: '🏢',
title: 'Enterprise Focus',
description: 'Solutions for large organizations',
},
{
icon: '🔒',
title: 'Risk Management',
description: 'AI governance and compliance',
},
],
customPageUrl: '/events/gartner-security-2026',
},
{
id: 'blackhat-2026',
slug: 'blackhat-2026',
name: 'Promptfoo at Black Hat USA 2026',
shortName: 'Black Hat 2026',
status: 'upcoming',
type: 'conference',
startDate: '2026-08-01T09:00:00-07:00', // PDT
endDate: '2026-08-06T18:00:00-07:00', // PDT
location: {
venue: 'Mandalay Bay Convention Center',
city: 'Las Vegas',
state: 'NV',
country: 'USA',
},
description:
'See live AI attack demos at Black Hat USA: prompt injection, jailbreaks, data exfiltration, and how to automate LLM red teaming at scale.',
fullDescription:
'Join us at Black Hat USA to see how Promptfoo helps security teams find and fix LLM vulnerabilities with automated red teaming, repeatable evals, and production guardrails.',
cardImage: '/img/events/blackhat-2026.jpg',
heroImage: '/img/events/blackhat-2026.jpg',
highlights: [
{
icon: '🎯',
title: 'Attack Demos',
description: 'Prompt injection, jailbreaks, data exfiltration',
},
{
icon: '🤖',
title: 'Red Team Automation',
description: 'Generate application-specific attack variants',
},
{
icon: '🔄',
title: 'CI/CD Integration',
description: 'Turn findings into regression tests',
},
],
customPageUrl: '/events/blackhat-2026',
},
// Past Events (2025)
{
id: 'scaleup-ai-2025',
slug: 'scaleup-ai-2025',
name: 'Promptfoo at ScaleUp:AI 2025',
shortName: 'ScaleUp:AI 2025',
status: 'past',
type: 'webinar',
startDate: '2025-12-03T09:00:00-05:00', // EST
endDate: '2025-12-03T18:00:00-05:00', // EST
location: {
venue: 'Insight Partners',
city: 'New York',
state: 'NY',
country: 'USA',
},
description:
"Featured in Insight Partners' ScaleUp:AI 2025 Partner Series. CEO Ian Webster discusses how Promptfoo is restoring trust and security in generative AI.",
fullDescription:
"Ian Webster, CEO and co-founder of Promptfoo, was featured in Insight Partners' ScaleUp:AI 2025 Partner Series, discussing how Promptfoo is defining the standard for enterprise AI security. The feature explores the company's journey from open-source tool to serving 200,000+ developers and 80+ Fortune 500 companies.",
cardImage: '/img/events/scaleup-ai-2025.jpg',
heroImage: '/img/events/scaleup-ai-2025.jpg',
highlights: [
{
icon: '🔐',
title: 'AI Security',
description: 'Closing the gap in AI defenses',
},
{
icon: '📈',
title: 'Growth Story',
description: 'From open source to enterprise',
},
{
icon: '🔮',
title: 'Future Vision',
description: 'Multi-agent security landscape',
},
],
customPageUrl: '/events/scaleup-ai-2025',
externalLinks: [
{
label: 'Read Full Article',
url: 'https://www.insightpartners.com/ideas/promptfoo-scale-up-ai/',
},
],
},
{
id: 'blackhat-2025',
slug: 'blackhat-2025',
name: 'Promptfoo at Black Hat USA 2025',
shortName: 'Black Hat 2025',
status: getEventStatus('2025-08-07T18:00:00-07:00'),
type: 'conference',
startDate: '2025-08-05T09:00:00-07:00', // PDT
endDate: '2025-08-07T18:00:00-07:00', // PDT
location: {
venue: 'Mandalay Bay Convention Center',
city: 'Las Vegas',
state: 'NV',
country: 'USA',
},
booth: 'Booth #4712',
description: 'Meet us at booth #4712 for live AI red teaming demos and security consultations.',
fullDescription:
'Join us at Black Hat USA 2025 for live AI red teaming demos, security consultations, and the latest in LLM vulnerability research. Visit our booth to see how Fortune 500 companies protect their AI applications.',
cardImage: '/img/events/blackhat-2025.jpg',
heroImage: '/img/events/blackhat-2025.jpg',
highlights: [
{
icon: '🎯',
title: 'Live Demos',
description: 'Watch AI red teaming attacks in real-time',
},
{
icon: '🔒',
title: 'Free Scan',
description: 'Get a complimentary AI vulnerability assessment',
},
{
icon: '🎁',
title: 'Exclusive Swag',
description: 'Limited edition Promptfoo gear',
},
],
demos: [
{
title: 'LLM Red Teaming Demo',
description:
'Watch our team attempt to jailbreak and exploit a live AI application using prompt injection, data exfiltration, and other OWASP Top 10 attacks.',
schedule: 'Running every 30 minutes at the booth',
},
],
externalLinks: [
{
label: 'Arsenal Labs - Aug 6',
url: 'https://www.blackhat.com/us-25/arsenal/schedule/index.html#promptfoo-44648',
},
{
label: 'Arsenal Labs - Aug 7',
url: 'https://www.blackhat.com/us-25/arsenal/schedule/#promptfoo-47875',
},
],
customPageUrl: '/events/blackhat-2025',
},
{
id: 'defcon-2025',
slug: 'defcon-2025',
name: 'Promptfoo Party at DEF CON 33',
shortName: 'DEF CON 33',
status: getEventStatus('2025-08-09T23:59:00-07:00'),
type: 'party',
startDate: '2025-08-09T20:00:00-07:00', // PDT
endDate: '2025-08-09T23:59:00-07:00', // PDT
location: {
venue: 'Millennium FANDOM Bar',
city: 'Las Vegas',
state: 'NV',
country: 'USA',
},
description:
'Join hackers, security researchers, and the open source community for the AI security party of DEF CON.',
fullDescription:
"Join hackers, security researchers, and the open source community for the AI security event of DEF CON at the galaxy's most iconic cantina. Free drinks, great vibes, and security war stories.",
cardImage: '/img/events/defcon-2025.jpg',
heroImage: '/img/events/defcon-2025.jpg',
highlights: [
{
icon: '🍺',
title: 'Open Bar',
description: 'Free drinks on us',
},
{
icon: '⚔️',
title: 'Mos Eisley Vibes',
description: 'Party in a wretched hive of scum and villainy',
},
{
icon: '🤖',
title: 'Community',
description: 'Network with security researchers',
},
],
registrationUrl: 'https://lu.ma/ljm23pj6?tk=qGE9ez&utm_source=pf-web',
customPageUrl: '/events/defcon-2025',
},
{
id: 'rsa-2025',
slug: 'rsa-2025',
name: 'Promptfoo at RSA Conference 2025',
shortName: 'RSA 2025',
status: 'past',
type: 'conference',
startDate: '2025-04-28T09:00:00-07:00', // PDT
endDate: '2025-05-01T18:00:00-07:00', // PDT
location: {
venue: 'Moscone Center',
city: 'San Francisco',
state: 'CA',
country: 'USA',
},
booth: 'Expo Floor',
description:
'We showcased AI red teaming capabilities and security solutions at RSA Conference 2025.',
fullDescription:
'Promptfoo was at RSA Conference 2025 on the Expo Floor. We demonstrated our AI red teaming platform and connected with security professionals about protecting LLM applications.',
cardImage: '/img/events/rsa-2025.jpg',
heroImage: '/img/events/rsa-2025.jpg',
highlights: [
{
icon: '🎯',
title: 'Live Demos',
description: 'AI red teaming demonstrations',
},
{
icon: '🤝',
title: 'Networking',
description: 'Connected with security leaders',
},
{
icon: '📊',
title: 'Research',
description: 'Shared latest security findings',
},
],
customPageUrl: '/events/rsa-2025',
},
{
id: 'bsides-sf-2025',
slug: 'bsides-sf-2025',
name: 'Promptfoo at BSides SF 2025',
shortName: 'BSides SF 2025',
status: 'past',
type: 'conference',
startDate: '2025-04-26T09:00:00-07:00', // PDT
endDate: '2025-04-27T18:00:00-07:00', // PDT
location: {
venue: 'City View at Metreon',
city: 'San Francisco',
state: 'CA',
country: 'USA',
},
description: 'We connected with the security community at BSides SF 2025 during RSA week.',
fullDescription:
'Promptfoo joined the BSides SF 2025 community event during RSA week. We participated in security discussions and connected with researchers working on AI security challenges.',
cardImage: '/img/events/bsides-sf-2025.jpg',
heroImage: '/img/events/bsides-sf-2025.jpg',
highlights: [
{
icon: '🤝',
title: 'Community',
description: 'Connected with security researchers',
},
{
icon: '💬',
title: 'Discussions',
description: 'AI security conversations',
},
],
customPageUrl: '/events/bsides-sf-2025',
},
{
id: 'bsides-seattle-2025',
slug: 'bsides-seattle-2025',
name: 'Promptfoo at BSides Seattle 2025',
shortName: 'BSides Seattle 2025',
status: 'past',
type: 'conference',
startDate: '2025-04-18T09:00:00-07:00', // PDT
endDate: '2025-04-19T18:00:00-07:00', // PDT
location: {
venue: 'Building 92 (Microsoft Visitor Center)',
city: 'Redmond',
state: 'WA',
country: 'USA',
},
description: 'We engaged with the Pacific Northwest security community at BSides Seattle 2025.',
fullDescription:
'Promptfoo participated in BSides Seattle 2025 at Building 92, engaging with the Pacific Northwest security community and discussing the latest in AI security threats and mitigations.',
cardImage: '/img/events/bsides-seattle-2025.jpg',
heroImage: '/img/events/bsides-seattle-2025.jpg',
highlights: [
{
icon: '🌲',
title: 'PNW Community',
description: 'Connected with Seattle security pros',
},
{
icon: '🔐',
title: 'AI Security',
description: 'Shared LLM security insights',
},
],
customPageUrl: '/events/bsides-seattle-2025',
},
{
id: 'telecom-talks-2025',
slug: 'telecom-talks-2025',
name: 'Promptfoo at Telecom Talks 2025',
shortName: 'Telecom Talks 2025',
status: 'past',
type: 'conference',
startDate: '2025-04-09T09:00:00-07:00', // PDT
endDate: '2025-04-09T18:00:00-07:00', // PDT
location: {
venue: 'SRI International',
city: 'Menlo Park',
state: 'CA',
country: 'USA',
},
description:
'Ian Webster joined Swisscom Outpost on stage to discuss AI security in telecommunications.',
fullDescription:
'Promptfoo CEO Ian Webster joined Swisscom Outpost on stage at Telecom Talks 2025 to discuss the unique challenges of securing AI systems in telecommunications infrastructure.',
cardImage: '/img/events/telecom-talks-2025.jpg',
heroImage: '/img/events/telecom-talks-2025.jpg',
highlights: [
{
icon: '📡',
title: 'Joint Session',
description: 'On stage with Swisscom Outpost',
},
{
icon: '🌐',
title: 'Telecom Focus',
description: 'Carrier-grade AI security',
},
],
customPageUrl: '/events/telecom-talks-2025',
},
{
id: 'sector-2025',
slug: 'sector-2025',
name: 'Promptfoo at SecTor 2025',
shortName: 'SecTor 2025',
status: getEventStatus('2025-10-02T18:00:00-04:00'),
type: 'conference',
startDate: '2025-09-30T09:00:00-04:00', // EDT
endDate: '2025-10-02T18:00:00-04:00', // EDT
location: {
venue: 'Metro Toronto Convention Centre',
city: 'Toronto',
state: 'ON',
country: 'Canada',
},
description:
"Canada's largest IT security conference. Arsenal demos and enterprise AI security discussions.",
fullDescription:
"Promptfoo was selected for the SecTor Arsenal, showcasing open-source AI security tools to Canada's enterprise security community at Canada's largest IT security conference.",
cardImage: '/img/events/sector-2025.jpg',
heroImage: '/img/events/sector-2025.jpg',
highlights: [
{
icon: '🍁',
title: 'Arsenal Listing',
description: 'Selected for SecTor Arsenal',
},
{
icon: '🇨🇦',
title: 'Canadian Enterprise',
description: 'Major banks and government',
},
],
customPageUrl: '/events/sector-2025',
},
{
id: 'ai-security-summit-2025',
slug: 'ai-security-summit-2025',
name: 'Promptfoo at AI Security Summit 2025',
shortName: 'AI Security Summit 2025',
status: getEventStatus('2025-10-23T18:00:00-07:00'),
type: 'conference',
startDate: '2025-10-22T09:00:00-07:00', // PDT
endDate: '2025-10-23T18:00:00-07:00', // PDT
location: {
venue: 'Westin St. Francis',
city: 'San Francisco',
state: 'CA',
country: 'USA',
},
description:
'Two days of AI security research, expert panels, and cutting-edge demonstrations.',
fullDescription:
'Ian Webster joined industry leaders as a panel speaker at AI Security Summit 2025 to discuss the evolving landscape of LLM vulnerabilities and practical defense strategies.',
cardImage: '/img/events/ai-security-summit-2025.jpg',
heroImage: '/img/events/ai-security-summit-2025.jpg',
highlights: [
{
icon: '🧠',
title: 'Panel Speaker',
description: 'Ian Webster on expert panel',
},
{
icon: '🔬',
title: 'Research',
description: 'Latest AI security findings',
},
],
customPageUrl: '/events/ai-security-summit-2025',
},
];
// Helper functions
export function getUpcomingEvents(): Event[] {
return events
.filter((event) => event.status === 'upcoming')
.sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime());
}
export function getPastEvents(): Event[] {
return events
.filter((event) => event.status === 'past')
.sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime());
}
export function getEventBySlug(slug: string): Event | undefined {
return events.find((event) => event.slug === slug);
}
export function getEventsByYear(year: number): Event[] {
return events.filter((event) => new Date(event.startDate).getFullYear() === year);
}
export function getEventsByType(type: EventType): Event[] {
return events.filter((event) => event.type === type);
}
export function getFeaturedEvent(): Event | undefined {
const upcoming = getUpcomingEvents();
return upcoming.length > 0 ? upcoming[0] : undefined;
}
export function formatEventDate(startDate: string, endDate: string): string {
const start = new Date(startDate);
const end = new Date(endDate);
const startMonth = start.toLocaleDateString('en-US', { month: 'short' });
const endMonth = end.toLocaleDateString('en-US', { month: 'short' });
const startDay = start.getDate();
const endDay = end.getDate();
const year = start.getFullYear();
if (startDate === endDate) {
return `${startMonth} ${startDay}, ${year}`;
}
if (startMonth === endMonth) {
return `${startMonth} ${startDay}-${endDay}, ${year}`;
}
return `${startMonth} ${startDay} - ${endMonth} ${endDay}, ${year}`;
}
export function formatEventLocation(location: EventLocation): string {
return `${location.city}, ${location.state}`;
}
+275
View File
@@ -0,0 +1,275 @@
import * as fs from 'fs';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
/**
* Tests for the cookie consent banner (site/static/js/consent.js).
* We load and evaluate the IIFE in a jsdom environment, then verify
* DOM state, cookie behavior, and script injection.
*/
const CONSENT_JS = fs.readFileSync(path.resolve(__dirname, '../../static/js/consent.js'), 'utf-8');
function setCookie(name: string, value: string) {
document.cookie = `${name}=${value};path=/`;
}
function getCookie(name: string): string | null {
const m = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`));
return m ? m[2] : null;
}
function clearCookies() {
document.cookie.split(';').forEach((c) => {
const name = c.split('=')[0].trim();
if (name) {
document.cookie = `${name}=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT`;
}
});
}
function runConsent() {
// eslint-disable-next-line no-eval
const fn = new Function(CONSENT_JS);
fn();
}
describe('consent.js', () => {
beforeEach(() => {
clearCookies();
document.body.innerHTML = '';
document.head.querySelectorAll('#cc-styles').forEach((el) => el.remove());
document.querySelectorAll('script[src]').forEach((el) => el.remove());
(window as any).__pf_scripts_loaded = false;
(window as any).__pf_manage_cookies = undefined;
// Stub location.hash
Object.defineProperty(window, 'location', {
writable: true,
value: {
...window.location,
hash: '',
pathname: '/',
search: '',
href: 'http://localhost/',
reload: vi.fn(),
},
});
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('EU visitor without prior consent', () => {
it('loads scripts when no pf_country cookie (defaults to non-EU)', () => {
runConsent();
expect(document.getElementById('cc-banner')).toBeNull();
expect((window as any).__pf_scripts_loaded).toBe(true);
});
it('shows banner for EU country code', () => {
setCookie('pf_country', 'DE');
runConsent();
expect(document.getElementById('cc-banner')).not.toBeNull();
});
it('shows banner for UK', () => {
setCookie('pf_country', 'GB');
runConsent();
expect(document.getElementById('cc-banner')).not.toBeNull();
});
it('shows banner for Switzerland', () => {
setCookie('pf_country', 'CH');
runConsent();
expect(document.getElementById('cc-banner')).not.toBeNull();
});
});
describe('non-EU visitor', () => {
it('does not show banner for US', () => {
setCookie('pf_country', 'US');
runConsent();
expect(document.getElementById('cc-banner')).toBeNull();
});
it('does not show banner for Japan', () => {
setCookie('pf_country', 'JP');
runConsent();
expect(document.getElementById('cc-banner')).toBeNull();
});
it('loads scripts immediately', () => {
setCookie('pf_country', 'US');
runConsent();
expect((window as any).__pf_scripts_loaded).toBe(true);
const gtagScript = document.querySelector('script[src*="googletagmanager"]');
expect(gtagScript).not.toBeNull();
});
it('does not load scripts when #manage-cookies hash is present', () => {
setCookie('pf_country', 'US');
(window as any).location.hash = '#manage-cookies';
runConsent();
expect((window as any).__pf_scripts_loaded).toBe(false);
expect(document.getElementById('cc-banner')).not.toBeNull();
});
});
describe('accept flow', () => {
it('sets pf_consent=1 and loads scripts on accept', () => {
setCookie('pf_country', 'DE');
runConsent();
const acceptBtn = document.getElementById('cc-accept')!;
acceptBtn.click();
expect(getCookie('pf_consent')).toBe('1');
expect(document.getElementById('cc-banner')).toBeNull();
expect((window as any).__pf_scripts_loaded).toBe(true);
});
});
describe('decline flow', () => {
it('sets pf_consent=0 and does not load scripts on decline', () => {
setCookie('pf_country', 'FR');
runConsent();
const declineBtn = document.getElementById('cc-decline')!;
declineBtn.click();
expect(getCookie('pf_consent')).toBe('0');
expect(document.getElementById('cc-banner')).toBeNull();
expect((window as any).__pf_scripts_loaded).toBe(false);
});
});
describe('returning visitor with prior consent', () => {
it('loads scripts immediately when pf_consent=1', () => {
setCookie('pf_country', 'DE');
setCookie('pf_consent', '1');
runConsent();
expect(document.getElementById('cc-banner')).toBeNull();
expect((window as any).__pf_scripts_loaded).toBe(true);
});
it('does nothing when pf_consent=0', () => {
setCookie('pf_country', 'DE');
setCookie('pf_consent', '0');
runConsent();
expect(document.getElementById('cc-banner')).toBeNull();
expect((window as any).__pf_scripts_loaded).toBe(false);
});
});
describe('withdraw consent', () => {
it('exposes __pf_manage_cookies global', () => {
setCookie('pf_country', 'DE');
runConsent();
expect(typeof (window as any).__pf_manage_cookies).toBe('function');
});
it('reopens banner when __pf_manage_cookies is called', () => {
setCookie('pf_country', 'DE');
setCookie('pf_consent', '1');
runConsent();
expect(document.getElementById('cc-banner')).toBeNull();
(window as any).__pf_manage_cookies();
expect(document.getElementById('cc-banner')).not.toBeNull();
});
it('reloads page when declining after scripts were loaded', () => {
setCookie('pf_country', 'DE');
runConsent();
// Accept first
document.getElementById('cc-accept')!.click();
expect((window as any).__pf_scripts_loaded).toBe(true);
// Reopen and decline
(window as any).__pf_manage_cookies();
document.getElementById('cc-decline')!.click();
expect(window.location.reload).toHaveBeenCalled();
});
it('does not reload when declining on first visit (no scripts loaded)', () => {
setCookie('pf_country', 'DE');
runConsent();
document.getElementById('cc-decline')!.click();
expect(window.location.reload).not.toHaveBeenCalled();
});
});
describe('script loading', () => {
it('only loads scripts once even if loadScripts called multiple times', () => {
setCookie('pf_country', 'US');
runConsent();
const scriptCount = document.querySelectorAll('script[src*="scripts.js"]').length;
expect(scriptCount).toBe(1);
});
it('injects both gtag.js and scripts.js', () => {
setCookie('pf_country', 'US');
runConsent();
expect(document.querySelector('script[src*="googletagmanager"]')).not.toBeNull();
expect(document.querySelector('script[src*="scripts.js"]')).not.toBeNull();
});
});
describe('EU country coverage', () => {
const euCountries = [
'AT',
'BE',
'BG',
'HR',
'CY',
'CZ',
'DK',
'EE',
'FI',
'FR',
'DE',
'GR',
'HU',
'IE',
'IT',
'LV',
'LT',
'LU',
'MT',
'NL',
'PL',
'PT',
'RO',
'SK',
'SI',
'ES',
'SE',
'IS',
'LI',
'NO', // EEA
'GB', // UK
'CH', // Switzerland
];
it.each(euCountries)('shows banner for %s', (country) => {
setCookie('pf_country', country);
runConsent();
expect(document.getElementById('cc-banner')).not.toBeNull();
// Cleanup for next iteration
document.getElementById('cc-banner')?.remove();
clearCookies();
(window as any).__pf_scripts_loaded = false;
(window as any).__pf_manage_cookies = undefined;
});
});
});
+77
View File
@@ -0,0 +1,77 @@
import { describe, expect, it, vi } from 'vitest';
import { onRequest } from '../../functions/_middleware';
function makeContext(opts: { contentType?: string; cfCountry?: string; cookies?: string }) {
const responseHeaders = new Headers();
if (opts.contentType) {
responseHeaders.set('Content-Type', opts.contentType);
}
const response = new Response('', { headers: responseHeaders });
const requestHeaders = new Headers();
if (opts.cfCountry) {
requestHeaders.set('CF-IPCountry', opts.cfCountry);
}
if (opts.cookies) {
requestHeaders.set('Cookie', opts.cookies);
}
return {
request: new Request('https://example.com/', { headers: requestHeaders }),
next: vi.fn().mockResolvedValue(response),
};
}
describe('_middleware', () => {
it('sets pf_country cookie on HTML responses', async () => {
const ctx = makeContext({ contentType: 'text/html; charset=utf-8', cfCountry: 'DE' });
const res = await onRequest(ctx);
const setCookie = res.headers.get('Set-Cookie');
expect(setCookie).toContain('pf_country=DE');
expect(setCookie).toContain('Secure');
expect(setCookie).toContain('SameSite=Lax');
expect(setCookie).toContain('Max-Age=86400');
});
it('does not set cookie on non-HTML responses', async () => {
const ctx = makeContext({ contentType: 'application/javascript', cfCountry: 'DE' });
const res = await onRequest(ctx);
expect(res.headers.get('Set-Cookie')).toBeNull();
});
it('does not set cookie on CSS responses', async () => {
const ctx = makeContext({ contentType: 'text/css', cfCountry: 'DE' });
const res = await onRequest(ctx);
expect(res.headers.get('Set-Cookie')).toBeNull();
});
it('does not set cookie on image responses', async () => {
const ctx = makeContext({ contentType: 'image/png', cfCountry: 'US' });
const res = await onRequest(ctx);
expect(res.headers.get('Set-Cookie')).toBeNull();
});
it('does not set cookie when CF-IPCountry header is missing', async () => {
const ctx = makeContext({ contentType: 'text/html' });
const res = await onRequest(ctx);
expect(res.headers.get('Set-Cookie')).toBeNull();
});
it('always refreshes country cookie to handle travel/VPN', async () => {
const ctx = makeContext({
contentType: 'text/html',
cfCountry: 'FR',
cookies: 'pf_country=US',
});
const res = await onRequest(ctx);
const setCookie = res.headers.get('Set-Cookie');
expect(setCookie).toContain('pf_country=FR');
});
it('passes through response from next()', async () => {
const ctx = makeContext({ contentType: 'text/html', cfCountry: 'GB' });
const res = await onRequest(ctx);
expect(ctx.next).toHaveBeenCalledOnce();
expect(res).toBeInstanceOf(Response);
});
});
+32
View File
@@ -0,0 +1,32 @@
import { useEffect, useRef } from 'react';
type ThemeMode = 'light' | 'dark';
export function useForcedTheme(theme: ThemeMode) {
const previousThemeRef = useRef<string | null>(null);
useEffect(() => {
previousThemeRef.current = document.documentElement.getAttribute('data-theme');
document.documentElement.setAttribute('data-theme', theme);
// Re-enforce if something (e.g. system preference change) overrides the theme
const observer = new MutationObserver(() => {
if (document.documentElement.getAttribute('data-theme') !== theme) {
document.documentElement.setAttribute('data-theme', theme);
}
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme'],
});
return () => {
observer.disconnect();
if (previousThemeRef.current) {
document.documentElement.setAttribute('data-theme', previousThemeRef.current);
} else {
document.documentElement.removeAttribute('data-theme');
}
};
}, [theme]);
}
+17
View File
@@ -0,0 +1,17 @@
import { useLocation } from '@docusaurus/router';
export function useIsDocsPage(): boolean {
const { pathname } = useLocation();
return pathname.startsWith('/docs/') || pathname === '/docs';
}
export function useIsEventDetailPage(): boolean {
const { pathname } = useLocation();
// Match /events/<slug> but not /events/ or /events (index page)
return /^\/events\/[^/]+/.test(pathname);
}
export function useIsStorePage(): boolean {
const { pathname } = useLocation();
return pathname === '/store' || pathname === '/store/';
}
+420
View File
@@ -0,0 +1,420 @@
import React from 'react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import { useColorMode } from '@docusaurus/theme-common';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import LinkedInIcon from '@mui/icons-material/LinkedIn';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Container from '@mui/material/Container';
import Divider from '@mui/material/Divider';
import Grid from '@mui/material/Grid';
import IconButton from '@mui/material/IconButton';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import Layout from '@theme/Layout';
import { SITE_CONSTANTS } from '../constants';
const AboutPageContent = () => {
const { colorMode } = useColorMode();
const theme = React.useMemo(
() =>
createTheme({
palette: {
mode: colorMode === 'dark' ? 'dark' : 'light',
},
}),
[colorMode],
);
return (
<ThemeProvider theme={theme}>
<Container maxWidth="lg">
<Box
sx={{
py: 8,
}}
>
<Typography
variant="h2"
component="h2"
align="center"
gutterBottom
sx={{
fontWeight: 'bold',
}}
>
Securing the Future of AI
</Typography>
<Typography
variant="h5"
component="h2"
align="center"
sx={{
color: 'text.secondary',
mb: 2,
}}
>
Promptfoo helps developers and enterprises build secure, reliable AI applications.
</Typography>
</Box>
<Box
sx={{
mb: 8,
}}
>
<Grid container spacing={4}>
<Grid size={{ xs: 12, md: 6 }}>
<Typography
variant="h4"
component="h3"
gutterBottom
sx={{
fontWeight: 'medium',
}}
>
About Us
</Typography>
<Typography variant="body1" component="p" sx={{ mb: 2 }}>
We are security and engineering practitioners who have scaled generative AI products
to hundreds of millions of users. We're building the tools that we wished we had
when we were on the front lines.
</Typography>
<Typography variant="body1" component="p" sx={{ mb: 2 }}>
Based in San Francisco, California, Promptfoo is now part of OpenAI. Promptfoo
remains open source, and we continue to build tools for secure, reliable AI
applications.
</Typography>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
}}
>
<img
src="/img/logo-panda.svg"
alt="Promptfoo Logo"
style={{ maxWidth: '100%', maxHeight: '150px', height: 'auto' }}
/>
</Box>
</Grid>
</Grid>
</Box>
<Divider sx={{ my: 8 }} />
<Box
sx={{
mb: 8,
}}
>
<Typography
variant="h4"
component="h3"
align="center"
sx={{
mb: 8,
fontWeight: 'medium',
}}
>
Our founders
</Typography>
<Grid
container
spacing={4}
sx={{
justifyContent: 'center',
}}
>
{[
{
name: 'Ian Webster',
title: 'CEO & Co-founder',
image: '/img/team/ian.jpeg',
bio: 'Ian previously led LLM engineering and developer platform teams at Discord, scaling AI products to 200M users while maintaining rigorous safety, security, and policy standards.',
linkedin: 'http://linkedin.com/in/ianww',
},
{
name: "Michael D'Angelo",
title: 'CTO & Co-founder',
image: '/img/team/michael.jpeg',
bio: 'Michael brings extensive experience in AI and engineering leadership. As the former VP of Engineering and Head of AI at Smile Identity, he has a track record of scaling ML solutions to serve over 100 million people across hundreds of enterprises.',
linkedin: 'https://www.linkedin.com/in/michaelldangelo/',
},
].map((leader) => (
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={leader.name}>
<Box
sx={{
textAlign: 'center',
}}
>
<Avatar
alt={leader.name}
src={leader.image}
sx={{ width: 150, height: 150, margin: '0 auto 1rem' }}
/>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 1,
mb: 1,
}}
>
<Typography
variant="h6"
component="h4"
sx={{
fontWeight: 'medium',
}}
>
{leader.name}
</Typography>
{leader.linkedin && (
<IconButton
component="a"
href={leader.linkedin}
target="_blank"
rel="noopener noreferrer"
size="small"
aria-label={`${leader.name}'s LinkedIn profile`}
sx={{ padding: 0 }}
>
<LinkedInIcon fontSize="small" />
</IconButton>
)}
</Box>
<Typography
variant="subtitle1"
gutterBottom
sx={{
color: 'text.secondary',
}}
>
{leader.title}
</Typography>
<Typography variant="body2" dangerouslySetInnerHTML={{ __html: leader.bio }} />
</Box>
</Grid>
))}
</Grid>
</Box>
<Divider sx={{ my: 8 }} />
<Box
sx={{
mb: 8,
}}
>
<Typography
variant="h4"
component="h3"
align="center"
sx={{
fontWeight: 'medium',
mb: 4,
}}
>
Early Supporters
</Typography>
<Typography
variant="body1"
component="p"
align="center"
sx={{
mb: 8,
}}
>
We're grateful to the investors and operators who backed Promptfoo early and helped us
build open-source, application-focused AI security.
</Typography>
<Grid
container
spacing={4}
sx={{
justifyContent: 'center',
}}
>
{[
{
name: 'Ganesh Bell',
image: '/img/team/ganesh.jpeg',
description: 'Managing Director, Insight Partners',
},
{
name: 'Zane Lackey',
image: '/img/team/zane.jpeg',
description: 'General Partner, Andreessen Horowitz\nFounder, Signal Sciences',
},
{
name: 'Joel de la Garza',
image: '/img/team/joel.jpeg',
description: 'Investment Partner, Andreessen Horowitz\nCISO, Box',
},
{
name: 'Tobi Lutke',
image: '/img/team/tobi.jpeg',
description: 'CEO, Shopify',
},
{
name: 'Stanislav Vishnevskiy',
image: '/img/team/stan.jpeg',
description: 'CTO, Discord',
},
{
name: 'Frederic Kerrest',
image: '/img/team/frederic.jpeg',
description: 'Vice-Chairman & Co-Founder, Okta',
},
{
name: 'Adam Ely',
image: '/img/team/adam.jpeg',
description: 'EVP, Head of Digital Products, Fidelity\nCISO, Fidelity',
},
].map((investor) => (
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={investor.name}>
<Box
sx={{
textAlign: 'center',
}}
>
<Avatar
alt={investor.name}
src={investor.image}
sx={{ width: 120, height: 120, margin: '0 auto 1rem' }}
/>
<Typography
variant="h6"
component="h4"
gutterBottom
sx={{
fontWeight: 'medium',
}}
>
{investor.name}
</Typography>
<Typography
variant="body2"
sx={{
color: 'text.secondary',
whiteSpace: 'pre-line',
}}
>
{investor.description}
</Typography>
</Box>
</Grid>
))}
</Grid>
</Box>
<Divider sx={{ my: 8 }} />
<Box
sx={{
mb: 8,
}}
>
<Typography
variant="h4"
component="h3"
align="center"
gutterBottom
sx={{
fontWeight: 'medium',
}}
>
An Incredible Open Source Community
</Typography>
<Typography variant="body1" component="p" align="center" sx={{ mb: 2 }}>
Promptfoo is proud to be supported by a vibrant community of over{' '}
{SITE_CONSTANTS.CONTRIBUTOR_COUNT} open source contributors.
</Typography>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
mt: 4,
}}
>
<a href="https://github.com/promptfoo/promptfoo/graphs/contributors">
<img
src="https://contrib.rocks/image?repo=promptfoo/promptfoo"
alt="Promptfoo Contributors"
/>
</a>
</Box>
</Box>
<Box
sx={{
textAlign: 'center',
mb: 8,
}}
>
<Typography
variant="h4"
component="h3"
gutterBottom
sx={{
fontWeight: 'medium',
}}
>
Ready to Secure Your AI Applications?
</Typography>
<Typography variant="body1" component="p" sx={{ mb: 2 }}>
Join leading enterprises who trust Promptfoo to fortify their AI applications.
</Typography>
<Link className="button button--primary button--lg" to="/contact/">
Get in Touch
</Link>
</Box>
</Container>
</ThemeProvider>
);
};
const AboutPage = () => {
const { siteConfig } = useDocusaurusContext();
const siteUrl = siteConfig.url;
return (
<Layout
title="About Promptfoo | AI Security Experts"
description="Learn about Promptfoo's mission to secure AI applications and our team of industry veterans."
>
<Head>
<meta property="og:title" content="About Promptfoo - Securing the Future of AI" />
<meta
property="og:description"
content="Learn about Promptfoo's mission to secure AI applications and our team of industry veterans."
/>
<meta property="og:image" content={`${siteUrl}/img/og/about-og.png`} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:type" content="website" />
<meta property="og:url" content={`${siteUrl}/about`} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="About Promptfoo - Securing the Future of AI" />
<meta
name="twitter:description"
content="Learn about Promptfoo's mission to secure AI applications and our team of industry veterans."
/>
<meta name="twitter:image" content={`${siteUrl}/img/og/about-og.png`} />
<link rel="canonical" href={`${siteUrl}/about`} />
</Head>
<AboutPageContent />
</Layout>
);
};
export default AboutPage;
+371
View File
@@ -0,0 +1,371 @@
import React from 'react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import ArticleIcon from '@mui/icons-material/Article';
import CodeIcon from '@mui/icons-material/Code';
import GitHubIcon from '@mui/icons-material/GitHub';
import IntegrationInstructionsIcon from '@mui/icons-material/IntegrationInstructions';
import SearchIcon from '@mui/icons-material/Search';
import SecurityIcon from '@mui/icons-material/Security';
import TerminalIcon from '@mui/icons-material/Terminal';
import Layout from '@theme/Layout';
import clsx from 'clsx';
import LogoContainer from '../components/LogoContainer';
import styles from './landing-page.module.css';
function HeroSection() {
return (
<section className={styles.heroSection}>
<div className="container">
<img
src="/img/code-scanning-hero.svg"
alt="LLM Security Code Scanning"
className={styles.heroImage}
/>
<div className={styles.logoSection}>
Promptfoo is trusted by teams at...
<LogoContainer className={styles.heroLogos} noBackground noBorder />
</div>
</div>
</section>
);
}
function CodeScanningHeader() {
return (
<header className={clsx('hero', styles.heroBanner)}>
<div className="container">
<div className={styles.heroContent}>
<h1 className={styles.heroTitle}>Secure AI code in development</h1>
<p className={styles.heroSubtitle}>
Find LLM vulnerabilities in your IDE and CI/CD - before they reach production
</p>
<div className={styles.heroButtons}>
<Link
className={clsx('button button--primary button--lg', styles.buttonPrimary)}
to="/contact/"
>
Request Demo
</Link>
</div>
</div>
</div>
<HeroSection />
</header>
);
}
function IntegrationOptionsSection() {
return (
<section className={styles.actionOrientedSection}>
<div className="container">
<div className={styles.sectionEyebrow}>SHIFT-LEFT SECURITY</div>
<h2 className={styles.sectionTitle}>Coverage across your development workflow</h2>
<p className={styles.sectionSubtitle}>
Catch vulnerabilities at the earliest possible momentfrom the first line of code to
deployment
</p>
<div className={styles.securityFlowContainer}>
<div className={styles.securityFlowStep}>
<div className={styles.approachIcon}>
<CodeIcon fontSize="large" />
</div>
<h3>IDE Integration</h3>
<p>Real-time scanning as developers write code</p>
<ul>
<li>Inline diagnostics and severity indicators</li>
<li>One-click quick fixes</li>
<li>AI-assisted remediation prompts</li>
<li>Scan on save or on demand</li>
</ul>
<div className={styles.capabilityDetails}>
<Link to="/contact/" className="button button--primary button--sm">
Request Access
</Link>
</div>
</div>
<div className={styles.securityFlowArrow}></div>
<div className={styles.securityFlowStep}>
<div className={styles.approachIcon}>
<GitHubIcon fontSize="large" />
</div>
<h3>Pull Request Review</h3>
<p>Automated security review before code merges</p>
<ul>
<li>Findings posted as PR comments</li>
<li>Suggested fixes inline</li>
<li>Severity-based blocking</li>
<li>Easy GitHub integration</li>
</ul>
<div className={styles.capabilityDetails}>
<Link
to="/code-scanning/github-action/"
className="button button--primary button--sm"
target="_blank"
>
Learn More
</Link>
</div>
</div>
<div className={styles.securityFlowArrow}></div>
<div className={styles.securityFlowStep}>
<div className={styles.approachIcon}>
<TerminalIcon fontSize="large" />
</div>
<h3>CI/CD Pipeline</h3>
<p>Integrate into any build and deployment process</p>
<ul>
<li>Jenkins, GitLab, CircleCI, and more</li>
<li>JSON output for automation</li>
<li>Configurable severity thresholds</li>
<li>Fail builds on critical findings</li>
</ul>
<div className={styles.capabilityDetails}>
<Link to="/docs/code-scanning/cli/" className="button button--secondary button--sm">
CLI Documentation
</Link>
</div>
</div>
</div>
</div>
</section>
);
}
function SeeItInActionSection() {
return (
<section className={styles.actionOrientedSection}>
<div className="container">
<div className={styles.sectionEyebrow}>SEE IT IN ACTION</div>
<h2 className={styles.sectionTitle}>Security feedback where developers work</h2>
<p className={styles.sectionSubtitle}>
AI agents trace data flows across your codebase to find vulnerabilities that span multiple
filesthen surface findings with actionable remediation
</p>
<div className={styles.showcaseRow}>
<div className={styles.showcaseText}>
<h3>Real-time IDE scanning</h3>
<p>
Inline diagnostics, severity indicators, and one-click fixes as you write code. Catch
vulnerabilities the moment they're introduced—before they ever leave your editor.
</p>
</div>
<div className={styles.showcaseImage}>
<img
loading="lazy"
src="/img/docs/code-scanning/vscode-extension.png"
alt="VS Code extension showing inline security diagnostics"
/>
</div>
</div>
<div className={clsx(styles.showcaseRow, styles.showcaseRowReverse)}>
<div className={styles.showcaseText}>
<h3>Automated PR review</h3>
<p>
Security findings posted as PR comments with suggested fixes before code merges. Block
risky changes automatically based on severity thresholds.
</p>
</div>
<div className={styles.showcaseImage}>
<img
loading="lazy"
src="/img/docs/code-scanning/github.png"
alt="GitHub PR with security findings posted as review comments"
/>
</div>
</div>
</div>
</section>
);
}
function VulnerabilityTypesSection() {
const vulnerabilities = [
{
severity: 'critical',
name: 'Prompt Injection',
description: 'Untrusted input reaches LLM prompts without proper sanitization or boundaries.',
},
{
severity: 'critical',
name: 'Data Exfiltration',
description: 'Indirect prompt injection vectors that could extract data through agent tools.',
},
{
severity: 'high',
name: 'PII Exposure',
description:
'Code that may leak sensitive user data to LLMs or log confidential information.',
},
{
severity: 'high',
name: 'Improper Output Handling',
description: 'LLM outputs used in dangerous contexts like SQL queries or shell commands.',
},
{
severity: 'medium',
name: 'Excessive Agency',
description: 'LLMs with overly broad tool access or missing approval gates for actions.',
},
{
severity: 'medium',
name: 'Jailbreak Risks',
description: 'Weak system prompts and guardrail bypasses that could allow harmful outputs.',
},
];
return (
<section className={styles.vulnerabilitySection}>
<div className="container">
<div className={styles.vulnEyebrow}>LLM-SPECIFIC DETECTION</div>
<h2 className={styles.vulnTitle}>Find what other scanners miss</h2>
<p className={styles.vulnSubtitle}>
Purpose-built for AI security risks that general SAST tools overlook
</p>
<div className={styles.vulnGrid}>
{vulnerabilities.map((vuln) => (
<div key={vuln.name} className={styles.vulnCard}>
<div className={`${styles.vulnSeverity} ${styles[vuln.severity]}`}>
{vuln.severity}
</div>
<h3 className={styles.vulnName}>{vuln.name}</h3>
<p className={styles.vulnDescription}>{vuln.description}</p>
</div>
))}
</div>
</div>
</section>
);
}
function ProofBannerSection() {
return (
<section className={styles.proofBanner}>
<div className={clsx('container', styles.proofBannerContainer)}>
<ArticleIcon className={styles.proofBannerIcon} />
<div className={styles.proofBannerContent}>
<h3 className={styles.proofBannerTitle}>See it in action</h3>
<p className={styles.proofBannerText}>
We tested the scanner against real CVEs in LangChain, Vanna.AI, and LlamaIndex. Read the
technical deep dive to see how it catches vulnerabilities that other tools miss.
</p>
</div>
<Link
className={clsx('button button--secondary', styles.proofBannerButton)}
to="/blog/building-a-security-scanner-for-llm-apps"
>
Read the technical breakdown
</Link>
</div>
</section>
);
}
function BenefitsSection() {
return (
<section className={styles.benefitsSection}>
<div className="container">
<div className={styles.sectionEyebrow}>WHY CODE SCANNING</div>
<h2 className={styles.sectionTitle}>Security that scales with AI adoption</h2>
<p className={styles.sectionSubtitle}>
Find vulnerabilities where fixes are 10x faster and cheaperwithout slowing down
development
</p>
<div className={styles.benefitsList}>
<div className={styles.benefitItem}>
<SearchIcon className={styles.benefitIcon} />
<div className={styles.benefitContent}>
<h3>Deep data flow analysis</h3>
<p>
AI agents trace how user inputs flow through your code to LLM prompts, catching
subtle vulnerabilities that span multiple files and modulesnot just surface-level
pattern matching.
</p>
</div>
</div>
<div className={styles.benefitItem}>
<SecurityIcon className={styles.benefitIcon} />
<div className={styles.benefitContent}>
<h3>LLM-specific detection</h3>
<p>
Purpose-built for AI security risks that general SAST tools miss. High signal, low
noiseno alert fatigue from irrelevant findings.
</p>
</div>
</div>
<div className={styles.benefitItem}>
<IntegrationInstructionsIcon className={styles.benefitIcon} />
<div className={styles.benefitContent}>
<h3>Embedded in developer workflow</h3>
<p>
Security feedback in the IDE and PR comments with actionable remediation. Developers
fix issues without context switching or separate dashboards.
</p>
</div>
</div>
<div className={styles.benefitItem}>
<CodeIcon className={styles.benefitIcon} />
<div className={styles.benefitContent}>
<h3>Complete development coverage</h3>
<p>
From the first line of code to deployment. IDE catches issues immediately, PR review
prevents merges, CI/CD ensures nothing slips through.
</p>
</div>
</div>
</div>
</div>
</section>
);
}
function CallToActionSection() {
return (
<section className={styles.finalCTA}>
<div className="container">
<h2 className={styles.finalCTATitle}>Secure AI development from day one</h2>
<p className={styles.finalCTASubtitle}>
Get complete coverage across your development workflowIDE, pull requests, and CI/CD.
</p>
<div className={styles.finalCTAButtons}>
<Link className="button button--primary button--lg" to="/contact/">
Request Demo
</Link>
</div>
</div>
</section>
);
}
export default function CodeScanning(): React.ReactElement {
return (
<Layout
title="Code Scanning for LLM Security"
description="AI-powered code scanning that finds LLM security vulnerabilities in pull requests. Detect prompt injection, PII exposure, and jailbreak risks before you merge."
>
<Head>
<meta property="og:image" content="https://www.promptfoo.dev/img/meta/code-scanning.png" />
<meta name="twitter:card" content="summary_large_image" />
</Head>
<div className={styles.pageContainer}>
<CodeScanningHeader />
<main className={styles.mainContent}>
<IntegrationOptionsSection />
<SeeItInActionSection />
<VulnerabilityTypesSection />
<ProofBannerSection />
<BenefitsSection />
<CallToActionSection />
</main>
</div>
</Layout>
);
}
@@ -0,0 +1,243 @@
import React from 'react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import ArticleIcon from '@mui/icons-material/Article';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import BiotechIcon from '@mui/icons-material/Biotech';
import GitHubIcon from '@mui/icons-material/GitHub';
import VolumeOffIcon from '@mui/icons-material/VolumeOff';
import Layout from '@theme/Layout';
import clsx from 'clsx';
import LogoContainer from '../../components/LogoContainer';
import styles from '../landing-page.module.css';
function HeroSection() {
return (
<section className={styles.heroSection}>
<div className="container">
<img
src="/img/docs/code-scanning/github.png"
alt="GitHub PR with security findings"
className={clsx(styles.heroImage, styles.heroImageGitHubScanner)}
/>
<div className={styles.logoSection}>
Promptfoo is trusted by teams at...
<LogoContainer className={styles.heroLogos} noBackground noBorder />
</div>
</div>
</section>
);
}
function GitHubActionHeader() {
return (
<header className={clsx('hero', styles.heroBanner)}>
<div className="container">
<div className={styles.heroContent}>
<h1 className={styles.heroTitle}>Security scanning for LLM apps</h1>
<p className={styles.heroSubtitle}>
Find AI-based vulnerabilities in pull requests before you merge.
</p>
<div className={styles.heroButtons}>
<Link
className={clsx('button button--primary button--lg', styles.buttonPrimary)}
to="https://github.com/apps/promptfoo-scanner"
>
<GitHubIcon />
Install on GitHub
</Link>
<Link
className={clsx('button button--secondary button--lg', styles.buttonSecondary)}
to="/docs/code-scanning/github-action/"
>
View Docs
</Link>
</div>
</div>
</div>
<HeroSection />
</header>
);
}
function VulnerabilityTypesSection() {
const vulnerabilities = [
{
severity: 'critical',
name: 'Prompt Injection',
description: 'Untrusted input reaches LLM prompts without proper sanitization or boundaries.',
},
{
severity: 'critical',
name: 'Data Exfiltration',
description: 'Indirect prompt injection vectors that could extract data through agent tools.',
},
{
severity: 'high',
name: 'PII Exposure',
description:
'Code that may leak sensitive user data to LLMs or log confidential information.',
},
{
severity: 'high',
name: 'Improper Output Handling',
description: 'LLM outputs used in dangerous contexts like SQL queries or shell commands.',
},
{
severity: 'medium',
name: 'Excessive Agency',
description: 'LLMs with overly broad tool access or missing approval gates for actions.',
},
{
severity: 'medium',
name: 'Jailbreak Risks',
description: 'Weak system prompts and guardrail bypasses that could allow harmful outputs.',
},
];
return (
<section className={styles.vulnerabilitySection}>
<div className="container">
<div className={styles.vulnEyebrow}>LLM-specific vulnerabilities</div>
<h2 className={styles.vulnTitle}>Catch issues that other review tools miss</h2>
<p className={styles.vulnSubtitle}>
Our scanner is laser-focused on the kinds of vulnerabilities that apps built on LLMs and
agents are uniquely susceptible to.
</p>
<div className={styles.vulnGrid}>
{vulnerabilities.map((vuln) => (
<div key={vuln.name} className={styles.vulnCard}>
<div className={`${styles.vulnSeverity} ${styles[vuln.severity]}`}>
{vuln.severity}
</div>
<h3 className={styles.vulnName}>{vuln.name}</h3>
<p className={styles.vulnDescription}>{vuln.description}</p>
</div>
))}
</div>
</div>
</section>
);
}
function ProofBannerSection() {
return (
<section className={styles.proofBanner}>
<div className={clsx('container', styles.proofBannerContainer)}>
<ArticleIcon className={styles.proofBannerIcon} />
<div className={styles.proofBannerContent}>
<h3 className={styles.proofBannerTitle}>See it in action</h3>
<p className={styles.proofBannerText}>
We tested the scanner against real CVEs in LangChain, Vanna.AI, and LlamaIndex. Read the
technical deep dive to see how it catches vulnerabilities that other tools miss.
</p>
</div>
<Link
className={clsx('button button--secondary', styles.proofBannerButton)}
to="/blog/building-a-security-scanner-for-llm-apps"
>
Read the technical breakdown
</Link>
</div>
</section>
);
}
function BenefitsSection() {
return (
<section className={styles.benefitsSection}>
<div className="container">
<div className={styles.sectionEyebrow}>Built for developers</div>
<h2 className={styles.sectionTitle}>Real security that fits your workflow</h2>
<p className={styles.sectionSubtitle}>Flag dangerous code without adding friction.</p>
<div className={styles.benefitsList}>
<div className={styles.benefitItem}>
<BiotechIcon className={styles.benefitIcon} />
<div className={styles.benefitContent}>
<h3>Deep tracing</h3>
<p>
Beyond the PR itself, the scanner agentically traces LLM inputs, outputs, and
capability changes deep into the larger repository to identify subtle yet critical
issues that human reviewers can struggle to catch.
</p>
</div>
</div>
<div className={styles.benefitItem}>
<VolumeOffIcon className={styles.benefitIcon} />
<div className={styles.benefitContent}>
<h3>No noise</h3>
<p>
Despite the comprehensive approach, it has a high bar for reporting, avoiding false
positives and alert fatigue. Maintainers can configure severity levels and provide
custom instructions to tailor sensitivity to their needs.
</p>
</div>
</div>
<div className={styles.benefitItem}>
<AutoFixHighIcon className={styles.benefitIcon} />
<div className={styles.benefitContent}>
<h3>Fix suggestions</h3>
<p>
Every finding includes a suggested remediation, as well as a prompt that can be
passed straight to an AI coding agent to further investigate and address the issue.
</p>
</div>
</div>
</div>
</div>
</section>
);
}
function CallToActionSection() {
return (
<section className={styles.finalCTA}>
<div className="container">
<h2 className={styles.finalCTATitle}>Start scanning PRs right now</h2>
<p className={styles.finalCTASubtitle}>No account, credit card, or API keys required.</p>
<div className={styles.finalCTAButtons}>
<Link
className={clsx('button button--primary button--lg', styles.buttonPrimary)}
to="https://github.com/apps/promptfoo-scanner"
>
<GitHubIcon />
Install on GitHub
</Link>
<Link
className={clsx('button button--secondary button--lg', styles.buttonSecondary)}
to="/docs/code-scanning/github-action/"
>
Read the Docs
</Link>
</div>
</div>
</section>
);
}
export default function GitHubAction(): React.ReactElement {
return (
<Layout
title="GitHub Action for LLM Security Scanning"
description="Automatically scan pull requests for LLM vulnerabilities. Find prompt injection, PII exposure, and AI security risks before you merge."
>
<Head>
<meta
property="og:image"
content="https://www.promptfoo.dev/img/docs/code-scanning/github.png"
/>
<meta name="twitter:card" content="summary_large_image" />
</Head>
<div className={styles.pageContainer}>
<GitHubActionHeader />
<main className={styles.mainContent}>
<VulnerabilityTypesSection />
<ProofBannerSection />
<BenefitsSection />
<CallToActionSection />
</main>
</div>
</Layout>
);
}
+304
View File
@@ -0,0 +1,304 @@
.pageWrapper {
padding: 4rem 0 5rem;
background:
radial-gradient(circle at top left, rgba(229, 58, 58, 0.06), transparent 32%),
radial-gradient(circle at top right, rgba(255, 122, 122, 0.05), transparent 28%);
}
.heroSection {
max-width: 760px;
margin-bottom: 3rem;
}
.heroChip {
margin-bottom: 1rem;
font-weight: 600;
color: var(--ifm-color-primary);
background: rgba(229, 58, 58, 0.08);
}
.heroTitle {
margin-bottom: 1rem;
font-weight: 700;
letter-spacing: -0.02em;
}
.heroSubtitle {
max-width: 720px;
color: var(--ifm-color-emphasis-700);
line-height: 1.5;
}
.mainLayout {
display: grid;
grid-template-columns: minmax(0, 1.5fr) minmax(320px, 0.8fr);
gap: 2rem;
align-items: start;
}
.contactCard,
.sidebarCard {
border: 1px solid var(--ifm-color-emphasis-200);
background: var(--ifm-background-surface-color);
box-shadow: 0 12px 40px rgba(15, 23, 42, 0.04);
}
.contactCard {
padding: 2rem;
border-radius: 20px;
}
.cardHeader {
margin-bottom: 1rem;
}
.contactForm {
max-width: 100%;
}
.formGrid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.submitRow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
margin-top: 1.5rem;
}
.emailLink {
color: var(--ifm-color-primary);
font-weight: 500;
}
.sidebarColumn {
position: sticky;
top: 2rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.sidebarCard {
padding: 1.5rem;
border-radius: 16px;
}
.logoGrid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.125rem 1.5rem;
align-items: center;
}
.logoItem {
display: flex;
align-items: center;
height: 32px;
}
.brandLogo {
display: block;
width: auto;
max-width: 100%;
height: auto;
filter: grayscale(100%);
opacity: 0.72;
transition: opacity 0.2s ease;
}
.shopifyLogo {
max-height: 32px;
}
.anthropicLogo {
max-height: 14px;
}
.microsoftLogo {
width: 100px;
height: 24px;
object-fit: cover;
object-position: left center;
}
.discordLogo {
max-height: 20px;
}
.doordashLogo {
max-height: 16px;
}
.carvanaLogo {
width: 102px;
height: 24px;
object-fit: cover;
object-position: left center;
}
[data-theme='dark'] .brandLogo {
filter: grayscale(100%) invert(100%);
opacity: 0.8;
}
.testimonialList {
display: flex;
flex-direction: column;
}
.testimonialItem {
padding: 0.875rem 0;
border-bottom: 1px solid var(--ifm-color-emphasis-200);
}
.testimonialItem:first-child {
padding-top: 0;
}
.testimonialItem:last-child {
padding-bottom: 0;
border-bottom: none;
}
.testimonialHeader {
display: flex;
align-items: center;
gap: 0.625rem;
margin-bottom: 0.625rem;
}
.testimonialSource {
font-size: 0.875rem !important;
font-weight: 600;
color: var(--ifm-color-emphasis-800);
}
.testimonialQuote {
margin-bottom: 0.5rem;
color: var(--ifm-color-emphasis-700);
font-size: 0.875rem !important;
line-height: 1.45;
}
.testimonialLink {
display: inline-flex;
align-items: center;
gap: 0.25rem;
color: var(--ifm-color-primary);
font-size: 0.8125rem !important;
font-weight: 500;
transition: transform 0.2s ease;
}
.testimonialLink:hover {
transform: translateX(2px);
}
.testimonialLinkIcon {
font-size: 1rem !important;
}
.openaiTestimonialLogo {
height: 20px;
filter: grayscale(100%);
opacity: 0.7;
flex-shrink: 0;
}
.anthropicTestimonialLogo {
height: 10px;
filter: grayscale(100%);
opacity: 0.7;
flex-shrink: 0;
}
.awsTestimonialLogo {
height: 22px;
filter: grayscale(100%);
opacity: 0.7;
flex-shrink: 0;
}
[data-theme='dark'] .openaiTestimonialLogo,
[data-theme='dark'] .anthropicTestimonialLogo,
[data-theme='dark'] .awsTestimonialLogo {
filter: grayscale(100%) invert(100%);
opacity: 0.85;
}
.resourceLinks {
display: flex;
flex-direction: column;
gap: 0.875rem;
}
.resourceLink {
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
color: var(--ifm-color-emphasis-800);
font-size: 0.95rem;
font-weight: 500;
text-decoration: none;
transition: color 0.2s ease;
}
.resourceLink:hover {
color: var(--ifm-color-primary);
}
.resourceLinkIcon {
font-size: 1rem;
transition: transform 0.2s ease;
}
.resourceLink:hover .resourceLinkIcon {
transform: translateX(2px);
}
@media (max-width: 996px) {
.pageWrapper {
padding-top: 3rem;
}
.mainLayout {
grid-template-columns: 1fr;
}
.sidebarColumn {
position: static;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
.pageWrapper {
padding: 2rem 0 3rem;
}
.heroSection {
margin-bottom: 2rem;
}
.contactCard,
.sidebarCard {
padding: 1.25rem;
}
.formGrid,
.sidebarColumn {
grid-template-columns: 1fr;
}
.submitRow {
flex-direction: column;
align-items: flex-start;
}
}
+359
View File
@@ -0,0 +1,359 @@
import React from 'react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import { useColorMode } from '@docusaurus/theme-common';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
import Container from '@mui/material/Container';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import MuiLink from '@mui/material/Link';
import MenuItem from '@mui/material/MenuItem';
import Paper from '@mui/material/Paper';
import Select from '@mui/material/Select';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import Layout from '@theme/Layout';
import styles from './contact.module.css';
const testimonials = [
{
href: 'https://vimeo.com/1023317525/be082a1029',
logo: '/img/brands/openai-logo.svg',
logoAlt: 'OpenAI',
logoClassName: styles.openaiTestimonialLogo,
quote: 'Promptfoo is really powerful. It is faster and more straightforward.',
source: 'Build Hours',
cta: 'Watch video',
},
{
href: 'https://github.com/anthropics/courses/tree/master/prompt_evaluations',
logo: '/img/brands/anthropic-logo.svg',
logoAlt: 'Anthropic',
logoClassName: styles.anthropicTestimonialLogo,
quote: 'A streamlined solution that significantly reduces testing effort.',
source: 'Courses',
cta: 'See course',
},
{
href: 'https://catalog.workshops.aws/promptfoo/',
logo: '/img/brands/aws-logo.svg',
logoAlt: 'AWS',
logoClassName: styles.awsTestimonialLogo,
quote: 'Promptfoo works particularly well with Amazon Bedrock.',
source: 'Workshops',
cta: 'View workshop',
},
];
function Contact(): React.ReactElement {
const isDarkTheme = useColorMode().colorMode === 'dark';
const theme = React.useMemo(
() =>
createTheme({
palette: {
mode: isDarkTheme ? 'dark' : 'light',
primary: {
main: isDarkTheme ? '#ff7a7a' : '#e53a3a',
dark: isDarkTheme ? '#e53a3a' : '#cb3434',
contrastText: isDarkTheme ? '#10191c' : '#ffffff',
},
},
}),
[isDarkTheme],
);
return (
<ThemeProvider theme={theme}>
<Box className={styles.pageWrapper}>
<Container maxWidth="lg">
<Box className={styles.heroSection}>
<Chip label="Enterprise" className={styles.heroChip} size="small" />
<Typography variant="h2" component="h1" className={styles.heroTitle}>
Talk to our AI security team
</Typography>
<Typography variant="h6" className={styles.heroSubtitle}>
We help security, platform, and ML teams evaluate risk, enforce policy, and ship
reliable AI applications.
</Typography>
</Box>
<Box className={styles.mainLayout}>
<Paper className={styles.contactCard} elevation={0}>
<Box className={styles.cardHeader}>
<Typography variant="h5" component="h2" sx={{ fontWeight: 600 }}>
Request a demo
</Typography>
<Typography variant="body1" color="text.secondary">
Tell us about your environment and what you want to accomplish.
</Typography>
</Box>
<form action="https://submit-form.com/ghriv7voL" className={styles.contactForm}>
<Box className={styles.formGrid}>
<TextField
fullWidth
id="name"
name="name"
label="Full name"
variant="outlined"
required
margin="normal"
/>
<TextField
fullWidth
id="email"
name="email"
label="Work email"
type="email"
variant="outlined"
required
margin="normal"
helperText="Please use your company email address"
/>
</Box>
<Box className={styles.formGrid}>
<TextField
fullWidth
id="company"
name="company"
label="Company"
variant="outlined"
required
margin="normal"
/>
<TextField
fullWidth
id="title"
name="title"
label="Job title"
variant="outlined"
margin="normal"
/>
</Box>
<FormControl fullWidth margin="normal" variant="outlined" required>
<InputLabel id="interested-in-label">I'm interested in</InputLabel>
<Select
labelId="interested-in-label"
id="interested-in"
name="interested-in"
label="I'm interested in"
>
<MenuItem value="Enterprise Security">
Enterprise Security & Red Teaming
</MenuItem>
<MenuItem value="AI Guardrails">AI Guardrails & Compliance</MenuItem>
<MenuItem value="Model Evaluation">Model Evaluation & Testing</MenuItem>
<MenuItem value="Custom Solution">Custom Enterprise Solution</MenuItem>
<MenuItem value="Other">Other</MenuItem>
</Select>
</FormControl>
<TextField
fullWidth
id="message"
name="message"
label="How can we help?"
multiline
rows={5}
variant="outlined"
required
margin="normal"
placeholder="Share a few details about your application, timeline, and deployment requirements."
/>
<Box className={styles.submitRow}>
<Button
type="submit"
variant="contained"
size="large"
endIcon={<ArrowForwardIcon />}
sx={{
px: 4,
py: 1.5,
textTransform: 'none',
fontWeight: 600,
}}
>
Contact sales
</Button>
<Typography variant="body2" color="text.secondary">
Or email{' '}
<MuiLink
href="mailto:inquiries@promptfoo.dev"
underline="hover"
className={styles.emailLink}
>
inquiries@promptfoo.dev
</MuiLink>
</Typography>
</Box>
</form>
</Paper>
<Box className={styles.sidebarColumn}>
<Paper className={styles.sidebarCard} elevation={0}>
<Typography variant="h6" sx={{ fontWeight: 600, marginBottom: '1rem' }}>
Trusted by leading teams
</Typography>
<Box className={styles.logoGrid}>
<Box className={styles.logoItem}>
<img
src="/img/brands/shopify-logo.svg"
alt="Shopify"
className={`${styles.brandLogo} ${styles.shopifyLogo}`}
/>
</Box>
<Box className={styles.logoItem}>
<img
src="/img/brands/anthropic-logo.svg"
alt="Anthropic"
className={`${styles.brandLogo} ${styles.anthropicLogo}`}
/>
</Box>
<Box className={styles.logoItem}>
<img
src="/img/brands/microsoft-logo.svg"
alt="Microsoft"
className={`${styles.brandLogo} ${styles.microsoftLogo}`}
/>
</Box>
<Box className={styles.logoItem}>
<img
src="/img/brands/discord-logo-blue.svg"
alt="Discord"
className={`${styles.brandLogo} ${styles.discordLogo}`}
/>
</Box>
<Box className={styles.logoItem}>
<img
src="/img/brands/doordash-logo.svg"
alt="DoorDash"
className={`${styles.brandLogo} ${styles.doordashLogo}`}
/>
</Box>
<Box className={styles.logoItem}>
<img
src="/img/brands/carvana-logo.svg"
alt="Carvana"
className={`${styles.brandLogo} ${styles.carvanaLogo}`}
/>
</Box>
</Box>
</Paper>
<Paper className={styles.sidebarCard} elevation={0}>
<Typography variant="h6" sx={{ fontWeight: 600, marginBottom: '1rem' }}>
What users say
</Typography>
<Box className={styles.testimonialList}>
{testimonials.map((testimonial) => (
<Box key={testimonial.source} className={styles.testimonialItem}>
<Box className={styles.testimonialHeader}>
<img
src={testimonial.logo}
alt={testimonial.logoAlt}
className={testimonial.logoClassName}
/>
<Typography variant="body2" className={styles.testimonialSource}>
{testimonial.source}
</Typography>
</Box>
<Typography variant="body2" className={styles.testimonialQuote}>
"{testimonial.quote}"
</Typography>
<MuiLink
href={testimonial.href}
target="_blank"
rel="noreferrer"
className={styles.testimonialLink}
underline="none"
>
{testimonial.cta}
<ArrowForwardIcon className={styles.testimonialLinkIcon} />
</MuiLink>
</Box>
))}
</Box>
</Paper>
<Paper className={styles.sidebarCard} elevation={0}>
<Typography variant="h6" sx={{ fontWeight: 600, marginBottom: '1rem' }}>
Resources
</Typography>
<Box className={styles.resourceLinks}>
<MuiLink
href="https://github.com/promptfoo/promptfoo"
target="_blank"
rel="noreferrer"
className={styles.resourceLink}
underline="none"
>
GitHub
<ArrowForwardIcon className={styles.resourceLinkIcon} />
</MuiLink>
<MuiLink
href="https://discord.gg/promptfoo"
target="_blank"
rel="noreferrer"
className={styles.resourceLink}
underline="none"
>
Discord community
<ArrowForwardIcon className={styles.resourceLinkIcon} />
</MuiLink>
<Link to="/docs/enterprise/" className={styles.resourceLink}>
Enterprise documentation
<ArrowForwardIcon className={styles.resourceLinkIcon} />
</Link>
</Box>
</Paper>
</Box>
</Box>
</Container>
</Box>
</ThemeProvider>
);
}
export default function Page(): React.ReactElement {
const { siteConfig } = useDocusaurusContext();
const siteUrl = siteConfig.url;
return (
<Layout
title="Contact Enterprise Sales"
description="Contact Promptfoo about enterprise AI security solutions, red teaming, guardrails, and compliance."
>
<Head>
<meta property="og:title" content="Contact Promptfoo" />
<meta
property="og:description"
content="Contact Promptfoo about enterprise AI security solutions, red teaming, guardrails, and compliance."
/>
<meta property="og:image" content={`${siteUrl}/img/og/contact-og.png`} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:type" content="website" />
<meta property="og:url" content={`${siteUrl}/contact`} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Contact Promptfoo" />
<meta
name="twitter:description"
content="Contact Promptfoo about enterprise AI security solutions, red teaming, guardrails, and compliance."
/>
<meta name="twitter:image" content={`${siteUrl}/img/og/contact-og.png`} />
<link rel="canonical" href={`${siteUrl}/contact`} />
</Head>
<Contact />
</Layout>
);
}
+60
View File
@@ -0,0 +1,60 @@
import React, { useEffect } from 'react';
import Layout from '@theme/Layout';
function getScalarTheme() {
if (typeof document === 'undefined') {
return 'alternate';
}
return document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'alternate';
}
export default function ApiReference() {
useEffect(() => {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/@scalar/api-reference';
script.setAttribute('data-theme', getScalarTheme());
script.addEventListener('load', () => {
// Once Scalar is loaded from the CDN, we can reference the object
(window as any).Scalar.createApiReference('#app', {
url: 'https://api.promptfoo.app/static/openapi.json',
});
});
document.head.appendChild(script);
return () => {
document.head.removeChild(script);
};
}, []);
// Hack to apply the theme to the Scalar app
// The 'light' theme is broken, so we use the 'alternate' theme instead
useEffect(() => {
const app = document.getElementById('app');
if (!app) {
return;
}
const applyTheme = () => {
app.dataset.theme = getScalarTheme();
};
applyTheme();
const observer = new MutationObserver(applyTheme);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme'],
});
return () => {
observer.disconnect();
};
}, []);
return (
<Layout title="API Reference | Promptfoo" description="API Reference">
<main id="app"></main>
</Layout>
);
}
@@ -0,0 +1,493 @@
/* AI Security Summit 2025 - Neural Network / AI Theme */
/* Color Palette: Deep purple (#7C3AED), Electric blue (#3B82F6), Neural pink (#EC4899) */
.summitPage {
background: #0a0a1a;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
}
/* Hero Banner */
.heroBanner {
position: relative;
height: 400px;
overflow: hidden;
}
.bannerImage {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.bannerOverlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(180deg, rgba(10, 10, 26, 0.4) 0%, rgba(10, 10, 26, 0.7) 100%);
}
.bannerContent {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 3rem;
background: linear-gradient(0deg, rgba(10, 10, 26, 0.95) 0%, transparent 100%);
}
.badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(124, 58, 237, 0.2);
border: 1px solid rgba(124, 58, 237, 0.4);
color: #e9d5ff;
border-radius: 100px;
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 1rem;
}
.badgeIcon {
font-size: 1rem;
}
.heroTitle {
font-size: clamp(2rem, 6vw, 3.5rem);
font-weight: 800;
line-height: 1.1;
color: #f1f5f9;
margin: 0;
}
.highlight {
background: linear-gradient(135deg, #7c3aed 0%, #3b82f6 50%, #ec4899 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Hero Content Section */
.heroContent {
padding: 3rem 0;
text-align: center;
border-bottom: 1px solid rgba(124, 58, 237, 0.1);
}
.heroSubtitle {
font-size: 1.25rem;
color: #94a3b8;
max-width: 600px;
margin: 0 auto 2rem;
line-height: 1.7;
}
.eventDetails {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2rem;
margin-bottom: 2rem;
}
.detail {
display: flex;
align-items: center;
gap: 0.5rem;
color: #cbd5e1;
font-size: 0.95rem;
}
.icon {
width: 1.25rem;
height: 1.25rem;
stroke-width: 2;
color: #7c3aed;
}
.heroCtas {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.primaryCta {
padding: 0.875rem 2rem;
background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%);
color: white;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
transition: all 0.3s ease;
}
.primaryCta:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(124, 58, 237, 0.4);
color: white;
}
.secondaryCta {
padding: 0.875rem 2rem;
background: transparent;
color: #c4b5fd;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
border: 1px solid rgba(124, 58, 237, 0.4);
transition: all 0.3s ease;
}
.secondaryCta:hover {
background: rgba(124, 58, 237, 0.1);
color: white;
}
/* Sections */
.speakerSection,
.themesSection,
.researchSection,
.statsSection,
.ctaSection {
padding: 4rem 0;
}
.speakerSection {
background: rgba(124, 58, 237, 0.02);
}
.sectionHeader {
text-align: center;
margin-bottom: 3rem;
}
.sectionTitle {
font-size: 2rem;
font-weight: 700;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.sectionSubtitle {
font-size: 1.1rem;
color: #94a3b8;
max-width: 600px;
margin: 0 auto;
}
/* Speaker Card */
.speakerCard {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.1) 0%, rgba(59, 130, 246, 0.05) 100%);
border: 1px solid rgba(124, 58, 237, 0.3);
border-radius: 16px;
padding: 2rem;
max-width: 700px;
margin: 0 auto;
}
.speakerInfo {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.speakerBadge {
display: inline-block;
width: fit-content;
padding: 0.375rem 1rem;
background: linear-gradient(135deg, #7c3aed 0%, #ec4899 100%);
color: white;
border-radius: 100px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.speakerName {
font-size: 1.5rem;
font-weight: 800;
color: #f8fafc;
margin: 0;
}
.speakerRole {
font-size: 1rem;
color: #7c3aed;
margin: 0;
font-weight: 600;
}
.speakerBio {
font-size: 1rem;
color: #94a3b8;
line-height: 1.7;
margin: 0;
}
.speakerTopics {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-top: 0.5rem;
}
.topic {
padding: 0.375rem 0.875rem;
background: rgba(124, 58, 237, 0.2);
color: #c4b5fd;
border-radius: 100px;
font-size: 0.8125rem;
font-weight: 600;
}
/* Themes Grid */
.themesGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.5rem;
}
.themeCard {
background: rgba(15, 15, 46, 0.6);
border: 1px solid rgba(124, 58, 237, 0.15);
border-radius: 12px;
padding: 1.5rem;
transition: all 0.3s ease;
}
.themeCard:hover {
border-color: rgba(124, 58, 237, 0.3);
transform: translateY(-2px);
}
.cardIcon {
font-size: 2rem;
margin-bottom: 1rem;
}
.themeCard h3 {
font-size: 1.125rem;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 0.5rem;
}
.themeCard p {
font-size: 0.9rem;
color: #94a3b8;
line-height: 1.6;
margin: 0;
}
/* Research Grid */
.researchSection {
background: rgba(59, 130, 246, 0.02);
}
.researchGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 2rem;
}
.researchCard {
padding: 1.5rem 0;
border-left: 2px solid rgba(124, 58, 237, 0.3);
padding-left: 1.5rem;
}
.researchNumber {
font-size: 2.5rem;
font-weight: 800;
background: linear-gradient(135deg, #7c3aed 0%, #3b82f6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.75rem;
line-height: 1;
}
.researchCard h3 {
font-size: 1.125rem;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 0.5rem;
}
.researchCard p {
font-size: 0.9rem;
color: #94a3b8;
line-height: 1.6;
margin: 0 0 1rem 0;
}
.researchLink {
font-size: 0.875rem;
font-weight: 600;
color: #7c3aed;
text-decoration: none;
transition: color 0.2s ease;
}
.researchLink:hover {
color: #a78bfa;
}
/* Stats Section */
.statsSection {
background: linear-gradient(135deg, rgba(124, 58, 237, 0.1) 0%, rgba(59, 130, 246, 0.05) 100%);
border-top: 1px solid rgba(124, 58, 237, 0.15);
border-bottom: 1px solid rgba(124, 58, 237, 0.15);
}
.statsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 2rem;
text-align: center;
}
.stat {
padding: 1.5rem;
}
.statNumber {
font-size: 2.5rem;
font-weight: 800;
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1;
margin-bottom: 0.5rem;
}
.statLabel {
font-size: 0.9rem;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* CTA Section */
.ctaContent {
max-width: 600px;
margin: 0 auto;
text-align: center;
background: linear-gradient(135deg, rgba(124, 58, 237, 0.08) 0%, rgba(59, 130, 246, 0.08) 100%);
border: 1px solid rgba(124, 58, 237, 0.2);
border-radius: 16px;
padding: 3rem 2rem;
}
.ctaTitle {
font-size: 1.75rem;
font-weight: 700;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.ctaText {
font-size: 1rem;
color: #94a3b8;
margin-bottom: 1.5rem;
line-height: 1.7;
}
.ctaButtons {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
/* Footer Navigation */
.footerNav {
padding: 2rem 0 3rem;
border-top: 1px solid rgba(124, 58, 237, 0.1);
}
.backLink {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: #94a3b8;
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
.backLink:hover {
color: #7c3aed;
}
.backIcon {
width: 1.25rem;
height: 1.25rem;
}
/* Responsive */
@media (max-width: 768px) {
.heroBanner {
height: 300px;
}
.bannerContent {
padding: 2rem 1.5rem;
}
.heroTitle {
font-size: 2rem;
}
.heroSubtitle {
font-size: 1.1rem;
}
.eventDetails {
flex-direction: column;
align-items: center;
gap: 1rem;
}
.heroCtas {
flex-direction: column;
align-items: center;
}
.primaryCta,
.secondaryCta {
width: 100%;
max-width: 280px;
text-align: center;
}
.ctaContent {
padding: 2rem 1.5rem;
}
.ctaButtons {
flex-direction: column;
align-items: center;
}
.speakerSection,
.themesSection,
.researchSection,
.statsSection,
.ctaSection {
padding: 3rem 0;
}
}
@@ -0,0 +1,325 @@
import React from 'react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import { useForcedTheme } from '@site/src/hooks/useForcedTheme';
import Layout from '@theme/Layout';
import styles from './ai-security-summit-2025.module.css';
export default function AISecuritySummit2025(): React.ReactElement {
useForcedTheme('dark');
const handleSmoothScroll = (e: React.MouseEvent<HTMLAnchorElement>, targetId: string) => {
e.preventDefault();
const element = document.querySelector(targetId);
if (element) {
const offset = 80;
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - offset;
window.scrollTo({ top: offsetPosition, behavior: 'smooth' });
}
};
return (
<Layout
title="AI Security Summit 2025"
description="Promptfoo at AI Security Summit 2025 (Oct 2223) at The Westin St. Francis, San Francisco. Ian Webster joins the panel 'Using AI for Offensive Security Testing.'"
>
<Head>
<meta property="og:title" content="Promptfoo at AI Security Summit 2025" />
<meta
property="og:description"
content="Promptfoo at AI Security Summit 2025 (Oct 2223, San Francisco). Community sponsor with Ian Webster on the panel 'Using AI for Offensive Security Testing.'"
/>
<meta property="og:type" content="website" />
<meta
property="og:url"
content="https://www.promptfoo.dev/events/ai-security-summit-2025"
/>
<meta name="twitter:card" content="summary_large_image" />
<meta
property="og:image"
content="https://www.promptfoo.dev/img/events/ai-security-summit-2025.jpg"
/>
<meta
name="twitter:image"
content="https://www.promptfoo.dev/img/events/ai-security-summit-2025.jpg"
/>
<meta
name="keywords"
content="AI Security Summit 2025, LLM security, AI red teaming, San Francisco, AI vulnerabilities, machine learning security"
/>
<link rel="canonical" href="https://www.promptfoo.dev/events/ai-security-summit-2025" />
</Head>
<main className={styles.summitPage}>
{/* Hero Banner */}
<section className={styles.heroBanner}>
<img
src="/img/events/ai-security-summit-2025.jpg"
alt="AI Security Summit 2025"
className={styles.bannerImage}
/>
<div className={styles.bannerOverlay} />
<div className={styles.bannerContent}>
<div className={styles.badge}>
<span className={styles.badgeIcon}>🧠</span>
AI Security Summit 2025
</div>
<h1 className={styles.heroTitle}>
The Future of <span className={styles.highlight}>AI Security</span>
</h1>
</div>
</section>
{/* Hero Content */}
<section className={styles.heroContent}>
<div className={styles.container}>
<p className={styles.heroSubtitle}>
Two days of leadership talks and practitioner sessions focused on real-world AI
security. Catch Ian Webster on the panel "Using AI for Offensive Security Testing."
</p>
<div className={styles.eventDetails}>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span>October 22-23, 2025</span>
</div>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<span>Westin St. Francis, San Francisco</span>
</div>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"
/>
</svg>
<span>Panel Speaker</span>
</div>
</div>
<div className={styles.heroCtas}>
<a
href="#highlights"
className={styles.primaryCta}
onClick={(e) => handleSmoothScroll(e, '#highlights')}
>
View Highlights
</a>
<Link to="/docs/red-team/" className={styles.secondaryCta}>
Learn Red Teaming
</Link>
</div>
</div>
</section>
{/* Speaker Spotlight */}
<section className={styles.speakerSection} id="highlights">
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>Speaker Spotlight</h2>
</div>
<div className={styles.speakerCard}>
<div className={styles.speakerInfo}>
<div className={styles.speakerBadge}>Panel Speaker</div>
<h3 className={styles.speakerName}>Ian Webster</h3>
<p className={styles.speakerRole}>CEO & Co-founder, Promptfoo</p>
<p className={styles.speakerBio}>
Ian joined industry leaders for the panel "Using AI for Offensive Security
Testing," covering how teams can use automation to discover LLM and agent
vulnerabilities earlier in the lifecycle.
</p>
<div className={styles.speakerTopics}>
<span className={styles.topic}>LLM Security</span>
<span className={styles.topic}>Red Teaming</span>
<span className={styles.topic}>Enterprise AI</span>
</div>
</div>
</div>
</div>
</section>
{/* Key Themes */}
<section className={styles.themesSection}>
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>Key Themes</h2>
<p className={styles.sectionSubtitle}>
Critical topics shaping the AI security landscape in 2025 and beyond.
</p>
</div>
<div className={styles.themesGrid}>
<div className={styles.themeCard}>
<div className={styles.cardIcon}>🎯</div>
<h3>Adversarial AI</h3>
<p>
Understanding how attackers exploit LLMs through prompt injection, jailbreaking,
and novel attack vectors targeting foundation models.
</p>
</div>
<div className={styles.themeCard}>
<div className={styles.cardIcon}>🔐</div>
<h3>Defense Strategies</h3>
<p>
Building robust guardrails and implementing comprehensive red teaming programs to
secure AI applications at scale.
</p>
</div>
<div className={styles.themeCard}>
<div className={styles.cardIcon}>🏢</div>
<h3>Enterprise Readiness</h3>
<p>
Navigating compliance requirements, governance frameworks, and security best
practices for production AI systems.
</p>
</div>
<div className={styles.themeCard}>
<div className={styles.cardIcon}>🔮</div>
<h3>Future Threats</h3>
<p>
Anticipating emerging vulnerabilities in multimodal models, agents, and
next-generation AI architectures.
</p>
</div>
</div>
</div>
</section>
{/* Research Highlights */}
<section className={styles.researchSection}>
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>Research Highlights</h2>
</div>
<div className={styles.researchGrid}>
<div className={styles.researchCard}>
<div className={styles.researchNumber}>01</div>
<h3>Automated Red Teaming</h3>
<p>
Demonstrated how open-source tools can systematically discover vulnerabilities in
LLM applications through automated adversarial testing.
</p>
<Link to="/docs/red-team/quickstart/" className={styles.researchLink}>
Try it yourself
</Link>
</div>
<div className={styles.researchCard}>
<div className={styles.researchNumber}>02</div>
<h3>Jailbreak Patterns</h3>
<p>
Analyzed common jailbreak techniques and their effectiveness across different
model providers, revealing gaps in current safety measures.
</p>
<Link to="/docs/red-team/strategies/" className={styles.researchLink}>
View strategies
</Link>
</div>
<div className={styles.researchCard}>
<div className={styles.researchNumber}>03</div>
<h3>Data Exfiltration</h3>
<p>
Showcased novel methods attackers use to extract sensitive information from RAG
systems and enterprise chatbots.
</p>
<Link to="/docs/red-team/plugins/pii/" className={styles.researchLink}>
Explore plugins
</Link>
</div>
</div>
</div>
</section>
{/* Stats */}
<section className={styles.statsSection}>
<div className={styles.container}>
<div className={styles.statsGrid}>
<div className={styles.stat}>
<div className={styles.statNumber}>Oct 2223</div>
<div className={styles.statLabel}>Dates</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>SF</div>
<div className={styles.statLabel}>Westin St. Francis</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>2</div>
<div className={styles.statLabel}>Days</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>1</div>
<div className={styles.statLabel}>Mission</div>
</div>
</div>
</div>
</section>
{/* Final CTA */}
<section className={styles.ctaSection}>
<div className={styles.container}>
<div className={styles.ctaContent}>
<h2 className={styles.ctaTitle}>Secure Your AI</h2>
<p className={styles.ctaText}>
Start red teaming your LLM applications today with Promptfoo's open-source security
testing framework.
</p>
<div className={styles.ctaButtons}>
<Link to="/docs/red-team/quickstart/" className={styles.primaryCta}>
Get Started
</Link>
<Link to="https://github.com/promptfoo/promptfoo" className={styles.secondaryCta}>
Star on GitHub
</Link>
</div>
</div>
</div>
</section>
{/* Footer Navigation */}
<section className={styles.footerNav}>
<div className={styles.container}>
<Link to="/events" className={styles.backLink}>
<svg
className={styles.backIcon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back to All Events
</Link>
</div>
</section>
</main>
</Layout>
);
}
@@ -0,0 +1,661 @@
/* Enable smooth scrolling globally */
html {
scroll-behavior: smooth;
}
.blackhatPage {
overflow-x: hidden;
position: relative;
background:
linear-gradient(45deg, #0a0a0a 0%, #1a0a1a 25%, #0a0a1a 50%, #1a0a0a 75%, #0a0a0a 100%),
radial-gradient(ellipse at top left, rgba(255, 0, 128, 0.15) 0%, transparent 50%),
radial-gradient(ellipse at bottom right, rgba(255, 0, 255, 0.15) 0%, transparent 50%),
radial-gradient(circle at 20% 80%, rgba(0, 255, 255, 0.1) 0%, transparent 30%),
radial-gradient(circle at 80% 20%, rgba(128, 0, 255, 0.1) 0%, transparent 30%),
radial-gradient(circle at 50% 50%, rgba(255, 0, 128, 0.05) 0%, transparent 70%);
background-size:
400% 400%,
100% 100%,
100% 100%,
100% 100%,
100% 100%,
100% 100%;
animation: gradientShift 20s ease infinite;
}
.blackhatPage::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
linear-gradient(
0deg,
transparent 0%,
rgba(10, 10, 10, 0.4) 40%,
rgba(10, 10, 10, 0.4) 60%,
transparent 100%
),
linear-gradient(
90deg,
transparent 0%,
rgba(10, 10, 10, 0.4) 40%,
rgba(10, 10, 10, 0.4) 60%,
transparent 100%
);
pointer-events: none;
z-index: 0;
opacity: 0.5;
animation: scanlines 8s linear infinite;
}
@keyframes gradientShift {
0%,
100% {
background-position:
0% 50%,
0% 0%,
100% 100%,
0% 100%,
100% 0%,
50% 50%;
}
25% {
background-position:
50% 100%,
50% 0%,
50% 100%,
50% 50%,
50% 50%,
50% 50%;
}
50% {
background-position:
100% 50%,
100% 0%,
0% 100%,
100% 0%,
0% 100%,
50% 50%;
}
75% {
background-position:
50% 0%,
50% 100%,
50% 0%,
50% 50%,
50% 50%,
50% 50%;
}
}
@keyframes scanlines {
0% {
transform: translateY(0);
}
100% {
transform: translateY(10px);
}
}
/* Hero Section */
.hero {
position: relative;
min-height: 90vh;
display: flex;
align-items: center;
background: transparent;
overflow: hidden;
z-index: 1;
}
.heroBackground {
position: relative;
width: 100%;
padding: 4rem 2rem;
z-index: 1;
display: flex;
align-items: center;
}
.heroBackground::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
}
.heroContent {
max-width: 1200px;
margin: 0 auto;
text-align: center;
width: 100%;
}
.badge {
display: inline-block;
padding: 0.5rem 1.5rem;
background: rgba(255, 0, 128, 0.1);
border: 1px solid rgba(255, 0, 128, 0.3);
border-radius: 2rem;
color: #ff0080;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 2rem;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
.heroTitle {
font-size: 4rem;
font-weight: 900;
line-height: 1.1;
margin-bottom: 1.5rem;
background: linear-gradient(135deg, #ffffff 0%, #cccccc 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.highlight {
background: linear-gradient(135deg, #ff0080 0%, #ff00ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.heroSubtitle {
font-size: 1.5rem;
color: #999;
max-width: 800px;
margin: 0 auto 3rem;
line-height: 1.6;
}
.heroButtons {
display: flex;
gap: 1.5rem;
justify-content: center;
margin-bottom: 3rem;
flex-wrap: wrap;
}
.primaryButton {
padding: 1rem 2.5rem;
background: linear-gradient(135deg, #ff0080 0%, #ff00ff 100%);
color: white;
text-decoration: none;
border-radius: 0.5rem;
font-weight: 600;
font-size: 1.125rem;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(255, 0, 128, 0.3);
}
.primaryButton:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(255, 0, 128, 0.4);
color: white;
text-decoration: none;
}
.secondaryButton {
padding: 1rem 2.5rem;
background: transparent;
color: white;
text-decoration: none;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 0.5rem;
font-weight: 600;
font-size: 1.125rem;
transition: all 0.3s ease;
}
.secondaryButton:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.5);
color: white;
text-decoration: none;
}
.eventDetails {
display: flex;
gap: 3rem;
justify-content: center;
flex-wrap: wrap;
}
.detail {
display: flex;
align-items: center;
gap: 0.5rem;
color: #999;
font-size: 1.125rem;
}
.icon {
width: 1.5rem;
height: 1.5rem;
color: #ff0080;
}
/* Demo Section - New Kickass Design */
.demoSection {
position: relative;
padding: 8rem 2rem;
background: transparent;
overflow: hidden;
z-index: 1;
}
.demoBackground {
position: relative;
width: 100%;
}
.demoBackground::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(
circle at 50% 50%,
rgba(255, 0, 128, 0.03) 0%,
rgba(255, 0, 255, 0.01) 25%,
transparent 70%
);
animation: rotate 30s linear infinite;
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.demoContainer {
position: relative;
max-width: 1400px;
margin: 0 auto;
z-index: 1;
}
.demoHeader {
text-align: center;
margin-bottom: 5rem;
}
.demoTitle {
font-size: 3.5rem;
font-weight: 900;
margin-bottom: 1rem;
background: linear-gradient(135deg, #ffffff 0%, #ff0080 40%, #ff00ff 60%, #ffffff 100%);
background-size: 200% 200%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: gradient 8s ease infinite;
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.demoSubtitle {
font-size: 1.5rem;
color: #888;
text-transform: uppercase;
letter-spacing: 0.2em;
font-weight: 300;
}
.demoGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 3rem;
perspective: 1000px;
}
.demoCard {
position: relative;
transform-style: preserve-3d;
animation: slideInUp 0.8s ease-out forwards;
opacity: 0;
}
.demoCard[data-demo='1'] {
animation-delay: 0.1s;
}
.demoCard[data-demo='2'] {
animation-delay: 0.2s;
}
.demoCard[data-demo='3'] {
animation-delay: 0.3s;
}
.demoCard[data-demo='4'] {
animation-delay: 0.4s;
}
@keyframes slideInUp {
0% {
opacity: 0;
transform: translateY(50px) rotateX(-10deg);
}
100% {
opacity: 1;
transform: translateY(0) rotateX(0);
}
}
.demoCardInner {
position: relative;
padding: 3rem;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%);
border-radius: 1.5rem;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
backdrop-filter: blur(10px);
height: 100%;
}
.demoCard:hover .demoCardInner {
transform: translateY(-10px) scale(1.02);
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 0, 128, 0.05) 100%);
}
.demoCardBorder {
position: absolute;
inset: -1px;
border-radius: 1.5rem;
padding: 1px;
background: linear-gradient(
135deg,
rgba(255, 0, 128, 0.4),
rgba(255, 0, 255, 0.4),
rgba(0, 255, 255, 0.2)
);
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: exclude;
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
opacity: 0.5;
transition: opacity 0.3s ease;
}
.demoCard:hover .demoCardBorder {
opacity: 1;
}
.demoIconWrapper {
position: relative;
width: 80px;
height: 80px;
margin-bottom: 2rem;
display: flex;
align-items: center;
justify-content: center;
}
.demoIcon {
font-size: 3rem !important;
color: #ff0080;
z-index: 2;
position: relative;
filter: drop-shadow(0 0 20px rgba(255, 0, 128, 0.5));
animation: iconFloat 3s ease-in-out infinite;
}
.demoCard:nth-child(even) .demoIcon {
animation-delay: 1.5s;
}
@keyframes iconFloat {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
.demoIconGlow {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
background: radial-gradient(
circle,
rgba(255, 0, 128, 0.3) 0%,
rgba(255, 0, 255, 0.2) 40%,
transparent 70%
);
transform: translate(-50%, -50%);
animation: pulse 2s ease-in-out infinite;
}
.demoContent h3 {
font-size: 1.75rem;
margin-bottom: 1rem;
color: white;
font-weight: 700;
letter-spacing: -0.02em;
}
.demoContent p {
color: #c0c0c0;
line-height: 1.8;
font-size: 1.125rem;
margin-bottom: 1.5rem;
}
.demoTag {
display: inline-block;
padding: 0.5rem 1rem;
background: linear-gradient(135deg, #ff0080 0%, #ff00ff 100%);
color: white;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.1em;
border-radius: 2rem;
text-transform: uppercase;
box-shadow: 0 4px 15px rgba(255, 0, 128, 0.3);
}
/* Keep the original container class for other sections */
.container {
max-width: 1200px;
margin: 0 auto;
}
.sectionTitle {
font-size: 2.5rem;
font-weight: 700;
text-align: center;
margin-bottom: 3rem;
color: white;
}
/* Calendar Section */
.calendarSection {
padding: 6rem 2rem;
background: transparent;
position: relative;
z-index: 1;
}
.calendarSubtitle {
text-align: center;
font-size: 1.25rem;
color: #999;
max-width: 800px;
margin: 0 auto 3rem;
line-height: 1.6;
}
.calendarWrapper {
max-width: 1000px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 1rem;
padding: 2rem;
min-height: 600px;
}
/* Why Section */
.whySection {
padding: 6rem 2rem;
background: transparent;
position: relative;
z-index: 1;
}
.statsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 3rem;
text-align: center;
}
.stat {
padding: 2rem;
}
.statNumber {
font-size: 3rem;
font-weight: 900;
background: linear-gradient(135deg, #ff0080 0%, #ff00ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
}
.statLabel {
color: #999;
font-size: 1.125rem;
}
/* Final CTA */
.finalCta {
padding: 6rem 2rem;
background: transparent;
text-align: center;
position: relative;
z-index: 1;
}
.finalCta h2 {
font-size: 3rem;
margin-bottom: 1.5rem;
background: linear-gradient(135deg, #ffffff 0%, #cccccc 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.finalCta p {
font-size: 1.25rem;
color: #999;
margin-bottom: 3rem;
}
.ctaButtons {
display: flex;
gap: 1.5rem;
justify-content: center;
flex-wrap: wrap;
}
/* Responsive */
@media (max-width: 768px) {
.heroTitle {
font-size: 2.5rem;
}
.heroSubtitle {
font-size: 1.25rem;
}
.sectionTitle {
font-size: 2rem;
}
.eventDetails {
flex-direction: column;
gap: 1rem;
}
.benefitItem {
flex-direction: column;
text-align: center;
}
.benefitIcon {
margin: 0 auto 1rem;
}
.statsGrid {
grid-template-columns: repeat(2, 1fr);
}
.demoGrid {
grid-template-columns: 1fr;
gap: 2rem;
}
.demoTitle {
font-size: 2.5rem;
}
.demoSubtitle {
font-size: 1.125rem;
}
.demoCardInner {
padding: 2rem;
}
.demoIconWrapper {
width: 60px;
height: 60px;
}
.demoIcon {
font-size: 2.5rem !important;
}
}
+378
View File
@@ -0,0 +1,378 @@
import React, { useEffect } from 'react';
import Cal, { getCalApi } from '@calcom/embed-react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import AssessmentIcon from '@mui/icons-material/Assessment';
import BugReportIcon from '@mui/icons-material/BugReport';
import SecurityIcon from '@mui/icons-material/Security';
import SpeedIcon from '@mui/icons-material/Speed';
import { useForcedTheme } from '@site/src/hooks/useForcedTheme';
import Layout from '@theme/Layout';
import { SITE_CONSTANTS } from '../../constants';
import styles from './blackhat-2025.module.css';
export default function BlackHat2025(): React.ReactElement {
useForcedTheme('dark');
useEffect(() => {
// Cal.com setup
(async function () {
const cal = await getCalApi({ namespace: 'promptfoo-at-blackhat' });
cal('ui', { hideEventTypeDetails: false, layout: 'month_view' });
})();
}, []);
const handleSmoothScroll = (e: React.MouseEvent<HTMLAnchorElement>, targetId: string) => {
e.preventDefault();
const element = document.querySelector(targetId);
if (element) {
const offset = 80; // Offset for fixed header
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - offset;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth',
});
}
};
return (
<Layout
title="Promptfoo at Black Hat USA 2025"
description="Meet Promptfoo at Black Hat USA 2025. Schedule a demo to see how we're revolutionizing AI security testing and red teaming for enterprise LLM applications."
>
<Head>
<meta property="og:title" content="Promptfoo at Black Hat USA 2025 | AI Security" />
<meta
property="og:description"
content="Visit booth #4712 at Black Hat USA 2025. See live demos of AI vulnerability testing, automated red teaming, and LLM security scanning. Schedule your meeting Aug 5-7 in Las Vegas."
/>
<meta
property="og:image"
content="https://www.promptfoo.dev/img/events/blackhat-2025.jpg"
/>
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content="https://www.promptfoo.dev/events/blackhat-2025" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Promptfoo" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Promptfoo at Black Hat USA 2025 | AI Security" />
<meta
name="twitter:description"
content="Visit booth #4712. Live demos of AI vulnerability testing & automated red teaming. Aug 5-7, Las Vegas."
/>
<meta
name="twitter:image"
content="https://www.promptfoo.dev/img/events/blackhat-2025.jpg"
/>
<meta name="twitter:site" content="@promptfoo" />
<meta
name="keywords"
content="Black Hat USA 2025, AI security, LLM security, prompt injection, jailbreaking, red teaming, AI vulnerability testing, OWASP LLM Top 10"
/>
<link rel="canonical" href="https://www.promptfoo.dev/events/blackhat-2025" />
</Head>
<main className={styles.blackhatPage}>
{/* Hero Section */}
<section className={styles.hero}>
<div className={styles.heroBackground}>
<div className={styles.heroContent}>
<div className={styles.badge}>Black Hat USA 2025</div>
<h1 className={styles.heroTitle}>
Security for AI used by
<br />
<span className={styles.highlight}>
{SITE_CONSTANTS.USER_COUNT_DISPLAY} Developers
</span>
</h1>
<p className={styles.heroSubtitle}>
Join us at Black Hat USA to see how Promptfoo helps security teams find and fix LLM
vulnerabilities before attackers do.
</p>
<div className={styles.heroButtons}>
<a
href="#schedule-demo"
className={styles.primaryButton}
onClick={(e) => handleSmoothScroll(e, '#schedule-demo')}
>
Schedule a Demo at Black Hat
</a>
<a
href="#learn-more"
className={styles.secondaryButton}
onClick={(e) => handleSmoothScroll(e, '#learn-more')}
>
Learn More
</a>
</div>
<div className={styles.eventDetails}>
<div className={styles.detail}>
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span>August 5-7, 2025</span>
</div>
<div className={styles.detail}>
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<span>Mandalay Bay, Las Vegas</span>
</div>
<div className={styles.detail}>
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
/>
</svg>
<span>Booth #4712</span>
</div>
</div>
<div className={styles.eventDetails} style={{ marginTop: '1rem' }}>
<div className={styles.detail}>
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
<span style={{ fontWeight: 'bold', color: '#ff6b6b' }}>
<Link
to="https://www.blackhat.com/us-25/arsenal/schedule/index.html#promptfoo-44648"
style={{ color: 'inherit' }}
>
Arsenal Labs - Aug 6
</Link>
{' & '}
<Link
to="https://www.blackhat.com/us-25/arsenal/schedule/#promptfoo-47875"
style={{ color: 'inherit' }}
>
7
</Link>
</span>
</div>
<div className={styles.detail}>
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"
/>
</svg>
<span style={{ fontWeight: 'bold', color: '#ff6b6b' }}>
AI Stage Talk: Aug 6, 1:30 PM
</span>
</div>
</div>
</div>
</div>
</section>
{/* What We'll Show Section */}
<section className={styles.demoSection} id="learn-more">
<div className={styles.demoBackground}>
<div className={styles.demoContainer}>
<div className={styles.demoHeader}>
<h2 className={styles.demoTitle}>Live From Vegas</h2>
</div>
<div className={styles.demoGrid}>
<div className={styles.demoCard} data-demo="1">
<div className={styles.demoCardInner}>
<div className={styles.demoIconWrapper}>
<SecurityIcon className={styles.demoIcon} />
<div className={styles.demoIconGlow} />
</div>
<div className={styles.demoContent}>
<h3>Real-Time Attack Demonstrations</h3>
<p>
Watch our security researchers perform demonstrations of prompt injection,
jailbreaking, and data exfiltration attacks. See how Promptfoo helps you
detect these threats early, reproduce them in testing, and track fixes over
time.
</p>
<div className={styles.demoTag}>LIVE DEMO</div>
</div>
</div>
<div className={styles.demoCardBorder} />
</div>
<div className={styles.demoCard} data-demo="2">
<div className={styles.demoCardInner}>
<div className={styles.demoIconWrapper}>
<BugReportIcon className={styles.demoIcon} />
<div className={styles.demoIconGlow} />
</div>
<div className={styles.demoContent}>
<h3>AI-Powered Red Team Automation</h3>
<p>
LLM-powered red teaming can generate thousands of application-specific
attack variations to uncover vulnerabilities that static scanners miss.
Experience how continuous testing adapts to your unique architecture and use
cases.
</p>
<div className={styles.demoTag}>INTERACTIVE</div>
</div>
</div>
<div className={styles.demoCardBorder} />
</div>
<div className={styles.demoCard} data-demo="3">
<div className={styles.demoCardInner}>
<div className={styles.demoIconWrapper}>
<SpeedIcon className={styles.demoIcon} />
<div className={styles.demoIconGlow} />
</div>
<div className={styles.demoContent}>
<h3>Seamless CI/CD Security Integration</h3>
<p>
See live demos of our GitHub Actions, GitLab CI, and Jenkins integrations.
Learn how Fortune 500 companies automatically catch LLM vulnerabilities in
development before they reach production environments.
</p>
<div className={styles.demoTag}>HANDS-ON</div>
</div>
</div>
<div className={styles.demoCardBorder} />
</div>
<div className={styles.demoCard} data-demo="4">
<div className={styles.demoCardInner}>
<div className={styles.demoIconWrapper}>
<AssessmentIcon className={styles.demoIcon} />
<div className={styles.demoIconGlow} />
</div>
<div className={styles.demoContent}>
<h3>Compliance & Risk Assessment</h3>
<p>
Get automated reports for OWASP LLM Top 10, NIST AI RMF, and EU AI Act
compliance. Our platform provides actionable remediation guidance and tracks
your security posture over time with executive-ready dashboards.
</p>
<div className={styles.demoTag}>SHOWCASE</div>
</div>
</div>
<div className={styles.demoCardBorder} />
</div>
</div>
</div>
</div>
</section>
{/* Calendar Section */}
<section className={styles.calendarSection} id="schedule-demo">
<div className={styles.container}>
<h2 className={styles.sectionTitle}>Meet us at Black Hat</h2>
<p className={styles.calendarSubtitle}>
Book a 30-minute demo slot to see Promptfoo in action. Our security experts will show
you how to find and fix vulnerabilities in your LLM applications.
</p>
<div className={styles.calendarWrapper}>
<Cal
namespace="promptfoo-at-blackhat"
calLink="team/promptfoo/promptfoo-at-blackhat"
style={{ width: '100%', height: '100%', overflow: 'scroll' }}
config={{ layout: 'month_view' }}
/>
</div>
</div>
</section>
{/* Why Meet Us Section */}
<section className={styles.whySection}>
<div className={styles.container}>
<h2 className={styles.sectionTitle}>Why Security Teams Choose Promptfoo</h2>
<div className={styles.statsGrid}>
<div className={styles.stat}>
<div className={styles.statNumber}>{`${SITE_CONSTANTS.USER_COUNT_DISPLAY}+`}</div>
<div className={styles.statLabel}>Developers</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>80+</div>
<div className={styles.statLabel}>Fortune 500 Companies</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>#4712</div>
<div className={styles.statLabel}>AI Zone Pavilion</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>Aug 67</div>
<div className={styles.statLabel}>Business Hall</div>
</div>
</div>
</div>
</section>
{/* Final CTA */}
<section className={styles.finalCta}>
<div className={styles.container}>
<h2>Don't Leave Your AI Vulnerable</h2>
<p>
Join hundreds of security teams who trust Promptfoo to protect their LLM applications.
</p>
<div className={styles.ctaButtons}>
<a
href="#schedule-demo"
className={styles.primaryButton}
onClick={(e) => handleSmoothScroll(e, '#schedule-demo')}
>
Book Your Demo Slot
</a>
<Link to="/security" className={styles.secondaryButton}>
Explore Our Security Platform
</Link>
</div>
</div>
</section>
</main>
</Layout>
);
}
@@ -0,0 +1,554 @@
/* Black Hat 2026 - Cyber Theme */
/* Color Palette: Magenta (#ff0080), purple (#ff00ff), dark */
.blackhatPage {
overflow-x: hidden;
position: relative;
background:
linear-gradient(45deg, #0a0a0a 0%, #1a0a1a 25%, #0a0a1a 50%, #1a0a0a 75%, #0a0a0a 100%),
radial-gradient(ellipse at top left, rgba(255, 0, 128, 0.15) 0%, transparent 50%),
radial-gradient(ellipse at bottom right, rgba(255, 0, 255, 0.15) 0%, transparent 50%),
radial-gradient(circle at 20% 80%, rgba(0, 255, 255, 0.1) 0%, transparent 30%),
radial-gradient(circle at 80% 20%, rgba(128, 0, 255, 0.1) 0%, transparent 30%);
background-size:
400% 400%,
100% 100%,
100% 100%,
100% 100%,
100% 100%;
animation: gradientShift 20s ease infinite;
}
@keyframes gradientShift {
0%,
100% {
background-position:
0% 50%,
0% 0%,
100% 100%,
0% 100%,
100% 0%;
}
50% {
background-position:
100% 50%,
100% 0%,
0% 100%,
100% 0%,
0% 100%;
}
}
/* Hero Section */
.hero {
position: relative;
min-height: 90vh;
display: flex;
align-items: center;
background: transparent;
overflow: hidden;
z-index: 1;
}
.heroBackground {
position: relative;
width: 100%;
padding: 4rem 2rem;
z-index: 1;
display: flex;
align-items: center;
}
.heroContent {
max-width: 1200px;
margin: 0 auto;
text-align: center;
width: 100%;
}
.badge {
display: inline-block;
padding: 0.5rem 1.5rem;
background: rgba(255, 0, 128, 0.1);
border: 1px solid rgba(255, 0, 128, 0.3);
border-radius: 2rem;
color: #ff0080;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 2rem;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
.heroTitle {
font-size: 4rem;
font-weight: 900;
line-height: 1.1;
margin-bottom: 1.5rem;
background: linear-gradient(135deg, #ffffff 0%, #cccccc 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.highlight {
background: linear-gradient(135deg, #ff0080 0%, #ff00ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.heroSubtitle {
font-size: 1.5rem;
color: #999;
max-width: 800px;
margin: 0 auto 3rem;
line-height: 1.6;
}
.heroButtons {
display: flex;
gap: 1.5rem;
justify-content: center;
margin-bottom: 3rem;
flex-wrap: wrap;
}
.primaryButton {
padding: 1rem 2.5rem;
background: linear-gradient(135deg, #ff0080 0%, #ff00ff 100%);
color: white;
text-decoration: none;
border-radius: 0.5rem;
font-weight: 600;
font-size: 1.125rem;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(255, 0, 128, 0.3);
}
.primaryButton:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(255, 0, 128, 0.4);
color: white;
text-decoration: none;
}
.secondaryButton {
padding: 1rem 2.5rem;
background: transparent;
color: white;
text-decoration: none;
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 0.5rem;
font-weight: 600;
font-size: 1.125rem;
transition: all 0.3s ease;
}
.secondaryButton:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.5);
color: white;
text-decoration: none;
}
.eventDetails {
display: flex;
gap: 3rem;
justify-content: center;
flex-wrap: wrap;
}
.detail {
display: flex;
align-items: center;
gap: 0.5rem;
color: #999;
font-size: 1.125rem;
}
.icon {
width: 1.5rem;
height: 1.5rem;
color: #ff0080;
}
/* Demo Section */
.demoSection {
position: relative;
padding: 8rem 2rem;
background: transparent;
overflow: hidden;
z-index: 1;
}
.demoBackground {
position: relative;
width: 100%;
}
.demoContainer {
position: relative;
max-width: 1400px;
margin: 0 auto;
z-index: 1;
}
.demoHeader {
text-align: center;
margin-bottom: 5rem;
}
.demoTitle {
font-size: 3.5rem;
font-weight: 900;
margin-bottom: 1rem;
background: linear-gradient(135deg, #ffffff 0%, #ff0080 40%, #ff00ff 60%, #ffffff 100%);
background-size: 200% 200%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: gradient 8s ease infinite;
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.demoGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 3rem;
perspective: 1000px;
}
.demoCard {
position: relative;
transform-style: preserve-3d;
animation: slideInUp 0.8s ease-out forwards;
opacity: 0;
}
.demoCard[data-demo='1'] {
animation-delay: 0.1s;
}
.demoCard[data-demo='2'] {
animation-delay: 0.2s;
}
.demoCard[data-demo='3'] {
animation-delay: 0.3s;
}
.demoCard[data-demo='4'] {
animation-delay: 0.4s;
}
@keyframes slideInUp {
0% {
opacity: 0;
transform: translateY(50px) rotateX(-10deg);
}
100% {
opacity: 1;
transform: translateY(0) rotateX(0);
}
}
.demoCardInner {
position: relative;
padding: 3rem;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%);
border-radius: 1.5rem;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
backdrop-filter: blur(10px);
height: 100%;
}
.demoCard:hover .demoCardInner {
transform: translateY(-10px) scale(1.02);
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 0, 128, 0.05) 100%);
}
.demoCardBorder {
position: absolute;
inset: -1px;
border-radius: 1.5rem;
padding: 1px;
background: linear-gradient(
135deg,
rgba(255, 0, 128, 0.4),
rgba(255, 0, 255, 0.4),
rgba(0, 255, 255, 0.2)
);
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: exclude;
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
opacity: 0.5;
transition: opacity 0.3s ease;
}
.demoCard:hover .demoCardBorder {
opacity: 1;
}
.demoIconWrapper {
position: relative;
width: 80px;
height: 80px;
margin-bottom: 2rem;
display: flex;
align-items: center;
justify-content: center;
}
.demoIcon {
font-size: 3rem !important;
color: #ff0080;
z-index: 2;
position: relative;
filter: drop-shadow(0 0 20px rgba(255, 0, 128, 0.5));
animation: iconFloat 3s ease-in-out infinite;
}
@keyframes iconFloat {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
.demoIconGlow {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
background: radial-gradient(
circle,
rgba(255, 0, 128, 0.3) 0%,
rgba(255, 0, 255, 0.2) 40%,
transparent 70%
);
transform: translate(-50%, -50%);
animation: pulse 2s ease-in-out infinite;
}
.demoContent h3 {
font-size: 1.75rem;
margin-bottom: 1rem;
color: white;
font-weight: 700;
}
.demoContent p {
color: #c0c0c0;
line-height: 1.8;
font-size: 1.125rem;
margin-bottom: 1.5rem;
}
.demoTag {
display: inline-block;
padding: 0.5rem 1rem;
background: linear-gradient(135deg, #ff0080 0%, #ff00ff 100%);
color: white;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.1em;
border-radius: 2rem;
text-transform: uppercase;
box-shadow: 0 4px 15px rgba(255, 0, 128, 0.3);
}
/* Container */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
}
.sectionTitle {
font-size: 2.5rem;
font-weight: 700;
text-align: center;
margin-bottom: 3rem;
color: white;
}
/* Why Section */
.whySection {
padding: 6rem 2rem;
background: transparent;
position: relative;
z-index: 1;
}
.statsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 3rem;
text-align: center;
}
.stat {
padding: 2rem;
}
.statNumber {
font-size: 3rem;
font-weight: 900;
background: linear-gradient(135deg, #ff0080 0%, #ff00ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.5rem;
}
.statLabel {
color: #999;
font-size: 1.125rem;
}
/* Final CTA */
.finalCta {
padding: 6rem 2rem;
background: transparent;
text-align: center;
position: relative;
z-index: 1;
}
.finalCta h2 {
font-size: 3rem;
margin-bottom: 1.5rem;
background: linear-gradient(135deg, #ffffff 0%, #cccccc 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.finalCta p {
font-size: 1.25rem;
color: #999;
margin-bottom: 3rem;
}
.ctaButtons {
display: flex;
gap: 1.5rem;
justify-content: center;
flex-wrap: wrap;
}
/* Footer Navigation */
.footerNav {
padding: 2rem 0 3rem;
border-top: 1px solid rgba(255, 0, 128, 0.1);
}
.backLink {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: #999;
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
.backLink:hover {
color: #ff0080;
}
.backIcon {
width: 1.25rem;
height: 1.25rem;
}
/* Responsive */
@media (max-width: 768px) {
.heroTitle {
font-size: 2.5rem;
}
.heroSubtitle {
font-size: 1.25rem;
}
.sectionTitle {
font-size: 2rem;
}
.eventDetails {
flex-direction: column;
gap: 1rem;
}
.statsGrid {
grid-template-columns: repeat(2, 1fr);
}
.demoGrid {
grid-template-columns: 1fr;
gap: 2rem;
}
.demoTitle {
font-size: 2.5rem;
}
.demoCardInner {
padding: 2rem;
}
.demoIconWrapper {
width: 60px;
height: 60px;
}
.demoIcon {
font-size: 2.5rem !important;
}
.finalCta h2 {
font-size: 2rem;
}
.ctaButtons {
flex-direction: column;
align-items: center;
}
.primaryButton,
.secondaryButton {
width: 100%;
max-width: 280px;
text-align: center;
}
}
+257
View File
@@ -0,0 +1,257 @@
import React from 'react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import BugReportIcon from '@mui/icons-material/BugReport';
import SecurityIcon from '@mui/icons-material/Security';
import SpeedIcon from '@mui/icons-material/Speed';
import { useForcedTheme } from '@site/src/hooks/useForcedTheme';
import Layout from '@theme/Layout';
import { SITE_CONSTANTS } from '../../constants';
import styles from './blackhat-2026.module.css';
export default function BlackHat2026(): React.ReactElement {
useForcedTheme('dark');
const handleSmoothScroll = (e: React.MouseEvent<HTMLAnchorElement>, targetId: string) => {
e.preventDefault();
const element = document.querySelector(targetId);
if (element) {
const offset = 80;
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.scrollY - offset;
window.scrollTo({ top: offsetPosition, behavior: 'smooth' });
}
};
return (
<Layout
title="Promptfoo at Black Hat USA 2026"
description="This is the biggest stage in offensive security. Meet Promptfoo at Black Hat for live AI attacks, automated red teaming, and practical defenses."
>
<Head>
<meta property="og:title" content="Promptfoo at Black Hat USA 2026 | AI Security" />
<meta
property="og:description"
content="This is the biggest stage in offensive security. Meet Promptfoo at Black Hat for live AI attacks, automated red teaming, and practical defenses. Aug 1-6, Las Vegas."
/>
<meta
property="og:image"
content="https://www.promptfoo.dev/img/events/blackhat-2026.jpg"
/>
<meta property="og:url" content="https://www.promptfoo.dev/events/blackhat-2026" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Promptfoo at Black Hat USA 2026 | AI Security" />
<meta
name="twitter:description"
content="Live demos of AI vulnerability testing & automated red teaming. Aug 1-6, Las Vegas."
/>
<meta
name="twitter:image"
content="https://www.promptfoo.dev/img/events/blackhat-2026.jpg"
/>
<meta
name="keywords"
content="Black Hat USA 2026, AI security, LLM security, prompt injection, jailbreaking, red teaming, AI vulnerability testing, OWASP LLM Top 10"
/>
<link rel="canonical" href="https://www.promptfoo.dev/events/blackhat-2026" />
</Head>
<main className={styles.blackhatPage}>
{/* Hero Section */}
<section className={styles.hero}>
<div className={styles.heroBackground}>
<div className={styles.heroContent}>
<div className={styles.badge}>Black Hat USA 2026</div>
<h1 className={styles.heroTitle}>
Break Your AI
<br />
<span className={styles.highlight}>Before Attackers Do</span>
</h1>
<p className={styles.heroSubtitle}>
This is the biggest stage in offensive security. Meet Promptfoo at Black Hat to see
live AI attacks, automated red teaming workflows, and practical defenses you can
deploy now.
</p>
<div className={styles.heroButtons}>
<a
href="#learn-more"
className={styles.primaryButton}
onClick={(e) => handleSmoothScroll(e, '#learn-more')}
>
Learn More
</a>
<Link to="/contact" className={styles.secondaryButton}>
Schedule a Meeting
</Link>
</div>
<div className={styles.eventDetails}>
<div className={styles.detail}>
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span>August 1-6, 2026</span>
</div>
<div className={styles.detail}>
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<span>Mandalay Bay, Las Vegas</span>
</div>
</div>
</div>
</div>
</section>
{/* What to Expect Section */}
<section className={styles.demoSection} id="learn-more">
<div className={styles.demoBackground}>
<div className={styles.demoContainer}>
<div className={styles.demoHeader}>
<h2 className={styles.demoTitle}>What to Expect</h2>
</div>
<div className={styles.demoGrid}>
<div className={styles.demoCard} data-demo="1">
<div className={styles.demoCardInner}>
<div className={styles.demoIconWrapper}>
<SecurityIcon className={styles.demoIcon} />
<div className={styles.demoIconGlow} />
</div>
<div className={styles.demoContent}>
<h3>Live AI Attack Demos</h3>
<p>
Prompt injection, jailbreaks, data exfiltration, and tool-use exploits
against real-world applications.
</p>
<div className={styles.demoTag}>LIVE DEMO</div>
</div>
</div>
<div className={styles.demoCardBorder} />
</div>
<div className={styles.demoCard} data-demo="2">
<div className={styles.demoCardInner}>
<div className={styles.demoIconWrapper}>
<BugReportIcon className={styles.demoIcon} />
<div className={styles.demoIconGlow} />
</div>
<div className={styles.demoContent}>
<h3>Automated Red Teaming</h3>
<p>
See how teams scale probing across models, prompts, and releases with zero
manual effort.
</p>
<div className={styles.demoTag}>INTERACTIVE</div>
</div>
</div>
<div className={styles.demoCardBorder} />
</div>
<div className={styles.demoCard} data-demo="3">
<div className={styles.demoCardInner}>
<div className={styles.demoIconWrapper}>
<SpeedIcon className={styles.demoIcon} />
<div className={styles.demoIconGlow} />
</div>
<div className={styles.demoContent}>
<h3>Defenses That Ship</h3>
<p>
Guardrails, detection rules, and remediation guidance you can take home.
</p>
<div className={styles.demoTag}>HANDS-ON</div>
</div>
</div>
<div className={styles.demoCardBorder} />
</div>
</div>
</div>
</div>
</section>
{/* Why Section */}
<section className={styles.whySection}>
<div className={styles.container}>
<h2 className={styles.sectionTitle}>Why Security Teams Choose Promptfoo</h2>
<div className={styles.statsGrid}>
<div className={styles.stat}>
<div className={styles.statNumber}>{SITE_CONSTANTS.USER_COUNT_DISPLAY}+</div>
<div className={styles.statLabel}>Developers</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>80+</div>
<div className={styles.statLabel}>Fortune 500 Companies</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>Aug 1-6</div>
<div className={styles.statLabel}>Las Vegas</div>
</div>
</div>
</div>
</section>
{/* Final CTA */}
<section className={styles.finalCta}>
<div className={styles.container}>
<h2>Attending Black Hat?</h2>
<p>Schedule a meeting to discuss your use case and see a tailored demo.</p>
<div className={styles.ctaButtons}>
<Link to="/contact" className={styles.primaryButton}>
Schedule a Meeting
</Link>
<Link to="https://discord.gg/promptfoo" className={styles.secondaryButton}>
Join our Discord
</Link>
</div>
</div>
</section>
{/* Footer Navigation */}
<section className={styles.footerNav}>
<div className={styles.container}>
<Link to="/events" className={styles.backLink}>
<svg
className={styles.backIcon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back to All Events
</Link>
</div>
</section>
</main>
</Layout>
);
}
@@ -0,0 +1,411 @@
/* BSides Seattle 2025 - PNW Theme */
/* Color Palette: Forest green (#228B22), slate gray (#475569), coffee brown (#6F4E37) */
.bsidesPage {
background: #0f172a;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
}
/* Hero Banner */
.heroBanner {
position: relative;
height: 400px;
overflow: hidden;
}
.bannerImage {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.bannerOverlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(180deg, rgba(15, 23, 42, 0.3) 0%, rgba(15, 23, 42, 0.7) 100%);
}
.bannerContent {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 3rem;
background: linear-gradient(0deg, rgba(15, 23, 42, 0.95) 0%, transparent 100%);
}
/* Rain Effect */
.rainContainer {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
overflow: hidden;
z-index: 1;
}
.raindrop {
position: absolute;
top: -20px;
width: 2px;
height: 15px;
background: linear-gradient(transparent, rgba(148, 163, 184, 0.6));
animation: rain linear infinite;
}
@keyframes rain {
to {
transform: translateY(420px);
}
}
.badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(34, 139, 34, 0.2);
border: 1px solid rgba(34, 139, 34, 0.4);
color: #86efac;
border-radius: 100px;
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 1rem;
}
.badgeIcon {
font-size: 1rem;
}
.heroTitle {
font-size: clamp(2rem, 6vw, 3.5rem);
font-weight: 800;
line-height: 1.1;
color: #f1f5f9;
margin: 0;
}
.highlight {
background: linear-gradient(135deg, #6f4e37 0%, #8b7355 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Hero Content Section */
.heroContent {
padding: 3rem 0;
text-align: center;
border-bottom: 1px solid rgba(34, 139, 34, 0.1);
}
.heroSubtitle {
font-size: 1.25rem;
color: #94a3b8;
max-width: 600px;
margin: 0 auto 2rem;
line-height: 1.7;
}
.eventDetails {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2rem;
margin-bottom: 2rem;
}
.detail {
display: flex;
align-items: center;
gap: 0.5rem;
color: #cbd5e1;
font-size: 0.95rem;
}
.icon {
width: 1.25rem;
height: 1.25rem;
stroke-width: 2;
color: #22c55e;
}
.heroCtas {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.primaryCta {
padding: 0.875rem 2rem;
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
color: white;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
transition: all 0.3s ease;
border: none;
cursor: pointer;
}
.primaryCta:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(34, 197, 94, 0.4);
color: white;
}
.secondaryCta {
padding: 0.875rem 2rem;
background: transparent;
color: #86efac;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
border: 1px solid rgba(34, 139, 34, 0.4);
transition: all 0.3s ease;
cursor: pointer;
}
.secondaryCta:hover {
background: rgba(34, 139, 34, 0.1);
color: white;
}
/* Sections */
.spiritSection,
.sharedSection,
.statsSection,
.ctaSection {
padding: 4rem 0;
}
.spiritSection {
background: rgba(34, 139, 34, 0.02);
}
.sectionHeader {
text-align: center;
margin-bottom: 3rem;
}
.sectionTitle {
font-size: 2rem;
font-weight: 700;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.sectionSubtitle {
font-size: 1.1rem;
color: #94a3b8;
max-width: 600px;
margin: 0 auto;
}
/* Spirit/Shared Grid */
.spiritGrid,
.sharedGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
.spiritCard,
.sharedCard {
background: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(34, 139, 34, 0.15);
border-radius: 12px;
padding: 1.5rem;
transition: all 0.3s ease;
}
.spiritCard:hover,
.sharedCard:hover {
border-color: rgba(34, 139, 34, 0.3);
transform: translateY(-2px);
}
.cardIcon {
font-size: 2rem;
margin-bottom: 1rem;
}
.spiritCard h3,
.sharedCard h3 {
font-size: 1.125rem;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 0.5rem;
}
.spiritCard p,
.sharedCard p {
font-size: 0.9rem;
color: #94a3b8;
line-height: 1.6;
margin: 0;
}
/* Stats Section */
.statsSection {
background: rgba(34, 139, 34, 0.05);
}
.statsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 2rem;
text-align: center;
}
.stat {
padding: 1.5rem;
}
.statNumber {
font-size: 2.5rem;
font-weight: 800;
background: linear-gradient(135deg, #22c55e 0%, #6f4e37 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1;
margin-bottom: 0.5rem;
}
.statLabel {
font-size: 0.9rem;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* CTA Section */
.ctaContent {
max-width: 600px;
margin: 0 auto;
text-align: center;
background: linear-gradient(135deg, rgba(34, 139, 34, 0.08) 0%, rgba(111, 78, 55, 0.08) 100%);
border: 1px solid rgba(34, 139, 34, 0.2);
border-radius: 16px;
padding: 3rem 2rem;
}
.ctaTitle {
font-size: 1.75rem;
font-weight: 700;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.ctaText {
font-size: 1rem;
color: #94a3b8;
margin-bottom: 1.5rem;
line-height: 1.7;
}
.ctaButtons {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
/* Footer Navigation */
.footerNav {
padding: 2rem 0 3rem;
border-top: 1px solid rgba(34, 139, 34, 0.1);
}
.backLink {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: #94a3b8;
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
.backLink:hover {
color: #22c55e;
}
.backIcon {
width: 1.25rem;
height: 1.25rem;
}
/* Reduced Motion */
@media (prefers-reduced-motion: reduce) {
.raindrop {
animation: none;
}
}
/* Responsive */
@media (max-width: 768px) {
.heroBanner {
height: 300px;
}
.bannerContent {
padding: 2rem 1.5rem;
}
.heroTitle {
font-size: 2rem;
}
.heroSubtitle {
font-size: 1.1rem;
}
.eventDetails {
flex-direction: column;
align-items: center;
gap: 1rem;
}
.heroCtas {
flex-direction: column;
align-items: center;
}
.primaryCta,
.secondaryCta {
width: 100%;
max-width: 280px;
text-align: center;
}
.ctaContent {
padding: 2rem 1.5rem;
}
.ctaButtons {
flex-direction: column;
align-items: center;
}
.spiritSection,
.sharedSection,
.statsSection,
.ctaSection {
padding: 3rem 0;
}
}
@@ -0,0 +1,291 @@
import React, { useMemo, useState } from 'react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import { useForcedTheme } from '@site/src/hooks/useForcedTheme';
import Layout from '@theme/Layout';
import styles from './bsides-seattle-2025.module.css';
export default function BSidesSeattle2025(): React.ReactElement {
const [rainEnabled, setRainEnabled] = useState(true);
const raindrops = useMemo(
() =>
[...Array(30)].map(() => ({
left: `${Math.random() * 100}%`,
animationDuration: `${0.5 + Math.random() * 0.5}s`,
animationDelay: `${Math.random() * 2}s`,
})),
[],
);
useForcedTheme('dark');
const handleSmoothScroll = (e: React.MouseEvent<HTMLAnchorElement>, targetId: string) => {
e.preventDefault();
const element = document.querySelector(targetId);
if (element) {
const offset = 80;
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - offset;
window.scrollTo({ top: offsetPosition, behavior: 'smooth' });
}
};
return (
<Layout
title="Promptfoo at BSides Seattle 2025"
description="Promptfoo sponsored BSides Seattle 2025 and joined the PNW security community in Redmond."
>
<Head>
<meta property="og:title" content="Promptfoo at BSides Seattle 2025" />
<meta
property="og:description"
content="Recap of Promptfoo at BSides Seattle 2025. Community-driven security, AI red teaming demos, and PNW vibes."
/>
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.promptfoo.dev/events/bsides-seattle-2025" />
<meta name="twitter:card" content="summary_large_image" />
<meta
property="og:image"
content="https://www.promptfoo.dev/img/events/bsides-seattle-2025.jpg"
/>
<meta
name="twitter:image"
content="https://www.promptfoo.dev/img/events/bsides-seattle-2025.jpg"
/>
<meta
name="keywords"
content="BSides Seattle 2025, security conference, AI security, LLM security, Pacific Northwest, Seattle"
/>
<link rel="canonical" href="https://www.promptfoo.dev/events/bsides-seattle-2025" />
</Head>
<main className={styles.bsidesPage}>
{/* Hero Banner */}
<section className={styles.heroBanner}>
<img
src="/img/events/bsides-seattle-2025.jpg"
alt="BSides Seattle 2025"
className={styles.bannerImage}
/>
<div className={styles.bannerOverlay} />
{rainEnabled && (
<div className={styles.rainContainer}>
{raindrops.map((drop, i) => (
<div key={i} className={styles.raindrop} style={drop} />
))}
</div>
)}
<div className={styles.bannerContent}>
<div className={styles.badge}>
<span className={styles.badgeIcon}></span>
BSides Seattle 2025
</div>
<h1 className={styles.heroTitle}>
Security, Community, <span className={styles.highlight}>Coffee</span>
</h1>
</div>
</section>
{/* Hero Content */}
<section className={styles.heroContent}>
<div className={styles.container}>
<p className={styles.heroSubtitle}>
We sponsored and joined the Pacific Northwest security community for two days of
hands-on learning, great conversations, and way too much caffeine.
</p>
<div className={styles.eventDetails}>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span>April 18-19, 2025</span>
</div>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<span>Building 92, Redmond, WA</span>
</div>
</div>
<div className={styles.heroCtas}>
<a
href="#recap"
className={styles.primaryCta}
onClick={(e) => handleSmoothScroll(e, '#recap')}
>
See the Recap
</a>
<button
type="button"
className={styles.secondaryCta}
onClick={() => setRainEnabled(!rainEnabled)}
>
{rainEnabled ? '🌧️ Seattle Mode' : '☀️ Sunny (rare)'}
</button>
</div>
</div>
</section>
{/* The BSides Spirit Section */}
<section className={styles.spiritSection} id="recap">
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>The BSides Spirit</h2>
<p className={styles.sectionSubtitle}>
BSides conferences are the heart of the security community. No corporate polish,
just passionate people sharing knowledge.
</p>
</div>
<div className={styles.spiritGrid}>
<div className={styles.spiritCard}>
<div className={styles.cardIcon}>🌲</div>
<h3>PNW Roots</h3>
<p>
Seattle's security community is tight-knit and welcoming. From Boeing to Amazon to
countless startups, the talent here is incredible.
</p>
</div>
<div className={styles.spiritCard}>
<div className={styles.cardIcon}>🤝</div>
<h3>Community First</h3>
<p>
BSides isn't about vendor pitches. It's about sharing real knowledge, making
connections, and helping each other level up.
</p>
</div>
<div className={styles.spiritCard}>
<div className={styles.cardIcon}>☕</div>
<h3>Fueled by Coffee</h3>
<p>
This is Seattle. The coffee flows freely, the conversations go deep, and nobody
judges your third (or fourth) cup.
</p>
</div>
</div>
</div>
</section>
{/* What We Shared Section */}
<section className={styles.sharedSection}>
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>What We Talked About</h2>
</div>
<div className={styles.sharedGrid}>
<div className={styles.sharedCard}>
<div className={styles.cardIcon}>🎯</div>
<h3>AI Red Teaming Walkthroughs</h3>
<p>
Quick walkthroughs and examples showing how to find vulnerabilities in LLM
applications using open source tools.
</p>
</div>
<div className={styles.sharedCard}>
<div className={styles.cardIcon}>🔓</div>
<h3>Jailbreaking Demos</h3>
<p>
Live demonstrations of prompt injection, jailbreaking, and data exfiltration
attacks against popular LLM providers.
</p>
</div>
<div className={styles.sharedCard}>
<div className={styles.cardIcon}>💬</div>
<h3>Hallway Conversations</h3>
<p>
Some of the best moments at BSides happen between sessions. We talked AI security
challenges with teams from across the PNW.
</p>
</div>
</div>
</div>
</section>
{/* Stats Section */}
<section className={styles.statsSection}>
<div className={styles.container}>
<div className={styles.statsGrid}>
<div className={styles.stat}>
<div className={styles.statNumber}>400+</div>
<div className={styles.statLabel}>Attendees</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>∞</div>
<div className={styles.statLabel}>Coffee Consumed</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>50+</div>
<div className={styles.statLabel}>Conversations</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>0</div>
<div className={styles.statLabel}>Sunny Days</div>
</div>
</div>
</div>
</section>
{/* Final CTA */}
<section className={styles.ctaSection}>
<div className={styles.container}>
<div className={styles.ctaContent}>
<h2 className={styles.ctaTitle}>Join the Community</h2>
<p className={styles.ctaText}>
Whether you're in Seattle or anywhere else, the AI security community is growing.
Let's connect.
</p>
<div className={styles.ctaButtons}>
<Link to="https://discord.gg/promptfoo" className={styles.primaryCta}>
Join Discord
</Link>
<Link to="https://github.com/promptfoo/promptfoo" className={styles.secondaryCta}>
Star on GitHub
</Link>
</div>
</div>
</div>
</section>
{/* Footer Navigation */}
<section className={styles.footerNav}>
<div className={styles.container}>
<Link to="/events" className={styles.backLink}>
<svg
className={styles.backIcon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back to All Events
</Link>
</div>
</section>
</main>
</Layout>
);
}
@@ -0,0 +1,578 @@
/* BSides Seattle 2026 - PNW Theme */
/* Color Palette: Forest green (#228B22), evergreen (#2d5a3d), coffee brown (#6F4E37), moss (#8fbc8f) */
.bsidesPage {
background: #0c1419;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
}
/* Hero Banner */
.heroBanner {
position: relative;
height: 600px;
overflow: hidden;
}
.bannerImage {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: contain;
object-position: center top;
background: #1a3d2e;
}
.bannerOverlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
180deg,
rgba(12, 20, 25, 0.2) 0%,
rgba(12, 20, 25, 0.4) 50%,
rgba(12, 20, 25, 0.9) 100%
);
}
.bannerContent {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 3rem;
background: linear-gradient(0deg, rgba(12, 20, 25, 0.98) 0%, transparent 100%);
z-index: 2;
}
/* Rain Effect */
.rainContainer {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
overflow: hidden;
z-index: 1;
}
.raindrop {
position: absolute;
top: -20px;
width: 2px;
height: 18px;
background: linear-gradient(transparent, rgba(180, 200, 210, 0.5));
animation: rain linear infinite;
}
@keyframes rain {
to {
transform: translateY(500px);
}
}
.badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(111, 78, 55, 0.25);
border: 1px solid rgba(111, 78, 55, 0.5);
color: #d4a574;
border-radius: 100px;
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 1rem;
}
.badgeIcon {
font-size: 1rem;
}
.heroTitle {
font-size: clamp(2rem, 6vw, 3.5rem);
font-weight: 800;
line-height: 1.1;
color: #f1f5f9;
margin: 0;
}
.highlight {
background: linear-gradient(135deg, #8fbc8f 0%, #6f8f6f 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Hero Content Section */
.heroContent {
position: relative;
padding: 3rem 0;
text-align: center;
border-bottom: 1px solid rgba(45, 90, 61, 0.2);
}
.heroSubtitle {
font-size: 1.25rem;
color: #94a3b8;
max-width: 600px;
margin: 0 auto 2rem;
line-height: 1.7;
}
/* Countdown Timer */
.countdown {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.countdownItem {
display: flex;
flex-direction: column;
align-items: center;
background: linear-gradient(135deg, rgba(45, 90, 61, 0.2) 0%, rgba(111, 78, 55, 0.15) 100%);
border: 1px solid rgba(45, 90, 61, 0.35);
border-radius: 12px;
padding: 1rem 1.25rem;
min-width: 80px;
}
.countdownNumber {
font-size: 2rem;
font-weight: 700;
color: #8fbc8f;
font-variant-numeric: tabular-nums;
line-height: 1;
}
.countdownLabel {
font-size: 0.75rem;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-top: 0.25rem;
}
.countdownSeparator {
font-size: 2rem;
font-weight: 700;
color: #228b22;
animation: blink 1s step-end infinite;
}
@keyframes blink {
50% {
opacity: 0;
}
}
.countdownExpired {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 2rem;
padding: 1rem 2rem;
background: linear-gradient(135deg, rgba(45, 90, 61, 0.25) 0%, rgba(111, 78, 55, 0.2) 100%);
border: 1px solid rgba(45, 90, 61, 0.4);
border-radius: 12px;
color: #8fbc8f;
font-size: 1.25rem;
font-weight: 600;
}
.eventDetails {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2rem;
margin-bottom: 2rem;
}
.detail {
display: flex;
align-items: center;
gap: 0.5rem;
color: #cbd5e1;
font-size: 0.95rem;
}
.icon {
width: 1.25rem;
height: 1.25rem;
stroke-width: 2;
color: #6f8f6f;
}
.heroCtas {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.primaryCta {
padding: 0.875rem 2rem;
background: linear-gradient(135deg, #2d5a3d 0%, #1e4d2b 100%);
color: white;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
transition: all 0.3s ease;
border: none;
cursor: pointer;
}
.primaryCta:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(45, 90, 61, 0.4);
color: white;
}
.secondaryCta {
padding: 0.875rem 2rem;
background: transparent;
color: #8fbc8f;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
border: 1px solid rgba(45, 90, 61, 0.5);
transition: all 0.3s ease;
cursor: pointer;
}
.secondaryCta:hover {
background: rgba(45, 90, 61, 0.15);
color: white;
}
.weatherToggle {
position: absolute;
bottom: 1rem;
right: 1.5rem;
background: rgba(12, 20, 25, 0.7);
border: 1px solid rgba(148, 163, 184, 0.2);
border-radius: 50%;
width: 2.5rem;
height: 2.5rem;
font-size: 1.25rem;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.weatherToggle:hover {
background: rgba(12, 20, 25, 0.9);
border-color: rgba(143, 188, 143, 0.4);
transform: scale(1.1);
}
/* Card Link */
.cardLink {
display: inline-block;
margin-top: 1rem;
color: #8fbc8f;
font-weight: 500;
text-decoration: none;
transition: color 0.2s ease;
}
.cardLink:hover {
color: #a8d4a8;
}
/* Sections */
.spiritSection,
.sharedSection,
.statsSection,
.ctaSection {
padding: 4rem 0;
}
.spiritSection {
background: rgba(45, 90, 61, 0.03);
}
.sectionHeader {
text-align: center;
margin-bottom: 3rem;
}
.sectionTitle {
font-size: 2rem;
font-weight: 700;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.sectionSubtitle {
font-size: 1.1rem;
color: #94a3b8;
max-width: 600px;
margin: 0 auto;
}
/* Spirit/Shared Grid */
.spiritGrid,
.sharedGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
.spiritCard,
.sharedCard {
background: rgba(12, 20, 25, 0.6);
border: 1px solid rgba(45, 90, 61, 0.2);
border-radius: 12px;
padding: 1.5rem;
transition: all 0.3s ease;
text-decoration: none;
display: block;
}
.spiritCard:hover,
.sharedCard:hover {
border-color: rgba(45, 90, 61, 0.4);
transform: translateY(-2px);
background: rgba(12, 20, 25, 0.8);
}
.cardIcon {
font-size: 2rem;
margin-bottom: 1rem;
}
.cardIconSvg {
width: 2.5rem;
height: 2.5rem;
margin-bottom: 1rem;
color: #6f8f6f;
}
.cardIconSvg svg {
width: 100%;
height: 100%;
}
.spiritCard h3,
.sharedCard h3 {
font-size: 1.125rem;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 0.5rem;
}
.spiritCard p,
.sharedCard p {
font-size: 0.9rem;
color: #94a3b8;
line-height: 1.6;
margin: 0;
}
/* Stats Section */
.statsSection {
background: linear-gradient(135deg, rgba(45, 90, 61, 0.06) 0%, rgba(111, 78, 55, 0.04) 100%);
}
.statsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 2rem;
text-align: center;
}
.stat {
padding: 1.5rem;
}
.statNumber {
font-size: 2.5rem;
font-weight: 800;
background: linear-gradient(135deg, #8fbc8f 0%, #6f4e37 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1;
margin-bottom: 0.5rem;
}
.statNumberText {
font-size: 1.75rem;
font-weight: 700;
background: linear-gradient(135deg, #8fbc8f 0%, #6f4e37 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1.2;
margin-bottom: 0.5rem;
}
.statLabel {
font-size: 0.9rem;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* CTA Section */
.ctaContent {
max-width: 600px;
margin: 0 auto;
text-align: center;
background: linear-gradient(135deg, rgba(45, 90, 61, 0.1) 0%, rgba(111, 78, 55, 0.08) 100%);
border: 1px solid rgba(45, 90, 61, 0.25);
border-radius: 16px;
padding: 3rem 2rem;
}
.ctaTitle {
font-size: 1.75rem;
font-weight: 700;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.ctaText {
font-size: 1rem;
color: #94a3b8;
margin-bottom: 1.5rem;
line-height: 1.7;
}
.ctaButtons {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
/* Footer Navigation */
.footerNav {
padding: 2rem 0 3rem;
border-top: 1px solid rgba(45, 90, 61, 0.15);
}
.backLink {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: #94a3b8;
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
.backLink:hover {
color: #8fbc8f;
}
.backIcon {
width: 1.25rem;
height: 1.25rem;
}
/* Reduced Motion */
@media (prefers-reduced-motion: reduce) {
.raindrop {
animation: none;
}
.countdownSeparator {
animation: none;
}
}
/* Responsive */
@media (max-width: 768px) {
.heroBanner {
height: 360px;
}
.bannerContent {
padding: 2rem 1.5rem;
}
.heroTitle {
font-size: 2rem;
}
.heroSubtitle {
font-size: 1.1rem;
}
.countdown {
gap: 0.25rem;
}
.countdownItem {
min-width: 60px;
padding: 0.75rem;
}
.countdownNumber {
font-size: 1.5rem;
}
.countdownSeparator {
font-size: 1.5rem;
}
.eventDetails {
flex-direction: column;
align-items: center;
gap: 1rem;
}
.heroCtas {
flex-direction: column;
align-items: center;
}
.primaryCta,
.secondaryCta {
width: 100%;
max-width: 280px;
text-align: center;
}
.ctaContent {
padding: 2rem 1.5rem;
}
.ctaButtons {
flex-direction: column;
align-items: center;
}
.spiritSection,
.sharedSection,
.statsSection,
.ctaSection {
padding: 3rem 0;
}
.weatherToggle {
bottom: 0.5rem;
right: 1rem;
width: 2rem;
height: 2rem;
font-size: 1rem;
}
}
@@ -0,0 +1,428 @@
import React, { useEffect, useMemo, useState } from 'react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import { useForcedTheme } from '@site/src/hooks/useForcedTheme';
import Layout from '@theme/Layout';
import { SITE_CONSTANTS } from '../../constants';
import styles from './bsides-seattle-2026.module.css';
// Event configuration
const EVENT_DATE = '2026-02-27T09:00:00-08:00';
const EVENT_DATE_DISPLAY = 'February 27-28, 2026';
const EVENT_LOCATION = 'Building 92, Redmond, WA';
// Icon components to reduce JSX verbosity
const TargetIcon = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
<circle cx="12" cy="12" r="10" />
<circle cx="12" cy="12" r="6" />
<circle cx="12" cy="12" r="2" />
<path d="M12 2v4M12 18v4M2 12h4M18 12h4" />
</svg>
);
const RefreshIcon = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
);
const CalendarIcon = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"
/>
</svg>
);
const ChatIcon = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 21.192a5.996 5.996 0 01-3.102-1.268.75.75 0 01.346-1.326 5.97 5.97 0 002.727-1.347A5.967 5.967 0 013 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z"
/>
</svg>
);
function CountdownTimer(): React.ReactElement {
const [timeLeft, setTimeLeft] = useState({
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
isExpired: false,
});
useEffect(() => {
const targetDate = new Date(EVENT_DATE).getTime();
const updateCountdown = () => {
const now = new Date().getTime();
const difference = targetDate - now;
if (difference > 0) {
setTimeLeft({
days: Math.floor(difference / (1000 * 60 * 60 * 24)),
hours: Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)),
minutes: Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)),
seconds: Math.floor((difference % (1000 * 60)) / 1000),
isExpired: false,
});
} else {
setTimeLeft({ days: 0, hours: 0, minutes: 0, seconds: 0, isExpired: true });
}
};
updateCountdown();
const interval = setInterval(updateCountdown, 1000);
return () => clearInterval(interval);
}, []);
if (timeLeft.isExpired) {
return (
<div className={styles.countdownExpired}>
<span>Event is happening now!</span>
</div>
);
}
return (
<div className={styles.countdown}>
<div className={styles.countdownItem}>
<span className={styles.countdownNumber}>{timeLeft.days}</span>
<span className={styles.countdownLabel}>Days</span>
</div>
<div className={styles.countdownSeparator}>:</div>
<div className={styles.countdownItem}>
<span className={styles.countdownNumber}>{timeLeft.hours.toString().padStart(2, '0')}</span>
<span className={styles.countdownLabel}>Hours</span>
</div>
<div className={styles.countdownSeparator}>:</div>
<div className={styles.countdownItem}>
<span className={styles.countdownNumber}>
{timeLeft.minutes.toString().padStart(2, '0')}
</span>
<span className={styles.countdownLabel}>Minutes</span>
</div>
<div className={styles.countdownSeparator}>:</div>
<div className={styles.countdownItem}>
<span className={styles.countdownNumber}>
{timeLeft.seconds.toString().padStart(2, '0')}
</span>
<span className={styles.countdownLabel}>Seconds</span>
</div>
</div>
);
}
export default function BSidesSeattle2026(): React.ReactElement {
const [rainEnabled, setRainEnabled] = useState(true);
const raindrops = useMemo(
() =>
[...Array(30)].map(() => ({
left: `${Math.random() * 100}%`,
animationDuration: `${0.5 + Math.random() * 0.5}s`,
animationDelay: `${Math.random() * 2}s`,
})),
[],
);
useForcedTheme('dark');
return (
<Layout
title="Promptfoo at BSides Seattle 2026"
description="Meet the Promptfoo team at BSides Seattle for hands-on AI red teaming demos, hallway-track threat intel, and practical ways to harden LLM apps."
>
<Head>
<meta property="og:title" content="Promptfoo at BSides Seattle 2026" />
<meta
property="og:description"
content="Meet the Promptfoo team at BSides Seattle for hands-on AI red teaming demos and practical ways to harden LLM apps. Feb 27-28, Building 92, Redmond."
/>
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.promptfoo.dev/events/bsides-seattle-2026" />
<meta name="twitter:card" content="summary_large_image" />
<meta
property="og:image"
content="https://www.promptfoo.dev/img/events/bsides-seattle-2026.jpg"
/>
<meta
name="twitter:image"
content="https://www.promptfoo.dev/img/events/bsides-seattle-2026.jpg"
/>
<meta
name="keywords"
content="BSides Seattle 2026, security conference, AI security, LLM security, Pacific Northwest, Seattle, red teaming"
/>
<link rel="canonical" href="https://www.promptfoo.dev/events/bsides-seattle-2026" />
</Head>
<main className={styles.bsidesPage}>
{/* Hero Banner */}
<section className={styles.heroBanner}>
<img
src="/img/events/bsides-seattle-2026.jpg"
alt="BSides Seattle 2026 - Red panda mascot in a PNW coffee shop with hackers"
className={styles.bannerImage}
/>
<div className={styles.bannerOverlay} />
{rainEnabled && (
<div className={styles.rainContainer}>
{raindrops.map((drop, i) => (
<div key={i} className={styles.raindrop} style={drop} />
))}
</div>
)}
<div className={styles.bannerContent}>
<div className={styles.badge}>
<span className={styles.badgeIcon}></span>
BSides Seattle 2026
</div>
<h1 className={styles.heroTitle}>
Red Teaming for AI <span className={styles.highlight}>in the PNW</span>
</h1>
</div>
</section>
{/* Hero Content */}
<section className={styles.heroContent}>
<div className={styles.container}>
<p className={styles.heroSubtitle}>
Meet Promptfoo Engineers and get live demos of AI red teaming: prompt injection,
jailbreaks, and data exfiltration against real-world LLM apps. Bring your use case and
leave with a testing plan you can run in CI.
</p>
<CountdownTimer />
<div className={styles.eventDetails}>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span>{EVENT_DATE_DISPLAY}</span>
</div>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<span>{EVENT_LOCATION}</span>
</div>
</div>
<div className={styles.heroCtas}>
<Link to="/contact" className={styles.primaryCta}>
Schedule a Meeting
</Link>
</div>
<button
type="button"
className={styles.weatherToggle}
onClick={() => setRainEnabled(!rainEnabled)}
aria-label={rainEnabled ? 'Disable rain effect' : 'Enable rain effect'}
title={
rainEnabled
? 'Too much Seattle? Click for sun'
: 'Missing the rain? Click to bring it back'
}
>
{rainEnabled ? '🌧️' : '☀️'}
</button>
</div>
</section>
{/* What to Expect Section */}
<section className={styles.spiritSection} id="learn-more">
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>What to Expect</h2>
<p className={styles.sectionSubtitle}>Open-Source, Developer-First AI Red Teaming</p>
</div>
<div className={styles.spiritGrid}>
<a
href="https://www.promptfoo.dev/docs/category/red-teaming/"
target="_blank"
rel="noopener noreferrer"
className={styles.spiritCard}
>
<div className={styles.cardIconSvg}>
<RefreshIcon />
</div>
<h3>Automated Red Teaming Workflows</h3>
<p>
Learn how teams turn one-off testing into repeatable coverage across prompts,
models, and releases.
</p>
</a>
<a
href="https://www.bsidesseattle.com/2026-sponsors.html"
target="_blank"
rel="noopener noreferrer"
className={styles.spiritCard}
>
<div className={styles.cardIconSvg}>
<TargetIcon />
</div>
<h3>Attend our Talks</h3>
<p>Join our Engineers for a session on Red Teaming for AI at 2:30PM each day.</p>
</a>
<a
href="https://discord.com/invite/promptfoo"
target="_blank"
rel="noopener noreferrer"
className={styles.spiritCard}
>
<div className={styles.cardIconSvg}>
<ChatIcon />
</div>
<h3>Join our Discord</h3>
<p>
Connect with our open source community on Discord to talk with peer developers and
security practitioners.
</p>
</a>
</div>
</div>
</section>
{/* Attending BSides Seattle Section */}
<section className={styles.sharedSection}>
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>Attending BSides Seattle?</h2>
<p className={styles.sectionSubtitle}>
Grab a slot for a quick walkthrough tailored to your stack.
</p>
</div>
<div className={styles.sharedGrid}>
<div className={styles.sharedCard}>
<div className={styles.cardIconSvg}>
<CalendarIcon />
</div>
<h3>Schedule a Meeting</h3>
<p>
Book time for a short, technical walkthrough of AI red teaming for your specific
use case.
</p>
<Link to="/contact" className={styles.cardLink}>
Book a Time
</Link>
</div>
<div className={styles.sharedCard}>
<div className={styles.cardIconSvg}>
<ChatIcon />
</div>
<h3>Can't Make It?</h3>
<p>
Join our Discord community to connect with our team and the AI security community.
</p>
<a
href="https://discord.gg/promptfoo"
target="_blank"
rel="noopener noreferrer"
className={styles.cardLink}
>
Join Discord →
</a>
</div>
</div>
</div>
</section>
{/* Stats Section */}
<section className={styles.statsSection}>
<div className={styles.container}>
<div className={styles.statsGrid}>
<div className={styles.stat}>
<div className={styles.statNumber}>{SITE_CONSTANTS.USER_COUNT_DISPLAY}+</div>
<div className={styles.statLabel}>Developers</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>{SITE_CONSTANTS.FORTUNE_500_COUNT}</div>
<div className={styles.statLabel}>Fortune 500</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumberText}>Open Source</div>
<div className={styles.statLabel}>MIT Licensed</div>
</div>
</div>
</div>
</section>
{/* Final CTA */}
<section className={styles.ctaSection}>
<div className={styles.container}>
<div className={styles.ctaContent}>
<h2 className={styles.ctaTitle}>See You in Seattle</h2>
<p className={styles.ctaText}>
Whether you're a local or flying in for the conference, we'd love to connect. Join
our community and stay updated on our event plans.
</p>
<div className={styles.ctaButtons}>
<a
href="https://discord.gg/promptfoo"
target="_blank"
rel="noopener noreferrer"
className={styles.primaryCta}
>
Join Discord
</a>
<Link to="/contact" className={styles.secondaryCta}>
Contact Us
</Link>
</div>
</div>
</div>
</section>
{/* Footer Navigation */}
<section className={styles.footerNav}>
<div className={styles.container}>
<Link to="/events" className={styles.backLink}>
<svg
className={styles.backIcon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back to All Events
</Link>
</div>
</section>
</main>
</Layout>
);
}
@@ -0,0 +1,371 @@
/* BSides SF 2025 - Golden Gate Theme */
/* Color Palette: Golden Gate orange (#FF6B35), fog gray (#94a3b8), SF blue (#1e3a5f) */
.bsidesPage {
background: #0f172a;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
}
/* Hero Banner */
.heroBanner {
position: relative;
height: 400px;
overflow: hidden;
}
.bannerImage {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.bannerOverlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(180deg, rgba(15, 23, 42, 0.3) 0%, rgba(15, 23, 42, 0.7) 100%);
}
.bannerContent {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 3rem;
background: linear-gradient(0deg, rgba(15, 23, 42, 0.95) 0%, transparent 100%);
}
.badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(255, 107, 53, 0.2);
border: 1px solid rgba(255, 107, 53, 0.4);
color: #fb923c;
border-radius: 100px;
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 1rem;
}
.badgeIcon {
font-size: 1rem;
}
.heroTitle {
font-size: clamp(2rem, 6vw, 3.5rem);
font-weight: 800;
line-height: 1.1;
color: #f1f5f9;
margin: 0;
}
.highlight {
background: linear-gradient(135deg, #ff6b35 0%, #f97316 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Hero Content Section */
.heroContent {
padding: 3rem 0;
text-align: center;
border-bottom: 1px solid rgba(255, 107, 53, 0.1);
}
.heroSubtitle {
font-size: 1.25rem;
color: #94a3b8;
max-width: 600px;
margin: 0 auto 2rem;
line-height: 1.7;
}
.eventDetails {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2rem;
margin-bottom: 2rem;
}
.detail {
display: flex;
align-items: center;
gap: 0.5rem;
color: #cbd5e1;
font-size: 0.95rem;
}
.icon {
width: 1.25rem;
height: 1.25rem;
stroke-width: 2;
color: #fb923c;
}
.heroCtas {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.primaryCta {
padding: 0.875rem 2rem;
background: linear-gradient(135deg, #ff6b35 0%, #f97316 100%);
color: white;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
transition: all 0.3s ease;
}
.primaryCta:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(255, 107, 53, 0.4);
color: white;
}
.secondaryCta {
padding: 0.875rem 2rem;
background: transparent;
color: #fb923c;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
border: 1px solid rgba(255, 107, 53, 0.4);
transition: all 0.3s ease;
}
.secondaryCta:hover {
background: rgba(255, 107, 53, 0.1);
color: white;
}
/* Sections */
.recapSection,
.whySection,
.ctaSection {
padding: 4rem 0;
}
.recapSection {
background: rgba(255, 107, 53, 0.02);
}
.sectionHeader {
text-align: center;
margin-bottom: 3rem;
}
.sectionTitle {
font-size: 2rem;
font-weight: 700;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.sectionSubtitle {
font-size: 1.1rem;
color: #94a3b8;
max-width: 500px;
margin: 0 auto;
}
/* Recap Grid */
.recapGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.5rem;
}
.recapCard {
background: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(255, 107, 53, 0.15);
border-radius: 12px;
padding: 1.5rem;
transition: all 0.3s ease;
}
.recapCard:hover {
border-color: rgba(255, 107, 53, 0.3);
transform: translateY(-2px);
}
.cardIcon {
font-size: 2rem;
margin-bottom: 1rem;
}
.recapCard h3 {
font-size: 1.125rem;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 0.5rem;
}
.recapCard p {
font-size: 0.9rem;
color: #94a3b8;
line-height: 1.6;
margin: 0;
}
/* Why Grid */
.whyGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 2rem;
}
.whyItem {
padding: 1.5rem 0;
border-left: 2px solid rgba(255, 107, 53, 0.3);
padding-left: 1.5rem;
}
.whyNumber {
font-size: 2.5rem;
font-weight: 800;
background: linear-gradient(135deg, #ff6b35 0%, #f97316 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.75rem;
line-height: 1;
}
.whyItem h3 {
font-size: 1.125rem;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 0.5rem;
}
.whyItem p {
font-size: 0.9rem;
color: #94a3b8;
line-height: 1.6;
margin: 0;
}
/* CTA Section */
.ctaContent {
max-width: 600px;
margin: 0 auto;
text-align: center;
background: linear-gradient(135deg, rgba(255, 107, 53, 0.08) 0%, rgba(249, 115, 22, 0.08) 100%);
border: 1px solid rgba(255, 107, 53, 0.2);
border-radius: 16px;
padding: 3rem 2rem;
}
.ctaTitle {
font-size: 1.75rem;
font-weight: 700;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.ctaText {
font-size: 1rem;
color: #94a3b8;
margin-bottom: 1.5rem;
line-height: 1.7;
}
.ctaButtons {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
/* Footer Navigation */
.footerNav {
padding: 2rem 0 3rem;
border-top: 1px solid rgba(255, 107, 53, 0.1);
}
.backLink {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: #94a3b8;
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
.backLink:hover {
color: #fb923c;
}
.backIcon {
width: 1.25rem;
height: 1.25rem;
}
/* Responsive */
@media (max-width: 768px) {
.heroBanner {
height: 300px;
}
.bannerContent {
padding: 2rem 1.5rem;
}
.heroTitle {
font-size: 2rem;
}
.heroSubtitle {
font-size: 1.1rem;
}
.eventDetails {
flex-direction: column;
align-items: center;
gap: 1rem;
}
.heroCtas {
flex-direction: column;
align-items: center;
}
.primaryCta,
.secondaryCta {
width: 100%;
max-width: 280px;
text-align: center;
}
.ctaContent {
padding: 2rem 1.5rem;
}
.ctaButtons {
flex-direction: column;
align-items: center;
}
.recapSection,
.whySection,
.ctaSection {
padding: 3rem 0;
}
}
+261
View File
@@ -0,0 +1,261 @@
import React from 'react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import { useForcedTheme } from '@site/src/hooks/useForcedTheme';
import Layout from '@theme/Layout';
import styles from './bsides-sf-2025.module.css';
export default function BSidesSF2025(): React.ReactElement {
useForcedTheme('dark');
const handleSmoothScroll = (e: React.MouseEvent<HTMLAnchorElement>, targetId: string) => {
e.preventDefault();
const element = document.querySelector(targetId);
if (element) {
const offset = 80;
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - offset;
window.scrollTo({ top: offsetPosition, behavior: 'smooth' });
}
};
return (
<Layout
title="Promptfoo at BSides SF 2025"
description="Recap of Promptfoo at BSides San Francisco 2025. Community connections and AI security discussions during RSA week."
>
<Head>
<meta property="og:title" content="Promptfoo at BSides SF 2025" />
<meta
property="og:description"
content="Recap of Promptfoo at BSides San Francisco 2025. Community-driven security and AI discussions."
/>
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.promptfoo.dev/events/bsides-sf-2025" />
<meta name="twitter:card" content="summary_large_image" />
<meta
property="og:image"
content="https://www.promptfoo.dev/img/events/bsides-sf-2025.jpg"
/>
<meta
name="twitter:image"
content="https://www.promptfoo.dev/img/events/bsides-sf-2025.jpg"
/>
<meta
name="keywords"
content="BSides SF 2025, BSides San Francisco, security conference, AI security, hacker community, RSA week"
/>
<link rel="canonical" href="https://www.promptfoo.dev/events/bsides-sf-2025" />
</Head>
<main className={styles.bsidesPage}>
{/* Hero Banner */}
<section className={styles.heroBanner}>
<img
src="/img/events/bsides-sf-2025.jpg"
alt="BSides SF 2025"
className={styles.bannerImage}
/>
<div className={styles.bannerOverlay} />
<div className={styles.bannerContent}>
<div className={styles.badge}>
<span className={styles.badgeIcon}>🌉</span>
BSides SF 2025
</div>
<h1 className={styles.heroTitle}>
Community <span className={styles.highlight}>First</span>
</h1>
</div>
</section>
{/* Hero Content */}
<section className={styles.heroContent}>
<div className={styles.container}>
<p className={styles.heroSubtitle}>
We connected with the security community at BSides San Francisco 2025, the grassroots
conference that brings hackers together during RSA week.
</p>
<div className={styles.eventDetails}>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span>April 26-27, 2025</span>
</div>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<span>City View at Metreon, SF</span>
</div>
</div>
<div className={styles.heroCtas}>
<a
href="#recap"
className={styles.primaryCta}
onClick={(e) => handleSmoothScroll(e, '#recap')}
>
View Recap
</a>
<Link to="/events" className={styles.secondaryCta}>
All Events
</Link>
</div>
</div>
</section>
{/* Recap Section */}
<section id="recap" className={styles.recapSection}>
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>Event Recap</h2>
<p className={styles.sectionSubtitle}>The best of BSides SF 2025</p>
</div>
<div className={styles.recapGrid}>
<div className={styles.recapCard}>
<div className={styles.cardIcon}>🤝</div>
<h3>Community Connections</h3>
<p>
Met with security researchers, bug bounty hunters, and AI enthusiasts who are
pushing the boundaries of AI security research.
</p>
</div>
<div className={styles.recapCard}>
<div className={styles.cardIcon}>💬</div>
<h3>AI Security Discussions</h3>
<p>
Engaged in deep conversations about LLM vulnerabilities, prompt injection, and the
future of AI red teaming with the community.
</p>
</div>
<div className={styles.recapCard}>
<div className={styles.cardIcon}>🎤</div>
<h3>Hallway Track</h3>
<p>
Some of the best conversations happened between sessionsthe hallway track is
where the real magic of BSides happens.
</p>
</div>
<div className={styles.recapCard}>
<div className={styles.cardIcon}>🍻</div>
<h3>After-Hours Networking</h3>
<p>
Connected with the community at evening events, building relationships that extend
beyond the conference.
</p>
</div>
</div>
</div>
</section>
{/* Why BSides Section */}
<section className={styles.whySection}>
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>Why We Love BSides</h2>
<p className={styles.sectionSubtitle}>The heart of the security community</p>
</div>
<div className={styles.whyGrid}>
<div className={styles.whyItem}>
<div className={styles.whyNumber}>01</div>
<h3>Grassroots Energy</h3>
<p>
BSides captures the authentic hacker spiritcommunity-organized, volunteer-run,
and focused on sharing knowledge over selling products.
</p>
</div>
<div className={styles.whyItem}>
<div className={styles.whyNumber}>02</div>
<h3>Diverse Perspectives</h3>
<p>
From first-time speakers to industry veterans, BSides brings together voices you
won't hear at bigger corporate conferences.
</p>
</div>
<div className={styles.whyItem}>
<div className={styles.whyNumber}>03</div>
<h3>Open Source Spirit</h3>
<p>
As an open-source company, we feel right at home in a community that values
transparency, collaboration, and giving back.
</p>
</div>
<div className={styles.whyItem}>
<div className={styles.whyNumber}>04</div>
<h3>Real Conversations</h3>
<p>
No booth barriers—just genuine discussions about security challenges, tool
recommendations, and shared experiences.
</p>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className={styles.ctaSection}>
<div className={styles.container}>
<div className={styles.ctaContent}>
<h2 className={styles.ctaTitle}>See You at BSides SF 2026</h2>
<p className={styles.ctaText}>
We'll be back for BSides SF 2026. Join us for more community connections, AI
security discussions, and hallway track conversations.
</p>
<div className={styles.ctaButtons}>
<Link to="/events/bsides-sf-2026" className={styles.primaryCta}>
BSides SF 2026 Details
</Link>
<Link to="/contact" className={styles.secondaryCta}>
Get in Touch
</Link>
</div>
</div>
</div>
</section>
{/* Footer Navigation */}
<section className={styles.footerNav}>
<div className={styles.container}>
<Link to="/events" className={styles.backLink}>
<svg
className={styles.backIcon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back to All Events
</Link>
</div>
</section>
</main>
</Layout>
);
}
@@ -0,0 +1,493 @@
/* BSides SF 2026 - Golden Gate Theme */
/* Color Palette: BSides orange (#F97316), purple (#8B5CF6), neon accents */
.bsidesPage {
background: #0a0f1a;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
}
/* Hero Banner */
.heroBanner {
position: relative;
height: 400px;
overflow: hidden;
}
.bannerImage {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.bannerOverlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(180deg, rgba(10, 15, 26, 0.4) 0%, rgba(10, 15, 26, 0.7) 100%);
}
.bannerContent {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 3rem;
background: linear-gradient(0deg, rgba(10, 15, 26, 0.95) 0%, transparent 100%);
}
.badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(249, 115, 22, 0.2);
border: 1px solid rgba(249, 115, 22, 0.4);
color: #fdba74;
border-radius: 100px;
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 1rem;
}
.badgeIcon {
font-size: 1rem;
}
.heroTitle {
font-size: clamp(2rem, 6vw, 3.5rem);
font-weight: 800;
line-height: 1.1;
color: #f1f5f9;
margin: 0;
}
.highlight {
background: linear-gradient(135deg, #f97316 0%, #8b5cf6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.heroMeta {
display: flex;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
flex-wrap: wrap;
}
.heroDate {
font-size: 1.125rem;
font-weight: 600;
color: #fdba74;
}
.heroDivider {
color: rgba(249, 115, 22, 0.5);
font-size: 0.875rem;
}
.heroVenue {
font-size: 1.125rem;
font-weight: 500;
color: #e2e8f0;
}
/* Hero Content Section */
.heroContent {
padding: 3rem 0;
text-align: center;
border-bottom: 1px solid rgba(249, 115, 22, 0.1);
}
.heroSubtitle {
font-size: 1.25rem;
color: #94a3b8;
max-width: 600px;
margin: 0 auto 2rem;
line-height: 1.7;
}
/* Countdown Timer */
.countdown {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.countdownItem {
display: flex;
flex-direction: column;
align-items: center;
background: linear-gradient(135deg, rgba(249, 115, 22, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%);
border: 1px solid rgba(249, 115, 22, 0.3);
border-radius: 12px;
padding: 1rem 1.25rem;
min-width: 80px;
}
.countdownNumber {
font-size: 2rem;
font-weight: 700;
color: #fdba74;
font-variant-numeric: tabular-nums;
line-height: 1;
}
.countdownLabel {
font-size: 0.75rem;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-top: 0.25rem;
}
.countdownSeparator {
font-size: 2rem;
font-weight: 700;
color: #f97316;
animation: blink 1s step-end infinite;
}
@keyframes blink {
50% {
opacity: 0;
}
}
.eventDetails {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2rem;
margin-bottom: 2rem;
}
.detail {
display: flex;
align-items: center;
gap: 0.5rem;
color: #cbd5e1;
font-size: 0.95rem;
}
.icon {
width: 1.25rem;
height: 1.25rem;
stroke-width: 2;
color: #f97316;
}
.heroCtas {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
}
.ctaBlurb {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.7);
margin: 0;
}
.primaryCta {
padding: 0.875rem 2rem;
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
color: white;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
transition: all 0.3s ease;
}
.primaryCta:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(249, 115, 22, 0.4);
color: white;
}
.secondaryCta {
padding: 0.875rem 2rem;
background: transparent;
color: #c4b5fd;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
border: 1px solid rgba(139, 92, 246, 0.4);
transition: all 0.3s ease;
}
.secondaryCta:hover {
background: rgba(139, 92, 246, 0.1);
color: white;
}
/* Sections */
.highlightsSection,
.whySection,
.ctaSection {
padding: 4rem 0;
}
.highlightsSection {
background: rgba(249, 115, 22, 0.02);
}
.sectionHeader {
text-align: center;
margin-bottom: 3rem;
}
.sectionTitle {
font-size: 2rem;
font-weight: 700;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.sectionSubtitle {
font-size: 1.1rem;
color: #94a3b8;
max-width: 500px;
margin: 0 auto;
}
/* Highlights Grid */
.highlightsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.5rem;
}
.highlightCard {
background: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(249, 115, 22, 0.15);
border-radius: 12px;
padding: 1.5rem;
transition: all 0.3s ease;
}
.highlightCard:hover {
border-color: rgba(249, 115, 22, 0.3);
transform: translateY(-2px);
}
.cardIcon {
font-size: 2rem;
margin-bottom: 1rem;
}
.highlightCard h3 {
font-size: 1.125rem;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 0.5rem;
}
.highlightCard p {
font-size: 0.9rem;
color: #94a3b8;
line-height: 1.6;
margin: 0;
}
/* Why Grid */
.whyGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 2rem;
}
.whyItem {
padding: 1.5rem 0;
border-left: 2px solid rgba(249, 115, 22, 0.3);
padding-left: 1.5rem;
}
.whyNumber {
font-size: 2.5rem;
font-weight: 800;
background: linear-gradient(135deg, #f97316 0%, #8b5cf6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.75rem;
line-height: 1;
}
.whyItem h3 {
font-size: 1.125rem;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 0.5rem;
}
.whyItem p {
font-size: 0.9rem;
color: #94a3b8;
line-height: 1.6;
margin: 0;
}
/* CTA Section */
.ctaGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 2rem;
max-width: 800px;
margin: 0 auto;
}
.ctaCard {
text-align: center;
background: linear-gradient(135deg, rgba(249, 115, 22, 0.08) 0%, rgba(139, 92, 246, 0.08) 100%);
border: 1px solid rgba(249, 115, 22, 0.2);
border-radius: 16px;
padding: 2rem 1.5rem;
display: flex;
flex-direction: column;
align-items: center;
}
.ctaCardTitle {
font-size: 1.25rem;
font-weight: 700;
color: #f1f5f9;
margin: 0 0 0.75rem 0;
}
.ctaCardText {
font-size: 0.95rem;
color: #94a3b8;
margin-bottom: 1.5rem;
line-height: 1.6;
flex-grow: 1;
}
/* Footer Navigation */
.footerNav {
padding: 2rem 0 3rem;
border-top: 1px solid rgba(249, 115, 22, 0.1);
}
.backLink {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: #94a3b8;
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
.backLink:hover {
color: #f97316;
}
.backIcon {
width: 1.25rem;
height: 1.25rem;
}
/* Reduced Motion */
@media (prefers-reduced-motion: reduce) {
.countdownSeparator {
animation: none;
}
}
/* Responsive */
@media (max-width: 768px) {
.heroBanner {
height: 300px;
}
.bannerContent {
padding: 2rem 1.5rem;
}
.heroTitle {
font-size: 2rem;
}
.heroMeta {
flex-direction: column;
gap: 0.25rem;
}
.heroDivider {
display: none;
}
.heroDate,
.heroVenue {
font-size: 1rem;
}
.heroSubtitle {
font-size: 1.1rem;
}
.countdown {
gap: 0.25rem;
}
.countdownItem {
min-width: 60px;
padding: 0.75rem;
}
.countdownNumber {
font-size: 1.5rem;
}
.countdownSeparator {
font-size: 1.5rem;
}
.eventDetails {
flex-direction: column;
align-items: center;
gap: 1rem;
}
.heroCtas {
flex-direction: column;
align-items: center;
}
.primaryCta,
.secondaryCta {
width: 100%;
max-width: 280px;
text-align: center;
}
.ctaGrid {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.ctaCard {
padding: 1.5rem 1.25rem;
}
.highlightsSection,
.whySection,
.ctaSection {
padding: 3rem 0;
}
}
+267
View File
@@ -0,0 +1,267 @@
import React, { useEffect, useState } from 'react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import { useForcedTheme } from '@site/src/hooks/useForcedTheme';
import Layout from '@theme/Layout';
import styles from './bsides-sf-2026.module.css';
function CountdownTimer(): React.ReactElement {
const [timeLeft, setTimeLeft] = useState({
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
});
useEffect(() => {
const targetDate = new Date('2026-03-21T09:00:00-07:00').getTime();
const updateCountdown = () => {
const now = new Date().getTime();
const difference = targetDate - now;
if (difference > 0) {
setTimeLeft({
days: Math.floor(difference / (1000 * 60 * 60 * 24)),
hours: Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)),
minutes: Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)),
seconds: Math.floor((difference % (1000 * 60)) / 1000),
});
}
};
updateCountdown();
const interval = setInterval(updateCountdown, 1000);
return () => clearInterval(interval);
}, []);
return (
<div className={styles.countdown}>
<div className={styles.countdownItem}>
<span className={styles.countdownNumber}>{timeLeft.days}</span>
<span className={styles.countdownLabel}>Days</span>
</div>
<div className={styles.countdownSeparator}>:</div>
<div className={styles.countdownItem}>
<span className={styles.countdownNumber}>{timeLeft.hours.toString().padStart(2, '0')}</span>
<span className={styles.countdownLabel}>Hours</span>
</div>
<div className={styles.countdownSeparator}>:</div>
<div className={styles.countdownItem}>
<span className={styles.countdownNumber}>
{timeLeft.minutes.toString().padStart(2, '0')}
</span>
<span className={styles.countdownLabel}>Minutes</span>
</div>
<div className={styles.countdownSeparator}>:</div>
<div className={styles.countdownItem}>
<span className={styles.countdownNumber}>
{timeLeft.seconds.toString().padStart(2, '0')}
</span>
<span className={styles.countdownLabel}>Seconds</span>
</div>
</div>
);
}
export default function BSidesSF2026(): React.ReactElement {
useForcedTheme('dark');
return (
<Layout
title="Promptfoo at BSides SF 2026"
description="Join Promptfoo at BSides San Francisco 2026. Community connections, AI security workshops, and hacker culture during RSA week."
>
<Head>
<meta property="og:title" content="Promptfoo at BSides SF 2026" />
<meta
property="og:description"
content="Join Promptfoo at BSides San Francisco 2026. Community-driven security and AI workshops."
/>
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.promptfoo.dev/events/bsides-sf-2026" />
<meta name="twitter:card" content="summary_large_image" />
<meta
property="og:image"
content="https://www.promptfoo.dev/img/events/bsides-sf-2026.jpg"
/>
<meta
name="twitter:image"
content="https://www.promptfoo.dev/img/events/bsides-sf-2026.jpg"
/>
<meta
name="keywords"
content="BSides SF 2026, BSides San Francisco, security conference, AI security, hacker community, RSA week"
/>
<link rel="canonical" href="https://www.promptfoo.dev/events/bsides-sf-2026" />
</Head>
<main className={styles.bsidesPage}>
{/* Hero Banner */}
<section className={styles.heroBanner}>
<img
src="/img/events/bsides-sf-2026.jpg"
alt="BSides SF 2026"
className={styles.bannerImage}
/>
<div className={styles.bannerOverlay} />
<div className={styles.bannerContent}>
<div className={styles.badge}>
<span className={styles.badgeIcon}>🌉</span>
BSidesSF 2026
</div>
<h1 className={styles.heroTitle}>
BSides <span className={styles.highlight}>San Francisco</span>
</h1>
<div className={styles.heroMeta}>
<span className={styles.heroDate}>March 21-22, 2026</span>
<span className={styles.heroDivider}></span>
<span className={styles.heroVenue}>San Francisco</span>
</div>
</div>
</section>
{/* Hero Content */}
<section className={styles.heroContent}>
<div className={styles.container}>
<p className={styles.heroSubtitle}>
Meet our AI red teaming team and see live demos of how Promptfoo secures AI
applicationsfrom pre-deployment testing to production monitoring.
</p>
<CountdownTimer />
<div className={styles.eventDetails}>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span>March 21-22, 2026</span>
</div>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<span>San Francisco, CA</span>
</div>
</div>
<div className={styles.heroCtas}>
<p className={styles.ctaBlurb}>Book a time to talk to us</p>
<Link to="/contact" className={styles.primaryCta}>
Schedule a Meeting
</Link>
</div>
</div>
</section>
{/* Highlights Section */}
<section id="highlights" className={styles.highlightsSection}>
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>What to Expect</h2>
<p className={styles.sectionSubtitle}>Community-driven AI security experiences</p>
</div>
<div className={styles.highlightsGrid}>
<div className={styles.highlightCard}>
<div className={styles.cardIcon}>🎯</div>
<h3>AI Red Teaming</h3>
<p>
Compare notes on LLM attack vectors, jailbreak techniques, and integrating
automated red teaming into your workflow.
</p>
</div>
<div className={styles.highlightCard}>
<div className={styles.cardIcon}>🤝</div>
<h3>Connect with AI Experts</h3>
<p>
Meet AI security professionals working on evals, guardrails, and MCP
securitylearn how they're protecting AI applications in production.
</p>
</div>
<div className={styles.highlightCard}>
<div className={styles.cardIcon}>🎬</div>
<h3>See it in Action</h3>
<p>
Quick-fire demos on the latest AI security research, tools, and techniques from
the AI security and red teaming experts.
</p>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className={styles.ctaSection}>
<div className={styles.container}>
<div className={styles.ctaGrid}>
<div className={styles.ctaCard}>
<h3 className={styles.ctaCardTitle}>Attending BSides SF?</h3>
<p className={styles.ctaCardText}>
Book a time to meet with our AI security and red teaming experts at the event.
</p>
<Link to="/contact" className={styles.primaryCta}>
Schedule a Meeting
</Link>
</div>
<div className={styles.ctaCard}>
<h3 className={styles.ctaCardTitle}>Can't make it?</h3>
<p className={styles.ctaCardText}>
Join our Discord community to connect with our team and our community.
</p>
<a
href="https://discord.com/invite/promptfoo"
target="_blank"
rel="noopener noreferrer"
className={styles.secondaryCta}
>
Join our Discord
</a>
</div>
</div>
</div>
</section>
{/* Footer Navigation */}
<section className={styles.footerNav}>
<div className={styles.container}>
<Link to="/events" className={styles.backLink}>
<svg
className={styles.backIcon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back to All Events
</Link>
</div>
</section>
</main>
</Layout>
);
}
@@ -0,0 +1,565 @@
/* DEFCON 2025 Cyberpunk x Star Wars Theme */
.defconPage {
background: #000;
color: #fff;
font-family: 'Courier New', monospace;
overflow-x: hidden;
min-height: 100vh;
}
/* Hero Section */
.hero {
position: relative;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.heroBackground {
position: relative;
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
z-index: 1;
}
/* Glitch effect background */
.glitchEffect {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0, 255, 0, 0.03) 2px,
rgba(0, 255, 0, 0.03) 4px
);
pointer-events: none;
z-index: 1;
}
/* Scanlines */
.scanlines {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to bottom, transparent 50%, rgba(0, 0, 0, 0.25) 50%);
background-size: 100% 4px;
animation: scanlines 8s linear infinite;
pointer-events: none;
z-index: 2;
}
@keyframes scanlines {
0% {
transform: translateY(0);
}
100% {
transform: translateY(10px);
}
}
.heroContent {
text-align: center;
position: relative;
z-index: 10;
}
.badge {
display: inline-block;
padding: 0.5rem 1.5rem;
border: 2px solid #00ff00;
color: #00ff00;
font-size: 0.875rem;
font-weight: bold;
letter-spacing: 0.2em;
margin-bottom: 2rem;
animation: neon 1.5s ease-in-out infinite alternate;
}
@keyframes neon {
from {
box-shadow:
0 0 10px #00ff00,
0 0 20px #00ff00,
0 0 30px #00ff00;
}
to {
box-shadow:
0 0 20px #00ff00,
0 0 30px #00ff00,
0 0 40px #00ff00;
}
}
.heroTitle {
font-size: clamp(3rem, 8vw, 6rem);
font-weight: bold;
line-height: 1.1;
margin: 0 0 1.5rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Glitch text effect */
.glitch {
position: relative;
color: #fff;
text-shadow: 0 0 10px rgba(255, 255, 255, 0.8);
}
.glitch::before,
.glitch::after {
content: attr(data-text);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.glitch::before {
animation: glitch-1 0.5s infinite;
color: #00ff00;
z-index: -1;
}
.glitch::after {
animation: glitch-2 0.5s infinite;
color: #ff00ff;
z-index: -2;
}
@keyframes glitch-1 {
0% {
clip: rect(44px, 450px, 56px, 0);
transform: translate(0);
}
20% {
clip: rect(20px, 450px, 30px, 0);
transform: translate(-2px, 2px);
}
40% {
clip: rect(85px, 450px, 95px, 0);
transform: translate(2px, -2px);
}
60% {
clip: rect(10px, 450px, 20px, 0);
transform: translate(-1px, 1px);
}
80% {
clip: rect(65px, 450px, 75px, 0);
transform: translate(1px, -1px);
}
100% {
clip: rect(25px, 450px, 35px, 0);
transform: translate(0);
}
}
@keyframes glitch-2 {
0% {
clip: rect(65px, 450px, 75px, 0);
transform: translate(0);
}
20% {
clip: rect(30px, 450px, 40px, 0);
transform: translate(2px, -2px);
}
40% {
clip: rect(80px, 450px, 90px, 0);
transform: translate(-2px, 2px);
}
60% {
clip: rect(15px, 450px, 25px, 0);
transform: translate(1px, -1px);
}
80% {
clip: rect(50px, 450px, 60px, 0);
transform: translate(-1px, 1px);
}
100% {
clip: rect(35px, 450px, 45px, 0);
transform: translate(0);
}
}
.highlight {
color: #ff00ff;
text-shadow:
0 0 20px #ff00ff,
0 0 40px #ff00ff;
}
.heroSubtitle {
font-size: clamp(1rem, 2vw, 1.25rem);
max-width: 600px;
margin: 0 auto 2rem;
color: #00ff00;
opacity: 0.9;
}
.heroButtons {
display: flex;
gap: 1rem;
justify-content: center;
margin-bottom: 3rem;
flex-wrap: wrap;
}
/* Buttons */
.primaryButton,
.secondaryButton {
padding: 1rem 2rem;
font-size: 1rem;
font-weight: bold;
text-decoration: none;
text-transform: uppercase;
letter-spacing: 0.1em;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
border: none;
cursor: pointer;
}
.primaryButton {
background: #ff00ff;
color: #000;
box-shadow: 0 0 20px rgba(255, 0, 255, 0.5);
}
.primaryButton:hover {
background: #ff44ff;
color: #000;
box-shadow: 0 0 30px rgba(255, 0, 255, 0.8);
transform: translateY(-2px);
}
.secondaryButton {
background: transparent;
color: #00ff00;
border: 2px solid #00ff00;
}
.secondaryButton:hover {
background: #00ff00;
color: #000;
box-shadow: 0 0 20px rgba(0, 255, 0, 0.5);
}
.buttonGlitch {
display: inline-block;
position: relative;
}
.buttonGlitch:hover {
animation: textGlitch 0.3s infinite;
}
@keyframes textGlitch {
0% {
transform: translate(0);
}
20% {
transform: translate(-2px, 2px);
}
40% {
transform: translate(-2px, -2px);
}
60% {
transform: translate(2px, 2px);
}
80% {
transform: translate(2px, -2px);
}
100% {
transform: translate(0);
}
}
/* Event Details */
.eventDetails {
display: flex;
gap: 2rem;
justify-content: center;
flex-wrap: wrap;
}
.detail {
display: flex;
align-items: center;
gap: 0.5rem;
color: #00ff00;
font-size: 0.9rem;
}
.icon {
width: 20px;
height: 20px;
stroke: currentColor;
}
/* Party Section */
.partySection {
padding: 5rem 0;
position: relative;
background: #0a0a0a;
}
.partyBackground {
position: relative;
}
.partyContainer {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
}
.partyHeader {
text-align: center;
margin-bottom: 3rem;
}
.partyTitle {
font-size: clamp(2rem, 4vw, 3rem);
color: #00ff00;
margin: 0;
font-family: 'Courier New', monospace;
}
.terminal {
color: #ff00ff;
animation: blink 1s infinite;
}
@keyframes blink {
0%,
50% {
opacity: 1;
}
51%,
100% {
opacity: 0;
}
}
.partyGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 2rem;
}
.partyCard {
position: relative;
background: #111;
border: 1px solid #333;
transition: all 0.3s ease;
}
.partyCard:hover {
border-color: #00ff00;
box-shadow: 0 0 20px rgba(0, 255, 0, 0.3);
transform: translateY(-5px);
}
.partyCardInner {
padding: 2rem;
}
.partyEmoji {
font-size: 3rem;
margin-bottom: 1rem;
}
.partyCard h3 {
color: #ff00ff;
font-size: 1.5rem;
margin: 0 0 1rem;
font-family: 'Courier New', monospace;
}
.partyCard p {
color: #00ff00;
opacity: 0.8;
line-height: 1.6;
margin: 0 0 1rem;
}
.partyTag {
display: inline-block;
padding: 0.25rem 0.75rem;
background: rgba(0, 255, 0, 0.1);
border: 1px solid #00ff00;
color: #00ff00;
font-size: 0.75rem;
font-weight: bold;
letter-spacing: 0.1em;
}
/* ASCII Art Section */
.asciiSection {
padding: 3rem 0;
background: #000;
text-align: center;
}
.asciiArt {
color: #00ff00;
font-size: clamp(0.5rem, 1.5vw, 1rem);
line-height: 1.2;
margin: 0;
text-shadow: 0 0 10px rgba(0, 255, 0, 0.5);
animation: flicker 3s infinite;
}
@keyframes flicker {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.8;
}
}
/* Community Section */
.communitySection {
padding: 5rem 0;
background: #0a0a0a;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
}
.sectionTitle {
font-size: clamp(2rem, 4vw, 3rem);
text-align: center;
margin: 0 0 3rem;
color: #ff00ff;
text-shadow: 0 0 20px rgba(255, 0, 255, 0.5);
}
.statsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
text-align: center;
}
.stat {
padding: 2rem;
background: rgba(255, 0, 255, 0.05);
border: 1px solid rgba(255, 0, 255, 0.2);
transition: all 0.3s ease;
}
.stat:hover {
background: rgba(255, 0, 255, 0.1);
border-color: #ff00ff;
transform: scale(1.05);
}
.statNumber {
font-size: 3rem;
font-weight: bold;
color: #00ff00;
text-shadow: 0 0 10px rgba(0, 255, 0, 0.5);
margin-bottom: 0.5rem;
}
.statLabel {
color: #ff00ff;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.1em;
}
/* Final CTA */
.finalCta {
padding: 5rem 0;
text-align: center;
background: #000;
position: relative;
}
.finalCta h2 {
font-size: clamp(2rem, 4vw, 3rem);
margin: 0 0 1rem;
color: #fff;
}
.finalCta p {
font-size: 1.25rem;
color: #00ff00;
max-width: 600px;
margin: 0 auto 2rem;
opacity: 0.9;
}
.ctaButtons {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
.blink {
animation: cursorBlink 1s infinite;
}
@keyframes cursorBlink {
0%,
49% {
opacity: 1;
}
50%,
100% {
opacity: 0;
}
}
/* Responsive */
@media (max-width: 768px) {
.heroTitle {
font-size: 3rem;
}
.eventDetails {
flex-direction: column;
align-items: center;
gap: 1rem;
}
.heroButtons {
flex-direction: column;
width: 100%;
max-width: 300px;
margin: 0 auto 3rem;
}
.primaryButton,
.secondaryButton {
width: 100%;
}
}
/* Dark theme overrides */
[data-theme='dark'] .defconPage {
--ifm-background-color: #000;
--ifm-navbar-background-color: #000;
--ifm-footer-background-color: #000;
}
+287
View File
@@ -0,0 +1,287 @@
import React from 'react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import { useForcedTheme } from '@site/src/hooks/useForcedTheme';
import Layout from '@theme/Layout';
import { SITE_CONSTANTS } from '../../constants';
import styles from './defcon-2025.module.css';
export default function Defcon2025(): React.ReactElement {
useForcedTheme('dark');
const handleSmoothScroll = (e: React.MouseEvent<HTMLAnchorElement>, targetId: string) => {
e.preventDefault();
const element = document.querySelector(targetId);
if (element) {
const offset = 80; // Offset for fixed header
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - offset;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth',
});
}
};
return (
<Layout
title="Promptfoo Party at DEF CON 33"
description="Join the Promptfoo crew for a party at DEF CON 33. Network with AI security researchers, hackers, and the open source community. Free drinks, great vibes, and security war stories."
>
<Head>
<meta property="og:title" content="Promptfoo Party at DEF CON 33 | AI Security Community" />
<meta
property="og:description"
content="The AI security party you don't want to miss at DEF CON 33. Join hackers, researchers, and the Promptfoo team for drinks and demos. August 9, 2025 in Las Vegas."
/>
<meta property="og:image" content="https://www.promptfoo.dev/img/events/defcon-2025.jpg" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content="https://www.promptfoo.dev/events/defcon-2025" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Promptfoo" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Promptfoo Party at DEF CON 33" />
<meta
name="twitter:description"
content="The AI security party at DEF CON 33. Free drinks, live demos. August 9, Las Vegas."
/>
<meta name="twitter:image" content="https://www.promptfoo.dev/img/events/defcon-2025.jpg" />
<meta name="twitter:site" content="@promptfoo" />
<meta
name="keywords"
content="DEF CON 33, DEF CON 2025, AI security party, hacker party, LLM security, prompt injection, red team, Las Vegas"
/>
<link rel="canonical" href="https://www.promptfoo.dev/events/defcon-2025" />
</Head>
<main className={styles.defconPage}>
{/* Hero Section */}
<section className={styles.hero}>
<div className={styles.heroBackground}>
<div className={styles.glitchEffect} />
<div className={styles.scanlines} />
<div className={styles.heroContent}>
<div className={styles.badge}>DEF CON 33</div>
<h1 className={styles.heroTitle}>
<span className={styles.glitch} data-text="PROMPTFOO">
PROMPTFOO
</span>
<br />
<span className={styles.highlight}>PARTY</span>
</h1>
<p className={styles.heroSubtitle}>
Join hackers, security researchers, and the open source community for the AI
security event of DEF CON at the galaxy's most iconic cantina. RSVP required.
Capacity is limited.
</p>
<div className={styles.heroButtons}>
<a
href="https://lu.ma/ljm23pj6?tk=qGE9ez&utm_source=pf-web"
className={styles.primaryButton}
target="_blank"
rel="noopener noreferrer"
>
<span className={styles.buttonGlitch}>RSVP NOW</span>
</a>
<a
href="#party-details"
className={styles.secondaryButton}
onClick={(e) => handleSmoothScroll(e, '#party-details')}
>
Party Details
</a>
</div>
<div className={styles.eventDetails}>
<div className={styles.detail}>
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span>Saturday, August 9, 2025</span>
</div>
<div className={styles.detail}>
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>6:00 PM - 8:00 PM</span>
</div>
<div className={styles.detail}>
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<span>Millennium FANDOM Bar</span>
</div>
<div className={styles.detail}>
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span style={{ fontWeight: 'bold', color: '#00ff00' }}>
Free drinks (while supplies last)
</span>
</div>
</div>
</div>
</div>
</section>
{/* What to Expect Section */}
<section className={styles.partySection} id="party-details">
<div className={styles.partyBackground}>
<div className={styles.partyContainer}>
<div className={styles.partyHeader}>
<h2 className={styles.partyTitle}>
<span className={styles.terminal}>$</span> cat party_details.txt
</h2>
</div>
<div className={styles.partyGrid}>
<div className={styles.partyCard}>
<div className={styles.partyCardInner}>
<div className={styles.partyEmoji}>🍺</div>
<h3>Open Bar</h3>
<p>Free drinks on us! Beer, cocktails, and non-alcoholic options.</p>
<div className={styles.partyTag}>[FREE_DRINKS]</div>
</div>
</div>
<div className={styles.partyCard}>
<div className={styles.partyCardInner}>
<div className={styles.partyEmoji}>⚔️</div>
<h3>Mos Eisley Vibes</h3>
<p>
Party in a wretched hive of scum and villainy. Expect lightsabers and
jailbroken LLMs.
</p>
<div className={styles.partyTag}>[CANTINA_MODE]</div>
</div>
</div>
<div className={styles.partyCard}>
<div className={styles.partyCardInner}>
<div className={styles.partyEmoji}>🤖</div>
<h3>Rebel Alliance Meetup</h3>
<p>Join the resistance against vulnerable AI systems.</p>
<div className={styles.partyTag}>[HACK_THE_EMPIRE]</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* ASCII Art Section */}
<section className={styles.asciiSection}>
<div className={styles.container}>
<pre className={styles.asciiArt}>
{`
____ __ ____
/ __ \\________ ____ ___ ____ / /_/ __/___ ____
/ /_/ / ___/ _ \\/ __ \`__ \\/ __ \\/ __/ /_/ __ \\/ __ \\
/ ____/ / / /_/ / / / / / / /_/ / /_/ __/ /_/ / /_/ /
/_/ /_/ \\___/_/ /_/ /_/ .___/\\__/_/ \\____/\\____/
/_/
x DEF CON 33
`}
</pre>
</div>
</section>
{/* Community Section */}
<section className={styles.communitySection}>
<div className={styles.container}>
<h2 className={styles.sectionTitle}>Join the Movement</h2>
<div className={styles.statsGrid}>
<div className={styles.stat}>
<div className={styles.statNumber}>{`${SITE_CONSTANTS.USER_COUNT_DISPLAY}+`}</div>
<div className={styles.statLabel}>Open Source Users</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>∞</div>
<div className={styles.statLabel}>Drinks Available</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>32M+</div>
<div className={styles.statLabel}>Security Tests Run</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>1</div>
<div className={styles.statLabel}>Party</div>
</div>
</div>
</div>
</section>
{/* Final CTA */}
<section className={styles.finalCta}>
<div className={styles.container}>
<h2>
<span className={styles.blink}>_</span> Don't Miss Out
</h2>
<p>
Space is limited. RSVP now to secure your spot at the AI security party of DEF CON.
</p>
<div className={styles.ctaButtons}>
<a
href="https://lu.ma/ljm23pj6?tk=qGE9ez&utm_source=pf-web"
className={styles.primaryButton}
target="_blank"
rel="noopener noreferrer"
>
<span className={styles.buttonGlitch}>CLAIM YOUR SPOT</span>
</a>
<Link to="/docs/intro" className={styles.secondaryButton}>
Learn About Promptfoo
</Link>
</div>
</div>
</section>
</main>
</Layout>
);
}
@@ -0,0 +1,441 @@
/* Gartner Security 2026 - Professional Enterprise Theme */
/* Color Palette: Navy blue (#1e3a5f), gold (#d4a537), slate */
.gartnerPage {
background: linear-gradient(180deg, #0a1628 0%, #0f1e36 50%, #0f172a 100%);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
}
/* Hero Section */
.hero {
position: relative;
min-height: 80vh;
display: flex;
align-items: center;
overflow: hidden;
}
.heroBackground {
position: relative;
width: 100%;
padding: 4rem 2rem;
}
.heroBackground::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background:
radial-gradient(ellipse at 30% 30%, rgba(30, 58, 95, 0.3) 0%, transparent 50%),
radial-gradient(ellipse at 70% 70%, rgba(212, 165, 55, 0.08) 0%, transparent 50%);
}
/* Risk Meter Animation */
.riskMeterContainer {
display: flex;
justify-content: center;
margin-bottom: 2rem;
}
.riskMeter {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem 1.5rem;
background: rgba(30, 58, 95, 0.3);
border: 1px solid rgba(212, 165, 55, 0.2);
border-radius: 100px;
}
.riskMeterLabel {
font-size: 0.8rem;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.riskMeterBars {
display: flex;
gap: 3px;
align-items: flex-end;
height: 20px;
}
.riskBar {
width: 4px;
background: #d4a537;
border-radius: 2px;
animation: riskPulse 2s ease-in-out infinite;
}
.riskBar:nth-child(1) {
height: 6px;
animation-delay: 0s;
}
.riskBar:nth-child(2) {
height: 10px;
animation-delay: 0.15s;
}
.riskBar:nth-child(3) {
height: 14px;
animation-delay: 0.3s;
}
.riskBar:nth-child(4) {
height: 18px;
animation-delay: 0.45s;
}
.riskBar:nth-child(5) {
height: 12px;
animation-delay: 0.6s;
}
@keyframes riskPulse {
0%,
100% {
opacity: 0.4;
transform: scaleY(1);
}
50% {
opacity: 1;
transform: scaleY(1.2);
}
}
.riskMeterStatus {
font-size: 0.75rem;
color: #22c55e;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.heroContent {
max-width: 1000px;
margin: 0 auto;
text-align: center;
position: relative;
z-index: 1;
}
.badge {
display: inline-block;
padding: 0.5rem 1.5rem;
background: rgba(30, 58, 95, 0.3);
border: 1px solid rgba(212, 165, 55, 0.3);
border-radius: 100px;
color: #d4a537;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 2rem;
}
.heroTitle {
font-size: clamp(2.5rem, 6vw, 4rem);
font-weight: 900;
line-height: 1.1;
margin-bottom: 1.5rem;
background: linear-gradient(135deg, #ffffff 0%, #e2e8f0 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.highlight {
background: linear-gradient(135deg, #d4a537 0%, #f0c85a 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.heroSubtitle {
font-size: 1.25rem;
color: #94a3b8;
max-width: 750px;
margin: 0 auto 2.5rem;
line-height: 1.7;
}
.heroButtons {
display: flex;
gap: 1rem;
justify-content: center;
margin-bottom: 3rem;
flex-wrap: wrap;
}
.primaryButton {
padding: 1rem 2.5rem;
background: linear-gradient(135deg, #d4a537 0%, #c49830 100%);
color: #0a1628;
text-decoration: none;
border-radius: 8px;
font-weight: 700;
font-size: 1rem;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(212, 165, 55, 0.3);
}
.primaryButton:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(212, 165, 55, 0.4);
color: #0a1628;
}
.secondaryButton {
padding: 1rem 2.5rem;
background: transparent;
color: #d4a537;
text-decoration: none;
border: 1px solid rgba(212, 165, 55, 0.4);
border-radius: 8px;
font-weight: 600;
font-size: 1rem;
transition: all 0.3s ease;
}
.secondaryButton:hover {
background: rgba(212, 165, 55, 0.1);
border-color: rgba(212, 165, 55, 0.6);
color: #f0c85a;
}
.eventDetails {
display: flex;
gap: 2.5rem;
justify-content: center;
flex-wrap: wrap;
}
.detail {
display: flex;
align-items: center;
gap: 0.5rem;
color: #94a3b8;
font-size: 1rem;
}
.icon {
width: 1.25rem;
height: 1.25rem;
color: #d4a537;
}
/* Offer Section */
.offerSection {
padding: 5rem 0;
}
.sectionHeader {
text-align: center;
margin-bottom: 3rem;
}
.sectionTitle {
font-size: 2.5rem;
font-weight: 700;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.sectionSubtitle {
font-size: 1.1rem;
color: #94a3b8;
max-width: 600px;
margin: 0 auto;
}
.offerGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.5rem;
}
.offerCard {
background: rgba(30, 58, 95, 0.15);
border: 1px solid rgba(30, 58, 95, 0.3);
border-radius: 16px;
padding: 2rem;
transition: all 0.3s ease;
}
.offerCard:hover {
border-color: rgba(212, 165, 55, 0.3);
transform: translateY(-4px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
}
.cardIcon {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.offerCard h3 {
font-size: 1.25rem;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.offerCard p {
font-size: 0.95rem;
color: #94a3b8;
line-height: 1.7;
margin: 0;
}
/* Stats Section */
.statsSection {
padding: 4rem 0;
background: rgba(30, 58, 95, 0.08);
}
.statsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 2rem;
text-align: center;
}
.stat {
padding: 1.5rem;
}
.statNumber {
font-size: 2.5rem;
font-weight: 800;
background: linear-gradient(135deg, #d4a537 0%, #1e3a5f 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1;
margin-bottom: 0.5rem;
}
.statLabel {
font-size: 0.9rem;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* CTA Section */
.ctaSection {
padding: 5rem 0;
}
.ctaContent {
max-width: 600px;
margin: 0 auto;
text-align: center;
background: linear-gradient(135deg, rgba(30, 58, 95, 0.2) 0%, rgba(212, 165, 55, 0.05) 100%);
border: 1px solid rgba(212, 165, 55, 0.2);
border-radius: 20px;
padding: 3rem 2rem;
}
.ctaTitle {
font-size: 2rem;
font-weight: 700;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.ctaText {
font-size: 1rem;
color: #94a3b8;
margin-bottom: 2rem;
line-height: 1.7;
}
.ctaButtons {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
/* Footer Navigation */
.footerNav {
padding: 2rem 0 3rem;
border-top: 1px solid rgba(30, 58, 95, 0.2);
}
.backLink {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: #94a3b8;
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
.backLink:hover {
color: #d4a537;
}
.backIcon {
width: 1.25rem;
height: 1.25rem;
}
/* Responsive */
@media (max-width: 768px) {
.hero {
min-height: auto;
padding: 4rem 0;
}
.heroTitle {
font-size: 2.25rem;
}
.heroSubtitle {
font-size: 1.1rem;
}
.heroButtons {
flex-direction: column;
align-items: center;
}
.primaryButton,
.secondaryButton {
width: 100%;
max-width: 280px;
text-align: center;
}
.eventDetails {
flex-direction: column;
gap: 1rem;
}
.sectionTitle {
font-size: 2rem;
}
.ctaContent {
padding: 2rem 1.5rem;
}
.ctaButtons {
flex-direction: column;
align-items: center;
}
}
@@ -0,0 +1,252 @@
import React from 'react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import { useForcedTheme } from '@site/src/hooks/useForcedTheme';
import Layout from '@theme/Layout';
import { SITE_CONSTANTS } from '../../constants';
import styles from './gartner-security-2026.module.css';
export default function GartnerSecurity2026(): React.ReactElement {
useForcedTheme('dark');
const handleSmoothScroll = (e: React.MouseEvent<HTMLAnchorElement>, targetId: string) => {
e.preventDefault();
const element = document.querySelector(targetId);
if (element) {
const offset = 80;
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.scrollY - offset;
window.scrollTo({ top: offsetPosition, behavior: 'smooth' });
}
};
return (
<Layout
title="Promptfoo at Gartner Security & Risk Management Summit 2026"
description="Turn AI risk into a measurable program. Meet Promptfoo for briefings on continuous red teaming, guardrails, and executive reporting."
>
<Head>
<meta
property="og:title"
content="Promptfoo at Gartner Security & Risk Management Summit 2026"
/>
<meta
property="og:description"
content="Turn AI risk into a measurable program. Meet Promptfoo for briefings on continuous red teaming, guardrails, and executive reporting. Jun 1-3, National Harbor MD."
/>
<meta
property="og:image"
content="https://www.promptfoo.dev/img/events/gartner-security-2026.jpg"
/>
<meta property="og:url" content="https://www.promptfoo.dev/events/gartner-security-2026" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="Promptfoo at Gartner Security & Risk Management Summit 2026"
/>
<meta
name="twitter:description"
content="Meet Promptfoo at Gartner Security 2026. Enterprise AI security, analyst briefings, and CISO discussions."
/>
<meta
name="twitter:image"
content="https://www.promptfoo.dev/img/events/gartner-security-2026.jpg"
/>
<meta
name="keywords"
content="Gartner Security 2026, risk management summit, AI security, enterprise security, CISO, Washington DC, AI governance"
/>
<link rel="canonical" href="https://www.promptfoo.dev/events/gartner-security-2026" />
</Head>
<main className={styles.gartnerPage}>
{/* Hero Section */}
<section className={styles.hero}>
<div className={styles.heroBackground}>
<div className={styles.heroContent}>
<div className={styles.badge}>Gartner Security & Risk Management Summit 2026</div>
<h1 className={styles.heroTitle}>
Make AI Risk
<br />
<span className={styles.highlight}>Measurable</span>
</h1>
<p className={styles.heroSubtitle}>
If you're building an AI security program, we can help you move from ad hoc testing
to continuous coverage. Meet Promptfoo for demos of automated red teaming, runtime
guardrails, and reporting security leadership can track.
</p>
{/* Risk Meter Animation */}
<div className={styles.riskMeterContainer}>
<div className={styles.riskMeter}>
<span className={styles.riskMeterLabel}>Risk Coverage</span>
<div className={styles.riskMeterBars}>
<div className={styles.riskBar} />
<div className={styles.riskBar} />
<div className={styles.riskBar} />
<div className={styles.riskBar} />
<div className={styles.riskBar} />
</div>
<span className={styles.riskMeterStatus}>Monitoring</span>
</div>
</div>
<div className={styles.heroButtons}>
<a
href="#learn-more"
className={styles.primaryButton}
onClick={(e) => handleSmoothScroll(e, '#learn-more')}
>
Learn More
</a>
<Link to="/contact" className={styles.secondaryButton}>
Schedule a Briefing
</Link>
</div>
<div className={styles.eventDetails}>
<div className={styles.detail}>
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span>June 1-3, 2026</span>
</div>
<div className={styles.detail}>
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<span>National Harbor, MD</span>
</div>
</div>
</div>
</div>
</section>
{/* What to Expect Section */}
<section className={styles.offerSection} id="learn-more">
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>What to Expect</h2>
<p className={styles.sectionSubtitle}>
Tools and frameworks for security leaders building AI governance programs.
</p>
</div>
<div className={styles.offerGrid}>
<div className={styles.offerCard}>
<div className={styles.cardIcon}>📊</div>
<h3>Operationalize AI Security</h3>
<p>
Learn how to move from ad hoc testing to structured coverage that tracks risk
reduction over time.
</p>
</div>
<div className={styles.offerCard}>
<div className={styles.cardIcon}>📋</div>
<h3>Executive-ready Reporting</h3>
<p>
See dashboards and artifacts that translate technical findings into board-level
summaries.
</p>
</div>
<div className={styles.offerCard}>
<div className={styles.cardIcon}>🎯</div>
<h3>Briefings and Architecture Reviews</h3>
<p>
Book dedicated time with our team to walk through your program design and get
personalized recommendations.
</p>
</div>
</div>
</div>
</section>
{/* Stats Section */}
<section className={styles.statsSection}>
<div className={styles.container}>
<div className={styles.statsGrid}>
<div className={styles.stat}>
<div className={styles.statNumber}>{SITE_CONSTANTS.USER_COUNT_DISPLAY}+</div>
<div className={styles.statLabel}>Developers</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>80+</div>
<div className={styles.statLabel}>Fortune 500 Companies</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>Jun 1-3</div>
<div className={styles.statLabel}>National Harbor, MD</div>
</div>
</div>
</div>
</section>
{/* Final CTA */}
<section className={styles.ctaSection}>
<div className={styles.container}>
<div className={styles.ctaContent}>
<h2 className={styles.ctaTitle}>Attending Gartner Security?</h2>
<p className={styles.ctaText}>
Reach out to book a demo or architecture review during the summit.
</p>
<div className={styles.ctaButtons}>
<Link to="/contact" className={styles.primaryButton}>
Schedule a Meeting
</Link>
<Link to="https://discord.gg/promptfoo" className={styles.secondaryButton}>
Join our Discord
</Link>
</div>
</div>
</div>
</section>
{/* Footer Navigation */}
<section className={styles.footerNav}>
<div className={styles.container}>
<Link to="/events" className={styles.backLink}>
<svg
className={styles.backIcon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back to All Events
</Link>
</div>
</section>
</main>
</Layout>
);
}
@@ -0,0 +1,503 @@
/* HumanX 2026 - AI Innovation Theme */
/* Color Palette: Deep purple (#7c3aed), cyan (#06b6d4), warm white */
.humanxPage {
background: linear-gradient(180deg, #0f0a1f 0%, #1a1025 50%, #0f172a 100%);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
}
/* Hero Section */
.hero {
position: relative;
min-height: 80vh;
display: flex;
align-items: center;
overflow: hidden;
}
.heroBackground {
position: relative;
width: 100%;
padding: 4rem 2rem;
}
.heroBackground::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background:
radial-gradient(ellipse at 30% 30%, rgba(124, 58, 237, 0.15) 0%, transparent 50%),
radial-gradient(ellipse at 70% 70%, rgba(6, 182, 212, 0.1) 0%, transparent 50%);
animation: float 20s ease-in-out infinite;
}
@keyframes float {
0%,
100% {
transform: translate(0, 0);
}
50% {
transform: translate(-2%, -2%);
}
}
/* AI Thinking Animation */
.aiThinkingContainer {
display: flex;
justify-content: center;
margin-bottom: 2rem;
}
.aiThinking {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1.5rem;
background: rgba(6, 182, 212, 0.1);
border: 1px solid rgba(6, 182, 212, 0.2);
border-radius: 100px;
font-size: 0.875rem;
color: #67e8f9;
}
.aiThinkingDots {
display: flex;
gap: 4px;
}
.aiThinkingDot {
width: 6px;
height: 6px;
background: #06b6d4;
border-radius: 50%;
animation: aiPulse 1.4s ease-in-out infinite;
}
.aiThinkingDot:nth-child(2) {
animation-delay: 0.2s;
}
.aiThinkingDot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes aiPulse {
0%,
60%,
100% {
opacity: 0.3;
transform: scale(1);
}
30% {
opacity: 1;
transform: scale(1.2);
}
}
/* Neural Nodes Floating Animation */
.neuralNodes {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
overflow: hidden;
}
.node {
position: absolute;
width: 8px;
height: 8px;
background: rgba(124, 58, 237, 0.4);
border-radius: 50%;
animation: nodeFloat 8s ease-in-out infinite;
}
.node::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 60px;
height: 1px;
background: linear-gradient(90deg, rgba(124, 58, 237, 0.3), transparent);
transform-origin: left center;
}
.node:nth-child(1) {
top: 20%;
left: 10%;
animation-delay: 0s;
}
.node:nth-child(2) {
top: 60%;
left: 85%;
animation-delay: 2s;
}
.node:nth-child(2)::after {
transform: rotate(180deg);
}
.node:nth-child(3) {
top: 80%;
left: 20%;
animation-delay: 4s;
}
.node:nth-child(4) {
top: 30%;
left: 90%;
animation-delay: 1s;
}
.node:nth-child(4)::after {
transform: rotate(200deg);
}
.node:nth-child(5) {
top: 70%;
left: 5%;
animation-delay: 3s;
}
@keyframes nodeFloat {
0%,
100% {
transform: translateY(0) scale(1);
opacity: 0.6;
}
50% {
transform: translateY(-20px) scale(1.1);
opacity: 1;
}
}
.heroContent {
max-width: 1000px;
margin: 0 auto;
text-align: center;
position: relative;
z-index: 1;
}
.badge {
display: inline-block;
padding: 0.5rem 1.5rem;
background: rgba(124, 58, 237, 0.15);
border: 1px solid rgba(124, 58, 237, 0.3);
border-radius: 100px;
color: #a78bfa;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 2rem;
}
.heroTitle {
font-size: clamp(2.5rem, 6vw, 4rem);
font-weight: 900;
line-height: 1.1;
margin-bottom: 1.5rem;
background: linear-gradient(135deg, #ffffff 0%, #e2e8f0 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.highlight {
background: linear-gradient(135deg, #7c3aed 0%, #06b6d4 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.heroSubtitle {
font-size: 1.25rem;
color: #94a3b8;
max-width: 700px;
margin: 0 auto 2.5rem;
line-height: 1.7;
}
.heroButtons {
display: flex;
gap: 1rem;
justify-content: center;
margin-bottom: 3rem;
flex-wrap: wrap;
}
.primaryButton {
padding: 1rem 2.5rem;
background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%);
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
font-size: 1rem;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(124, 58, 237, 0.3);
}
.primaryButton:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(124, 58, 237, 0.4);
color: white;
}
.secondaryButton {
padding: 1rem 2.5rem;
background: transparent;
color: #a78bfa;
text-decoration: none;
border: 1px solid rgba(124, 58, 237, 0.4);
border-radius: 8px;
font-weight: 600;
font-size: 1rem;
transition: all 0.3s ease;
}
.secondaryButton:hover {
background: rgba(124, 58, 237, 0.1);
border-color: rgba(124, 58, 237, 0.6);
color: white;
}
.eventDetails {
display: flex;
gap: 2.5rem;
justify-content: center;
flex-wrap: wrap;
}
.detail {
display: flex;
align-items: center;
gap: 0.5rem;
color: #94a3b8;
font-size: 1rem;
}
.icon {
width: 1.25rem;
height: 1.25rem;
color: #7c3aed;
}
/* Demo Section */
.demoSection {
padding: 5rem 0;
}
.sectionHeader {
text-align: center;
margin-bottom: 3rem;
}
.sectionTitle {
font-size: 2.5rem;
font-weight: 700;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.sectionSubtitle {
font-size: 1.1rem;
color: #94a3b8;
max-width: 600px;
margin: 0 auto;
}
.demoGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.5rem;
}
.demoCard {
background: rgba(124, 58, 237, 0.05);
border: 1px solid rgba(124, 58, 237, 0.15);
border-radius: 16px;
padding: 2rem;
transition: all 0.3s ease;
}
.demoCard:hover {
border-color: rgba(124, 58, 237, 0.3);
transform: translateY(-4px);
box-shadow: 0 20px 40px rgba(124, 58, 237, 0.1);
}
.cardIcon {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.demoCard h3 {
font-size: 1.25rem;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.demoCard p {
font-size: 0.95rem;
color: #94a3b8;
line-height: 1.7;
margin: 0;
}
/* Stats Section */
.statsSection {
padding: 4rem 0;
background: rgba(124, 58, 237, 0.03);
}
.statsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 2rem;
text-align: center;
}
.stat {
padding: 1.5rem;
}
.statNumber {
font-size: 2.5rem;
font-weight: 800;
background: linear-gradient(135deg, #7c3aed 0%, #06b6d4 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1;
margin-bottom: 0.5rem;
}
.statLabel {
font-size: 0.9rem;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* CTA Section */
.ctaSection {
padding: 5rem 0;
}
.ctaContent {
max-width: 600px;
margin: 0 auto;
text-align: center;
background: linear-gradient(135deg, rgba(124, 58, 237, 0.1) 0%, rgba(6, 182, 212, 0.05) 100%);
border: 1px solid rgba(124, 58, 237, 0.2);
border-radius: 20px;
padding: 3rem 2rem;
}
.ctaTitle {
font-size: 2rem;
font-weight: 700;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.ctaText {
font-size: 1rem;
color: #94a3b8;
margin-bottom: 2rem;
line-height: 1.7;
}
.ctaButtons {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
/* Footer Navigation */
.footerNav {
padding: 2rem 0 3rem;
border-top: 1px solid rgba(124, 58, 237, 0.1);
}
.backLink {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: #94a3b8;
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
.backLink:hover {
color: #7c3aed;
}
.backIcon {
width: 1.25rem;
height: 1.25rem;
}
/* Responsive */
@media (max-width: 768px) {
.hero {
min-height: auto;
padding: 4rem 0;
}
.heroTitle {
font-size: 2.25rem;
}
.heroSubtitle {
font-size: 1.1rem;
}
.heroButtons {
flex-direction: column;
align-items: center;
}
.primaryButton,
.secondaryButton {
width: 100%;
max-width: 280px;
text-align: center;
}
.eventDetails {
flex-direction: column;
gap: 1rem;
}
.sectionTitle {
font-size: 2rem;
}
.ctaContent {
padding: 2rem 1.5rem;
}
.ctaButtons {
flex-direction: column;
align-items: center;
}
}
+243
View File
@@ -0,0 +1,243 @@
import React from 'react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import { useForcedTheme } from '@site/src/hooks/useForcedTheme';
import Layout from '@theme/Layout';
import { SITE_CONSTANTS } from '../../constants';
import styles from './humanx-2026.module.css';
export default function HumanX2026(): React.ReactElement {
useForcedTheme('dark');
const handleSmoothScroll = (e: React.MouseEvent<HTMLAnchorElement>, targetId: string) => {
e.preventDefault();
const element = document.querySelector(targetId);
if (element) {
const offset = 80;
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.scrollY - offset;
window.scrollTo({ top: offsetPosition, behavior: 'smooth' });
}
};
return (
<Layout
title="Promptfoo at HumanX 2026"
description="For AI leaders shipping real products: see how to evaluate and secure LLM apps and agents without slowing teams down."
>
<Head>
<meta property="og:title" content="Promptfoo at HumanX 2026 | AI Security" />
<meta
property="og:description"
content="For AI leaders shipping real products: see how to evaluate and secure LLM apps and agents without slowing teams down. Apr 6-9, Moscone Center South, SF."
/>
<meta property="og:image" content="https://www.promptfoo.dev/img/events/humanx-2026.jpg" />
<meta property="og:url" content="https://www.promptfoo.dev/events/humanx-2026" />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Promptfoo at HumanX 2026 | AI Security" />
<meta
name="twitter:description"
content="Meet Promptfoo at HumanX 2026. AI security demos, enterprise solutions, and networking with AI leaders."
/>
<meta name="twitter:image" content="https://www.promptfoo.dev/img/events/humanx-2026.jpg" />
<meta
name="keywords"
content="HumanX 2026, AI conference, AI security, LLM security, enterprise AI, San Francisco, AI leadership"
/>
<link rel="canonical" href="https://www.promptfoo.dev/events/humanx-2026" />
</Head>
<main className={styles.humanxPage}>
{/* Hero Section */}
<section className={styles.hero}>
{/* Floating Neural Nodes */}
<div className={styles.neuralNodes}>
<div className={styles.node} />
<div className={styles.node} />
<div className={styles.node} />
<div className={styles.node} />
<div className={styles.node} />
</div>
<div className={styles.heroBackground}>
<div className={styles.heroContent}>
<div className={styles.badge}>HumanX 2026</div>
<h1 className={styles.heroTitle}>
Ship AI
<br />
<span className={styles.highlight}>You Can Trust</span>
</h1>
<p className={styles.heroSubtitle}>
AI is moving fast. Security and evaluation need to keep up. Meet Promptfoo for live
demos on testing and securing LLM features across copilots, RAG, and agents, before
launch and continuously in production.
</p>
{/* AI Thinking Animation */}
<div className={styles.aiThinkingContainer}>
<div className={styles.aiThinking}>
<span>AI is evaluating</span>
<div className={styles.aiThinkingDots}>
<div className={styles.aiThinkingDot} />
<div className={styles.aiThinkingDot} />
<div className={styles.aiThinkingDot} />
</div>
</div>
</div>
<div className={styles.heroButtons}>
<a
href="#learn-more"
className={styles.primaryButton}
onClick={(e) => handleSmoothScroll(e, '#learn-more')}
>
Learn More
</a>
<Link to="/contact" className={styles.secondaryButton}>
Schedule a Meeting
</Link>
</div>
<div className={styles.eventDetails}>
<div className={styles.detail}>
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span>April 6-9, 2026</span>
</div>
<div className={styles.detail}>
<svg
className={styles.icon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<span>Moscone Center South, San Francisco</span>
</div>
</div>
</div>
</div>
</section>
{/* What We'll Show Section */}
<section className={styles.demoSection} id="learn-more">
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>What to Expect</h2>
<p className={styles.sectionSubtitle}>Practical workflows for AI teams.</p>
</div>
<div className={styles.demoGrid}>
<div className={styles.demoCard}>
<div className={styles.cardIcon}>📊</div>
<h3>Evals That Measure What Matters</h3>
<p>
Reliability, safety, and policy adherence, with repeatable benchmarks you can
track over time.
</p>
</div>
<div className={styles.demoCard}>
<div className={styles.cardIcon}>🎯</div>
<h3>Red Teaming for Agents and Tools</h3>
<p>
Find risky actions, data exfiltration paths, and permission misuse before your
users do.
</p>
</div>
<div className={styles.demoCard}>
<div className={styles.cardIcon}>📋</div>
<h3>Governance Without Bottlenecks</h3>
<p>
Turn testing results into auditable artifacts that support reviews, approvals, and
rollouts.
</p>
</div>
</div>
</div>
</section>
{/* Stats Section */}
<section className={styles.statsSection}>
<div className={styles.container}>
<div className={styles.statsGrid}>
<div className={styles.stat}>
<div className={styles.statNumber}>{SITE_CONSTANTS.USER_COUNT_DISPLAY}+</div>
<div className={styles.statLabel}>Developers</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>80+</div>
<div className={styles.statLabel}>Fortune 500 Companies</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>Apr 6-9</div>
<div className={styles.statLabel}>San Francisco</div>
</div>
</div>
</div>
</section>
{/* Final CTA */}
<section className={styles.ctaSection}>
<div className={styles.container}>
<div className={styles.ctaContent}>
<h2 className={styles.ctaTitle}>Attending HumanX?</h2>
<p className={styles.ctaText}>
Book a short demo and we'll map a testing plan to your AI roadmap.
</p>
<div className={styles.ctaButtons}>
<Link to="/contact" className={styles.primaryButton}>
Schedule a Meeting
</Link>
<Link to="https://discord.gg/promptfoo" className={styles.secondaryButton}>
Join our Discord
</Link>
</div>
</div>
</div>
</section>
{/* Footer Navigation */}
<section className={styles.footerNav}>
<div className={styles.container}>
<Link to="/events" className={styles.backLink}>
<svg
className={styles.backIcon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back to All Events
</Link>
</div>
</section>
</main>
</Layout>
);
}
+402
View File
@@ -0,0 +1,402 @@
.main {
padding: 0;
background: linear-gradient(180deg, rgba(248, 250, 252, 0.5) 0%, transparent 30%);
}
[data-theme='dark'] .main {
background: linear-gradient(180deg, rgba(15, 23, 42, 0.3) 0%, transparent 30%);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 3.5rem 1.5rem 5rem;
}
/* Header */
.header {
text-align: center;
margin-bottom: 3.5rem;
position: relative;
}
.header::after {
content: '';
position: absolute;
bottom: -1.75rem;
left: 50%;
transform: translateX(-50%);
width: 60px;
height: 3px;
background: linear-gradient(90deg, #2563eb 0%, #1d4ed8 100%);
border-radius: 2px;
opacity: 0.8;
}
.title {
font-size: 3rem;
font-weight: 800;
margin: 0 0 1rem 0;
color: var(--ifm-heading-color);
letter-spacing: -0.03em;
line-height: 1.1;
}
.subtitle {
font-size: 1.1875rem;
color: var(--ifm-color-emphasis-600);
margin: 0;
max-width: 560px;
margin-left: auto;
margin-right: auto;
line-height: 1.65;
font-weight: 400;
}
/* Section Headers */
.sectionHeader {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
margin-top: 3rem;
}
.sectionTitle {
font-size: 1.25rem;
font-weight: 700;
color: var(--ifm-heading-color);
margin: 0;
letter-spacing: -0.01em;
}
.sectionLine {
flex: 1;
height: 1px;
background: linear-gradient(90deg, var(--ifm-color-emphasis-200) 0%, transparent 100%);
}
/* Events Grid */
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.75rem;
}
/* Empty State */
.empty {
text-align: center;
padding: 5rem 2rem;
background: linear-gradient(135deg, rgba(248, 250, 252, 0.8) 0%, rgba(241, 245, 249, 0.6) 100%);
border-radius: 16px;
position: relative;
}
.empty::before {
content: '';
position: absolute;
inset: 0;
border-radius: 16px;
padding: 1px;
background: linear-gradient(135deg, rgba(148, 163, 184, 0.2) 0%, rgba(148, 163, 184, 0.05) 100%);
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: exclude;
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
pointer-events: none;
}
[data-theme='dark'] .empty {
background: linear-gradient(135deg, rgba(30, 41, 59, 0.5) 0%, rgba(15, 23, 42, 0.7) 100%);
}
.empty p {
color: var(--ifm-color-emphasis-600);
margin: 0 0 1.25rem 0;
font-size: 1.0625rem;
}
.resetButton {
padding: 0.625rem 1.25rem;
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition:
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.2s ease;
box-shadow:
0 2px 8px rgba(37, 99, 235, 0.25),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.resetButton:hover {
transform: translateY(-2px);
box-shadow:
0 4px 16px rgba(37, 99, 235, 0.35),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.resetButton:active {
transform: translateY(0);
}
/* CTA Section */
.cta {
margin-top: 5rem;
padding: 3.5rem;
background: linear-gradient(135deg, #f0f7ff 0%, #e0edff 50%, #f5f9ff 100%);
border-radius: 20px;
text-align: center;
position: relative;
overflow: hidden;
}
.cta::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(ellipse 60% 50% at 20% 30%, rgba(37, 99, 235, 0.1) 0%, transparent 50%),
radial-gradient(ellipse 50% 40% at 80% 70%, rgba(99, 102, 241, 0.08) 0%, transparent 50%);
pointer-events: none;
}
.cta::after {
content: '';
position: absolute;
top: -1px;
left: -1px;
right: -1px;
bottom: -1px;
border-radius: 20px;
padding: 1px;
background: linear-gradient(
135deg,
rgba(37, 99, 235, 0.25) 0%,
rgba(99, 102, 241, 0.1) 50%,
rgba(37, 99, 235, 0.15) 100%
);
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: exclude;
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
pointer-events: none;
}
[data-theme='dark'] .cta {
background: linear-gradient(
135deg,
rgba(37, 99, 235, 0.15) 0%,
rgba(30, 41, 59, 0.8) 50%,
rgba(15, 23, 42, 0.9) 100%
);
}
[data-theme='dark'] .cta::before {
background:
radial-gradient(ellipse 60% 50% at 20% 30%, rgba(37, 99, 235, 0.2) 0%, transparent 50%),
radial-gradient(ellipse 50% 40% at 80% 70%, rgba(99, 102, 241, 0.15) 0%, transparent 50%);
}
[data-theme='dark'] .cta::after {
background: linear-gradient(
135deg,
rgba(37, 99, 235, 0.4) 0%,
rgba(99, 102, 241, 0.15) 50%,
rgba(37, 99, 235, 0.25) 100%
);
}
.ctaContent {
max-width: 540px;
margin: 0 auto;
position: relative;
z-index: 1;
}
.ctaTitle {
font-size: 1.75rem;
font-weight: 750;
margin: 0 0 0.875rem 0;
color: var(--ifm-heading-color);
letter-spacing: -0.02em;
}
.ctaDescription {
font-size: 1.0625rem;
color: var(--ifm-color-emphasis-700);
margin: 0 0 1.75rem 0;
line-height: 1.65;
}
[data-theme='dark'] .ctaDescription {
color: var(--ifm-color-emphasis-600);
}
.ctaButton {
display: inline-flex;
align-items: center;
gap: 0.625rem;
padding: 0.9375rem 2rem;
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
color: white;
border-radius: 10px;
font-size: 0.9375rem;
font-weight: 600;
text-decoration: none;
box-shadow:
0 4px 14px rgba(37, 99, 235, 0.35),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
transition:
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.2s ease;
}
.ctaButton:hover {
transform: translateY(-2px);
box-shadow:
0 8px 20px rgba(37, 99, 235, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
color: white;
text-decoration: none;
}
.ctaButton:active {
transform: translateY(0);
}
/* Responsive Design */
@media (max-width: 996px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
.title {
font-size: 2.5rem;
}
}
@media (max-width: 768px) {
.container {
padding: 2.5rem 1rem 4rem;
}
.header {
margin-bottom: 2.5rem;
}
.header::after {
bottom: -1.25rem;
width: 50px;
}
.title {
font-size: 2rem;
}
.subtitle {
font-size: 1rem;
}
.grid {
grid-template-columns: 1fr;
gap: 1.25rem;
}
.cta {
margin-top: 3.5rem;
padding: 2.5rem 1.5rem;
border-radius: 16px;
}
.cta::after {
border-radius: 16px;
}
.ctaTitle {
font-size: 1.375rem;
}
.ctaDescription {
font-size: 0.9375rem;
margin-bottom: 1.5rem;
}
.ctaButton {
width: 100%;
justify-content: center;
padding: 1rem 1.5rem;
}
.sectionHeader {
margin-top: 2rem;
margin-bottom: 1rem;
}
.sectionTitle {
font-size: 1.125rem;
}
}
/* Small Mobile */
@media (max-width: 480px) {
.container {
padding: 2rem 1rem 3.5rem;
}
.title {
font-size: 1.75rem;
}
.empty {
padding: 3.5rem 1.5rem;
}
.cta {
padding: 2rem 1.25rem;
}
.ctaTitle {
font-size: 1.25rem;
}
}
/* Touch device optimization */
@media (hover: none) {
.resetButton:hover {
transform: none;
box-shadow:
0 2px 8px rgba(37, 99, 235, 0.25),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.resetButton:active {
transform: scale(0.98);
}
.ctaButton:hover {
transform: none;
box-shadow:
0 4px 14px rgba(37, 99, 235, 0.35),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
}
.ctaButton:active {
transform: scale(0.98);
}
}
+162
View File
@@ -0,0 +1,162 @@
import React, { useMemo, useState } from 'react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import Layout from '@theme/Layout';
import { EventCard, EventFilters, FeaturedEvent } from '../../components/Events';
import {
type Event,
events,
getFeaturedEvent,
getPastEvents,
getUpcomingEvents,
} from '../../data/events';
import styles from './index.module.css';
import type { FilterStatus, FilterYear } from '../../components/Events';
export default function EventsPage(): React.ReactElement {
const [statusFilter, setStatusFilter] = useState<FilterStatus>('all');
const [yearFilter, setYearFilter] = useState<FilterYear>('all');
const featuredEvent = getFeaturedEvent();
// Get available years from events
const availableYears = useMemo(() => {
const years = new Set(events.map((event) => new Date(event.startDate).getFullYear()));
return Array.from(years).sort((a, b) => b - a);
}, []);
// Filter events based on selected filters
const filteredEvents = useMemo(() => {
let result: Event[];
// Apply status filter
if (statusFilter === 'upcoming') {
result = getUpcomingEvents();
} else if (statusFilter === 'past') {
result = getPastEvents();
} else {
// Sort all events: upcoming first (by date asc), then past (by date desc)
const upcoming = getUpcomingEvents();
const past = getPastEvents();
result = [...upcoming, ...past];
}
// Apply year filter
if (yearFilter !== 'all') {
result = result.filter((event) => new Date(event.startDate).getFullYear() === yearFilter);
}
return result;
}, [statusFilter, yearFilter]);
// Calculate event counts for filters
const eventCounts = useMemo(() => {
let allEvents = events;
let upcomingEvents = getUpcomingEvents();
let pastEvents = getPastEvents();
// If year filter is applied, filter counts
if (yearFilter !== 'all') {
allEvents = events.filter((event) => new Date(event.startDate).getFullYear() === yearFilter);
upcomingEvents = upcomingEvents.filter(
(event) => new Date(event.startDate).getFullYear() === yearFilter,
);
pastEvents = pastEvents.filter(
(event) => new Date(event.startDate).getFullYear() === yearFilter,
);
}
return {
all: allEvents.length,
upcoming: upcomingEvents.length,
past: pastEvents.length,
};
}, [yearFilter]);
return (
<Layout
title="Events | Promptfoo"
description="Meet the Promptfoo team at conferences and events. See live AI security demos, attend workshops, and connect with our security experts."
>
<Head>
<meta property="og:title" content="Promptfoo Events | AI Security Conferences" />
<meta
property="og:description"
content="Meet the Promptfoo team at security conferences. See live AI red teaming demos, attend workshops, and connect with our experts."
/>
<meta property="og:type" content="website" />
<meta property="og:url" content="https://promptfoo.dev/events" />
<meta property="og:image" content="https://www.promptfoo.dev/img/og/events-og.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://www.promptfoo.dev/img/og/events-og.png" />
<link rel="canonical" href="https://promptfoo.dev/events" />
</Head>
<main className={styles.main}>
<div className={styles.container}>
{/* Page Header */}
<header className={styles.header}>
<h1 className={styles.title}>Events</h1>
<p className={styles.subtitle}>
Meet the team, see live demos, and learn how enterprises secure their AI applications.
</p>
</header>
{/* Featured Event */}
{featuredEvent && <FeaturedEvent event={featuredEvent} />}
{/* Filters */}
<EventFilters
selectedStatus={statusFilter}
selectedYear={yearFilter}
availableYears={availableYears}
onStatusChange={setStatusFilter}
onYearChange={setYearFilter}
eventCounts={eventCounts}
/>
{/* Events Grid */}
{filteredEvents.length > 0 ? (
<div className={styles.grid}>
{filteredEvents.map((event) => (
<EventCard key={event.id} event={event} />
))}
</div>
) : (
<div className={styles.empty}>
<p>No events found matching your filters.</p>
<button
type="button"
className={styles.resetButton}
onClick={() => {
setStatusFilter('all');
setYearFilter('all');
}}
>
Clear filters
</button>
</div>
)}
{/* CTA Section */}
<section className={styles.cta}>
<div className={styles.ctaContent}>
<h2 className={styles.ctaTitle}>Can't make it to an event?</h2>
<p className={styles.ctaDescription}>
Book a personalized demo with our team and see how Promptfoo can secure your AI
applications.
</p>
<Link to="/contact" className={styles.ctaButton}>
Book a demo
</Link>
</div>
</section>
</div>
</main>
</Layout>
);
}
+371
View File
@@ -0,0 +1,371 @@
/* RSA 2025 - Enterprise Security Theme */
/* Color Palette: RSA blue (#0066CC), cyber cyan (#06B6D4), professional navy (#1e3a5f) */
.rsaPage {
background: #0a0f1a;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
}
/* Hero Banner */
.heroBanner {
position: relative;
height: 400px;
overflow: hidden;
}
.bannerImage {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.bannerOverlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(180deg, rgba(10, 15, 26, 0.4) 0%, rgba(10, 15, 26, 0.7) 100%);
}
.bannerContent {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 3rem;
background: linear-gradient(0deg, rgba(10, 15, 26, 0.95) 0%, transparent 100%);
}
.badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(6, 182, 212, 0.2);
border: 1px solid rgba(6, 182, 212, 0.4);
color: #67e8f9;
border-radius: 100px;
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 1rem;
}
.badgeIcon {
font-size: 1rem;
}
.heroTitle {
font-size: clamp(2rem, 6vw, 3.5rem);
font-weight: 800;
line-height: 1.1;
color: #f1f5f9;
margin: 0;
}
.highlight {
background: linear-gradient(135deg, #06b6d4 0%, #0066cc 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Hero Content Section */
.heroContent {
padding: 3rem 0;
text-align: center;
border-bottom: 1px solid rgba(6, 182, 212, 0.1);
}
.heroSubtitle {
font-size: 1.25rem;
color: #94a3b8;
max-width: 600px;
margin: 0 auto 2rem;
line-height: 1.7;
}
.eventDetails {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2rem;
margin-bottom: 2rem;
}
.detail {
display: flex;
align-items: center;
gap: 0.5rem;
color: #cbd5e1;
font-size: 0.95rem;
}
.icon {
width: 1.25rem;
height: 1.25rem;
stroke-width: 2;
color: #06b6d4;
}
.heroCtas {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.primaryCta {
padding: 0.875rem 2rem;
background: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%);
color: white;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
transition: all 0.3s ease;
}
.primaryCta:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(6, 182, 212, 0.4);
color: white;
}
.secondaryCta {
padding: 0.875rem 2rem;
background: transparent;
color: #93c5fd;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
border: 1px solid rgba(0, 102, 204, 0.4);
transition: all 0.3s ease;
}
.secondaryCta:hover {
background: rgba(0, 102, 204, 0.1);
color: white;
}
/* Sections */
.recapSection,
.showcaseSection,
.ctaSection {
padding: 4rem 0;
}
.recapSection {
background: rgba(6, 182, 212, 0.02);
}
.sectionHeader {
text-align: center;
margin-bottom: 3rem;
}
.sectionTitle {
font-size: 2rem;
font-weight: 700;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.sectionSubtitle {
font-size: 1.1rem;
color: #94a3b8;
max-width: 500px;
margin: 0 auto;
}
/* Recap Grid */
.recapGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.5rem;
}
.recapCard {
background: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(6, 182, 212, 0.15);
border-radius: 12px;
padding: 1.5rem;
transition: all 0.3s ease;
}
.recapCard:hover {
border-color: rgba(6, 182, 212, 0.3);
transform: translateY(-2px);
}
.cardIcon {
font-size: 2rem;
margin-bottom: 1rem;
}
.recapCard h3 {
font-size: 1.125rem;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 0.5rem;
}
.recapCard p {
font-size: 0.9rem;
color: #94a3b8;
line-height: 1.6;
margin: 0;
}
/* Showcase Grid */
.showcaseGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 2rem;
}
.showcaseItem {
padding: 1.5rem 0;
border-left: 2px solid rgba(6, 182, 212, 0.3);
padding-left: 1.5rem;
}
.showcaseNumber {
font-size: 2.5rem;
font-weight: 800;
background: linear-gradient(135deg, #06b6d4 0%, #0066cc 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.75rem;
line-height: 1;
}
.showcaseItem h3 {
font-size: 1.125rem;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 0.5rem;
}
.showcaseItem p {
font-size: 0.9rem;
color: #94a3b8;
line-height: 1.6;
margin: 0;
}
/* CTA Section */
.ctaContent {
max-width: 600px;
margin: 0 auto;
text-align: center;
background: linear-gradient(135deg, rgba(6, 182, 212, 0.08) 0%, rgba(0, 102, 204, 0.08) 100%);
border: 1px solid rgba(6, 182, 212, 0.2);
border-radius: 16px;
padding: 3rem 2rem;
}
.ctaTitle {
font-size: 1.75rem;
font-weight: 700;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.ctaText {
font-size: 1rem;
color: #94a3b8;
margin-bottom: 1.5rem;
line-height: 1.7;
}
.ctaButtons {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
/* Footer Navigation */
.footerNav {
padding: 2rem 0 3rem;
border-top: 1px solid rgba(6, 182, 212, 0.1);
}
.backLink {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: #94a3b8;
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
.backLink:hover {
color: #06b6d4;
}
.backIcon {
width: 1.25rem;
height: 1.25rem;
}
/* Responsive */
@media (max-width: 768px) {
.heroBanner {
height: 300px;
}
.bannerContent {
padding: 2rem 1.5rem;
}
.heroTitle {
font-size: 2rem;
}
.heroSubtitle {
font-size: 1.1rem;
}
.eventDetails {
flex-direction: column;
align-items: center;
gap: 1rem;
}
.heroCtas {
flex-direction: column;
align-items: center;
}
.primaryCta,
.secondaryCta {
width: 100%;
max-width: 280px;
text-align: center;
}
.ctaContent {
padding: 2rem 1.5rem;
}
.ctaButtons {
flex-direction: column;
align-items: center;
}
.recapSection,
.showcaseSection,
.ctaSection {
padding: 3rem 0;
}
}
+255
View File
@@ -0,0 +1,255 @@
import React from 'react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import { useForcedTheme } from '@site/src/hooks/useForcedTheme';
import Layout from '@theme/Layout';
import styles from './rsa-2025.module.css';
export default function RSA2025(): React.ReactElement {
useForcedTheme('dark');
const handleSmoothScroll = (e: React.MouseEvent<HTMLAnchorElement>, targetId: string) => {
e.preventDefault();
const element = document.querySelector(targetId);
if (element) {
const offset = 80;
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - offset;
window.scrollTo({ top: offsetPosition, behavior: 'smooth' });
}
};
return (
<Layout
title="Promptfoo at RSA Conference 2025"
description="Recap of Promptfoo at RSA Conference 2025. AI red teaming demos and enterprise AI security discussions."
>
<Head>
<meta property="og:title" content="Promptfoo at RSA Conference 2025" />
<meta
property="og:description"
content="Recap of Promptfoo at RSA Conference 2025. AI red teaming demos and enterprise security."
/>
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.promptfoo.dev/events/rsa-2025" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="og:image" content="https://www.promptfoo.dev/img/events/rsa-2025.jpg" />
<meta name="twitter:image" content="https://www.promptfoo.dev/img/events/rsa-2025.jpg" />
<meta
name="keywords"
content="RSA Conference 2025, AI security, LLM security, enterprise security, San Francisco, red teaming"
/>
<link rel="canonical" href="https://www.promptfoo.dev/events/rsa-2025" />
</Head>
<main className={styles.rsaPage}>
{/* Hero Banner */}
<section className={styles.heroBanner}>
<img
src="/img/events/rsa-2025.jpg"
alt="RSA Conference 2025"
className={styles.bannerImage}
/>
<div className={styles.bannerOverlay} />
<div className={styles.bannerContent}>
<div className={styles.badge}>
<span className={styles.badgeIcon}>🛡</span>
RSA Conference 2025
</div>
<h1 className={styles.heroTitle}>
Enterprise <span className={styles.highlight}>AI Security</span>
</h1>
</div>
</section>
{/* Hero Content */}
<section className={styles.heroContent}>
<div className={styles.container}>
<p className={styles.heroSubtitle}>
We showcased AI red teaming capabilities at RSA Conference 2025, connecting with
security leaders and demonstrating how enterprises protect their AI applications.
</p>
<div className={styles.eventDetails}>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span>April 28 - May 1, 2025</span>
</div>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<span>Moscone Center, San Francisco</span>
</div>
</div>
<div className={styles.heroCtas}>
<a
href="#recap"
className={styles.primaryCta}
onClick={(e) => handleSmoothScroll(e, '#recap')}
>
View Recap
</a>
<Link to="/contact" className={styles.secondaryCta}>
Contact Us
</Link>
</div>
</div>
</section>
{/* Recap Section */}
<section id="recap" className={styles.recapSection}>
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>Event Recap</h2>
<p className={styles.sectionSubtitle}>Highlights from RSA Conference 2025</p>
</div>
<div className={styles.recapGrid}>
<div className={styles.recapCard}>
<div className={styles.cardIcon}>🎯</div>
<h3>Live AI Red Teaming</h3>
<p>
Demonstrated real-time AI vulnerability testing, showing how enterprises can
identify and mitigate LLM security risks before deployment.
</p>
</div>
<div className={styles.recapCard}>
<div className={styles.cardIcon}>🤝</div>
<h3>Enterprise Connections</h3>
<p>
Connected with Fortune 500 security teams, discussing their AI security challenges
and how Promptfoo helps protect production AI systems.
</p>
</div>
<div className={styles.recapCard}>
<div className={styles.cardIcon}>📊</div>
<h3>Research Sharing</h3>
<p>
Shared practical testing patterns: prompt injection regression tests, RAG data
exfil probes, and how to operationalize red teaming in CI.
</p>
</div>
<div className={styles.recapCard}>
<div className={styles.cardIcon}>🏆</div>
<h3>Expo Floor Presence</h3>
<p>
Showcased our open-source AI security platform to the global security community at
RSA Conference's expo floor.
</p>
</div>
</div>
</div>
</section>
{/* What We Showcased */}
<section className={styles.showcaseSection}>
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>What We Showcased</h2>
<p className={styles.sectionSubtitle}>Enterprise-grade AI security solutions</p>
</div>
<div className={styles.showcaseGrid}>
<div className={styles.showcaseItem}>
<div className={styles.showcaseNumber}>01</div>
<h3>OWASP Top 10 for LLMs</h3>
<p>
Complete coverage of the OWASP Top 10 vulnerabilities for Large Language Models,
with automated detection and remediation guidance.
</p>
</div>
<div className={styles.showcaseItem}>
<div className={styles.showcaseNumber}>02</div>
<h3>Continuous Red Teaming</h3>
<p>
CI/CD integration for continuous AI security testing, ensuring every deployment is
protected against emerging threats.
</p>
</div>
<div className={styles.showcaseItem}>
<div className={styles.showcaseNumber}>03</div>
<h3>Audit-Friendly Reporting</h3>
<p>
Detailed test logs and repeatable security checks, helping enterprises document AI
security for governance and compliance reviews.
</p>
</div>
<div className={styles.showcaseItem}>
<div className={styles.showcaseNumber}>04</div>
<h3>Custom Attack Vectors</h3>
<p>
Industry-specific attack simulations tailored to financial services, healthcare,
and other regulated industries.
</p>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className={styles.ctaSection}>
<div className={styles.container}>
<div className={styles.ctaContent}>
<h2 className={styles.ctaTitle}>See You at RSA 2026</h2>
<p className={styles.ctaText}>
We'll be back at RSA Conference 2026. Check out our plans and stay connected for
updates on booth location and demos.
</p>
<div className={styles.ctaButtons}>
<Link to="/events/rsa-2026" className={styles.primaryCta}>
RSA 2026 Details
</Link>
<Link to="/contact" className={styles.secondaryCta}>
Get in Touch
</Link>
</div>
</div>
</div>
</section>
{/* Footer Navigation */}
<section className={styles.footerNav}>
<div className={styles.container}>
<Link to="/events" className={styles.backLink}>
<svg
className={styles.backIcon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back to All Events
</Link>
</div>
</section>
</main>
</Layout>
);
}
+487
View File
@@ -0,0 +1,487 @@
/* RSA 2026 - Clean Banner Design */
/* Color Palette: RSA blue (#0066CC), cyber cyan (#06B6D4), professional navy (#1e3a5f) */
.rsaPage {
background: #0a0f1a;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
}
/* Hero Banner */
.heroBanner {
position: relative;
height: 400px;
overflow: hidden;
}
.bannerImage {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.bannerOverlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(180deg, rgba(10, 15, 26, 0.4) 0%, rgba(10, 15, 26, 0.7) 100%);
}
.bannerContent {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 3rem;
background: linear-gradient(0deg, rgba(10, 15, 26, 0.95) 0%, transparent 100%);
}
.badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(6, 182, 212, 0.2);
border: 1px solid rgba(6, 182, 212, 0.4);
color: #67e8f9;
border-radius: 100px;
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 1rem;
}
.badgeIcon {
font-size: 1rem;
}
.heroTitle {
font-size: clamp(2rem, 6vw, 3.5rem);
font-weight: 800;
line-height: 1.1;
color: #f1f5f9;
margin: 0;
}
.highlight {
background: linear-gradient(135deg, #06b6d4 0%, #0066cc 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Hero Content Section */
.heroContent {
padding: 3rem 0;
text-align: center;
border-bottom: 1px solid rgba(6, 182, 212, 0.1);
}
.heroSubtitle {
font-size: 1.25rem;
color: #94a3b8;
max-width: 600px;
margin: 0 auto 2rem;
line-height: 1.7;
}
/* Countdown Timer */
.countdown {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.countdownItem {
display: flex;
flex-direction: column;
align-items: center;
background: rgba(6, 182, 212, 0.1);
border: 1px solid rgba(6, 182, 212, 0.2);
border-radius: 12px;
padding: 1rem 1.25rem;
min-width: 80px;
}
.countdownNumber {
font-size: 2rem;
font-weight: 700;
color: #67e8f9;
font-variant-numeric: tabular-nums;
line-height: 1;
}
.countdownLabel {
font-size: 0.75rem;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-top: 0.25rem;
}
.countdownSeparator {
font-size: 2rem;
font-weight: 700;
color: #06b6d4;
animation: blink 1s step-end infinite;
}
@keyframes blink {
50% {
opacity: 0;
}
}
.eventDetails {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2rem;
margin-bottom: 2rem;
}
.detail {
display: flex;
align-items: center;
gap: 0.5rem;
color: #cbd5e1;
font-size: 0.95rem;
}
.icon {
width: 1.25rem;
height: 1.25rem;
stroke-width: 2;
color: #06b6d4;
}
.heroCtas {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
}
.ctaBlurb {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.7);
margin: 0;
}
.primaryCta {
padding: 0.875rem 2rem;
background: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%);
color: white;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
transition: all 0.3s ease;
}
.primaryCta:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(6, 182, 212, 0.4);
color: white;
}
.secondaryCta {
padding: 0.875rem 2rem;
background: transparent;
color: #93c5fd;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
border: 1px solid rgba(0, 102, 204, 0.4);
transition: all 0.3s ease;
}
.secondaryCta:hover {
background: rgba(0, 102, 204, 0.1);
color: white;
}
/* Sections */
.highlightsSection,
.showcaseSection,
.ctaSection {
padding: 4rem 0;
}
.highlightsSection {
background: rgba(6, 182, 212, 0.02);
}
.sectionHeader {
text-align: center;
margin-bottom: 3rem;
}
.sectionTitle {
font-size: 2rem;
font-weight: 700;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.sectionSubtitle {
font-size: 1.1rem;
color: #94a3b8;
max-width: 500px;
margin: 0 auto;
}
/* Highlights Grid */
.highlightsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.5rem;
}
.highlightCard {
background: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(6, 182, 212, 0.15);
border-radius: 12px;
padding: 1.5rem;
transition: all 0.3s ease;
}
.highlightCard:hover {
border-color: rgba(6, 182, 212, 0.3);
transform: translateY(-2px);
}
.highlightIcon {
font-size: 2rem;
margin-bottom: 1rem;
}
.highlightCard h3 {
font-size: 1.125rem;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 0.5rem;
}
.highlightCard p {
font-size: 0.9rem;
color: #94a3b8;
line-height: 1.6;
margin: 0;
}
/* Showcase Grid */
.showcaseGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 2rem;
}
.showcaseItem {
padding: 1.5rem 0;
border-left: 2px solid rgba(6, 182, 212, 0.3);
padding-left: 1.5rem;
}
.showcaseNumber {
font-size: 2.5rem;
font-weight: 800;
background: linear-gradient(135deg, #06b6d4 0%, #0066cc 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.75rem;
line-height: 1;
}
.showcaseItem h3 {
font-size: 1.125rem;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 0.5rem;
}
.showcaseItem p {
font-size: 0.9rem;
color: #94a3b8;
line-height: 1.6;
margin: 0;
}
/* CTA Section */
.ctaGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 2rem;
max-width: 800px;
margin: 0 auto;
}
.ctaCard {
text-align: center;
background: linear-gradient(135deg, rgba(6, 182, 212, 0.08) 0%, rgba(0, 102, 204, 0.08) 100%);
border: 1px solid rgba(6, 182, 212, 0.2);
border-radius: 16px;
padding: 2rem 1.5rem;
display: flex;
flex-direction: column;
align-items: center;
}
.ctaCardTitle {
font-size: 1.25rem;
font-weight: 700;
color: #f1f5f9;
margin: 0 0 0.75rem 0;
}
.ctaCardText {
font-size: 0.95rem;
color: #94a3b8;
margin-bottom: 1.5rem;
line-height: 1.6;
flex-grow: 1;
}
.ctaPrimary {
padding: 0.875rem 2rem;
background: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%);
color: white;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
transition: all 0.3s ease;
}
.ctaPrimary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(6, 182, 212, 0.4);
color: white;
}
.ctaSecondary {
padding: 0.875rem 2rem;
background: transparent;
color: #93c5fd;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
border: 1px solid rgba(0, 102, 204, 0.4);
transition: all 0.3s ease;
}
.ctaSecondary:hover {
background: rgba(0, 102, 204, 0.1);
color: white;
}
/* Footer Navigation */
.footerNav {
padding: 2rem 0 3rem;
border-top: 1px solid rgba(6, 182, 212, 0.1);
}
.backLink {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: #94a3b8;
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
.backLink:hover {
color: #06b6d4;
}
.backIcon {
width: 1.25rem;
height: 1.25rem;
}
/* Reduced Motion */
@media (prefers-reduced-motion: reduce) {
.countdownSeparator {
animation: none;
}
}
/* Responsive */
@media (max-width: 768px) {
.heroBanner {
height: 300px;
}
.bannerContent {
padding: 2rem 1.5rem;
}
.heroTitle {
font-size: 2rem;
}
.heroSubtitle {
font-size: 1.1rem;
}
.countdown {
gap: 0.25rem;
}
.countdownItem {
min-width: 60px;
padding: 0.75rem;
}
.countdownNumber {
font-size: 1.5rem;
}
.countdownSeparator {
font-size: 1.5rem;
}
.eventDetails {
flex-direction: column;
align-items: center;
gap: 1rem;
}
.heroCtas {
flex-direction: column;
align-items: center;
}
.primaryCta,
.secondaryCta,
.ctaPrimary,
.ctaSecondary {
width: 100%;
max-width: 280px;
text-align: center;
}
.ctaGrid {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.ctaCard {
padding: 1.5rem 1.25rem;
}
.highlightsSection,
.ctaSection {
padding: 3rem 0;
}
}
+259
View File
@@ -0,0 +1,259 @@
import React, { useEffect, useState } from 'react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import { useForcedTheme } from '@site/src/hooks/useForcedTheme';
import Layout from '@theme/Layout';
import styles from './rsa-2026.module.css';
function CountdownTimer(): React.ReactElement {
const [timeLeft, setTimeLeft] = useState({
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
});
useEffect(() => {
const targetDate = new Date('2026-03-23T09:00:00-07:00').getTime();
const updateCountdown = () => {
const now = new Date().getTime();
const difference = targetDate - now;
if (difference > 0) {
setTimeLeft({
days: Math.floor(difference / (1000 * 60 * 60 * 24)),
hours: Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)),
minutes: Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60)),
seconds: Math.floor((difference % (1000 * 60)) / 1000),
});
}
};
updateCountdown();
const interval = setInterval(updateCountdown, 1000);
return () => clearInterval(interval);
}, []);
return (
<div className={styles.countdown}>
<div className={styles.countdownItem}>
<span className={styles.countdownNumber}>{timeLeft.days}</span>
<span className={styles.countdownLabel}>Days</span>
</div>
<div className={styles.countdownSeparator}>:</div>
<div className={styles.countdownItem}>
<span className={styles.countdownNumber}>{timeLeft.hours.toString().padStart(2, '0')}</span>
<span className={styles.countdownLabel}>Hours</span>
</div>
<div className={styles.countdownSeparator}>:</div>
<div className={styles.countdownItem}>
<span className={styles.countdownNumber}>
{timeLeft.minutes.toString().padStart(2, '0')}
</span>
<span className={styles.countdownLabel}>Minutes</span>
</div>
<div className={styles.countdownSeparator}>:</div>
<div className={styles.countdownItem}>
<span className={styles.countdownNumber}>
{timeLeft.seconds.toString().padStart(2, '0')}
</span>
<span className={styles.countdownLabel}>Seconds</span>
</div>
</div>
);
}
export default function RSA2026(): React.ReactElement {
useForcedTheme('dark');
return (
<Layout
title="Promptfoo at RSA Conference 2026"
description="Meet Promptfoo at RSA Conference 2026. Live AI red teaming demos, security consultations, and enterprise AI security discussions in San Francisco."
>
<Head>
<meta property="og:title" content="Promptfoo at RSA Conference 2026" />
<meta
property="og:description"
content="Meet Promptfoo at RSA Conference 2026. Live AI red teaming demos and enterprise security."
/>
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.promptfoo.dev/events/rsa-2026" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="og:image" content="https://www.promptfoo.dev/img/events/rsa-2026.jpg" />
<meta name="twitter:image" content="https://www.promptfoo.dev/img/events/rsa-2026.jpg" />
<meta
name="keywords"
content="RSA Conference 2026, AI security, LLM security, enterprise security, San Francisco, red teaming"
/>
<link rel="canonical" href="https://www.promptfoo.dev/events/rsa-2026" />
</Head>
<main className={styles.rsaPage}>
{/* Hero Banner */}
<section className={styles.heroBanner}>
<img
src="/img/events/rsa-2026.jpg"
alt="RSA Conference 2026"
className={styles.bannerImage}
/>
<div className={styles.bannerOverlay} />
<div className={styles.bannerContent}>
<div className={styles.badge}>
<span className={styles.badgeIcon}>🚀</span>
Coming March 2026
</div>
<h1 className={styles.heroTitle}>
RSA Conference <span className={styles.highlight}>2026</span>
</h1>
</div>
</section>
{/* Hero Content */}
<section className={styles.heroContent}>
<div className={styles.container}>
<p className={styles.heroSubtitle}>
Meet our team for live demos of our AI security platformred teaming, evals,
guardrails, and foundation model security reports for your AI applications.
</p>
<CountdownTimer />
<div className={styles.eventDetails}>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span>March 23-26, 2026</span>
</div>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<span>Moscone Center, San Francisco</span>
</div>
</div>
<div className={styles.heroCtas}>
<p className={styles.ctaBlurb}>Book a time to talk to us</p>
<Link to="/contact" className={styles.primaryCta}>
Schedule a Meeting
</Link>
</div>
</div>
</section>
{/* Highlights Section */}
<section id="highlights" className={styles.highlightsSection}>
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>What to Expect</h2>
<p className={styles.sectionSubtitle}>
Join us for the premier AI security experience
</p>
</div>
<div className={styles.highlightsGrid}>
<div className={styles.highlightCard}>
<div className={styles.highlightIcon}>🎯</div>
<h3>AI Red Teaming</h3>
<p>
See live demos of LLM attack vectors, jailbreak techniques, and how to integrate
automated red teaming into your workflow.
</p>
</div>
<div className={styles.highlightCard}>
<div className={styles.highlightIcon}>🤝</div>
<h3>Connect with AI Experts</h3>
<p>
Meet AI security professionals working on evals, guardrails, and MCP
securitylearn how they're protecting AI applications in production.
</p>
</div>
<div className={styles.highlightCard}>
<div className={styles.highlightIcon}>🎬</div>
<h3>See it in Action</h3>
<p>
Quick-fire demos on the latest AI security research, tools, and techniques from
the AI security and red teaming experts.
</p>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className={styles.ctaSection}>
<div className={styles.container}>
<div className={styles.ctaGrid}>
<div className={styles.ctaCard}>
<h3 className={styles.ctaCardTitle}>Attending RSA?</h3>
<p className={styles.ctaCardText}>
Book a time to meet with our AI security and red teaming experts at the event.
</p>
<Link to="/contact" className={styles.primaryCta}>
Schedule a Meeting
</Link>
</div>
<div className={styles.ctaCard}>
<h3 className={styles.ctaCardTitle}>Can't make it?</h3>
<p className={styles.ctaCardText}>
Join our Discord community to connect with our team and our community.
</p>
<a
href="https://discord.com/invite/promptfoo"
target="_blank"
rel="noopener noreferrer"
className={styles.secondaryCta}
>
Join our Discord
</a>
</div>
</div>
</div>
</section>
{/* Footer Navigation */}
<section className={styles.footerNav}>
<div className={styles.container}>
<Link to="/events" className={styles.backLink}>
<svg
className={styles.backIcon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back to All Events
</Link>
</div>
</section>
</main>
</Layout>
);
}
@@ -0,0 +1,450 @@
/* ScaleUp:AI 2025 - Insight Partners / VC Thought Leadership Theme */
/* Color Palette: Deep indigo (#4F46E5), slate (#475569), professional navy (#1e293b) */
.scaleupPage {
background: #0f172a;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
}
/* Hero Banner */
.heroBanner {
position: relative;
height: 400px;
overflow: hidden;
}
.bannerImage {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.bannerOverlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(180deg, rgba(15, 23, 42, 0.4) 0%, rgba(15, 23, 42, 0.7) 100%);
}
.bannerContent {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 3rem;
background: linear-gradient(0deg, rgba(15, 23, 42, 0.95) 0%, transparent 100%);
}
.badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(79, 70, 229, 0.2);
border: 1px solid rgba(79, 70, 229, 0.4);
color: #a5b4fc;
border-radius: 100px;
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 1rem;
}
.badgeIcon {
font-size: 1rem;
}
.heroTitle {
font-size: clamp(2rem, 6vw, 3.5rem);
font-weight: 800;
line-height: 1.1;
color: #f1f5f9;
margin: 0;
}
.highlight {
background: linear-gradient(135deg, #818cf8 0%, #6366f1 50%, #4f46e5 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Hero Content Section */
.heroContent {
padding: 3rem 0;
text-align: center;
border-bottom: 1px solid rgba(79, 70, 229, 0.1);
}
.heroSubtitle {
font-size: 1.25rem;
color: #94a3b8;
max-width: 600px;
margin: 0 auto 2rem;
line-height: 1.7;
}
.eventDetails {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2rem;
margin-bottom: 2rem;
}
.detail {
display: flex;
align-items: center;
gap: 0.5rem;
color: #cbd5e1;
font-size: 0.95rem;
}
.icon {
width: 1.25rem;
height: 1.25rem;
stroke-width: 2;
color: #818cf8;
}
.heroCtas {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.primaryCta {
padding: 0.875rem 2rem;
background: linear-gradient(135deg, #4f46e5 0%, #4338ca 100%);
color: white;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
transition: all 0.3s ease;
}
.primaryCta:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(79, 70, 229, 0.4);
color: white;
}
.secondaryCta {
padding: 0.875rem 2rem;
background: transparent;
color: #a5b4fc;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
border: 1px solid rgba(79, 70, 229, 0.4);
transition: all 0.3s ease;
}
.secondaryCta:hover {
background: rgba(79, 70, 229, 0.1);
color: white;
}
/* Quote Section */
.quoteSection {
padding: 4rem 0;
background: rgba(79, 70, 229, 0.02);
}
.quoteContent {
max-width: 700px;
margin: 0 auto;
text-align: center;
}
.quote {
font-size: 1.5rem;
font-weight: 500;
color: #e2e8f0;
line-height: 1.6;
margin: 0;
font-style: italic;
}
.quoteAttribution {
margin-top: 1.5rem;
}
.quoteAuthor {
font-size: 1.125rem;
font-weight: 600;
color: #f1f5f9;
}
.quoteRole {
font-size: 0.9rem;
color: #818cf8;
margin-top: 0.25rem;
}
/* Sections */
.highlightsSection,
.keyPointsSection,
.aboutSection,
.ctaSection {
padding: 4rem 0;
}
.keyPointsSection {
background: rgba(79, 70, 229, 0.02);
}
.sectionHeader {
text-align: center;
margin-bottom: 3rem;
}
.sectionTitle {
font-size: 2rem;
font-weight: 700;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.sectionSubtitle {
font-size: 1.1rem;
color: #94a3b8;
max-width: 500px;
margin: 0 auto;
}
/* Highlights Grid */
.highlightsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.5rem;
}
.highlightCard {
background: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(79, 70, 229, 0.15);
border-radius: 12px;
padding: 1.5rem;
transition: all 0.3s ease;
}
.highlightCard:hover {
border-color: rgba(79, 70, 229, 0.3);
transform: translateY(-2px);
}
.cardIcon {
font-size: 2rem;
margin-bottom: 1rem;
}
.highlightCard h3 {
font-size: 1.125rem;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 0.5rem;
}
.highlightCard p {
font-size: 0.9rem;
color: #94a3b8;
line-height: 1.6;
margin: 0;
}
/* Key Points Grid */
.keyPointsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 2rem;
}
.keyPoint {
padding: 1.5rem 0;
border-left: 2px solid rgba(79, 70, 229, 0.3);
padding-left: 1.5rem;
}
.keyPointNumber {
font-size: 2.5rem;
font-weight: 800;
background: linear-gradient(135deg, #818cf8 0%, #4f46e5 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.75rem;
line-height: 1;
}
.keyPoint h3 {
font-size: 1.125rem;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 0.5rem;
}
.keyPoint p {
font-size: 0.9rem;
color: #94a3b8;
line-height: 1.6;
margin: 0;
}
/* About Section */
.aboutContent {
max-width: 700px;
margin: 0 auto;
background: linear-gradient(135deg, rgba(79, 70, 229, 0.08) 0%, rgba(30, 41, 59, 0.1) 100%);
border: 1px solid rgba(79, 70, 229, 0.15);
border-radius: 16px;
padding: 2rem;
}
.aboutText h3 {
font-size: 1.25rem;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 1rem;
}
.aboutText p {
font-size: 1rem;
color: #94a3b8;
line-height: 1.7;
margin-bottom: 1.5rem;
}
.aboutLink {
color: #818cf8;
font-weight: 600;
text-decoration: none;
transition: color 0.2s ease;
}
.aboutLink:hover {
color: #a5b4fc;
}
/* CTA Section */
.ctaContent {
max-width: 600px;
margin: 0 auto;
text-align: center;
background: linear-gradient(135deg, rgba(79, 70, 229, 0.08) 0%, rgba(30, 41, 59, 0.1) 100%);
border: 1px solid rgba(79, 70, 229, 0.2);
border-radius: 16px;
padding: 3rem 2rem;
}
.ctaTitle {
font-size: 1.75rem;
font-weight: 700;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.ctaText {
font-size: 1rem;
color: #94a3b8;
margin-bottom: 1.5rem;
line-height: 1.7;
}
.ctaButtons {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
/* Footer Navigation */
.footerNav {
padding: 2rem 0 3rem;
border-top: 1px solid rgba(79, 70, 229, 0.1);
}
.backLink {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: #94a3b8;
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
.backLink:hover {
color: #818cf8;
}
.backIcon {
width: 1.25rem;
height: 1.25rem;
}
/* Responsive */
@media (max-width: 768px) {
.heroBanner {
height: 300px;
}
.bannerContent {
padding: 2rem 1.5rem;
}
.heroTitle {
font-size: 2rem;
}
.heroSubtitle {
font-size: 1.1rem;
}
.eventDetails {
flex-direction: column;
align-items: center;
gap: 1rem;
}
.heroCtas {
flex-direction: column;
align-items: center;
}
.primaryCta,
.secondaryCta {
width: 100%;
max-width: 280px;
text-align: center;
}
.quote {
font-size: 1.1rem;
}
.ctaContent {
padding: 2rem 1.5rem;
}
.ctaButtons {
flex-direction: column;
align-items: center;
}
.highlightsSection,
.keyPointsSection,
.aboutSection,
.ctaSection,
.quoteSection {
padding: 3rem 0;
}
}
+317
View File
@@ -0,0 +1,317 @@
import React from 'react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import { useForcedTheme } from '@site/src/hooks/useForcedTheme';
import Layout from '@theme/Layout';
import { SITE_CONSTANTS } from '../../constants';
import styles from './scaleup-ai-2025.module.css';
export default function ScaleUpAI2025(): React.ReactElement {
useForcedTheme('dark');
const handleSmoothScroll = (e: React.MouseEvent<HTMLAnchorElement>, targetId: string) => {
e.preventDefault();
const element = document.querySelector(targetId);
if (element) {
const offset = 80;
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - offset;
window.scrollTo({ top: offsetPosition, behavior: 'smooth' });
}
};
return (
<Layout
title="Promptfoo at ScaleUp:AI 2025"
description="How Promptfoo is restoring trust and security in generative AI. Featured in Insight Partners' ScaleUp:AI 2025 Partner Series."
>
<Head>
<meta property="og:title" content="Promptfoo at ScaleUp:AI 2025 - Insight Partners" />
<meta
property="og:description"
content="How Promptfoo is restoring trust and security in generative AI. Ian Webster discusses the future of AI security."
/>
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.promptfoo.dev/events/scaleup-ai-2025" />
<meta name="twitter:card" content="summary_large_image" />
<meta
property="og:image"
content="https://www.promptfoo.dev/img/events/scaleup-ai-2025.jpg"
/>
<meta
name="twitter:image"
content="https://www.promptfoo.dev/img/events/scaleup-ai-2025.jpg"
/>
<meta
name="keywords"
content="ScaleUp:AI 2025, Insight Partners, AI security, Promptfoo, Ian Webster, venture capital, AI investment"
/>
<link rel="canonical" href="https://www.promptfoo.dev/events/scaleup-ai-2025" />
</Head>
<main className={styles.scaleupPage}>
{/* Hero Banner */}
<section className={styles.heroBanner}>
<img
src="/img/events/scaleup-ai-2025.jpg"
alt="ScaleUp:AI 2025"
className={styles.bannerImage}
/>
<div className={styles.bannerOverlay} />
<div className={styles.bannerContent}>
<div className={styles.badge}>
<span className={styles.badgeIcon}>🚀</span>
Insight Partners ScaleUp:AI 2025
</div>
<h1 className={styles.heroTitle}>
Restoring <span className={styles.highlight}>Trust in AI</span>
</h1>
</div>
</section>
{/* Hero Content */}
<section className={styles.heroContent}>
<div className={styles.container}>
<p className={styles.heroSubtitle}>
Featured in Insight Partners' ScaleUp:AI 2025 Partner Series. CEO Ian Webster shares
how Promptfoo is defining the standard for enterprise AI security.
</p>
<div className={styles.eventDetails}>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span>December 3, 2025</span>
</div>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
/>
</svg>
<span>Insight Partners</span>
</div>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
<span>Ian Webster, CEO</span>
</div>
</div>
<div className={styles.heroCtas}>
<a
href="https://www.insightpartners.com/ideas/promptfoo-scale-up-ai/"
target="_blank"
rel="noopener noreferrer"
className={styles.primaryCta}
>
Read Full Article
</a>
<a
href="#highlights"
className={styles.secondaryCta}
onClick={(e) => handleSmoothScroll(e, '#highlights')}
>
Key Takeaways
</a>
</div>
</div>
</section>
{/* Quote Section */}
<section className={styles.quoteSection}>
<div className={styles.container}>
<div className={styles.quoteContent}>
<blockquote className={styles.quote}>
"AI is underhyped. The software industry looks so different five to ten years from
now."
</blockquote>
<div className={styles.quoteAttribution}>
<div className={styles.quoteAuthor}>Ian Webster</div>
<div className={styles.quoteRole}>CEO & Co-founder, Promptfoo</div>
</div>
</div>
</div>
</section>
{/* Highlights Section */}
<section id="highlights" className={styles.highlightsSection}>
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>Key Takeaways</h2>
<p className={styles.sectionSubtitle}>Insights from the ScaleUp:AI feature</p>
</div>
<div className={styles.highlightsGrid}>
<div className={styles.highlightCard}>
<div className={styles.cardIcon}>🔐</div>
<h3>Closing the Security Gap</h3>
<p>
Traditional cybersecurity tools were built for predictable software. LLMs operate
in natural language—attackers can manipulate them simply by talking to them.
</p>
</div>
<div className={styles.highlightCard}>
<div className={styles.cardIcon}>🤖</div>
<h3>Machines Testing Machines</h3>
<p>
Promptfoo's AI models behave like attackers, red teaming applications through chat
interfaces and APIs to uncover vulnerabilities before deployment.
</p>
</div>
<div className={styles.highlightCard}>
<div className={styles.cardIcon}>📈</div>
<h3>Rapid Adoption</h3>
<p>
As featured by Insight Partners: 200,000+ developers and 80+ Fortune 500 companies
use Promptfoo, backed by an $18.4M Series A led by Insight.
</p>
</div>
<div className={styles.highlightCard}>
<div className={styles.cardIcon}>🔮</div>
<h3>Future of AI Security</h3>
<p>
As AI systems evolve into autonomous multi-agent systems, observability and
continuous security testing become critical for enterprise deployments.
</p>
</div>
</div>
</div>
</section>
{/* Key Points Section */}
<section className={styles.keyPointsSection}>
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>From the Article</h2>
<p className={styles.sectionSubtitle}>Why AI security matters now</p>
</div>
<div className={styles.keyPointsGrid}>
<div className={styles.keyPoint}>
<div className={styles.keyPointNumber}>01</div>
<h3>The Problem</h3>
<p>
"Old-school defenses aren't reliable because they're designed to read code, not
conversation." Traditional security can't protect systems that operate in natural
language.
</p>
</div>
<div className={styles.keyPoint}>
<div className={styles.keyPointNumber}>02</div>
<h3>The Origin</h3>
<p>
Ian Webster built the first version of Promptfoo while leading AI products at
Discord—shipping to 200 million users taught him "all of the wonderful things, and
also the terrible things" that follow.
</p>
</div>
<div className={styles.keyPoint}>
<div className={styles.keyPointNumber}>03</div>
<h3>The Urgency</h3>
<p>
"Global 2000 companies are going to be buying or making decisions in the AI
security space in the next 12 months. There's no time to lose."
</p>
</div>
<div className={styles.keyPoint}>
<div className={styles.keyPointNumber}>04</div>
<h3>The Vision</h3>
<p>
"If we want to see a world where AI really benefits people and companies, we need
to put those tools in the hands of developers." Building AI that's intelligent and
accountable.
</p>
</div>
</div>
</div>
</section>
{/* About Insight Section */}
<section className={styles.aboutSection}>
<div className={styles.container}>
<div className={styles.aboutContent}>
<div className={styles.aboutText}>
<h3>About ScaleUp:AI</h3>
<p>
ScaleUp:AI is Insight Partners' premier content series highlighting insights from
the companies and leaders shaping the future of AI. As a global software investor
and operational partner, Insight Partners has backed many of the world's most
transformative technology companies.
</p>
<a
href="https://www.insightpartners.com/ideas/promptfoo-scale-up-ai/"
target="_blank"
rel="noopener noreferrer"
className={styles.aboutLink}
>
Read the Full Feature
</a>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className={styles.ctaSection}>
<div className={styles.container}>
<div className={styles.ctaContent}>
<h2 className={styles.ctaTitle}>Ready to Secure Your AI?</h2>
<p className={styles.ctaText}>
Join {SITE_CONSTANTS.USER_COUNT_DISPLAY}+ developers and{' '}
{SITE_CONSTANTS.FORTUNE_500_COUNT}+ Fortune 500 companies who trust Promptfoo to
find and fix vulnerabilities in their AI applications.
</p>
<div className={styles.ctaButtons}>
<Link to="/docs/intro" className={styles.primaryCta}>
Get Started Free
</Link>
<Link to="/contact" className={styles.secondaryCta}>
Talk to Sales
</Link>
</div>
</div>
</div>
</section>
{/* Footer Navigation */}
<section className={styles.footerNav}>
<div className={styles.container}>
<Link to="/events" className={styles.backLink}>
<svg
className={styles.backIcon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back to All Events
</Link>
</div>
</section>
</main>
</Layout>
);
}
@@ -0,0 +1,542 @@
/* SecTor 2025 - Canadian / Toronto Theme */
/* Color Palette: Deep crimson (#B22234), white, maple accents, dark slate */
.sectorPage {
background: #0f172a;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
}
/* Hero Banner */
.heroBanner {
position: relative;
height: 400px;
overflow: hidden;
}
.bannerImage {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.bannerOverlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(180deg, rgba(15, 23, 42, 0.4) 0%, rgba(15, 23, 42, 0.7) 100%);
}
.bannerContent {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 3rem;
background: linear-gradient(0deg, rgba(15, 23, 42, 0.95) 0%, transparent 100%);
}
.badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(178, 34, 52, 0.2);
border: 1px solid rgba(178, 34, 52, 0.4);
color: #fca5a5;
border-radius: 100px;
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 1rem;
}
.badgeIcon {
font-size: 1rem;
}
.heroTitle {
font-size: clamp(2rem, 6vw, 3.5rem);
font-weight: 800;
line-height: 1.1;
color: #f1f5f9;
margin: 0;
}
.highlight {
background: linear-gradient(135deg, #b22234 0%, #ef4444 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Hero Content Section */
.heroContent {
padding: 3rem 0;
text-align: center;
border-bottom: 1px solid rgba(178, 34, 52, 0.1);
}
.heroSubtitle {
font-size: 1.25rem;
color: #94a3b8;
max-width: 600px;
margin: 0 auto 2rem;
line-height: 1.7;
}
.eventDetails {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2rem;
margin-bottom: 2rem;
}
.detail {
display: flex;
align-items: center;
gap: 0.5rem;
color: #cbd5e1;
font-size: 0.95rem;
}
.icon {
width: 1.25rem;
height: 1.25rem;
stroke-width: 2;
color: #b22234;
}
.heroCtas {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.primaryCta {
padding: 0.875rem 2rem;
background: linear-gradient(135deg, #b22234 0%, #991b1b 100%);
color: white;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
transition: all 0.3s ease;
}
.primaryCta:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(178, 34, 52, 0.4);
color: white;
}
.secondaryCta {
padding: 0.875rem 2rem;
background: transparent;
color: #fca5a5;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
border: 1px solid rgba(178, 34, 52, 0.4);
transition: all 0.3s ease;
}
.secondaryCta:hover {
background: rgba(178, 34, 52, 0.1);
color: white;
}
/* Sections */
.arsenalSection,
.whySection,
.conversationsSection,
.statsSection,
.themesSection,
.ctaSection {
padding: 4rem 0;
}
.arsenalSection {
background: rgba(178, 34, 52, 0.02);
}
.sectionHeader {
text-align: center;
margin-bottom: 3rem;
}
.sectionTitle {
font-size: 2rem;
font-weight: 700;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.sectionSubtitle {
font-size: 1.1rem;
color: #94a3b8;
max-width: 600px;
margin: 0 auto;
}
/* Arsenal Card */
.arsenalCard {
background: linear-gradient(135deg, rgba(178, 34, 52, 0.1) 0%, rgba(178, 34, 52, 0.05) 100%);
border: 1px solid rgba(178, 34, 52, 0.3);
border-radius: 16px;
padding: 2rem;
position: relative;
overflow: hidden;
}
.arsenalCard::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #b22234 0%, #ef4444 50%, #b22234 100%);
}
.arsenalBadge {
display: inline-block;
padding: 0.375rem 1rem;
background: linear-gradient(135deg, #b22234 0%, #dc2626 100%);
color: white;
border-radius: 100px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 1rem;
}
.arsenalTitle {
font-size: 1.5rem;
font-weight: 800;
color: #f8fafc;
margin-bottom: 0.75rem;
}
.arsenalDescription {
font-size: 1rem;
color: #94a3b8;
line-height: 1.7;
margin-bottom: 1.5rem;
max-width: 700px;
}
.arsenalFeatures {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1.5rem;
}
.arsenalFeature {
display: flex;
gap: 1rem;
align-items: flex-start;
}
.featureIcon {
font-size: 1.5rem;
flex-shrink: 0;
}
.arsenalFeature h4 {
font-size: 1rem;
font-weight: 700;
color: #f8fafc;
margin: 0 0 0.25rem 0;
}
.arsenalFeature p {
font-size: 0.875rem;
color: #94a3b8;
margin: 0;
line-height: 1.5;
}
/* Why Grid */
.whyGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.5rem;
}
.whyCard {
background: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(178, 34, 52, 0.15);
border-radius: 12px;
padding: 1.5rem;
text-align: center;
transition: all 0.3s ease;
}
.whyCard:hover {
border-color: rgba(178, 34, 52, 0.3);
transform: translateY(-2px);
}
.whyEmoji {
font-size: 2.5rem;
margin-bottom: 1rem;
}
.whyCard h3 {
font-size: 1.125rem;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 0.5rem;
}
.whyCard p {
font-size: 0.9rem;
color: #94a3b8;
line-height: 1.6;
margin: 0;
}
/* Conversations Grid */
.conversationsSection {
background: rgba(178, 34, 52, 0.02);
}
.conversationsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.5rem;
}
.conversationCard {
background: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(178, 34, 52, 0.15);
border-radius: 12px;
padding: 1.5rem;
transition: all 0.3s ease;
}
.conversationCard:hover {
border-color: rgba(178, 34, 52, 0.3);
transform: translateY(-2px);
}
.conversationIcon {
font-size: 2rem;
margin-bottom: 1rem;
}
.conversationCard h3 {
font-size: 1.125rem;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 0.5rem;
}
.conversationCard p {
font-size: 0.9rem;
color: #94a3b8;
line-height: 1.6;
margin: 0;
}
/* Stats Section */
.statsSection {
background: linear-gradient(135deg, rgba(178, 34, 52, 0.1) 0%, rgba(178, 34, 52, 0.05) 100%);
border-top: 1px solid rgba(178, 34, 52, 0.15);
border-bottom: 1px solid rgba(178, 34, 52, 0.15);
}
.statsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 2rem;
text-align: center;
}
.stat {
padding: 1.5rem;
}
.statNumber {
font-size: 2.5rem;
font-weight: 800;
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1;
margin-bottom: 0.5rem;
}
.statLabel {
font-size: 0.9rem;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Themes Grid */
.themesGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
.themeQuote {
background: rgba(15, 23, 42, 0.6);
border: 1px solid rgba(178, 34, 52, 0.15);
border-left: 4px solid #b22234;
border-radius: 0 12px 12px 0;
padding: 1.5rem;
margin: 0;
}
.themeQuote p {
font-size: 1rem;
color: #cbd5e1;
line-height: 1.7;
margin: 0 0 1rem 0;
font-style: italic;
}
.themeQuote cite {
font-size: 0.875rem;
color: #b22234;
font-style: normal;
font-weight: 600;
}
/* CTA Section */
.ctaContent {
max-width: 600px;
margin: 0 auto;
text-align: center;
background: linear-gradient(135deg, rgba(178, 34, 52, 0.08) 0%, rgba(30, 41, 59, 0.1) 100%);
border: 1px solid rgba(178, 34, 52, 0.2);
border-radius: 16px;
padding: 3rem 2rem;
}
.ctaMaple {
font-size: 3rem;
margin-bottom: 1rem;
}
.ctaTitle {
font-size: 1.75rem;
font-weight: 700;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.ctaText {
font-size: 1rem;
color: #94a3b8;
margin-bottom: 1.5rem;
line-height: 1.7;
}
.ctaButtons {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
/* Footer Navigation */
.footerNav {
padding: 2rem 0 3rem;
border-top: 1px solid rgba(178, 34, 52, 0.1);
}
.backLink {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: #94a3b8;
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
.backLink:hover {
color: #b22234;
}
.backIcon {
width: 1.25rem;
height: 1.25rem;
}
/* Responsive */
@media (max-width: 768px) {
.heroBanner {
height: 300px;
}
.bannerContent {
padding: 2rem 1.5rem;
}
.heroTitle {
font-size: 2rem;
}
.heroSubtitle {
font-size: 1.1rem;
}
.eventDetails {
flex-direction: column;
align-items: center;
gap: 1rem;
}
.heroCtas {
flex-direction: column;
align-items: center;
}
.primaryCta,
.secondaryCta {
width: 100%;
max-width: 280px;
text-align: center;
}
.arsenalCard {
padding: 1.5rem;
}
.arsenalTitle {
font-size: 1.25rem;
}
.ctaContent {
padding: 2rem 1.5rem;
}
.ctaButtons {
flex-direction: column;
align-items: center;
}
.arsenalSection,
.whySection,
.conversationsSection,
.statsSection,
.themesSection,
.ctaSection {
padding: 3rem 0;
}
}
+352
View File
@@ -0,0 +1,352 @@
import React from 'react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import { useForcedTheme } from '@site/src/hooks/useForcedTheme';
import Layout from '@theme/Layout';
import styles from './sector-2025.module.css';
export default function SecTor2025(): React.ReactElement {
useForcedTheme('dark');
const handleSmoothScroll = (e: React.MouseEvent<HTMLAnchorElement>, targetId: string) => {
e.preventDefault();
const element = document.querySelector(targetId);
if (element) {
const offset = 80;
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - offset;
window.scrollTo({ top: offsetPosition, behavior: 'smooth' });
}
};
return (
<Layout
title="Promptfoo at SecTor 2025"
description="Promptfoo at SecTor 2025 in Toronto. Arsenal demos, security tools showcase, and connecting with Canada's enterprise security community."
>
<Head>
<meta property="og:title" content="Promptfoo at SecTor 2025 - Toronto" />
<meta
property="og:description"
content="Join Promptfoo at SecTor 2025 in Toronto. Arsenal demos, LLM security tools, and enterprise AI security discussions."
/>
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.promptfoo.dev/events/sector-2025" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="og:image" content="https://www.promptfoo.dev/img/events/sector-2025.jpg" />
<meta name="twitter:image" content="https://www.promptfoo.dev/img/events/sector-2025.jpg" />
<meta
name="keywords"
content="SecTor 2025, Toronto security conference, Arsenal, AI security tools, Canada cybersecurity, LLM security"
/>
<link rel="canonical" href="https://www.promptfoo.dev/events/sector-2025" />
</Head>
<main className={styles.sectorPage}>
{/* Hero Banner */}
<section className={styles.heroBanner}>
<img src="/img/events/sector-2025.jpg" alt="SecTor 2025" className={styles.bannerImage} />
<div className={styles.bannerOverlay} />
<div className={styles.bannerContent}>
<div className={styles.badge}>
<span className={styles.badgeIcon}>🍁</span>
SecTor 2025 Toronto
</div>
<h1 className={styles.heroTitle}>
North of the Border <span className={styles.highlight}>Security</span>
</h1>
</div>
</section>
{/* Hero Content */}
<section className={styles.heroContent}>
<div className={styles.container}>
<p className={styles.heroSubtitle}>
Canada's largest IT security conference. We brought open-source AI security tools to
the Arsenal and connected with enterprise security teams across the country.
</p>
<div className={styles.eventDetails}>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span>September 30 - October 2, 2025</span>
</div>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<span>Metro Toronto Convention Centre</span>
</div>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
<span>Arsenal Listing</span>
</div>
</div>
<div className={styles.heroCtas}>
<a
href="#arsenal"
className={styles.primaryCta}
onClick={(e) => handleSmoothScroll(e, '#arsenal')}
>
See the Arsenal
</a>
<Link to="/docs/red-team/" className={styles.secondaryCta}>
Try Promptfoo
</Link>
</div>
</div>
</section>
{/* Arsenal Section */}
<section id="arsenal" className={styles.arsenalSection}>
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>Arsenal Showcase</h2>
<p className={styles.sectionSubtitle}>
Promptfoo was selected for the SecTor Arsenal, showcasing open-source security tools
to Canada's enterprise security community.
</p>
</div>
<div className={styles.arsenalCard}>
<div className={styles.arsenalBadge}>SecTor Arsenal 2025</div>
<h3 className={styles.arsenalTitle}>Promptfoo: Open-Source LLM Red Teaming</h3>
<p className={styles.arsenalDescription}>
Live demonstrations of automated AI security testing, including prompt injection
detection, jailbreak attempts, and data exfiltration scenarios against real-world
LLM applications.
</p>
<div className={styles.arsenalFeatures}>
<div className={styles.arsenalFeature}>
<div className={styles.featureIcon}>🎯</div>
<div>
<h4>Automated Red Teaming</h4>
<p>Systematic vulnerability discovery across 15+ attack categories</p>
</div>
</div>
<div className={styles.arsenalFeature}>
<div className={styles.featureIcon}>🔓</div>
<div>
<h4>Live Jailbreaking</h4>
<p>Real-time demonstrations against production guardrails</p>
</div>
</div>
<div className={styles.arsenalFeature}>
<div className={styles.featureIcon}>📊</div>
<div>
<h4>Security Reports</h4>
<p>Enterprise-ready vulnerability assessments and remediation guidance</p>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Why SecTor Section */}
<section className={styles.whySection}>
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>Why SecTor</h2>
</div>
<div className={styles.whyGrid}>
<div className={styles.whyCard}>
<div className={styles.whyEmoji}>🇨🇦</div>
<h3>Canadian Enterprise</h3>
<p>
SecTor brings together Canada's largest financial institutions, government
agencies, and technology companies under one roof.
</p>
</div>
<div className={styles.whyCard}>
<div className={styles.whyEmoji}>🛡️</div>
<h3>Security-First Culture</h3>
<p>
The Canadian security community takes compliance and data protection seriously.
Perfect audience for enterprise AI security tools.
</p>
</div>
<div className={styles.whyCard}>
<div className={styles.whyEmoji}>🤝</div>
<h3>Quality Connections</h3>
<p>
Smaller than RSA, more focused conversations. We had in-depth discussions about
real deployment challenges.
</p>
</div>
</div>
</div>
</section>
{/* Conversations Section */}
<section className={styles.conversationsSection}>
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>Key Conversations</h2>
</div>
<div className={styles.conversationsGrid}>
<div className={styles.conversationCard}>
<div className={styles.conversationIcon}>🏦</div>
<h3>Financial Services</h3>
<p>
Discussions with major Canadian banks about securing customer-facing AI assistants
and internal LLM tools under strict regulatory requirements.
</p>
</div>
<div className={styles.conversationCard}>
<div className={styles.conversationIcon}>🏛️</div>
<h3>Government</h3>
<p>
Federal and provincial teams exploring how to safely deploy AI while meeting
Canadian privacy regulations and security standards.
</p>
</div>
<div className={styles.conversationCard}>
<div className={styles.conversationIcon}>🏥</div>
<h3>Healthcare</h3>
<p>
Healthcare organizations interested in AI assistants for patient communication
while protecting sensitive health information.
</p>
</div>
<div className={styles.conversationCard}>
<div className={styles.conversationIcon}>⚡</div>
<h3>Critical Infrastructure</h3>
<p>
Energy and utilities companies evaluating AI for operational efficiency while
maintaining strict security controls.
</p>
</div>
</div>
</div>
</section>
{/* Stats Section */}
<section className={styles.statsSection}>
<div className={styles.container}>
<div className={styles.statsGrid}>
<div className={styles.stat}>
<div className={styles.statNumber}>3</div>
<div className={styles.statLabel}>Days</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>Toronto</div>
<div className={styles.statLabel}>Location</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>Oct 12</div>
<div className={styles.statLabel}>Arsenal Lab</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>MTCC</div>
<div className={styles.statLabel}>Venue</div>
</div>
</div>
</div>
</section>
{/* Common Themes */}
<section className={styles.themesSection}>
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>Common Themes</h2>
</div>
<div className={styles.themesGrid}>
<blockquote className={styles.themeQuote}>
<p>
Teams were looking for AI security tools that fit their compliance requirements,
with detailed reports suitable for auditors.
</p>
<cite>Theme: Financial services</cite>
</blockquote>
<blockquote className={styles.themeQuote}>
<p>
Government teams wanted ways to test AI assistants while meeting Canadian privacy
regulations and GRC requirements.
</p>
<cite>Theme: Public sector</cite>
</blockquote>
<blockquote className={styles.themeQuote}>
<p>
Open source was important—being able to audit the testing methodology was a key
requirement for many teams.
</p>
<cite>Theme: Enterprise security</cite>
</blockquote>
</div>
</div>
</section>
{/* CTA Section */}
<section className={styles.ctaSection}>
<div className={styles.container}>
<div className={styles.ctaContent}>
<div className={styles.ctaMaple}>🍁</div>
<h2 className={styles.ctaTitle}>Merci • Thank You</h2>
<p className={styles.ctaText}>
Thanks to the SecTor team and everyone who stopped by the Arsenal. We're excited to
continue working with the Canadian security community.
</p>
<div className={styles.ctaButtons}>
<Link to="/docs/red-team/quickstart/" className={styles.primaryCta}>
Get Started
</Link>
<Link to="https://discord.gg/promptfoo" className={styles.secondaryCta}>
Join Discord
</Link>
</div>
</div>
</div>
</section>
{/* Footer Navigation */}
<section className={styles.footerNav}>
<div className={styles.container}>
<Link to="/events" className={styles.backLink}>
<svg
className={styles.backIcon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back to All Events
</Link>
</div>
</section>
</main>
</Layout>
);
}
@@ -0,0 +1,662 @@
/* Telecom Talks 2025 - Network/Signal Theme */
/* Color Palette: Electric cyan (#00D4FF), deep navy (#0A1628), signal green (#00FF88) */
.telecomPage {
background: #0a1628;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1.5rem;
}
/* Hero Banner */
.heroBanner {
position: relative;
height: 400px;
overflow: hidden;
}
.bannerImage {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.bannerOverlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(180deg, rgba(10, 22, 40, 0.4) 0%, rgba(10, 22, 40, 0.7) 100%);
}
.bannerContent {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 3rem;
background: linear-gradient(0deg, rgba(10, 22, 40, 0.95) 0%, transparent 100%);
}
.badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(0, 212, 255, 0.2);
border: 1px solid rgba(0, 212, 255, 0.4);
color: #67e8f9;
border-radius: 100px;
font-size: 0.875rem;
font-weight: 600;
margin-bottom: 1rem;
}
.badgeIcon {
font-size: 1rem;
}
.heroTitle {
font-size: clamp(2rem, 6vw, 3.5rem);
font-weight: 800;
line-height: 1.1;
color: #f1f5f9;
margin: 0;
}
.highlight {
background: linear-gradient(135deg, #00d4ff 0%, #00ff88 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Hero Content Section */
.heroContent {
padding: 3rem 0;
text-align: center;
border-bottom: 1px solid rgba(0, 212, 255, 0.1);
}
.heroSubtitle {
font-size: 1.25rem;
color: #94a3b8;
max-width: 600px;
margin: 0 auto 2rem;
line-height: 1.7;
}
.eventDetails {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2rem;
margin-bottom: 2rem;
}
.detail {
display: flex;
align-items: center;
gap: 0.5rem;
color: #cbd5e1;
font-size: 0.95rem;
}
.icon {
width: 1.25rem;
height: 1.25rem;
stroke-width: 2;
color: #00d4ff;
}
.heroCtas {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
.primaryCta {
padding: 0.875rem 2rem;
background: linear-gradient(135deg, #00d4ff 0%, #00b4d8 100%);
color: #0a1628;
font-weight: 700;
border-radius: 8px;
text-decoration: none;
transition: all 0.3s ease;
}
.primaryCta:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0, 212, 255, 0.4);
color: #0a1628;
}
.secondaryCta {
padding: 0.875rem 2rem;
background: transparent;
color: #67e8f9;
font-weight: 600;
border-radius: 8px;
text-decoration: none;
border: 1px solid rgba(0, 212, 255, 0.4);
transition: all 0.3s ease;
}
.secondaryCta:hover {
background: rgba(0, 212, 255, 0.1);
color: white;
}
/* Terminal Section */
.terminalSection {
padding: 4rem 0;
}
.terminal {
background: rgba(10, 22, 40, 0.9);
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
max-width: 800px;
margin: 0 auto;
}
.terminalHeader {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem 1rem;
background: rgba(0, 212, 255, 0.1);
border-bottom: 1px solid rgba(0, 212, 255, 0.2);
}
.terminalDots {
display: flex;
gap: 0.5rem;
}
.terminalDots span {
width: 12px;
height: 12px;
border-radius: 50%;
background: rgba(0, 212, 255, 0.4);
}
.terminalDots span:first-child {
background: #ff5f57;
}
.terminalDots span:nth-child(2) {
background: #ffbd2e;
}
.terminalDots span:last-child {
background: #28ca41;
}
.terminalTitle {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.8125rem;
color: #64748b;
}
.terminalBody {
padding: 1.5rem;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.875rem;
line-height: 1.8;
}
.logLine {
display: flex;
gap: 1rem;
align-items: flex-start;
}
.timestamp {
color: #64748b;
flex-shrink: 0;
}
.logSuccess {
color: #00ff88;
font-weight: 600;
flex-shrink: 0;
min-width: 80px;
}
.logInfo {
color: #00d4ff;
font-weight: 600;
flex-shrink: 0;
min-width: 80px;
}
.logWarning {
color: #fbbf24;
font-weight: 600;
flex-shrink: 0;
min-width: 80px;
}
.logText {
color: #cbd5e1;
}
/* Sections */
.sessionSection,
.challengesSection,
.discussionsSection,
.statsSection,
.ctaSection {
padding: 4rem 0;
}
.sessionSection {
background: rgba(0, 212, 255, 0.02);
}
.sectionHeader {
text-align: center;
margin-bottom: 3rem;
}
.sectionTitle {
font-size: 2rem;
font-weight: 700;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.sectionSubtitle {
font-size: 1.1rem;
color: #94a3b8;
max-width: 600px;
margin: 0 auto;
}
/* Session Card */
.sessionCard {
background: linear-gradient(135deg, rgba(0, 212, 255, 0.1) 0%, rgba(0, 255, 136, 0.05) 100%);
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 16px;
padding: 2rem;
position: relative;
overflow: hidden;
}
.sessionCard::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #00d4ff 0%, #00ff88 50%, #00d4ff 100%);
}
.sessionMeta {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.sessionBadge {
display: inline-block;
padding: 0.375rem 1rem;
background: linear-gradient(135deg, #00d4ff 0%, #00ff88 100%);
color: #0a1628;
border-radius: 100px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.sessionTime {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.8125rem;
color: #64748b;
}
.sessionTitle {
font-size: 1.5rem;
font-weight: 800;
color: #f8fafc;
margin-bottom: 0.75rem;
}
.sessionDescription {
font-size: 1rem;
color: #94a3b8;
line-height: 1.7;
margin-bottom: 1.5rem;
max-width: 700px;
}
.sessionSpeakers {
display: flex;
align-items: center;
gap: 1.5rem;
flex-wrap: wrap;
}
.speaker {
display: flex;
align-items: center;
gap: 1rem;
}
.speakerAvatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: linear-gradient(135deg, #00d4ff 0%, #00ff88 100%);
color: #0a1628;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.875rem;
}
.speakerName {
font-size: 1rem;
font-weight: 700;
color: #f8fafc;
}
.speakerCompany {
font-size: 0.8125rem;
color: #64748b;
}
.speakerDivider {
font-size: 1.5rem;
color: #00d4ff;
font-weight: 300;
}
/* Challenges Grid */
.challengesSection {
background: rgba(0, 212, 255, 0.02);
}
.challengesGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.5rem;
}
.challengeCard {
background: rgba(10, 22, 40, 0.6);
border: 1px solid rgba(0, 212, 255, 0.15);
border-radius: 12px;
padding: 1.5rem;
text-align: center;
transition: all 0.3s ease;
}
.challengeCard:hover {
border-color: rgba(0, 212, 255, 0.3);
transform: translateY(-2px);
}
.challengeIcon {
font-size: 2rem;
margin-bottom: 1rem;
}
.challengeCard h3 {
font-size: 1.125rem;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 0.5rem;
}
.challengeCard p {
font-size: 0.9rem;
color: #94a3b8;
line-height: 1.6;
margin: 0;
}
/* Discussions List */
.discussionsList {
display: flex;
flex-direction: column;
gap: 1.5rem;
max-width: 800px;
margin: 0 auto;
}
.discussionItem {
display: flex;
gap: 1.5rem;
align-items: flex-start;
background: rgba(0, 212, 255, 0.05);
border: 1px solid rgba(0, 212, 255, 0.15);
border-radius: 12px;
padding: 1.5rem;
transition: all 0.3s ease;
}
.discussionItem:hover {
border-color: rgba(0, 212, 255, 0.3);
}
.discussionNumber {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 1.5rem;
font-weight: 800;
color: #00d4ff;
opacity: 0.6;
flex-shrink: 0;
}
.discussionContent h3 {
font-size: 1.125rem;
font-weight: 700;
color: #f8fafc;
margin: 0 0 0.5rem 0;
}
.discussionContent p {
font-size: 0.9rem;
color: #94a3b8;
line-height: 1.7;
margin: 0;
}
/* Stats Section */
.statsSection {
background: linear-gradient(135deg, rgba(0, 212, 255, 0.1) 0%, rgba(0, 255, 136, 0.05) 100%);
border-top: 1px solid rgba(0, 212, 255, 0.15);
border-bottom: 1px solid rgba(0, 212, 255, 0.15);
}
.statsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 2rem;
text-align: center;
}
.stat {
padding: 1.5rem;
}
.statNumber {
font-size: 2.5rem;
font-weight: 800;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
background: linear-gradient(135deg, #00d4ff 0%, #00ff88 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1;
margin-bottom: 0.5rem;
}
.statLabel {
font-size: 0.9rem;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* CTA Section */
.ctaContent {
max-width: 600px;
margin: 0 auto;
text-align: center;
background: linear-gradient(135deg, rgba(0, 212, 255, 0.08) 0%, rgba(0, 255, 136, 0.05) 100%);
border: 1px solid rgba(0, 212, 255, 0.2);
border-radius: 16px;
padding: 3rem 2rem;
}
.ctaTitle {
font-size: 1.75rem;
font-weight: 700;
color: #f1f5f9;
margin-bottom: 0.75rem;
}
.ctaText {
font-size: 1rem;
color: #94a3b8;
margin-bottom: 1.5rem;
line-height: 1.7;
}
.ctaButtons {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
}
/* Footer Navigation */
.footerNav {
padding: 2rem 0 3rem;
border-top: 1px solid rgba(0, 212, 255, 0.1);
}
.backLink {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: #94a3b8;
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
.backLink:hover {
color: #00d4ff;
}
.backIcon {
width: 1.25rem;
height: 1.25rem;
}
/* Responsive */
@media (max-width: 768px) {
.heroBanner {
height: 300px;
}
.bannerContent {
padding: 2rem 1.5rem;
}
.heroTitle {
font-size: 2rem;
}
.heroSubtitle {
font-size: 1.1rem;
}
.eventDetails {
flex-direction: column;
align-items: center;
gap: 1rem;
}
.heroCtas {
flex-direction: column;
align-items: center;
}
.primaryCta,
.secondaryCta {
width: 100%;
max-width: 280px;
text-align: center;
}
.terminalBody {
padding: 1rem;
font-size: 0.75rem;
}
.logLine {
flex-wrap: wrap;
gap: 0.5rem;
}
.sessionCard {
padding: 1.5rem;
}
.sessionTitle {
font-size: 1.25rem;
}
.sessionSpeakers {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.speakerDivider {
display: none;
}
.discussionItem {
flex-direction: column;
gap: 1rem;
padding: 1.5rem;
}
.ctaContent {
padding: 2rem 1.5rem;
}
.ctaButtons {
flex-direction: column;
align-items: center;
}
.terminalSection,
.sessionSection,
.challengesSection,
.discussionsSection,
.statsSection,
.ctaSection {
padding: 3rem 0;
}
}
@@ -0,0 +1,377 @@
import React from 'react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import { useForcedTheme } from '@site/src/hooks/useForcedTheme';
import Layout from '@theme/Layout';
import styles from './telecom-talks-2025.module.css';
export default function TelecomTalks2025(): React.ReactElement {
useForcedTheme('dark');
const handleSmoothScroll = (e: React.MouseEvent<HTMLAnchorElement>, targetId: string) => {
e.preventDefault();
const element = document.querySelector(targetId);
if (element) {
const offset = 80;
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - offset;
window.scrollTo({ top: offsetPosition, behavior: 'smooth' });
}
};
return (
<Layout
title="Telecom Talks 2025"
description="Promptfoo at Telecom Talks 2025. Ian Webster on stage with Swisscom Outpost discussing AI security in telecommunications."
>
<Head>
<meta property="og:title" content="Promptfoo at Telecom Talks 2025" />
<meta
property="og:description"
content="AI security for telecommunications. Ian Webster joined Swisscom Outpost on stage at Telecom Talks 2025 in Menlo Park."
/>
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.promptfoo.dev/events/telecom-talks-2025" />
<meta name="twitter:card" content="summary_large_image" />
<meta
property="og:image"
content="https://www.promptfoo.dev/img/events/telecom-talks-2025.jpg"
/>
<meta
name="twitter:image"
content="https://www.promptfoo.dev/img/events/telecom-talks-2025.jpg"
/>
<meta
name="keywords"
content="Telecom Talks 2025, telecommunications security, AI security, LLM security, Swisscom, 5G security"
/>
<link rel="canonical" href="https://www.promptfoo.dev/events/telecom-talks-2025" />
</Head>
<main className={styles.telecomPage}>
{/* Hero Banner */}
<section className={styles.heroBanner}>
<img
src="/img/events/telecom-talks-2025.jpg"
alt="Telecom Talks 2025"
className={styles.bannerImage}
/>
<div className={styles.bannerOverlay} />
<div className={styles.bannerContent}>
<div className={styles.badge}>
<span className={styles.badgeIcon}>📡</span>
Telecom Talks 2025
</div>
<h1 className={styles.heroTitle}>
Securing the <span className={styles.highlight}>Signal</span>
</h1>
</div>
</section>
{/* Hero Content */}
<section className={styles.heroContent}>
<div className={styles.container}>
<p className={styles.heroSubtitle}>
AI security meets telecom. Ian Webster joined Swisscom Outpost on stage to discuss
practical GenAI security challenges for real deployments.
</p>
<div className={styles.eventDetails}>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<span>April 9, 2025</span>
</div>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<span>SRI International, Menlo Park</span>
</div>
<div className={styles.detail}>
<svg className={styles.icon} viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"
/>
</svg>
<span>On Stage with Swisscom Outpost</span>
</div>
</div>
<div className={styles.heroCtas}>
<a
href="#session"
className={styles.primaryCta}
onClick={(e) => handleSmoothScroll(e, '#session')}
>
View Session
</a>
<Link to="/docs/red-team/" className={styles.secondaryCta}>
Explore Red Teaming
</Link>
</div>
</div>
</section>
{/* Terminal Event Log */}
<section className={styles.terminalSection}>
<div className={styles.container}>
<div className={styles.terminal}>
<div className={styles.terminalHeader}>
<div className={styles.terminalDots}>
<span />
<span />
<span />
</div>
<span className={styles.terminalTitle}>event.log</span>
</div>
<div className={styles.terminalBody}>
<div className={styles.logLine}>
<span className={styles.timestamp}>[CONNECTED]</span>
<span className={styles.logSuccess}>LOCATION</span>
<span className={styles.logText}>Telecom Talks 2025 SRI International</span>
</div>
<div className={styles.logLine}>
<span className={styles.timestamp}>[SESSION]</span>
<span className={styles.logInfo}>PANEL</span>
<span className={styles.logText}>
GenAI security challenges with Swisscom Outpost
</span>
</div>
<div className={styles.logLine}>
<span className={styles.timestamp}>[DEMO]</span>
<span className={styles.logWarning}>LIVE</span>
<span className={styles.logText}>LLM red teaming concepts and failure modes</span>
</div>
<div className={styles.logLine}>
<span className={styles.timestamp}>[NETWORK]</span>
<span className={styles.logSuccess}>CONNECT</span>
<span className={styles.logText}>
Conversations with telecom security leaders
</span>
</div>
</div>
</div>
</div>
</section>
{/* Session Section */}
<section id="session" className={styles.sessionSection}>
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>The Session</h2>
</div>
<div className={styles.sessionCard}>
<div className={styles.sessionMeta}>
<span className={styles.sessionBadge}>Joint Presentation</span>
<span className={styles.sessionTime}>
April 9, 2025 SRI International, Menlo Park
</span>
</div>
<h3 className={styles.sessionTitle}>AI Security in Telecommunications</h3>
<p className={styles.sessionDescription}>
Ian Webster joined Swisscom's security team on stage to discuss the unique
challenges of securing AI systems in telecommunications infrastructure. From
customer service chatbots to network operations assistants, telecom companies are
deploying LLMs at scale—and facing new security challenges.
</p>
<div className={styles.sessionSpeakers}>
<div className={styles.speaker}>
<div className={styles.speakerAvatar}>IW</div>
<div>
<div className={styles.speakerName}>Ian Webster</div>
<div className={styles.speakerCompany}>Promptfoo</div>
</div>
</div>
<div className={styles.speakerDivider}>+</div>
<div className={styles.speaker}>
<div className={styles.speakerAvatar}>SC</div>
<div>
<div className={styles.speakerName}>Swisscom Outpost</div>
<div className={styles.speakerCompany}>Security Team</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Telecom Challenges Section */}
<section className={styles.challengesSection}>
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>Telecom Security Challenges</h2>
<p className={styles.sectionSubtitle}>
Why AI security is critical for the telecommunications industry.
</p>
</div>
<div className={styles.challengesGrid}>
<div className={styles.challengeCard}>
<div className={styles.challengeIcon}>🌐</div>
<h3>5G & Edge Computing</h3>
<p>
AI at the network edge introduces new attack surfaces. From base stations to
customer premises equipment, LLMs are everywhere.
</p>
</div>
<div className={styles.challengeCard}>
<div className={styles.challengeIcon}>🔌</div>
<h3>API Exposure</h3>
<p>
Telecom APIs increasingly use AI for routing, fraud detection, and customer
service. Each endpoint is a potential vulnerability.
</p>
</div>
<div className={styles.challengeCard}>
<div className={styles.challengeIcon}>📱</div>
<h3>Customer Data</h3>
<p>
Call records, location data, billing information. AI systems processing this data
need rigorous security testing.
</p>
</div>
<div className={styles.challengeCard}>
<div className={styles.challengeIcon}>⚡</div>
<h3>Critical Infrastructure</h3>
<p>
Networks that power emergency services, hospitals, and financial systems. The
stakes for AI security couldn't be higher.
</p>
</div>
</div>
</div>
</section>
{/* Key Discussions */}
<section className={styles.discussionsSection}>
<div className={styles.container}>
<div className={styles.sectionHeader}>
<h2 className={styles.sectionTitle}>Key Discussions</h2>
</div>
<div className={styles.discussionsList}>
<div className={styles.discussionItem}>
<div className={styles.discussionNumber}>01</div>
<div className={styles.discussionContent}>
<h3>Carrier-Grade LLM Security</h3>
<p>
How major telecom providers are approaching AI security at scale, including
lessons learned from early deployments and best practices emerging across the
industry.
</p>
</div>
</div>
<div className={styles.discussionItem}>
<div className={styles.discussionNumber}>02</div>
<div className={styles.discussionContent}>
<h3>Network API Protection</h3>
<p>
Strategies for securing AI-powered network APIs against prompt injection, data
exfiltration, and unauthorized access attempts.
</p>
</div>
</div>
<div className={styles.discussionItem}>
<div className={styles.discussionNumber}>03</div>
<div className={styles.discussionContent}>
<h3>Regulatory Compliance</h3>
<p>
Navigating GDPR, telecommunications regulations, and emerging AI governance
frameworks while deploying innovative AI solutions.
</p>
</div>
</div>
</div>
</div>
</section>
{/* Signal Stats */}
<section className={styles.statsSection}>
<div className={styles.container}>
<div className={styles.statsGrid}>
<div className={styles.stat}>
<div className={styles.statNumber}>1</div>
<div className={styles.statLabel}>Day</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>200+</div>
<div className={styles.statLabel}>Attendees</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>0ms</div>
<div className={styles.statLabel}>Latency to Security</div>
</div>
<div className={styles.stat}>
<div className={styles.statNumber}>5G</div>
<div className={styles.statLabel}>Speed</div>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className={styles.ctaSection}>
<div className={styles.container}>
<div className={styles.ctaContent}>
<h2 className={styles.ctaTitle}>Establish Connection</h2>
<p className={styles.ctaText}>
Ready to secure your telecom AI deployments? Start testing with Promptfoo's
open-source red teaming framework.
</p>
<div className={styles.ctaButtons}>
<Link to="/docs/red-team/quickstart/" className={styles.primaryCta}>
Get Started
</Link>
<Link to="https://github.com/promptfoo/promptfoo" className={styles.secondaryCta}>
View Source
</Link>
</div>
</div>
</div>
</section>
{/* Footer Navigation */}
<section className={styles.footerNav}>
<div className={styles.container}>
<Link to="/events" className={styles.backLink}>
<svg
className={styles.backIcon}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back to All Events
</Link>
</div>
</section>
</main>
</Layout>
);
}
+85
View File
@@ -0,0 +1,85 @@
import React from 'react';
import { useColorMode } from '@docusaurus/theme-common';
import Box from '@mui/material/Box';
import Container from '@mui/material/Container';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import Layout from '@theme/Layout';
const FeedbackPageContent = () => {
const { colorMode } = useColorMode();
const theme = React.useMemo(
() =>
createTheme({
palette: {
mode: colorMode === 'dark' ? 'dark' : 'light',
primary: {
main: '#0066cc',
},
},
}),
[colorMode],
);
const iframeStyle: React.CSSProperties = {
width: '100%',
height: '1100px',
border: 0,
borderRadius: '8px',
overflow: 'hidden',
};
return (
<ThemeProvider theme={theme}>
<Container maxWidth="md" sx={{ py: 8 }}>
<Box sx={{ textAlign: 'center', mb: 6 }}>
<Typography variant="h3" component="h1" gutterBottom sx={{ fontWeight: 600, mb: 2 }}>
We'd love your feedback
</Typography>
<Typography
variant="h6"
color="text.secondary"
sx={{ maxWidth: '600px', mx: 'auto', lineHeight: 1.6 }}
>
Help us improve Promptfoo by sharing your thoughts and suggestions
</Typography>
</Box>
<Box
sx={{
boxShadow:
colorMode === 'dark'
? '0 4px 20px rgba(0, 0, 0, 0.3)'
: '0 4px 20px rgba(0, 0, 0, 0.1)',
borderRadius: '8px',
overflow: 'hidden',
mb: 4,
}}
>
<iframe
src="https://docs.google.com/forms/d/e/1FAIpQLScAnqlqX-ep-aOn6umjXXDVafc1sLTOEd5W6rMAPKllLk0CIA/viewform?embedded=1"
title="PromptFoo Feedback Form"
sandbox="allow-scripts allow-forms allow-same-origin"
style={iframeStyle}
loading="lazy"
scrolling="no"
>
Loading
</iframe>
</Box>
</Container>
</ThemeProvider>
);
};
const Feedback = () => {
return (
<Layout title="Feedback" description="Share your feedback and help us improve Promptfoo">
<FeedbackPageContent />
</Layout>
);
};
export default Feedback;
+202
View File
@@ -0,0 +1,202 @@
import React from 'react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import AccountBalanceIcon from '@mui/icons-material/AccountBalance';
import SecurityUpdateGoodIcon from '@mui/icons-material/SecurityUpdateGood';
import ShieldIcon from '@mui/icons-material/Shield';
import VerifiedUserIcon from '@mui/icons-material/VerifiedUser';
import Layout from '@theme/Layout';
import clsx from 'clsx';
import LogoContainer from '../components/LogoContainer';
import styles from './landing-page.module.css';
function HeroSection() {
return (
<section className={styles.heroSection}>
<div className="container">
<img src="/img/guardrails-framed.png" alt="AI Guardrails" className={styles.heroImage} />
<div className={styles.logoSection}>
Promptfoo is trusted by teams at...
<LogoContainer className={styles.heroLogos} noBackground noBorder />
</div>
{/*
<h2>Guardrails that evolve with emerging threats</h2>
<p>
Break free from static guardrails with our adaptive system that learns from red team
findings and real-world attacks.
</p>
*/}
</div>
</section>
);
}
function GuardrailsHeader() {
return (
<header className={clsx('hero', styles.heroBanner)}>
<div className="container">
<div className={styles.heroContent}>
<h1 className={styles.heroTitle}>AI Guardrails that learn & adapt</h1>
<p className={styles.heroSubtitle}>
Self-improving protection powered by continuous red teaming feedback
</p>
<div className={styles.heroButtons}>
<Link
className={clsx('button button--primary button--lg', styles.buttonPrimary)}
to="/contact/"
>
Request a Demo
</Link>
</div>
</div>
</div>
<HeroSection />
</header>
);
}
function BenefitsSection() {
return (
<section className={styles.benefitsSection}>
<div className="container">
<div className={styles.sectionEyebrow}>THE ADAPTIVE ADVANTAGE</div>
<h2 className={styles.sectionTitle}>Why choose adaptive guardrails?</h2>
<p className={styles.sectionSubtitle}>
Move beyond static rules to guardrails that evolve with emerging threats
</p>
<div className={styles.benefitsList}>
<div className={styles.benefitItem}>
<ShieldIcon className={styles.benefitIcon} />
<div className={styles.benefitContent}>
<h3>Self-improving protection</h3>
<p>
Unlike static guardrails, our system continuously learns from red team findings,
becoming more effective against new and evolving threats over time.
</p>
</div>
</div>
<div className={styles.benefitItem}>
<SecurityUpdateGoodIcon className={styles.benefitIcon} />
<div className={styles.benefitContent}>
<h3>Validate any guardrail system</h3>
<p>
Use our red teaming capabilities to test and improve third-party guardrails you
already have in place, providing an independent verification layer.
</p>
</div>
</div>
<div className={styles.benefitItem}>
<AccountBalanceIcon className={styles.benefitIcon} />
<div className={styles.benefitContent}>
<h3>Fast, flexible deployment</h3>
<p>
Implement in minutes with minimal code changes, on cloud or on-premises, supporting
all major LLM providers and custom models.
</p>
</div>
</div>
<div className={styles.benefitItem}>
<VerifiedUserIcon className={styles.benefitIcon} />
<div className={styles.benefitContent}>
<h3>Data-driven improvement</h3>
<p>
Our system uses actual attack data to refine defenses, creating a feedback loop that
makes your guardrails stronger with every attempted breach.
</p>
</div>
</div>
</div>
</div>
</section>
);
}
function ActionOrientedSection() {
return (
<section className={styles.actionOrientedSection}>
<div className="container">
<div className={styles.sectionEyebrow}>ADAPTIVE LEARNING</div>
<h2 className={styles.sectionTitle}>Guardrails that get smarter over time</h2>
<p className={styles.sectionSubtitle}>
Our unique feedback loop between red teaming and guardrails creates a continuously
improving defense system.
</p>
<div className={styles.screenshotPlaceholder}>
<img
loading="lazy"
src="/img/guardrails-table.png"
alt="Adaptive guardrails implementation"
/>
</div>
</div>
</section>
);
}
function ContinuousMonitoringSection() {
return (
<section className={styles.actionOrientedSection}>
<div className="container">
<div className={styles.sectionEyebrow}>INDEPENDENT VALIDATION</div>
<h2 className={styles.sectionTitle}>Third-Party Guardrail Validation</h2>
<p className={styles.sectionSubtitle}>
Already using another guardrail system? Our platform can validate and improve any
guardrail solution.
</p>
<div className={styles.screenshotPlaceholder}>
<img
loading="lazy"
src="/img/continuous-monitoring.png"
srcSet="/img/continuous-monitoring.png 1x, /img/continuous-monitoring@2x.png 2x"
alt="Third-party guardrail validation"
/>
</div>
</div>
</section>
);
}
function CallToActionSection() {
return (
<section className={styles.finalCTA}>
<div className="container">
<h2 className={styles.finalCTATitle}>Upgrade to guardrails that learn and adapt</h2>
<p className={styles.finalCTASubtitle}>
Join leading enterprises using adaptive AI guardrails to stay ahead of emerging threats.
</p>
<div className={styles.finalCTAButtons}>
<Link className="button button--primary button--lg" to="/contact/">
Request Demo
</Link>
</div>
<div className={styles.finalCTATrust}>
<p> Self-improving Third-party validation Cloud or on-premise</p>
</div>
</div>
</section>
);
}
export default function Guardrails(): React.ReactElement {
return (
<Layout
title="AI Guardrails"
description="Comprehensive AI guardrails to ensure safe, compliant, and brand-aligned outputs. Protect against harmful content, PII exposure, prompt injections, and regulatory violations."
>
<Head>
<meta property="og:image" content="https://www.promptfoo.dev/img/meta/guardrails.png" />
<meta name="twitter:card" content="summary_large_image" />
</Head>
<div className={styles.pageContainer}>
<GuardrailsHeader />
<main className={styles.mainContent}>
<ActionOrientedSection />
<ContinuousMonitoringSection />
<BenefitsSection />
<CallToActionSection />
</main>
</div>
</Layout>
);
}
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More