Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions web/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
.next/
out/
22 changes: 22 additions & 0 deletions web/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
@import "tailwindcss";

:root {
--bg: #09090b;
--fg: #fafafa;
--muted: #71717a;
--border: #27272a;
--accent: #dc2626;
--accent-muted: #991b1b;
--surface: #18181b;
--surface-hover: #27272a;
}

body {
background: var(--bg);
color: var(--fg);
font-family: "Geist Sans", system-ui, -apple-system, sans-serif;
}

code, pre, .mono {
font-family: "Geist Mono", ui-monospace, monospace;
}
20 changes: 20 additions & 0 deletions web/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
title: "Prompt Injection Database",
description:
"Curated database of prompt injection attacks for defensive AI security research",
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className="dark">
<body className="min-h-screen antialiased">{children}</body>
</html>
);
}
43 changes: 43 additions & 0 deletions web/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { getPrompts, getStats, getTechniques, getSources } from "@/lib/data";
import { PromptBrowser } from "./prompt-browser";

export default function Home() {
const prompts = getPrompts();
const stats = getStats();
const techniques = getTechniques();
const sources = getSources();

return (
<main className="mx-auto max-w-6xl px-4 py-8">
<header className="mb-8">
<h1 className="text-3xl font-bold tracking-tight">
Prompt Injection Database
</h1>
<p className="mt-2 text-[var(--muted)]">
{stats.total.toLocaleString()} curated attack prompts for defensive AI
security research
</p>
<div className="mt-4 flex flex-wrap gap-4 text-sm">
<span className="rounded bg-[var(--surface)] px-3 py-1">
{Object.keys(stats.byTechnique).length} techniques
</span>
<span className="rounded bg-[var(--surface)] px-3 py-1">
Avg sophistication: {stats.avgSophistication}
</span>
<span className="rounded bg-[var(--surface)] px-3 py-1">
{stats.curated.toLocaleString()} curated
</span>
<span className="rounded bg-[var(--surface)] px-3 py-1">
{Object.keys(stats.bySource).length} sources
</span>
</div>
</header>

<PromptBrowser
prompts={prompts}
techniques={techniques}
sources={sources}
/>
</main>
);
}
228 changes: 228 additions & 0 deletions web/app/prompt-browser.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
"use client";

import { useState, useMemo } from "react";
import type { Prompt } from "@/lib/data";

interface Props {
prompts: Prompt[];
techniques: string[];
sources: string[];
}

export function PromptBrowser({ prompts, techniques, sources }: Props) {
const [query, setQuery] = useState("");
const [technique, setTechnique] = useState("");
const [source, setSource] = useState("");
const [minScore, setMinScore] = useState(0);
const [selected, setSelected] = useState<Prompt | null>(null);
const [page, setPage] = useState(0);

const PAGE_SIZE = 25;

const filtered = useMemo(() => {
const q = query.toLowerCase();
return prompts.filter((p) => {
if (q && !p.content.toLowerCase().includes(q)) return false;
if (technique && p.technique !== technique) return false;
if (source && p.source !== source) return false;
if (p.sophistication_score < minScore) return false;
return true;
});
}, [prompts, query, technique, source, minScore]);

const pageCount = Math.ceil(filtered.length / PAGE_SIZE);
const visible = filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);

function resetFilters() {
setQuery("");
setTechnique("");
setSource("");
setMinScore(0);
setPage(0);
}

