From 922e90f5d7000ef3f7b78546917f32c3bafe8e96 Mon Sep 17 00:00:00 2001 From: Berk Durmus Date: Sat, 24 Jan 2026 15:55:54 +0300 Subject: [PATCH] feat(ai-regex-generator): add BYOK AI regex generator --- README.md | 1 + components/seo/AiRegexGeneratorSEO.tsx | 77 ++++ .../utils/ai-regex-generator.utils.test.ts | 76 ++++ components/utils/ai-regex-generator.utils.ts | 239 ++++++++++ components/utils/tools-list.ts | 6 + pages/utilities/ai-regex-generator.tsx | 419 ++++++++++++++++++ 6 files changed, 818 insertions(+) create mode 100644 components/seo/AiRegexGeneratorSEO.tsx create mode 100644 components/utils/ai-regex-generator.utils.test.ts create mode 100644 components/utils/ai-regex-generator.utils.ts create mode 100644 pages/utilities/ai-regex-generator.tsx diff --git a/README.md b/README.md index 0273187..ea58cb6 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Here is the list of all utilities: - [Number Base Changer](https://jam.dev/utilities/number-base-changer) - [CSS Inliner for Email](https://jam.dev/utilities/css-inliner-for-email) - [Regex Tester](https://jam.dev/utilities/regex-tester) +- [AI Regex Generator](https://jam.dev/utilities/ai-regex-generator) - [Image Resizer](https://jam.dev/utilities/image-resizer) - [CSS Units Converter](https://jam.dev/utilities/css-units-converter) - [JWT Parser](https://jam.dev/utilities/jwt-parser) diff --git a/components/seo/AiRegexGeneratorSEO.tsx b/components/seo/AiRegexGeneratorSEO.tsx new file mode 100644 index 0000000..f06ea05 --- /dev/null +++ b/components/seo/AiRegexGeneratorSEO.tsx @@ -0,0 +1,77 @@ +import Link from "next/link"; + +export default function AiRegexGeneratorSEO() { + return ( +
+
+

+ Generate regex patterns from natural language with Jam's AI Regex + Generator. Bring your own API key, keep data in your browser, and get a + clean pattern with explanations and examples. +

+
+ +
+

How to use the AI Regex Generator

+

+ Describe what you want to match, add optional sample text, and let the + generator build a JavaScript-compatible regex. You can instantly test + the output against your own text. +

+
+ +
+

Privacy-friendly by design

+

+ This tool runs entirely in the browser and uses your own API key. Your + key stays on your device and is never sent to Jam servers. +

+
+ +
+

Why use an AI regex generator?

+
    +
  • + Faster debugging:
    Quickly generate patterns that can + filter logs, parse errors, or validate input. +
  • +
  • + Clear explanations:
    Understand why a regex works so + you can maintain it confidently. +
  • +
  • + Real-world examples:
    Get sample matches to validate + behavior before shipping. +
  • +
+
+ +
+

More regex tools

+

+ Need to dive deeper? Pair this with our{" "} + Regex Tester to visualize + matches, flags, and capture groups. +

+
+ +
+

FAQs

+
    +
  • + Does this tool store my API key?
    Only if you choose to + remember it in your browser. It is never sent to Jam servers. +
  • +
  • + Which providers are supported?
    OpenAI and Anthropic + are supported, and you can choose the model you want to use. +
  • +
  • + Is the generated regex always correct?
    AI output can + be imperfect. Always validate with your own test cases. +
  • +
+
+
+ ); +} diff --git a/components/utils/ai-regex-generator.utils.test.ts b/components/utils/ai-regex-generator.utils.test.ts new file mode 100644 index 0000000..cd50dc4 --- /dev/null +++ b/components/utils/ai-regex-generator.utils.test.ts @@ -0,0 +1,76 @@ +import { + buildRegexPrompt, + getDefaultModel, + parseRegexResponse, +} from "./ai-regex-generator.utils"; + +describe("buildRegexPrompt", () => { + test("includes description and optional fields", () => { + const prompt = buildRegexPrompt({ + description: "Match email addresses", + sampleText: "test@example.com", + expectedMatches: "test@example.com", + }); + + expect(prompt.system).toContain("regex patterns"); + expect(prompt.user).toContain("Match email addresses"); + expect(prompt.user).toContain("Sample text:"); + expect(prompt.user).toContain("Expected matches:"); + expect(prompt.user).toContain('"pattern"'); + }); +}); + +describe("parseRegexResponse", () => { + test("parses valid JSON response", () => { + const result = parseRegexResponse( + JSON.stringify({ + pattern: "\\d+", + flags: "g", + regex: "/\\d+/g", + explanation: "Matches digits", + examples: [{ text: "123", matches: ["123"] }], + warnings: [], + }) + ); + + expect(result.regex).toBe("/\\d+/g"); + expect(result.pattern).toBe("\\d+"); + expect(result.flags).toBe("g"); + expect(result.explanation).toContain("digits"); + expect(result.examples).toHaveLength(1); + }); + + test("parses fenced JSON response", () => { + const result = parseRegexResponse( + "```json\n" + + JSON.stringify({ + pattern: "[a-z]+", + flags: "i", + regex: "/[a-z]+/i", + explanation: "Matches letters", + examples: [], + warnings: [], + }) + + "\n```" + ); + + expect(result.regex).toBe("/[a-z]+/i"); + expect(result.pattern).toBe("[a-z]+"); + }); + + test("falls back to regex extraction when JSON is invalid", () => { + const result = parseRegexResponse( + "Try using /[A-Z]{2,}/g to match uppercase strings." + ); + + expect(result.regex).toBe("/[A-Z]{2,}/g"); + expect(result.warnings[0]).toContain("could not be parsed"); + }); +}); + +describe("getDefaultModel", () => { + test("returns provider defaults", () => { + expect(getDefaultModel("openai")).toBe("gpt-4o-mini"); + expect(getDefaultModel("anthropic")).toBe("claude-3-haiku-20240307"); + }); +}); diff --git a/components/utils/ai-regex-generator.utils.ts b/components/utils/ai-regex-generator.utils.ts new file mode 100644 index 0000000..d3635a5 --- /dev/null +++ b/components/utils/ai-regex-generator.utils.ts @@ -0,0 +1,239 @@ +export type AiProvider = "openai" | "anthropic"; + +export interface AiRegexExample { + text: string; + matches: string[]; +} + +export interface AiRegexResult { + pattern: string; + flags: string; + regex: string; + explanation: string; + examples: AiRegexExample[]; + warnings: string[]; + raw?: string; +} + +export interface AiRegexRequest { + description: string; + sampleText?: string; + expectedMatches?: string; +} + +export interface AiRegexProviderRequest extends AiRegexRequest { + provider: AiProvider; + apiKey: string; + model?: string; +} + +const DEFAULT_OPENAI_MODEL = "gpt-4o-mini"; +const DEFAULT_ANTHROPIC_MODEL = "claude-3-haiku-20240307"; + +const buildPromptContext = (request: AiRegexRequest) => { + const parts = [ + `Task: Generate a JavaScript-compatible regex for this description:`, + request.description.trim(), + ]; + + if (request.sampleText?.trim()) { + parts.push("", "Sample text:", request.sampleText.trim()); + } + + if (request.expectedMatches?.trim()) { + parts.push("", "Expected matches:", request.expectedMatches.trim()); + } + + parts.push( + "", + "Return only valid JSON with the following fields:", + `{"pattern":"...","flags":"gimsuy","regex":"/.../flags","explanation":"...","examples":[{"text":"...","matches":["..."]}],"warnings":["..."]}`, + "Rules:", + "- pattern should NOT include slashes.", + "- flags should include only valid JS regex flags (gimsuy).", + "- regex should include slashes + flags.", + "- explanation should be concise and practical.", + "- warnings should be empty array if none.", + "- If unsure, include warnings explaining assumptions." + ); + + return parts.join("\n"); +}; + +export const buildRegexPrompt = (request: AiRegexRequest) => ({ + system: + "You are a senior engineer generating safe JavaScript regex patterns.", + user: buildPromptContext(request), +}); + +const stripCodeFences = (input: string) => { + const fenced = input.match(/```(?:json)?\s*([\s\S]*?)```/i); + if (fenced?.[1]) { + return fenced[1].trim(); + } + return input.trim(); +}; + +const extractJson = (input: string) => { + const start = input.indexOf("{"); + const end = input.lastIndexOf("}"); + if (start !== -1 && end !== -1 && end > start) { + return input.slice(start, end + 1); + } + return input; +}; + +const buildRegexFromParts = (pattern: string, flags: string) => { + const trimmedPattern = pattern.trim(); + const trimmedFlags = flags.trim(); + if (!trimmedPattern) { + return ""; + } + return `/${trimmedPattern}/${trimmedFlags}`; +}; + +const extractRegexFromText = (input: string) => { + const firstSlash = input.indexOf("/"); + const lastSlash = input.lastIndexOf("/"); + if (firstSlash !== -1 && lastSlash > firstSlash) { + const pattern = input.slice(firstSlash + 1, lastSlash); + const flags = input.slice(lastSlash + 1).match(/[gimsuy]+/)?.[0] || ""; + return { pattern, flags, regex: `/${pattern}/${flags}` }; + } + return { pattern: "", flags: "", regex: "" }; +}; + +export const parseRegexResponse = (content: string): AiRegexResult => { + const cleaned = stripCodeFences(content); + const jsonCandidate = extractJson(cleaned); + + try { + const parsed = JSON.parse(jsonCandidate) as Partial; + const pattern = (parsed.pattern || "").trim(); + const flags = (parsed.flags || "").trim(); + const regex = + (parsed.regex || "").trim() || buildRegexFromParts(pattern, flags); + + return { + pattern, + flags, + regex, + explanation: (parsed.explanation || "").trim(), + examples: parsed.examples ?? [], + warnings: parsed.warnings ?? [], + raw: content, + }; + } catch { + const fallback = extractRegexFromText(cleaned); + return { + pattern: fallback.pattern, + flags: fallback.flags, + regex: fallback.regex, + explanation: cleaned.slice(0, 600).trim(), + examples: [], + warnings: [ + "AI response could not be parsed as JSON. Showing best-effort extraction.", + ], + raw: content, + }; + } +}; + +export const callOpenAI = async ( + request: AiRegexProviderRequest +): Promise => { + const { model = DEFAULT_OPENAI_MODEL, apiKey } = request; + const prompt = buildRegexPrompt(request); + + const response = await fetch("https://api.openai.com/v1/chat/completions", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ + model, + temperature: 0.2, + messages: [ + { role: "system", content: prompt.system }, + { role: "user", content: prompt.user }, + ], + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `OpenAI request failed (${response.status}): ${errorText || "Unknown error"}` + ); + } + + const data = (await response.json()) as { + choices?: { message?: { content?: string } }[]; + }; + const content = data.choices?.[0]?.message?.content?.trim(); + if (!content) { + throw new Error("OpenAI response missing content."); + } + return parseRegexResponse(content); +}; + +export const callAnthropic = async ( + request: AiRegexProviderRequest +): Promise => { + const { model = DEFAULT_ANTHROPIC_MODEL, apiKey } = request; + const prompt = buildRegexPrompt(request); + + const response = await fetch("https://api.anthropic.com/v1/messages", { + method: "POST", + headers: { + "Content-Type": "application/json", + "anthropic-version": "2023-06-01", + "x-api-key": apiKey, + }, + body: JSON.stringify({ + model, + max_tokens: 700, + temperature: 0.2, + system: prompt.system, + messages: [{ role: "user", content: prompt.user }], + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error( + `Anthropic request failed (${response.status}): ${errorText || "Unknown error"}` + ); + } + + const data = (await response.json()) as { + content?: { text?: string }[]; + }; + const content = data.content?.map((item) => item.text || "").join("").trim(); + if (!content) { + throw new Error("Anthropic response missing content."); + } + return parseRegexResponse(content); +}; + +export const generateAiRegex = async ( + request: AiRegexProviderRequest +): Promise => { + if (!request.apiKey.trim()) { + throw new Error("API key is required."); + } + + if (!request.description.trim()) { + throw new Error("Description is required."); + } + + if (request.provider === "openai") { + return callOpenAI(request); + } + + return callAnthropic(request); +}; + +export const getDefaultModel = (provider: AiProvider) => + provider === "openai" ? DEFAULT_OPENAI_MODEL : DEFAULT_ANTHROPIC_MODEL; diff --git a/components/utils/tools-list.ts b/components/utils/tools-list.ts index aa8ef64..4327f45 100644 --- a/components/utils/tools-list.ts +++ b/components/utils/tools-list.ts @@ -101,6 +101,12 @@ export const tools = [ "Test and debug your regular expressions in real-time. Provides quick feedback on pattern matching for strings.", link: "/utilities/regex-tester", }, + { + title: "AI Regex Generator", + description: + "Generate regex patterns from natural language with your own API key. Get explanations and examples instantly.", + link: "/utilities/ai-regex-generator", + }, { title: "CSS Units Converter", description: diff --git a/pages/utilities/ai-regex-generator.tsx b/pages/utilities/ai-regex-generator.tsx new file mode 100644 index 0000000..eea44c5 --- /dev/null +++ b/pages/utilities/ai-regex-generator.tsx @@ -0,0 +1,419 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import PageHeader from "@/components/PageHeader"; +import Header from "@/components/Header"; +import { CMDK } from "@/components/CMDK"; +import Meta from "@/components/Meta"; +import { Card } from "@/components/ds/CardComponent"; +import { Label } from "@/components/ds/LabelComponent"; +import { Textarea } from "@/components/ds/TextareaComponent"; +import { Input } from "@/components/ds/InputComponent"; +import { Button } from "@/components/ds/ButtonComponent"; +import { Checkbox } from "@/components/ds/CheckboxComponent"; +import { Combobox } from "@/components/ds/ComboboxComponent"; +import CallToActionGrid from "@/components/CallToActionGrid"; +import RegexHighlightText from "@/components/RegexHighlightText"; +import { useCopyToClipboard } from "@/components/hooks/useCopyToClipboard"; +import { createRegex } from "@/components/utils/regex-tester.utils"; +import AiRegexGeneratorSEO from "@/components/seo/AiRegexGeneratorSEO"; +import { + AiProvider, + AiRegexResult, + generateAiRegex, + getDefaultModel, +} from "@/components/utils/ai-regex-generator.utils"; + +const STORAGE_PREFIX = "jam-ai-regex"; + +const providerOptions = [ + { value: "openai", label: "OpenAI" }, + { value: "anthropic", label: "Anthropic" }, +]; + +const modelOptions: Record = { + openai: [ + { value: "gpt-4o-mini", label: "gpt-4o-mini (fast)" }, + { value: "gpt-4o", label: "gpt-4o (quality)" }, + ], + anthropic: [ + { value: "claude-3-haiku-20240307", label: "Claude 3 Haiku" }, + { value: "claude-3-5-sonnet-20240620", label: "Claude 3.5 Sonnet" }, + ], +}; + +const loadStoredKey = (provider: AiProvider) => { + if (typeof window === "undefined") return { key: "", remember: false }; + const localKey = localStorage.getItem(`${STORAGE_PREFIX}:${provider}:key`); + if (localKey) { + return { key: localKey, remember: true }; + } + const sessionKey = sessionStorage.getItem(`${STORAGE_PREFIX}:${provider}:key`); + if (sessionKey) { + return { key: sessionKey, remember: false }; + } + return { key: "", remember: false }; +}; + +const persistKey = ( + provider: AiProvider, + apiKey: string, + remember: boolean +) => { + if (typeof window === "undefined") return; + const localKey = `${STORAGE_PREFIX}:${provider}:key`; + const sessionKey = `${STORAGE_PREFIX}:${provider}:key`; + + if (!apiKey) { + localStorage.removeItem(localKey); + sessionStorage.removeItem(sessionKey); + return; + } + + if (remember) { + localStorage.setItem(localKey, apiKey); + sessionStorage.removeItem(sessionKey); + } else { + sessionStorage.setItem(sessionKey, apiKey); + localStorage.removeItem(localKey); + } +}; + +export default function AiRegexGenerator() { + const [description, setDescription] = useState(""); + const [sampleText, setSampleText] = useState(""); + const [expectedMatches, setExpectedMatches] = useState(""); + const [provider, setProvider] = useState("openai"); + const [model, setModel] = useState(getDefaultModel("openai")); + const [apiKey, setApiKey] = useState(""); + const [rememberKey, setRememberKey] = useState(false); + const [result, setResult] = useState(null); + const [error, setError] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + const [testString, setTestString] = useState(""); + const [testResult, setTestResult] = useState(""); + const [testMatches, setTestMatches] = useState(null); + + const { + buttonText: copyRegexText, + handleCopy: handleCopyRegex, + } = useCopyToClipboard(); + const { + buttonText: copyExplanationText, + handleCopy: handleCopyExplanation, + } = useCopyToClipboard(); + + const activeRegex = result?.regex || ""; + + useEffect(() => { + const stored = loadStoredKey(provider); + setApiKey(stored.key); + setRememberKey(stored.remember); + setModel((current) => { + const available = modelOptions[provider].some( + (option) => option.value === current + ); + return available ? current : getDefaultModel(provider); + }); + }, [provider]); + + useEffect(() => { + persistKey(provider, apiKey, rememberKey); + }, [provider, apiKey, rememberKey]); + + useEffect(() => { + if (!activeRegex || !testString) { + setTestResult(""); + setTestMatches(null); + return; + } + + try { + const regex = createRegex(activeRegex); + let matches: string[] = []; + + if (regex.flags.includes("g")) { + matches = Array.from(testString.matchAll(regex)).map((m) => m[0]); + } else { + const match = testString.match(regex); + if (match) { + matches = [match[0]]; + } + } + + if (matches.length > 0) { + const suffix = matches.length > 1 ? "matches" : "match"; + setTestResult(`Match found: ${matches.length} ${suffix}`); + setTestMatches(matches); + } else { + setTestResult("No match found"); + setTestMatches(null); + } + } catch (testError) { + if (testError instanceof Error) { + setTestResult(testError.message); + } else { + setTestResult("Regex test failed."); + } + setTestMatches(null); + } + }, [activeRegex, testString]); + + const selectedModelOptions = useMemo( + () => modelOptions[provider], + [provider] + ); + + const handleGenerate = useCallback(async () => { + setError(""); + setIsLoading(true); + setResult(null); + + try { + const output = await generateAiRegex({ + provider, + apiKey, + model, + description, + sampleText, + expectedMatches, + }); + setResult(output); + if (!testString && sampleText) { + setTestString(sampleText); + } + } catch (requestError) { + if (requestError instanceof Error) { + setError(requestError.message); + } else { + setError("Request failed. Please try again."); + } + } finally { + setIsLoading(false); + } + }, [ + provider, + apiKey, + model, + description, + sampleText, + expectedMatches, + testString, + ]); + + const handleClear = useCallback(() => { + setDescription(""); + setSampleText(""); + setExpectedMatches(""); + setResult(null); + setError(""); + setTestString(""); + }, []); + + const canGenerate = description.trim().length > 0 && apiKey.trim().length > 0; + + return ( +
+ +
+ + +
+ +
+ +
+ +
+ +