return (
<div>
{/* Filters */}
<div className="mb-6 space-y-3">
<input
type="text"
placeholder="Search prompt content..."
value={query}
onChange={(e) => {
setQuery(e.target.value);
setPage(0);
}}
className="w-full rounded border border-[var(--border)] bg-[var(--surface)] px-4 py-2.5 text-sm placeholder-[var(--muted)] outline-none focus:border-[var(--accent)]"
/>

<div className="flex flex-wrap gap-3">
<select
value={technique}
onChange={(e) => {
setTechnique(e.target.value);
setPage(0);
}}
className="rounded border border-[var(--border)] bg-[var(--surface)] px-3 py-2 text-sm"
>
<option value="">All techniques</option>
{techniques.map((t) => (
<option key={t} value={t}>
{t.replace(/_/g, " ")}
</option>
))}
</select>

<select
value={source}
onChange={(e) => {
setSource(e.target.value);
setPage(0);
}}
className="rounded border border-[var(--border)] bg-[var(--surface)] px-3 py-2 text-sm"
>
<option value="">All sources</option>
{sources.map((s) => (
<option key={s} value={s}>
{s}
</option>
))}
</select>

<label className="flex items-center gap-2 text-sm text-[var(--muted)]">
Min score:
<input
type="range"
min={0}
max={15}
value={minScore}
onChange={(e) => {
setMinScore(Number(e.target.value));
setPage(0);
}}
className="w-24"
/>
<span className="mono w-6 text-[var(--fg)]">{minScore}</span>
</label>

<button
onClick={resetFilters}
className="rounded border border-[var(--border)] px-3 py-2 text-sm text-[var(--muted)] hover:bg-[var(--surface-hover)]"
>
Reset
</button>
</div>

<p className="text-sm text-[var(--muted)]">
{filtered.length.toLocaleString()} prompts match
</p>
</div>

{/* Detail panel */}
{selected && (
<div className="mb-6 rounded border border-[var(--border)] bg-[var(--surface)] p-4">
<div className="mb-3 flex items-start justify-between">
<div>
<span className="mono text-sm text-[var(--accent)]">
#{selected.id}
</span>
<span className="ml-3 rounded bg-[var(--accent-muted)] px-2 py-0.5 text-xs font-medium text-[var(--fg)]">
{selected.technique.replace(/_/g, " ")}
</span>
<span className="ml-2 rounded bg-[var(--surface-hover)] px-2 py-0.5 text-xs">
{selected.complexity}
</span>
<span className="ml-2 text-xs text-[var(--muted)]">
score: {selected.sophistication_score}
</span>
</div>
<button
onClick={() => setSelected(null)}
className="text-[var(--muted)] hover:text-[var(--fg)]"
>
&times;
</button>
</div>

<pre className="mono max-h-80 overflow-auto whitespace-pre-wrap text-sm leading-relaxed">
{selected.content}
</pre>

<div className="mt-3 flex flex-wrap gap-2">
{selected.tags.map((tag) => (
<span
key={tag}
className="rounded bg-[var(--bg)] px-2 py-0.5 text-xs text-[var(--muted)]"
>
{tag}
</span>
))}
</div>

{selected.owasp_ids.length > 0 && (
<div className="mt-2 text-xs text-[var(--muted)]">
OWASP: {selected.owasp_ids.join(", ")}
</div>
)}

<div className="mt-2 text-xs text-[var(--muted)]">
Source: {selected.source}
</div>
</div>
)}

{/* Prompt list */}
<div className="space-y-1">
{visible.map((p) => (
<button
key={p.id}
onClick={() => setSelected(p)}
className={`w-full rounded px-4 py-3 text-left transition-colors ${
selected?.id === p.id
? "border border-[var(--accent)] bg-[var(--surface)]"
: "border border-transparent hover:bg-[var(--surface)]"
}`}
>
<div className="flex items-center gap-3">
<span className="mono shrink-0 text-xs text-[var(--muted)]">
#{p.id}
</span>
<span className="shrink-0 rounded bg-[var(--accent-muted)] px-1.5 py-0.5 text-xs">
{p.technique.replace(/_/g, " ")}
</span>
<span className="mono shrink-0 text-xs text-[var(--muted)]">
{p.sophistication_score}
</span>
<span className="truncate text-sm">
{p.content.slice(0, 120).replace(/\n/g, " ")}
</span>
</div>
</button>
))}
</div>

{/* Pagination */}
{pageCount > 1 && (
<div className="mt-6 flex items-center justify-center gap-4">
<button
disabled={page === 0}
onClick={() => setPage(page - 1)}
className="rounded border border-[var(--border)] px-3 py-1.5 text-sm disabled:opacity-30"
>
Previous
</button>
<span className="text-sm text-[var(--muted)]">
Page {page + 1} of {pageCount}
</span>
<button
disabled={page >= pageCount - 1}
onClick={() => setPage(page + 1)}
className="rounded border border-[var(--border)] px-3 py-1.5 text-sm disabled:opacity-30"
>
Next
</button>
</div>
)}
</div>
);
}
6 changes: 6 additions & 0 deletions web/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
7 changes: 7 additions & 0 deletions web/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
output: "export",
};

export default nextConfig;
Loading
Loading