diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
deleted file mode 100755
index c5edfc3..0000000
--- a/.github/workflows/ci.yml
+++ /dev/null
@@ -1,69 +0,0 @@
-name: CI — Lint & Test
-
-on:
- push:
- branches: [ main ]
- pull_request:
- branches: [ main ]
- workflow_call:
-
-permissions:
- contents: read
-
-jobs:
- # ── PHP Syntax Check ─────────────────────────────────────────────
- lint-php:
- name: PHP Lint (${{ matrix.php }})
- runs-on: ubuntu-latest
- strategy:
- matrix:
- php: ['7.4', '8.1', '8.3']
- steps:
- - uses: actions/checkout@v4
-
- - name: Set up PHP ${{ matrix.php }}
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php }}
-
- - name: Syntax check all PHP files
- run: find . -name '*.php' -not -path './vendor/*' -print0 | xargs -0 -n1 php -l
-
- # ── WordPress Coding Standards (PHP CodeSniffer) ──────────────────
- lint-phpcs:
- name: WordPress Coding Standards
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Set up PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: '8.1'
-
- - name: Install Composer dependencies
- run: |
- composer install --no-interaction --prefer-dist --no-progress
-
- - name: Run PHPCS with WordPress-Core standard
- run: |
- composer run lint
-
- # ── JavaScript Lint ──────────────────────────────────────────────
- lint-js:
- name: JavaScript Linting
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Set up Node
- uses: actions/setup-node@v4
- with:
- node-version: '20'
-
- - name: Check JavaScript syntax
- run: |
- for f in $(find assets/js -name '*.js' -type f); do
- node --check "$f" || exit 1
- done
- echo "All JavaScript files passed syntax check"
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
deleted file mode 100755
index 848805d..0000000
--- a/.github/workflows/deploy.yml
+++ /dev/null
@@ -1,254 +0,0 @@
-name: Deploy to WordPress
-
-on:
- push:
- branches: [ main ]
-
-permissions:
- contents: write
-
-# Cancel previous deploys if a new push arrives while one is running.
-concurrency:
- group: deploy-production
- cancel-in-progress: true
-
-jobs:
- # -- Run tests first --
- test:
- name: Pre-deploy tests
- uses: ./.github/workflows/ci.yml
-
- # -- Validate deployment readiness --
- validate:
- name: Validate deployment
- runs-on: ubuntu-latest
- needs: test
- steps:
- - uses: actions/checkout@v4
- with:
- ref: main
-
- - name: Verify required files exist
- run: |
- echo "Checking for required plugin files..."
- required_files=(
- "peptide-search-ai.php"
- "includes/"
- "assets/"
- "readme.txt"
- )
-
- for file in "${required_files[@]}"; do
- if [ ! -e "$file" ]; then
- echo "❌ Missing required file: $file"
- exit 1
- fi
- echo "✓ Found: $file"
- done
-
- - name: Verify PSA_VERSION constant
- run: |
- echo "Checking PSA_VERSION constant..."
- if grep -q "define( 'PSA_VERSION'" peptide-search-ai.php; then
- version=$(grep "define( 'PSA_VERSION'" peptide-search-ai.php | sed "s/.*'\([^']*\)'.*/\1/")
- echo "✓ PSA_VERSION found: $version"
- else
- echo "❌ PSA_VERSION constant not found in peptide-search-ai.php"
- exit 1
- fi
-
- - name: Set up PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: '8.1'
-
- - name: Validate PHP syntax on all files
- run: |
- echo "Validating PHP syntax..."
- find . -name '*.php' -not -path './vendor/*' -not -path './node_modules/*' -print0 | \
- xargs -0 -n1 php -l > /dev/null && echo "✓ All PHP files are valid"
-
- - name: Check for sensitive files
- run: |
- echo "Scanning for sensitive files..."
- found_sensitive=0
-
- sensitive_patterns=(
- "\.env"
- "\.env\."
- "credentials"
- "\.key$"
- "\.pem$"
- "private_key"
- "\.sql$"
- )
-
- for pattern in "${sensitive_patterns[@]}"; do
- if find . -name "$pattern" -not -path './vendor/*' -not -path './.git/*' 2>/dev/null | grep -q .; then
- echo "❌ Found sensitive file matching pattern: $pattern"
- find . -name "$pattern" -not -path './vendor/*' -not -path './.git/*' 2>/dev/null
- found_sensitive=1
- fi
- done
-
- if [ $found_sensitive -eq 1 ]; then
- exit 1
- fi
- echo "✓ No sensitive files detected"
-
- # -- Push clean plugin files to the deploy branch --
- sync-deploy-branch:
- name: Sync deploy branch
- runs-on: ubuntu-latest
- needs: [test, validate]
- environment: production
- steps:
- - uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Sync deploy branch
- run: |
- git config user.name "github-actions[bot]"
- git config user.email "github-actions[bot]@users.noreply.github.com"
-
- # Fetch the remote deploy branch (may not exist on first run).
- git fetch origin deploy:deploy 2>/dev/null || true
-
- # If deploy branch exists, check it out; otherwise create it.
- if git rev-parse --verify deploy >/dev/null 2>&1; then
- git checkout deploy
- else
- git checkout --orphan deploy
- git rm -rf --cached . > /dev/null 2>&1 || true
- fi
-
- # Remove all tracked files so we start clean each time.
- git rm -rf . > /dev/null 2>&1 || true
-
- # Checkout only the plugin runtime files from main.
- git checkout main -- \
- assets/ \
- includes/ \
- peptide-search-ai.php \
- readme.txt
-
- # Commit only if there are actual changes.
- git add -A
- if git diff-index --quiet HEAD -- 2>/dev/null; then
- echo "No changes to deploy."
- else
- git commit -m "Deploy ${GITHUB_SHA::7} from main"
- fi
-
- # Normal push (fast-forward).
- git push origin deploy
-
- # -- Deploy to Hostinger via SSH --
- deploy:
- name: Deploy to Hostinger
- runs-on: ubuntu-latest
- needs: sync-deploy-branch
- environment: production
- steps:
- - name: Pull deploy branch on Hostinger
- uses: appleboy/ssh-action@v1
- with:
- host: ${{ secrets.SSH_HOST }}
- username: ${{ secrets.SSH_USERNAME }}
- key: ${{ secrets.SSH_PRIVATE_KEY }}
- port: ${{ secrets.SSH_PORT }}
- script_stop: true
- envs: DEPLOY_KEY
- script: |
- # Ensure GitHub host key is trusted (one-time setup).
- mkdir -p ~/.ssh
- ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
-
- # Install deploy key so Hostinger can pull from the private repo.
- if [ ! -f ~/.ssh/deploy_key ]; then
- echo "$DEPLOY_KEY" > ~/.ssh/deploy_key
- chmod 600 ~/.ssh/deploy_key
- fi
-
- # Configure SSH to use the deploy key for github.com.
- if ! grep -q "github.com" ~/.ssh/config 2>/dev/null; then
- cat >> ~/.ssh/config <<'SSHEOF'
- Host github.com
- IdentityFile ~/.ssh/deploy_key
- StrictHostKeyChecking no
- SSHEOF
- chmod 600 ~/.ssh/config
- fi
-
- PLUGIN_DIR="$HOME/domains/peptiderepo.com/public_html/wp-content/plugins/peptide-search-ai"
- REPO_URL="git@github.com:peptiderepo/peptide-search-ai.git"
-
- if [ -d "$PLUGIN_DIR/.git" ]; then
- # Existing clone -- just pull latest.
- cd "$PLUGIN_DIR"
- git fetch origin deploy
- git reset --hard origin/deploy
- else
- # First deploy -- clone the deploy branch.
- rm -rf "$PLUGIN_DIR"
- git clone -b deploy "$REPO_URL" "$PLUGIN_DIR"
- fi
-
- echo "Deployed to $PLUGIN_DIR"
- ls -la "$PLUGIN_DIR"
- env:
- DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
-
- - name: Verify deployment on server
- uses: appleboy/ssh-action@v1
- with:
- host: ${{ secrets.SSH_HOST }}
- username: ${{ secrets.SSH_USERNAME }}
- key: ${{ secrets.SSH_PRIVATE_KEY }}
- port: ${{ secrets.SSH_PORT }}
- script_stop: true
- script: |
- PLUGIN_DIR="$HOME/domains/peptiderepo.com/public_html/wp-content/plugins/peptide-search-ai"
-
- echo "Verifying deployment..."
- if [ ! -d "$PLUGIN_DIR" ]; then
- echo "❌ Plugin directory not found at $PLUGIN_DIR"
- exit 1
- fi
-
- # Check for required files
- required_files=(
- "peptide-search-ai.php"
- "includes"
- "assets"
- "readme.txt"
- )
-
- for file in "${required_files[@]}"; do
- if [ ! -e "$PLUGIN_DIR/$file" ]; then
- echo "❌ Missing file: $file"
- exit 1
- fi
- echo "✓ Found: $file"
- done
-
- # Extract and log deployed version
- if [ -f "$PLUGIN_DIR/peptide-search-ai.php" ]; then
- version=$(grep "Version:" "$PLUGIN_DIR/peptide-search-ai.php" | head -1 | sed 's/.*Version: *//; s/ *.*//')
- echo "✓ Deployed version: $version"
- else
- echo "❌ Plugin file not found"
- exit 1
- fi
-
- echo "✓ Deployment verification successful"
-
- - name: Deploy summary
- run: |
- echo "## Deployment Complete" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "- **Branch:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY
- echo "- **Commit:** \`${GITHUB_SHA::7}\`" >> $GITHUB_STEP_SUMMARY
- echo "- **Method:** SSH direct deploy (git pull on Hostinger)" >> $GITHUB_STEP_SUMMARY
- echo "- **Deployed to:** peptiderepo.com" >> $GITHUB_STEP_SUMMARY
diff --git a/.gitignore b/.gitignore
deleted file mode 100755
index 6429e3d..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,15 +0,0 @@
-# Dependencies
-vendor/
-node_modules/
-
-# IDE / OS
-.idea/
-.vscode/
-*.swp
-*.swo
-.DS_Store
-Thumbs.db
-
-# Build artifacts
-*.zip
-*.tar.gz
diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist
deleted file mode 100755
index 53b4be7..0000000
--- a/.phpcs.xml.dist
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
- PHP CodeSniffer configuration for Peptide Search AI WordPress plugin
-
-
-
-
-
-
-
-
- peptide-search-ai.php
- includes/
-
-
- /vendor/*
- /node_modules/*
- /.git/*
- /.github/*
-
-
-
-
-
-
-
-
-
diff --git a/assets/css/peptide-search.css b/assets/css/peptide-search.css
deleted file mode 100755
index e271197..0000000
--- a/assets/css/peptide-search.css
+++ /dev/null
@@ -1,387 +0,0 @@
-/* ==========================================================================
- Peptide Search AI — Frontend Styles
- ========================================================================== */
-
-/* --- Search Form --- */
-.psa-search-wrap {
- max-width: 720px;
- margin: 2rem auto;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif;
-}
-
-.psa-search-form {
- margin-bottom: 1.5rem;
-}
-
-.psa-search-input-wrap {
- display: flex;
- gap: 0;
- border: 2px solid #2563eb;
- border-radius: 8px;
- overflow: hidden;
- transition: border-color 0.2s;
-}
-
-.psa-search-input-wrap:focus-within {
- border-color: #1d4ed8;
- box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
-}
-
-.psa-search-input {
- flex: 1;
- padding: 14px 18px;
- font-size: 16px;
- border: none;
- outline: none;
- background: #fff;
- color: #1e293b;
-}
-
-.psa-search-input::placeholder {
- color: #94a3b8;
-}
-
-.psa-search-input:focus {
- outline: 2px solid #2563eb;
- outline-offset: 2px;
-}
-
-.psa-search-btn {
- padding: 14px 24px;
- background: #2563eb;
- color: #fff;
- border: none;
- font-size: 15px;
- font-weight: 600;
- cursor: pointer;
- transition: background 0.2s;
- white-space: nowrap;
-}
-
-.psa-search-btn:hover {
- background: #1d4ed8;
-}
-
-.psa-search-btn:focus-visible {
- outline: 2px solid #fff;
- outline-offset: -2px;
-}
-
-.psa-search-icon {
- margin-right: 4px;
-}
-
-/* --- Search Results --- */
-.psa-search-results {
- margin-top: 1rem;
-}
-
-.psa-result-count {
- font-size: 14px;
- color: #64748b;
- margin-bottom: 1rem;
-}
-
-.psa-result-item {
- display: block;
- padding: 16px 20px;
- margin-bottom: 8px;
- background: #fff;
- border: 1px solid #e2e8f0;
- border-radius: 8px;
- text-decoration: none;
- color: inherit;
- transition: border-color 0.2s, box-shadow 0.2s;
-}
-
-.psa-result-item:hover {
- border-color: #2563eb;
- box-shadow: 0 2px 8px rgba(37, 99, 235, 0.1);
-}
-
-.psa-result-title {
- font-size: 18px;
- font-weight: 600;
- color: #1e293b;
- margin: 0 0 6px;
-}
-
-.psa-result-meta {
- font-size: 13px;
- color: #64748b;
- margin-bottom: 6px;
-}
-
-.psa-result-meta span {
- margin-right: 16px;
-}
-
-.psa-result-excerpt {
- font-size: 14px;
- color: #475569;
- line-height: 1.5;
- margin: 0;
-}
-
-.psa-result-badge {
- display: inline-block;
- font-size: 11px;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- padding: 2px 8px;
- border-radius: 4px;
- margin-left: 8px;
- vertical-align: middle;
-}
-
-.psa-badge-ai {
- background: #fef3c7;
- color: #92400e;
-}
-
-.psa-badge-verified {
- background: #d1fae5;
- color: #065f46;
-}
-
-.psa-badge-manual {
- background: #dbeafe;
- color: #1e40af;
-}
-
-/* --- Checking / Loading State --- */
-.psa-checking {
- margin-top: 1.5rem;
-}
-
-.psa-checking-inner {
- text-align: center;
- padding: 32px 24px;
- background: #f8fafc;
- border: 1px solid #e2e8f0;
- border-radius: 12px;
-}
-
-.psa-checking-inner p {
- margin: 12px 0 0;
- font-size: 15px;
- color: #64748b;
-}
-
-/* --- Spinner --- */
-.psa-spinner {
- display: inline-block;
- width: 36px;
- height: 36px;
- border: 4px solid #bfdbfe;
- border-top-color: #2563eb;
- border-radius: 50%;
- animation: psa-spin 0.8s linear infinite;
-}
-
-@keyframes psa-spin {
- to { transform: rotate(360deg); }
-}
-
-/* --- Pending / Being Added Page --- */
-.psa-pending {
- margin-top: 1.5rem;
-}
-
-.psa-pending-inner {
- text-align: center;
- padding: 40px 32px;
- background: linear-gradient(135deg, #eff6ff 0%, #f0f9ff 100%);
- border: 1px solid #bfdbfe;
- border-radius: 16px;
-}
-
-.psa-pending-icon {
- font-size: 48px;
- line-height: 1;
- margin-bottom: 8px;
-}
-
-.psa-pending-inner h3 {
- margin: 0 0 12px;
- font-size: 21px;
- color: #1e40af;
- font-weight: 700;
-}
-
-.psa-pending-inner > p {
- margin: 0 0 28px;
- font-size: 15px;
- color: #3b82f6;
- max-width: 500px;
- margin-left: auto;
- margin-right: auto;
- line-height: 1.6;
-}
-
-
-/* --- Invalid Peptide --- */
-.psa-invalid {
- margin-top: 1.5rem;
-}
-
-.psa-invalid-inner {
- text-align: center;
- padding: 32px 24px;
- background: #fafafa;
- border: 1px solid #e2e8f0;
- border-radius: 12px;
-}
-
-.psa-invalid-inner h3 {
- margin: 0 0 12px;
- font-size: 20px;
- color: #334155;
-}
-
-.psa-invalid-inner p {
- margin: 0;
- font-size: 15px;
- color: #64748b;
- max-width: 500px;
- margin-left: auto;
- margin-right: auto;
- line-height: 1.6;
-}
-
-/* --- Error State --- */
-.psa-error {
- margin-top: 1.5rem;
- padding: 16px 20px;
- background: #fef2f2;
- border: 1px solid #fca5a5;
- border-radius: 8px;
- color: #991b1b;
- font-size: 14px;
-}
-
-/* --- Single Peptide Page Data --- */
-.psa-peptide-data {
- margin-top: 2rem;
-}
-
-.psa-quick-facts {
- background: #f8fafc;
- border: 1px solid #e2e8f0;
- border-radius: 10px;
- padding: 20px 24px;
- margin-bottom: 2rem;
-}
-
-.psa-quick-facts h3 {
- margin: 0 0 12px;
- font-size: 16px;
- font-weight: 700;
- color: #334155;
- text-transform: uppercase;
- letter-spacing: 0.5px;
-}
-
-.psa-facts-table {
- width: 100%;
- border-collapse: collapse;
-}
-
-.psa-facts-table th {
- text-align: left;
- padding: 8px 16px 8px 0;
- font-weight: 600;
- color: #475569;
- width: 180px;
- vertical-align: top;
- border-bottom: 1px solid #e2e8f0;
- font-size: 14px;
-}
-
-.psa-facts-table td {
- padding: 8px 0;
- color: #1e293b;
- border-bottom: 1px solid #e2e8f0;
- font-size: 14px;
-}
-
-.psa-facts-table tr:last-child th,
-.psa-facts-table tr:last-child td {
- border-bottom: none;
-}
-
-.psa-sequence {
- font-family: "SF Mono", "Fira Code", "Fira Mono", Menlo, Consolas, monospace;
- font-size: 13px;
- word-break: break-all;
- background: #f1f5f9;
- padding: 4px 8px;
- border-radius: 4px;
-}
-
-.psa-section {
- margin-bottom: 2rem;
-}
-
-.psa-section h3 {
- font-size: 18px;
- font-weight: 700;
- color: #1e293b;
- margin: 0 0 12px;
- padding-bottom: 8px;
- border-bottom: 2px solid #2563eb;
-}
-
-.psa-section-content {
- font-size: 15px;
- line-height: 1.77;
- color: #334155;
-}
-
-.psa-source-badge {
- margin-top: 2rem;
- padding: 12px 16px;
- border-radius: 8px;
- font-size: 13px;
- line-height: 1.5;
-}
-
-.psa-source-badge.psa-badge-ai {
- background: #fffbeb;
- border: 1px solid #fde68a;
- color: #92400e;
-}
-
-.psa-source-badge.psa-badge-verified {
- background: #ecfdf5;
- border: 1px solid #6ee7b7;
- color: #065f46;
-}
-
-.psa-source-badge.psa-badge-manual {
- background: #eff6ff;
- border: 1px solid #93c5fd;
- color: #1e40af;
-}
-
-/* --- Responsive --- */
-@media (max-width: 600px) {
- .psa-search-input-wrap,
- .psa-notify-input-wrap {
- flex-direction: column;
- }
-
- .psa-search-btn,
- .psa-notify-btn {
- border-radius: 0;
- }
-
- .psa-facts-table th {
- width: 120px;
- }
-
- .psa-pending-inner {
- padding: 28px 20px;
- }
-}
diff --git a/assets/js/peptide-search.js b/assets/js/peptide-search.js
deleted file mode 100755
index b9c48a9..0000000
--- a/assets/js/peptide-search.js
+++ /dev/null
@@ -1,214 +0,0 @@
-/**
- * Peptide Search AI — Frontend JavaScript
- *
- * Supports multiple shortcode instances on the same page by using
- * class-based selectors scoped to each .psa-search-wrap container.
- *
- * Flow:
- * 1. User searches -> AJAX call to backend
- * 2. If found -> show results
- * 3. If not found -> backend automatically validates + queues generation
- * -> user sees "pending" page
- * 4. If invalid peptide name -> show "not recognized" message
- */
-(function ($) {
- 'use strict';
-
- /**
- * Initialize a single search instance scoped to its .psa-search-wrap container.
- */
- function initSearchInstance($wrap) {
- var $form = $wrap.find('.psa-search-form');
- var $input = $wrap.find('.psa-search-input');
- var $results = $wrap.find('.psa-search-results');
- var $checking = $wrap.find('.psa-checking');
- var $pending = $wrap.find('.psa-pending');
- var $invalid = $wrap.find('.psa-invalid');
- var $error = $wrap.find('.psa-error');
-
- function debounce(fn, delay) {
- var timer;
- return function () {
- var args = arguments, ctx = this;
- clearTimeout(timer);
- timer = setTimeout(function () { fn.apply(ctx, args); }, delay);
- };
- }
-
- function hideAll() {
- $results.hide();
- $results.attr('aria-busy', 'false');
- $checking.hide();
- $pending.hide();
- $invalid.hide();
- $error.hide();
- }
-
- function showError(msg) {
- hideAll();
- $error.html('Error: ' + esc(msg)).show();
- $error.attr('role', 'alert');
- }
-
- function esc(str) {
- var d = document.createElement('div');
- d.appendChild(document.createTextNode(str));
- return d.innerHTML;
- }
-
- function sourceBadge(source) {
- var map = {
- 'pubchem': { label: 'Verified', cls: 'psa-badge-verified' },
- 'manual': { label: 'Curated', cls: 'psa-badge-manual' }
- };
- var b = map[source];
- if (!b) return '';
- return '' + b.label + ' ';
- }
-
- /**
- * Render search results.
- */
- function renderResults(data) {
- hideAll();
-
- var html = '
' + data.total + ' peptide' +
- (data.total !== 1 ? 's' : '') + ' found
';
-
- data.results.forEach(function (item) {
- html += '';
- html += '' + esc(item.title);
- if (item.source) html += sourceBadge(item.source);
- html += ' ';
-
- var meta = [];
- if (item.molecular_weight) meta.push('MW: ' + esc(item.molecular_weight) + ' ');
- if (item.molecular_formula) meta.push('Formula: ' + esc(item.molecular_formula) + ' ');
- if (item.sequence && item.sequence.length <= 40) meta.push('Seq: ' + esc(item.sequence) + ' ');
- if (meta.length) html += '' + meta.join('') + '
';
-
- if (item.excerpt) html += '' + esc(item.excerpt) + '
';
- html += ' ';
- });
-
- $results.html(html).show();
- }
-
- /**
- * Show the pending/in-progress message.
- */
- function showPending(name) {
- hideAll();
- $pending.find('.psa-pending-inner p').html(
- '' + esc(name) + ' is currently being added to our database. Please check back again later.'
- );
- $pending.show();
- }
-
- /**
- * Show the invalid peptide message.
- */
- function showInvalid(name, reason) {
- hideAll();
- $invalid.find('.psa-invalid-text').html(
- '' + esc(name) + ' does not appear to be a recognized peptide. Please check the spelling or try a different search term.'
- );
- $invalid.show();
- }
-
- /**
- * Perform the search. The backend handles everything:
- * - If found: returns results
- * - If not found + valid peptide: validates, creates placeholder, queues generation
- * - If not found + invalid: returns invalid status
- */
- function doSearch(query) {
- if (query.length < 2) {
- hideAll();
- return;
- }
-
- hideAll();
- $checking.show();
- $results.attr('aria-busy', 'true');
-
- $.ajax({
- url: psaAjax.ajaxurl,
- method: 'GET',
- data: {
- action: 'psa_search',
- nonce: psaAjax.nonce,
- q: query
- },
- timeout: 30000,
- success: function (response) {
- $results.attr('aria-busy', 'false');
- if (!response.success) {
- showError(response.data || 'Search failed.');
- return;
- }
- var d = response.data;
- switch (d.status) {
- case 'found':
- renderResults(d.data);
- break;
- case 'pending':
- showPending(d.peptide_name);
- break;
- case 'invalid':
- showInvalid(query, d.message);
- break;
- case 'rate_limited':
- showError(d.message || 'Too many requests. Please try again later.');
- break;
- default:
- showError('Unexpected response.');
- }
- },
- error: function (jqXHR, textStatus, errorThrown) {
- $results.attr('aria-busy', 'false');
- var errorMsg = 'Could not connect to the server. Please try again.';
- if (textStatus === 'timeout') {
- errorMsg = 'Request timed out. The server took too long to respond. Please try again.';
- } else if (textStatus === 'error') {
- if (jqXHR.status === 0) {
- errorMsg = 'Network error. Please check your connection and try again.';
- } else if (jqXHR.status === 500) {
- errorMsg = 'Server error. Please try again later.';
- } else if (jqXHR.status === 503) {
- errorMsg = 'Server is temporarily unavailable. Please try again later.';
- }
- }
- showError(errorMsg);
- }
- });
- }
-
- // --- Event Handlers (scoped to this instance) ---
-
- $form.on('submit', function (e) {
- e.preventDefault();
- doSearch($input.val().trim());
- });
-
- var debouncedSearch = debounce(function () {
- var q = $input.val().trim();
- if (q.length >= 2) {
- doSearch(q);
- } else {
- hideAll();
- }
- }, 400);
-
- $input.on('input', debouncedSearch);
- }
-
- // --- Initialize all search instances on page ---
-
- $(document).ready(function () {
- $('.psa-search-wrap').each(function () {
- initSearchInstance($(this));
- });
- });
-
-})(jQuery);
diff --git a/composer.json b/composer.json
deleted file mode 100755
index 59495fc..0000000
--- a/composer.json
+++ /dev/null
@@ -1,36 +0,0 @@
-{
- "name": "peptiderepo/peptide-search-ai",
- "description": "Searchable peptide database with AI-powered auto-population for WordPress",
- "type": "wordpress-plugin",
- "license": "GPL-2.0-or-later",
- "authors": [
- {
- "name": "Terence",
- "role": "Lead Developer"
- }
- ],
- "require": {
- "php": ">=7.4"
- },
- "require-dev": {
- "phpunit/phpunit": "^9.0 || ^10.0",
- "squizlabs/php_codesniffer": "^3.7",
- "wp-coding-standards/wpcs": "^3.0",
- "dealerdirect/phpcodesniffer-composer-installer": "^1.0"
- },
- "autoload": {
- "psr-4": {
- "PSA\\": "includes/"
- }
- },
- "config": {
- "allow-plugins": {
- "dealerdirect/phpcodesniffer-composer-installer": true
- }
- },
- "scripts": {
- "test": "phpunit",
- "lint": "phpcs",
- "lint:fix": "phpcbf"
- }
-}
diff --git a/includes/class-psa-admin.php b/includes/class-psa-admin.php
deleted file mode 100755
index c93c56a..0000000
--- a/includes/class-psa-admin.php
+++ /dev/null
@@ -1,247 +0,0 @@
- array( __CLASS__, 'sanitize_settings' ),
- )
- );
-
- // OpenRouter Section.
- add_settings_section(
- 'psa_ai_section',
- __( 'OpenRouter API Settings', 'peptide-search-ai' ),
- array( __CLASS__, 'render_section_description' ),
- 'peptide-search-ai'
- );
- add_settings_field(
- 'api_key',
- __( 'API Key', 'peptide-search-ai' ),
- array( __CLASS__, 'render_api_key_field' ),
- 'peptide-search-ai',
- 'psa_ai_section'
- );
- add_settings_field(
- 'ai_model',
- __( 'Generation Model', 'peptide-search-ai' ),
- array( __CLASS__, 'render_model_field' ),
- 'peptide-search-ai',
- 'psa_ai_section'
- );
- add_settings_field(
- 'validation_model',
- __( 'Validation Model', 'peptide-search-ai' ),
- array( __CLASS__, 'render_validation_model_field' ),
- 'peptide-search-ai',
- 'psa_ai_section'
- );
-
- // Behavior Section.
- add_settings_section(
- 'psa_behavior_section',
- __( 'Behavior Settings', 'peptide-search-ai' ),
- null,
- 'peptide-search-ai'
- );
- add_settings_field(
- 'auto_publish',
- __( 'Auto-Publish', 'peptide-search-ai' ),
- array( __CLASS__, 'render_publish_field' ),
- 'peptide-search-ai',
- 'psa_behavior_section'
- );
- add_settings_field(
- 'use_pubchem',
- __( 'PubChem Enrichment', 'peptide-search-ai' ),
- array( __CLASS__, 'render_pubchem_field' ),
- 'peptide-search-ai',
- 'psa_behavior_section'
- );
- }
-
- /**
- * Sanitize settings input.
- *
- * @param array $input Raw input from the form.
- * @return array Sanitized settings.
- */
- public static function sanitize_settings( $input ) {
- return array(
- 'api_key' => sanitize_text_field( $input['api_key'] ?? '' ),
- 'ai_model' => sanitize_text_field( $input['ai_model'] ?? '' ),
- 'validation_model' => sanitize_text_field( $input['validation_model'] ?? '' ),
- 'auto_publish' => sanitize_text_field( $input['auto_publish'] ?? 'draft' ),
- // Checkbox: if key is absent in POST, it was unchecked → '0'.
- 'use_pubchem' => ! empty( $input['use_pubchem'] ) ? '1' : '0',
- );
- }
-
- // -------------------------------------------------------------------------
- // Field renderers
- // -------------------------------------------------------------------------
-
- public static function render_section_description() {
- ?>
-
- OpenRouter to access AI models. OpenRouter provides a unified API for hundreds of models including open-source options like Gemini Flash, Llama, Mistral, Qwen, and DeepSeek, as well as commercial models.', 'peptide-search-ai' ),
- esc_url( 'https://openrouter.ai/' )
- );
- ?>
-
-
-
-
-
-
-
-
-
- openrouter.ai/settings/keys .
- define( 'PSA_OPENROUTER_KEY', 'your-key-here' );
-
-
-
-
- google/gemini-2.5-flash.
- openrouter.ai/models .
- meta-llama/llama-4-maverick, deepseek/deepseek-chat-v3-0324, qwen/qwen-2.5-72b-instruct.
-
-
-
-
- google/gemini-2.0-flash-001.
- meta-llama/llama-4-scout, mistralai/mistral-small-3.1-24b-instruct.
-
-
-
- >
- >
-
-
-
-
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
[peptide_search]
-
[peptide_search placeholder="Search peptides..."]
-
-
-
-
GET /wp-json/peptides/v1/search?q=BPC-157
-
-
- false,
- 'canonical_name' => $name,
- 'reason' => 'Invalid characters in peptide name.',
- );
- }
-
- $options = self::get_settings();
- $api_key = $options['api_key'];
- $model = $options['validation_model'];
-
- if ( empty( $api_key ) ) {
- return new WP_Error( 'no_api_key', 'OpenRouter API key is not configured.' );
- }
-
- // Check transient cache first (avoid repeated API calls for same term).
- $cache_key = 'psa_validate_' . md5( strtolower( trim( $name ) ) );
- $cached = get_transient( $cache_key );
- if ( false !== $cached ) {
- return $cached;
- }
-
- $prompt = self::build_validation_prompt( $name );
- // Allow filtering of validation prompt before sending.
- $prompt = apply_filters( 'psa_validation_prompt', $prompt, $name );
- $response = self::call_openrouter(
- $api_key,
- $model ? $model : 'google/gemini-2.0-flash-001',
- $prompt,
- PSA_Config::VALIDATION_MAX_TOKENS
- );
-
- if ( is_wp_error( $response ) ) {
- return $response;
- }
-
- $data = self::parse_ai_response( $response );
- if ( is_wp_error( $data ) ) {
- return array(
- 'is_valid' => false,
- 'canonical_name' => $name,
- 'reason' => 'Could not verify this peptide name.',
- );
- }
-
- $result = array(
- 'is_valid' => ! empty( $data['is_valid'] ) && true === $data['is_valid'],
- 'canonical_name' => sanitize_text_field( $data['canonical_name'] ?? $name ),
- 'reason' => sanitize_text_field( $data['reason'] ?? '' ),
- );
-
- // Cache for 24 hours (using PSA_Config for consistency).
- set_transient( $cache_key, $result, DAY_IN_SECONDS ); // Validation cache TTL
-
- return $result;
- }
-
- /**
- * Background generation handler (called via WP-Cron or synchronously).
- *
- * @param int $post_id The placeholder post ID.
- * @param string $peptide_name The peptide to research.
- */
- public static function background_generate( $post_id, $peptide_name ) {
- $post = get_post( $post_id );
- if ( ! $post || 'draft' !== $post->post_status || 'peptide' !== $post->post_type ) {
- error_log( 'PSA: background_generate skipped — post ' . $post_id . ' not a draft peptide.' );
- return;
- }
-
- // Only process posts that are still in 'pending' state.
- $source = get_post_meta( $post_id, 'psa_source', true );
- if ( 'pending' !== $source ) {
- error_log( 'PSA: background_generate skipped — post ' . $post_id . ' source is "' . $source . '", not "pending".' );
- return;
- }
-
- $options = self::get_settings();
- $auto_publish = $options['auto_publish'];
-
- error_log( 'PSA: Generating content for "' . $peptide_name . '" (post ' . $post_id . ') using model: ' . ( $options['ai_model'] ? $options['ai_model'] : 'google/gemini-2.5-flash' ) );
-
- // Allow developers to hook before generation starts.
- do_action( 'psa_before_generation', $post_id, $peptide_name );
-
- // Generate the full content.
- $result = self::generate_peptide_content( $peptide_name );
- if ( is_wp_error( $result ) ) {
- $err_msg = $result->get_error_message();
- error_log( 'PSA: Generation FAILED for "' . $peptide_name . '": ' . $err_msg );
- update_post_meta( $post_id, 'psa_generation_error', $err_msg );
- return;
- }
-
- error_log( 'PSA: AI content generated successfully for "' . $peptide_name . '". Fields: ' . implode( ', ', array_keys( $result ) ) );
-
- // FIX: Use 'overview' field (which the prompt returns) with fallback to 'description'.
- $post_content = $result['overview'] ?? $result['description'] ?? '';
-
- // Safeguard: if post_content is still empty after generation, log a warning.
- if ( empty( trim( $post_content ) ) ) {
- error_log( 'PSA: WARNING — AI returned valid JSON but post content (overview/description) is empty for "' . $peptide_name . '". Available keys: ' . implode( ', ', array_keys( $result ) ) );
- update_post_meta( $post_id, 'psa_generation_error', 'AI returned empty overview/description content. Available fields: ' . implode( ', ', array_keys( $result ) ) );
- return;
- }
-
- // Try PubChem enrichment.
- $pubchem_data = null;
- if ( ! empty( $options['use_pubchem'] ) ) {
- $pubchem_result = PSA_PubChem::lookup( $peptide_name );
- if ( is_wp_error( $pubchem_result ) ) {
- // Log PubChem API failures.
- error_log( 'PSA: PubChem lookup failed for "' . $peptide_name . '": ' . $pubchem_result->get_error_message() );
- } elseif ( is_array( $pubchem_result ) ) {
- $pubchem_data = $pubchem_result;
- error_log( 'PSA: PubChem data found for "' . $peptide_name . '" (CID: ' . ( $pubchem_data['cid'] ?? 'unknown' ) . ')' );
- }
- }
-
- // Update the placeholder with real content.
- $post_status = ( 'publish' === $auto_publish ) ? 'publish' : 'draft';
-
- $update_result = wp_update_post(
- array(
- 'ID' => $post_id,
- 'post_title' => sanitize_text_field( $result['name'] ?? $peptide_name ),
- 'post_content' => wp_kses_post( $post_content ),
- 'post_status' => $post_status,
- ),
- true
- );
-
- if ( is_wp_error( $update_result ) ) {
- error_log( 'PSA: wp_update_post FAILED for post ' . $post_id . ': ' . $update_result->get_error_message() );
- update_post_meta( $post_id, 'psa_generation_error', 'Failed to update post: ' . $update_result->get_error_message() );
- return;
- }
-
- // Save all meta fields — prefer PubChem data for molecular properties.
- self::save_peptide_meta( $post_id, $result, $pubchem_data );
-
- // Create a matching Knowledge Base article.
- self::create_kb_article( $post_id, $result, $peptide_name, $post_status );
-
- // Clean up generation tracking.
- delete_post_meta( $post_id, 'psa_generation_started' );
- delete_post_meta( $post_id, 'psa_generation_error' );
- update_post_meta( $post_id, 'psa_generation_completed', time() );
-
- // Allow developers to hook after generation completes.
- do_action( 'psa_after_generation', $post_id, $result, $peptide_name );
-
- error_log( 'PSA: Successfully completed generation for "' . $peptide_name . '" (post ' . $post_id . ', status: ' . $post_status . ')' );
- }
-
- /**
- * Generate full peptide content via AI.
- *
- * @param string $peptide_name The peptide to research.
- * @return array|WP_Error Parsed peptide data or error.
- */
- public static function generate_peptide_content( $peptide_name ) {
- $options = self::get_settings();
- $api_key = $options['api_key'];
- $model = $options['ai_model'];
-
- if ( empty( $api_key ) ) {
- return new WP_Error( 'no_api_key', 'OpenRouter API key is not configured.' );
- }
-
- $prompt = self::build_generation_prompt( $peptide_name );
- // Allow filtering of generation prompt before sending.
- $prompt = apply_filters( 'psa_generation_prompt', $prompt, $peptide_name );
- $response = self::call_openrouter(
- $api_key,
- $model ? $model : 'google/gemini-2.5-flash',
- $prompt,
- PSA_Config::GENERATION_MAX_TOKENS
- );
-
- if ( is_wp_error( $response ) ) {
- return $response;
- }
-
- return self::parse_ai_response( $response );
- }
-
- // -------------------------------------------------------------------------
- // Private helpers
- // -------------------------------------------------------------------------
-
- /**
- * Get plugin settings with defaults.
- *
- * @return array
- */
- private static function get_settings() {
- $defaults = array(
- 'api_key' => '',
- 'ai_model' => '',
- 'validation_model' => '',
- 'auto_publish' => 'draft',
- 'use_pubchem' => '1',
- );
-
- $settings = wp_parse_args( get_option( 'psa_settings', array() ), $defaults );
-
- // Prefer wp-config.php constant over database value for security.
- if ( defined( 'PSA_OPENROUTER_KEY' ) && PSA_OPENROUTER_KEY ) {
- $settings['api_key'] = PSA_OPENROUTER_KEY;
- }
-
- // Allow filtering of settings before use.
- $settings = apply_filters( 'psa_ai_settings', $settings );
-
- return $settings;
- }
-
- /**
- * Save meta fields for a peptide post.
- *
- * @param int $post_id The post ID.
- * @param array $ai_data AI-generated data.
- * @param array|null $pubchem_data PubChem data (or null).
- */
- private static function save_peptide_meta( $post_id, $ai_data, $pubchem_data ) {
- $meta = array(
- 'psa_sequence' => $ai_data['sequence'] ?? '',
- 'psa_molecular_weight' => ! empty( $pubchem_data['molecular_weight'] )
- ? $pubchem_data['molecular_weight']
- : ( $ai_data['molecular_weight'] ?? '' ),
- 'psa_molecular_formula' => ! empty( $pubchem_data['molecular_formula'] )
- ? $pubchem_data['molecular_formula']
- : ( $ai_data['molecular_formula'] ?? '' ),
- 'psa_aliases' => $ai_data['aliases'] ?? '',
- 'psa_mechanism' => $ai_data['mechanism'] ?? '',
- 'psa_overview' => $ai_data['overview'] ?? $ai_data['description'] ?? '',
- 'psa_category_label' => $ai_data['category_label'] ?? '',
- 'psa_origin' => $ai_data['origin'] ?? '',
- 'psa_research_apps' => $ai_data['research_benefits'] ?? ( $ai_data['research_applications'] ?? '' ),
- 'psa_safety_profile' => $ai_data['safety_side_effects'] ?? ( $ai_data['safety_profile'] ?? '' ),
- 'psa_dosage_info' => $ai_data['administration_dosing'] ?? ( $ai_data['dosage_info'] ?? '' ),
- 'psa_legal_regulatory' => $ai_data['legal_regulatory'] ?? '',
- 'psa_references' => $ai_data['references'] ?? '',
- 'psa_category' => $ai_data['category'] ?? '',
- 'psa_source' => ! empty( $pubchem_data ) ? 'pubchem' : 'ai-generated',
- 'psa_pubchem_cid' => $pubchem_data['cid'] ?? '',
- );
-
- // Allow filtering of meta before save.
- $meta = apply_filters( 'psa_peptide_meta', $meta, $post_id, $ai_data, $pubchem_data );
-
- foreach ( $meta as $key => $value ) {
- update_post_meta( $post_id, $key, $value );
- }
- }
-
- /**
- * Validate peptide name input characters and block injection patterns.
- *
- * @param string $name The peptide name to validate.
- * @return bool True if input looks legitimate.
- */
- private static function validate_peptide_input( $name ) {
- // Allow only characters that appear in legitimate peptide names.
- if ( ! preg_match( '/^[\p{L}\d\s\-\(\),\.\/\+\[\]]+$/u', $name ) ) {
- return false;
- }
- // Block obvious prompt injection patterns.
- $blocked = array( 'ignore', 'instruction', 'override', 'system prompt', 'forget', 'disregard', 'pretend' );
- $lower = strtolower( $name );
- foreach ( $blocked as $word ) {
- if ( strpos( $lower, $word ) !== false ) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Build the lightweight validation prompt.
- *
- * Uses wp_json_encode() to safely encode the peptide name within the prompt.
- * This ensures special characters and quotes are properly escaped before sending to the AI.
- * The JSON-encoded format also helps the AI parser recognize the input clearly.
- *
- * @param string $name The peptide name to validate.
- * @return string Prompt text ready to send to OpenRouter API.
- */
- private static function build_validation_prompt( $name ) {
- $json_name = wp_json_encode( $name );
- return <<get_error_code() ) {
- return $result;
- }
-
- // Rate limited — retry with exponential backoff.
- if ( $attempt < $max_retries ) {
- $delay = $base_delay * pow( 2, $attempt - 1 ); // 5s, 10s, 20s
- // Avoid blocking PHP workers during WP-Cron execution.
- if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
- error_log( 'PSA: Rate limited during cron on attempt ' . $attempt . '/' . $max_retries . '. Returning to free PHP worker.' );
- return new WP_Error( 'rate_limited', 'Rate limited during background generation. Will retry on next cron trigger.' );
- }
- error_log( 'PSA: Rate limited on attempt ' . $attempt . '/' . $max_retries . '. Retrying in ' . $delay . 's...' );
- sleep( $delay );
- }
- }
-
- error_log( 'PSA: All ' . $max_retries . ' attempts failed due to rate limiting.' );
- return new WP_Error( 'rate_limited', 'OpenRouter rate limit reached after ' . $max_retries . ' retries. Consider adding credits at openrouter.ai or switching to a non-free model.' );
- }
-
- /**
- * Single OpenRouter API call (no retry).
- *
- * @param string $api_key The OpenRouter API key.
- * @param string $model The model identifier.
- * @param string $prompt The prompt text.
- * @param int $max_tokens Maximum tokens to generate.
- * @return string|WP_Error Response text or error.
- */
- private static function call_openrouter_once( $api_key, $model, $prompt, $max_tokens = 4096 ) {
- $site_url = get_bloginfo( 'url' );
- $site_name = get_bloginfo( 'name' );
-
- $response = wp_remote_post(
- self::API_URL,
- array(
- 'timeout' => 120,
- 'headers' => array(
- 'Content-Type' => 'application/json',
- 'Authorization' => 'Bearer ' . $api_key,
- 'HTTP-Referer' => $site_url,
- 'X-Title' => $site_name,
- ),
- 'body' => wp_json_encode(
- array(
- 'model' => $model,
- 'messages' => array(
- array(
- 'role' => 'system',
- 'content' => 'You are a scientific database assistant. Return only valid JSON.',
- ),
- array(
- 'role' => 'user',
- 'content' => $prompt,
- ),
- ),
- 'max_tokens' => $max_tokens,
- 'temperature' => 0.3,
- )
- ),
- )
- );
-
- if ( is_wp_error( $response ) ) {
- error_log( 'PSA: OpenRouter connection error: ' . $response->get_error_message() );
- return new WP_Error( 'api_error', 'Failed to connect to OpenRouter API: ' . $response->get_error_message() );
- }
-
- $code = wp_remote_retrieve_response_code( $response );
- $raw_body = wp_remote_retrieve_body( $response );
- $body = json_decode( $raw_body, true );
-
- error_log( 'PSA: OpenRouter response code: ' . $code . ' for model: ' . $model );
-
- if ( 200 !== $code ) {
- if ( 429 === $code ) {
- error_log( 'PSA: OpenRouter rate limited (429)' );
- return new WP_Error( 'rate_limited', 'OpenRouter rate limit reached. Please try again in a few moments.' );
- }
- if ( 402 === $code ) {
- error_log( 'PSA: OpenRouter insufficient credits (402)' );
- return new WP_Error( 'insufficient_credits', 'OpenRouter account has insufficient credits. Please add funds at openrouter.ai.' );
- }
- $msg = $body['error']['message'] ?? ( 'Unknown error (HTTP ' . $code . ')' );
- error_log( 'PSA: OpenRouter API error (' . $code . '): ' . $msg );
- error_log( 'PSA: OpenRouter raw response: ' . substr( $raw_body, 0, 500 ) );
- return new WP_Error( 'api_error', 'OpenRouter API error: ' . $msg );
- }
-
- if ( empty( $body['choices'][0]['message']['content'] ) ) {
- error_log( 'PSA: OpenRouter returned empty content. Raw: ' . substr( $raw_body, 0, 500 ) );
- return new WP_Error( 'api_error', 'OpenRouter API returned an empty response.' );
- }
-
- return $body['choices'][0]['message']['content'];
- }
-
- /**
- * Parse AI response text into structured data.
- *
- * @param string $response_text Raw response text.
- * @return array|WP_Error Parsed data or error.
- */
- private static function parse_ai_response( $response_text ) {
- // Strip any ... tags (some models like DeepSeek include reasoning).
- $cleaned = preg_replace( '/.*?<\/think>/s', '', $response_text );
-
- // Strip markdown code fences if present.
- $cleaned = preg_replace( '/^```(?:json)?\s*/m', '', $cleaned );
- $cleaned = preg_replace( '/\s*```\s*$/m', '', $cleaned );
- $cleaned = trim( $cleaned );
-
- $data = json_decode( $cleaned, true );
- if ( JSON_ERROR_NONE !== json_last_error() ) {
- error_log( 'PSA: JSON parse error: ' . json_last_error_msg() . '. Raw (first 500 chars): ' . substr( $response_text, 0, 500 ) );
- return new WP_Error(
- 'parse_error',
- 'Failed to parse AI response as JSON: ' . json_last_error_msg()
- );
- }
-
- return $data;
- }
-
- /**
- * Create a matching Echo Knowledge Base article with full structured content.
- *
- * @param int $peptide_post_id The peptide post ID.
- * @param array $ai_data The parsed AI data.
- * @param string $peptide_name The peptide name.
- * @param string $post_status The post status (default: 'draft').
- */
- private static function create_kb_article( $peptide_post_id, $ai_data, $peptide_name, $post_status = 'draft' ) {
- if ( ! post_type_exists( 'epkb_post_type_1' ) ) {
- error_log( 'PSA: Echo Knowledge Base not active — skipping KB article.' );
- return;
- }
-
- $title = sanitize_text_field( $ai_data['name'] ?? $peptide_name );
- $existing = get_posts(
- array(
- 'post_type' => 'epkb_post_type_1',
- 'title' => $title,
- 'post_status' => 'any',
- 'numberposts' => 1,
- )
- );
-
- if ( ! empty( $existing ) ) {
- error_log( 'PSA: KB article already exists for "' . $peptide_name . '".' );
- return;
- }
-
- // Build header meta line.
- $cat_label = ! empty( $ai_data['category_label'] ) ? $ai_data['category_label'] : '';
- $origin = ! empty( $ai_data['origin'] ) ? $ai_data['origin'] : '';
- $sequence = ! empty( $ai_data['sequence'] ) ? $ai_data['sequence'] : '';
-
- $header = '';
- if ( $cat_label ) {
- $header .= 'Category: ' . esc_html( $cat_label );
- }
- if ( $origin ) {
- $header .= ' | Origin: ' . esc_html( $origin );
- }
- if ( $sequence ) {
- $header .= ' | Sequence: ' . esc_html( $sequence );
- }
-
- // Build full article content.
- $c = $header ? '' . $header . '
' . "\n\n" : '';
-
- // Overview.
- $c .= 'Overview ' . "\n";
- $c .= self::paragraphs( $ai_data['overview'] ?? $ai_data['description'] ?? '' );
-
- // Mechanism of Action.
- if ( ! empty( $ai_data['mechanism'] ) ) {
- $c .= 'Mechanism of Action ' . "\n";
- $c .= self::paragraphs( $ai_data['mechanism'] );
- }
-
- // Research & Potential Benefits (with h3 subheadings).
- if ( ! empty( $ai_data['research_benefits'] ) || ! empty( $ai_data['research_applications'] ) ) {
- $c .= 'Research & Potential Benefits ' . "\n";
- $c .= 'The following potential benefits are based on animal studies and preclinical research. Human clinical trial data is extremely limited.
' . "\n";
- $research = $ai_data['research_benefits'] ?? $ai_data['research_applications'] ?? '';
- $c .= self::research_with_subheadings( $research );
- }
-
- // Administration & Dosing.
- if ( ! empty( $ai_data['administration_dosing'] ) || ! empty( $ai_data['dosage_info'] ) ) {
- $c .= 'Administration & Dosing (Research Context) ' . "\n";
- $c .= 'Important: The following dosing information is derived from animal studies and extrapolated research. This peptide is not approved for human use. This information is provided for educational purposes only.
' . "\n";
- $c .= self::paragraphs( $ai_data['administration_dosing'] ?? $ai_data['dosage_info'] ?? '' );
- }
-
- // Safety & Side Effects.
- if ( ! empty( $ai_data['safety_side_effects'] ) || ! empty( $ai_data['safety_profile'] ) ) {
- $c .= 'Safety & Side Effects ' . "\n";
- $c .= self::paragraphs( $ai_data['safety_side_effects'] ?? $ai_data['safety_profile'] ?? '' );
- }
-
- // Legal & Regulatory Status.
- if ( ! empty( $ai_data['legal_regulatory'] ) ) {
- $c .= 'Legal & Regulatory Status ' . "\n";
- $c .= self::paragraphs( $ai_data['legal_regulatory'] );
- }
-
- // References.
- if ( ! empty( $ai_data['references'] ) ) {
- $c .= 'Notable Research References ' . "\n";
- $c .= self::paragraphs( $ai_data['references'] );
- }
-
- // Medical Disclaimer.
- $c .= "\n" . 'Medical Disclaimer: The information on this page is provided for educational and research purposes only. This peptide is not approved for human therapeutic use. Nothing on this page constitutes medical advice. Always consult a licensed healthcare professional before using any peptide, supplement, or research chemical.
';
-
- $kb_post_id = wp_insert_post(
- array(
- 'post_title' => $title,
- 'post_content' => wp_kses_post( $c ),
- 'post_type' => 'epkb_post_type_1',
- 'post_status' => $post_status,
- ),
- true
- );
-
- if ( is_wp_error( $kb_post_id ) ) {
- error_log( 'PSA: Failed to create KB article: ' . $kb_post_id->get_error_message() );
- return;
- }
-
- // Assign KB category.
- $category_map = array(
- 'GH Secretagogues' => 'gh-secretagogues',
- 'Healing & Repair' => 'healing-repair',
- 'Melanocortin Peptides' => 'melanocortin-peptides',
- 'Metabolic & Anti-Aging' => 'metabolic-anti-aging',
- 'Nootropic & Neuroprotective' => 'nootropic-neuroprotective',
- );
-
- $cat_name = trim( $ai_data['category'] ?? '' );
- if ( isset( $category_map[ $cat_name ] ) ) {
- $term = get_term_by( 'slug', $category_map[ $cat_name ], 'epkb_post_type_1_category' );
- if ( $term ) {
- wp_set_object_terms( $kb_post_id, array( (int) $term->term_id ), 'epkb_post_type_1_category' );
- }
- }
-
- update_post_meta( $peptide_post_id, 'psa_kb_article_id', $kb_post_id );
- update_post_meta( $kb_post_id, 'psa_peptide_post_id', $peptide_post_id );
-
- error_log( 'PSA: Created KB article #' . $kb_post_id . ' for "' . $peptide_name . '"' );
- }
-
- /**
- * Convert text to HTML paragraphs.
- */
- private static function paragraphs( $text ) {
- if ( empty( $text ) ) {
- return '';
- }
- $text = str_replace( "\r\n", "\n", $text );
- $parts = preg_split( '/\n{2,}/', trim( $text ) );
- $out = '';
- foreach ( $parts as $p ) {
- $p = trim( $p );
- if ( '' === $p ) {
- continue;
- }
- $out .= '' . wp_kses_post( $p ) . '
' . "\n";
- }
- return $out;
- }
-
- /**
- * Parse research text into h3 subheadings + paragraphs.
- */
- private static function research_with_subheadings( $text ) {
- if ( empty( $text ) ) {
- return '';
- }
- $text = str_replace( "\r\n", "\n", trim( $text ) );
- $blocks = preg_split( '/\n{2,}/', $text );
- $out = '';
- foreach ( $blocks as $block ) {
- $block = trim( $block );
- if ( '' === $block ) {
- continue;
- }
- // If block is short (under 80 chars) and has no period, treat as a subheading.
- if ( strlen( $block ) < 80 && strpos( $block, '.' ) === false ) {
- $out .= '' . esc_html( $block ) . ' ' . "\n";
- } else {
- // Check if first line is a subheading followed by content.
- $first_nl = strpos( $block, "\n" );
- if ( false !== $first_nl ) {
- $first_line = trim( substr( $block, 0, $first_nl ) );
- $rest = trim( substr( $block, $first_nl + 1 ) );
- if ( strlen( $first_line ) < 80 && strpos( $first_line, '.' ) === false ) {
- $out .= '' . esc_html( $first_line ) . ' ' . "\n";
- $out .= '' . wp_kses_post( $rest ) . '
' . "\n";
- continue;
- }
- }
- $out .= '' . wp_kses_post( $block ) . '
' . "\n";
- }
- }
- return $out;
- }
-}
diff --git a/includes/class-psa-config.php b/includes/class-psa-config.php
deleted file mode 100755
index 7ac6a9e..0000000
--- a/includes/class-psa-config.php
+++ /dev/null
@@ -1,41 +0,0 @@
- $type,
- 'message' => $message,
- ),
- $http_code
- );
- }
-}
diff --git a/includes/class-psa-post-type.php b/includes/class-psa-post-type.php
deleted file mode 100755
index 79043dd..0000000
--- a/includes/class-psa-post-type.php
+++ /dev/null
@@ -1,162 +0,0 @@
- 'Amino Acid Sequence',
- 'psa_molecular_weight' => 'Molecular Weight',
- 'psa_molecular_formula' => 'Molecular Formula',
- 'psa_aliases' => 'Aliases / Synonyms',
- 'psa_mechanism' => 'Mechanism of Action',
- 'psa_research_apps' => 'Research Applications',
- 'psa_safety_profile' => 'Safety & Side Effects',
- 'psa_dosage_info' => 'Dosage Information',
- 'psa_references' => 'References & Citations',
- 'psa_source' => 'Data Source',
- 'psa_pubchem_cid' => 'PubChem CID',
- );
-
- /**
- * Register admin-only hooks (meta boxes, save).
- */
- public static function init_admin() {
- add_action( 'add_meta_boxes', array( __CLASS__, 'add_meta_boxes' ) );
- add_action( 'save_post_peptide', array( __CLASS__, 'save_meta' ) );
- }
-
- /**
- * Register the peptide CPT.
- */
- public static function register_peptide_post_type() {
- if ( post_type_exists( 'peptide' ) ) {
- return;
- }
-
- $labels = array(
- 'name' => 'Peptides',
- 'singular_name' => 'Peptide',
- 'menu_name' => 'Peptides',
- 'add_new' => 'Add Peptide',
- 'add_new_item' => 'Add New Peptide',
- 'edit_item' => 'Edit Peptide',
- 'new_item' => 'New Peptide',
- 'view_item' => 'View Peptide',
- 'search_items' => 'Search Peptides',
- 'not_found' => 'No peptides found',
- 'not_found_in_trash' => 'No peptides found in Trash',
- );
-
- register_post_type(
- 'peptide',
- array(
- 'labels' => $labels,
- 'public' => true,
- 'has_archive' => true,
- 'rewrite' => array( 'slug' => 'peptides' ),
- 'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
- 'menu_icon' => 'dashicons-database',
- 'show_in_rest' => true,
- 'capability_type' => 'post',
- 'hierarchical' => false,
- )
- );
- }
-
- /**
- * Add meta boxes to the peptide editor.
- */
- public static function add_meta_boxes() {
- add_meta_box(
- 'psa_peptide_data',
- 'Peptide Scientific Data',
- array( __CLASS__, 'render_meta_box' ),
- 'peptide',
- 'normal',
- 'high'
- );
- }
-
- /**
- * Render the meta box fields.
- *
- * @param WP_Post $post The current post object.
- */
- public static function render_meta_box( $post ) {
- wp_nonce_field( 'psa_save_meta', 'psa_meta_nonce' );
-
- $textarea_fields = array(
- 'psa_sequence',
- 'psa_mechanism',
- 'psa_research_apps',
- 'psa_safety_profile',
- 'psa_dosage_info',
- 'psa_references',
- );
-
- echo '';
- }
-
- /**
- * Save meta field values.
- *
- * @param int $post_id The post ID.
- */
- public static function save_meta( $post_id ) {
- // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
- if ( ! isset( $_POST['psa_meta_nonce'] ) ||
- ! wp_verify_nonce( wp_unslash( $_POST['psa_meta_nonce'] ), 'psa_save_meta' ) ) {
- return;
- }
- if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
- return;
- }
- if ( ! current_user_can( 'edit_post', $post_id ) ) {
- return;
- }
-
- foreach ( self::META_FIELDS as $key => $label ) {
- if ( isset( $_POST[ $key ] ) ) {
- $value = sanitize_textarea_field( wp_unslash( $_POST[ $key ] ) );
- update_post_meta( $post_id, $key, $value );
- }
- }
- }
-}
diff --git a/includes/class-psa-pubchem.php b/includes/class-psa-pubchem.php
deleted file mode 100755
index cfe20da..0000000
--- a/includes/class-psa-pubchem.php
+++ /dev/null
@@ -1,152 +0,0 @@
- 15,
- 'headers' => array( 'Accept' => 'application/json' ),
- )
- );
-
- if ( is_wp_error( $response ) ) {
- return new WP_Error( 'pubchem_api_error', 'PubChem API request failed: ' . $response->get_error_message() );
- }
-
- $code = wp_remote_retrieve_response_code( $response );
- if ( 200 !== $code ) {
- return new WP_Error( 'pubchem_http_error', 'PubChem returned HTTP ' . $code );
- }
-
- $body = json_decode( wp_remote_retrieve_body( $response ), true );
-
- return isset( $body['IdentifierList']['CID'][0] )
- ? (int) $body['IdentifierList']['CID'][0]
- : null;
- }
-
- /**
- * Get molecular properties for a given CID.
- *
- * @param int $cid PubChem Compound ID.
- * @return array|WP_Error|null Properties array, WP_Error on failure, or null if not found.
- */
- private static function get_properties( $cid ) {
- $props = 'MolecularFormula,MolecularWeight,IUPACName,CanonicalSMILES,InChI';
- $url = self::API_BASE . '/compound/cid/' . (int) $cid . '/property/' . $props . '/JSON';
-
- $response = wp_remote_get(
- $url,
- array(
- 'timeout' => 15,
- 'headers' => array( 'Accept' => 'application/json' ),
- )
- );
-
- if ( is_wp_error( $response ) ) {
- return new WP_Error( 'pubchem_api_error', 'PubChem API request failed: ' . $response->get_error_message() );
- }
-
- $code = wp_remote_retrieve_response_code( $response );
- if ( 200 !== $code ) {
- return new WP_Error( 'pubchem_http_error', 'PubChem returned HTTP ' . $code );
- }
-
- $body = json_decode( wp_remote_retrieve_body( $response ), true );
-
- if ( ! isset( $body['PropertyTable']['Properties'][0] ) ) {
- return null;
- }
-
- $p = $body['PropertyTable']['Properties'][0];
-
- return array(
- 'molecular_formula' => $p['MolecularFormula'] ?? '',
- 'molecular_weight' => isset( $p['MolecularWeight'] ) ? $p['MolecularWeight'] . ' Da' : '',
- 'iupac_name' => $p['IUPACName'] ?? '',
- 'smiles' => $p['CanonicalSMILES'] ?? '',
- 'inchi' => $p['InChI'] ?? '',
- );
- }
-}
diff --git a/includes/class-psa-search.php b/includes/class-psa-search.php
deleted file mode 100755
index 03f9ab6..0000000
--- a/includes/class-psa-search.php
+++ /dev/null
@@ -1,505 +0,0 @@
-query(
- "DELETE FROM {$wpdb->options}
- WHERE option_name LIKE '_transient_psa_search_%'
- OR option_name LIKE '_transient_timeout_psa_search_%'"
- );
- }
-
- /**
- * Register REST API routes for external/headless access.
- * Note: REST route only returns existing published results — it does NOT trigger generation.
- */
- public static function register_rest_routes() {
- register_rest_route(
- 'peptides/v1',
- '/search',
- array(
- 'methods' => 'GET',
- 'callback' => array( __CLASS__, 'rest_search' ),
- 'permission_callback' => '__return_true',
- 'args' => array(
- 'q' => array(
- 'required' => true,
- 'sanitize_callback' => 'sanitize_text_field',
- 'validate_callback' => function ( $param ) {
- return is_string( $param ) && strlen( $param ) >= PSA_Config::MIN_QUERY_LENGTH;
- },
- ),
- ),
- )
- );
- }
-
- /**
- * REST search callback — returns published peptides only (no generation trigger).
- *
- * @param WP_REST_Request $request The REST request.
- * @return WP_REST_Response
- */
- public static function rest_search( $request ) {
- // Rate limiting: max REST_RATE_LIMIT requests per IP per hour.
- $ip = psa_get_client_ip();
- $rate_key = 'psa_rest_rate_' . md5( $ip );
- $count = (int) get_transient( $rate_key );
-
- if ( $count >= PSA_Config::REST_RATE_LIMIT ) {
- return new WP_REST_Response(
- array( 'error' => 'Rate limit exceeded. Please try again later.' ),
- 429
- );
- }
-
- set_transient( $rate_key, $count + 1, HOUR_IN_SECONDS );
-
- $query = $request->get_param( 'q' );
-
- // Max input length validation.
- if ( mb_strlen( $query ) > PSA_Config::MAX_QUERY_LENGTH ) {
- return new WP_REST_Response(
- array( 'error' => 'Search query must not exceed ' . PSA_Config::MAX_QUERY_LENGTH . ' characters.' ),
- 400
- );
- }
-
- $results = self::search_peptides( $query );
- return new WP_REST_Response( $results, 200 );
- }
-
- /**
- * Render the [peptide_search] shortcode.
- *
- * Uses class-based selectors (no IDs) to support multiple shortcode instances
- * on the same page (e.g., hero section + search overlay). Each instance is
- * independently functional via scoped JS initialization.
- *
- * @param array $atts Shortcode attributes.
- * @return string HTML output.
- */
- public static function render_search_form( $atts ) {
- self::$instance_count++;
- $instance = self::$instance_count;
-
- $atts = shortcode_atts(
- array(
- 'placeholder' => 'Search for a peptide (e.g., BPC-157, Thymosin Beta-4)...',
- ),
- $atts,
- 'peptide_search'
- );
-
- ob_start();
- ?>
-
-
-
-
-
-
-
Searching our database...
-
-
-
-
-
⏳
-
This peptide is currently being added to our database. Please check back again later.
-
-
-
-
-
- PSA_Config::MAX_QUERY_LENGTH ) {
- wp_send_json_error( 'Search query must be between ' . PSA_Config::MIN_QUERY_LENGTH . ' and ' . PSA_Config::MAX_QUERY_LENGTH . ' characters.' );
- }
-
- // 1. Search existing published peptides.
- $results = self::search_peptides( $query );
-
- if ( ! empty( $results['results'] ) ) {
- wp_send_json_success(
- array(
- 'status' => 'found',
- 'data' => $results,
- )
- );
- return;
- }
-
- // 2. Check if already pending — handles retries if timed out.
- $pending = self::find_pending_peptide( $query );
- if ( $pending ) {
- self::handle_pending_retry( $pending );
- return; // handle_pending_retry() exits with wp_send_json_success()
- }
-
- // 3. Check rate limit before attempting new generation.
- if ( ! self::check_rate_limit() ) {
- wp_send_json_success(
- array(
- 'status' => 'rate_limited',
- 'message' => 'Too many requests. Please try again later.',
- )
- );
- return;
- }
-
- // 4. Validate and generate.
- self::handle_new_generation( $query );
- return; // handle_new_generation() exits with wp_send_json_*()
- }
-
- /**
- * Handle a pending peptide — retry if timed out, else return pending status.
- *
- * @param WP_Post $pending The pending post.
- * @return void Exits with wp_send_json_success().
- */
- private static function handle_pending_retry( $pending ) {
- $started = (int) get_post_meta( $pending->ID, 'psa_generation_started', true );
- $retry_count = (int) get_post_meta( $pending->ID, 'psa_generation_attempts', true );
- $timed_out = $started && ( time() - $started ) > PSA_Config::PENDING_TIMEOUT;
-
- if ( $timed_out ) {
- if ( $retry_count >= PSA_Config::MAX_GENERATION_RETRIES ) {
- // Max retries exceeded — mark as failed.
- error_log( 'PSA: Max retries (' . PSA_Config::MAX_GENERATION_RETRIES . ') exceeded for "' . $pending->post_title . '" (post ' . $pending->ID . '). Marking as failed.' );
- update_post_meta( $pending->ID, 'psa_source', 'failed' );
- update_post_meta( $pending->ID, 'psa_generation_error', 'Generation failed after ' . PSA_Config::MAX_GENERATION_RETRIES . ' attempts.' );
- wp_send_json_success(
- array(
- 'status' => 'invalid',
- 'message' => 'We were unable to generate content for this peptide. Please try again later.',
- )
- );
- return;
- }
-
- // Retry generation.
- error_log( 'PSA: Retrying stale pending peptide ' . $pending->post_title . ' (post ' . $pending->ID . ', attempt ' . ( $retry_count + 1 ) . '/' . PSA_Config::MAX_GENERATION_RETRIES . ')' );
- update_post_meta( $pending->ID, 'psa_generation_started', time() );
- update_post_meta( $pending->ID, 'psa_generation_attempts', $retry_count + 1 );
- self::schedule_background_generation( $pending->ID, $pending->post_title );
- }
-
- wp_send_json_success(
- array(
- 'status' => 'pending',
- 'peptide_name' => $pending->post_title,
- )
- );
- }
-
- /**
- * Check rate limit for AJAX generation requests.
- *
- * @return bool True if under limit, false if limit exceeded.
- */
- private static function check_rate_limit() {
- $ip = psa_get_client_ip();
- $rate_key = 'psa_rate_' . md5( $ip );
- $count = (int) get_transient( $rate_key );
-
- return $count < PSA_Config::AJAX_RATE_LIMIT;
- }
-
- /**
- * Handle a new generation request — validate, create placeholder, and schedule generation.
- *
- * @param string $query The search query (peptide name).
- * @return void Exits with wp_send_json_* responses.
- */
- private static function handle_new_generation( $query ) {
- // Global daily generation cap to prevent cost overruns.
- $daily_key = 'psa_daily_gen_' . gmdate( 'Y-m-d' );
- $daily_count = (int) get_transient( $daily_key );
- if ( $daily_count >= PSA_Config::DAILY_GENERATION_CAP ) {
- wp_send_json_success(
- array(
- 'status' => 'rate_limited',
- 'message' => 'Our system has reached its daily limit for adding new peptides. Please try again tomorrow.',
- )
- );
- return;
- }
-
- // Validate peptide name via AI.
- $validation = PSA_AI_Generator::validate_peptide_name( $query );
-
- if ( is_wp_error( $validation ) ) {
- error_log( 'PSA: Validation error for "' . $query . '": ' . $validation->get_error_message() );
- wp_send_json_success(
- array(
- 'status' => 'invalid',
- 'message' => 'Could not verify this peptide name.',
- )
- );
- }
-
- if ( empty( $validation['is_valid'] ) ) {
- wp_send_json_success(
- array(
- 'status' => 'invalid',
- 'message' => $validation['reason'] ?? 'This does not appear to be a recognized peptide.',
- )
- );
- }
-
- // It's a real peptide — increment rate limit and create placeholder.
- $ip = psa_get_client_ip();
- $rate_key = 'psa_rate_' . md5( $ip );
- $count = (int) get_transient( $rate_key );
- set_transient( $rate_key, $count + 1, HOUR_IN_SECONDS );
-
- $canonical_name = sanitize_text_field( $validation['canonical_name'] ?? $query );
- $placeholder_id = self::create_pending_placeholder( $canonical_name );
-
- if ( is_wp_error( $placeholder_id ) ) {
- wp_send_json_error( 'Failed to create peptide entry. Please try again.' );
- }
-
- // Increment global daily generation counter.
- $daily_key = 'psa_daily_gen_' . gmdate( 'Y-m-d' );
- $daily_count = (int) get_transient( $daily_key );
- set_transient( $daily_key, $daily_count + 1, DAY_IN_SECONDS );
-
- // Schedule async background generation.
- error_log( 'PSA: Scheduling async generation for "' . $canonical_name . '" (post ' . $placeholder_id . ')' );
- self::schedule_background_generation( $placeholder_id, $canonical_name );
-
- wp_send_json_success(
- array(
- 'status' => 'pending',
- 'peptide_name' => $canonical_name,
- )
- );
- }
-
- /**
- * Find an existing pending peptide post by name.
- *
- * Performs a multi-table query to locate pending draft posts by title or alias.
- * Queries the posts table joined with postmeta for 'psa_source' = 'pending' status,
- * plus LEFT JOIN on postmeta for aliases. Returns the most recent match.
- *
- * @param string $query Search term (peptide name or alias).
- * @return WP_Post|null Pending post object or null if not found.
- */
- public static function find_pending_peptide( $query ) {
- global $wpdb;
-
- $like = '%' . $wpdb->esc_like( $query ) . '%';
-
- // phpcs:ignore WordPress.DB.DirectDatabaseQuery
- $post_id = $wpdb->get_var(
- $wpdb->prepare(
- "SELECT p.ID
- FROM {$wpdb->posts} p
- INNER JOIN {$wpdb->postmeta} src ON p.ID = src.post_id
- AND src.meta_key = 'psa_source' AND src.meta_value = 'pending'
- LEFT JOIN {$wpdb->postmeta} alias ON p.ID = alias.post_id
- AND alias.meta_key = 'psa_aliases'
- WHERE p.post_type = 'peptide'
- AND p.post_status = 'draft'
- AND ( p.post_title LIKE %s OR alias.meta_value LIKE %s )
- ORDER BY p.post_date DESC
- LIMIT 1",
- $like,
- $like
- )
- );
-
- return $post_id ? get_post( $post_id ) : null;
- }
-
- /**
- * Create a placeholder draft post so we can track the pending state.
- *
- * @param string $peptide_name The canonical peptide name.
- * @return int|WP_Error Post ID or error.
- */
- public static function create_pending_placeholder( $peptide_name ) {
- $post_id = wp_insert_post(
- array(
- 'post_type' => 'peptide',
- 'post_title' => $peptide_name,
- 'post_content' => '',
- 'post_status' => 'draft',
- 'post_author' => psa_get_admin_user_id(),
- ),
- true
- );
-
- if ( ! is_wp_error( $post_id ) ) {
- update_post_meta( $post_id, 'psa_source', 'pending' );
- update_post_meta( $post_id, 'psa_generation_started', time() );
- // Allow developers to hook into placeholder creation.
- do_action( 'psa_peptide_created', $post_id, $peptide_name );
- }
-
- return $post_id;
- }
-
- /**
- * Schedule background generation using WP-Cron (non-blocking).
- *
- * @param int $post_id The placeholder post ID.
- * @param string $peptide_name The peptide to research.
- */
- public static function schedule_background_generation( $post_id, $peptide_name ) {
- $args = array( $post_id, $peptide_name );
- if ( ! wp_next_scheduled( 'psa_generate_peptide_background', $args ) ) {
- wp_schedule_single_event( time(), 'psa_generate_peptide_background', $args );
- }
- spawn_cron();
- }
-
- /**
- * Core search logic — queries CPT by title and aliases (published only).
- * Includes transient caching and batch post/meta priming to avoid N+1 queries.
- *
- * @param string $query Search term.
- * @return array { results: array, total: int, query: string }
- */
- public static function search_peptides( $query ) {
- global $wpdb;
-
- // Check transient cache first.
- $cache_key = 'psa_search_' . md5( sanitize_text_field( $query ) );
- $cached = get_transient( $cache_key );
- if ( false !== $cached ) {
- return $cached;
- }
-
- $like = '%' . $wpdb->esc_like( $query ) . '%';
-
- // phpcs:ignore WordPress.DB.DirectDatabaseQuery
- $post_ids = $wpdb->get_col(
- $wpdb->prepare(
- "SELECT DISTINCT p.ID
- FROM {$wpdb->posts} p
- LEFT JOIN {$wpdb->postmeta} m ON p.ID = m.post_id AND m.meta_key = 'psa_aliases'
- WHERE p.post_type = 'peptide'
- AND p.post_status = 'publish'
- AND ( p.post_title LIKE %s OR m.meta_value LIKE %s )
- ORDER BY p.post_title ASC
- LIMIT 20",
- $like,
- $like
- )
- );
-
- if ( ! empty( $post_ids ) ) {
- // Prime the WordPress object cache for all posts and their meta in batch.
- _prime_post_caches( $post_ids );
- update_meta_cache( 'post', $post_ids );
- }
-
- $results = array();
- foreach ( $post_ids as $id ) {
- $results[] = self::format_peptide( (int) $id );
- }
-
- $response = array(
- 'results' => $results,
- 'total' => count( $results ),
- 'query' => $query,
- );
-
- // Cache results for 6 hours.
- set_transient( $cache_key, $response, 6 * HOUR_IN_SECONDS );
-
- // Allow filtering of search results before return.
- $response = apply_filters( 'psa_search_results', $response, $query );
-
- return $response;
- }
-
- /**
- * Format a peptide post for JSON response.
- *
- * @param int $post_id The post ID.
- * @return array
- */
- public static function format_peptide( $post_id ) {
- $post = get_post( $post_id );
- if ( ! $post ) {
- return array();
- }
-
- return array(
- 'id' => $post->ID,
- 'title' => $post->post_title,
- 'url' => get_permalink( $post->ID ),
- 'excerpt' => wp_trim_words( $post->post_content, 30 ),
- 'sequence' => get_post_meta( $post->ID, 'psa_sequence', true ),
- 'molecular_weight' => get_post_meta( $post->ID, 'psa_molecular_weight', true ),
- 'molecular_formula' => get_post_meta( $post->ID, 'psa_molecular_formula', true ),
- 'source' => get_post_meta( $post->ID, 'psa_source', true ),
- );
- }
-}
diff --git a/includes/class-psa-template.php b/includes/class-psa-template.php
deleted file mode 100755
index 5cc1506..0000000
--- a/includes/class-psa-template.php
+++ /dev/null
@@ -1,131 +0,0 @@
- $post_id,
- 'source' => $source,
- 'pubchem_cid' => $pubchem_cid,
- );
-
- // Allow filtering of template data before rendering.
- $data = apply_filters( 'psa_template_data', $data, $post_id );
-
- // Restore values from possibly filtered data.
- $post_id = $data['post_id'] ?? $post_id;
- $source = $data['source'] ?? $source;
- $pubchem_cid = $data['pubchem_cid'] ?? $pubchem_cid;
-
- $html = '';
-
- // Quick facts box.
- $weight = get_post_meta( $post_id, 'psa_molecular_weight', true );
- $formula = get_post_meta( $post_id, 'psa_molecular_formula', true );
- $seq = get_post_meta( $post_id, 'psa_sequence', true );
- $aliases = get_post_meta( $post_id, 'psa_aliases', true );
-
- if ( $weight || $formula || $seq ) {
- $html .= '
';
- $html .= '
Quick Facts ';
- $html .= '
';
-
- if ( $aliases ) {
- $html .= 'Also Known As ' . esc_html( $aliases ) . ' ';
- }
- if ( $seq ) {
- $html .= 'Sequence ' . esc_html( $seq ) . ' ';
- }
- if ( $formula ) {
- $html .= 'Molecular Formula ' . esc_html( $formula ) . ' ';
- }
- if ( $weight ) {
- $html .= 'Molecular Weight ' . esc_html( $weight ) . ' ';
- }
- if ( $pubchem_cid ) {
- $cid_int = intval( $pubchem_cid );
- $html .= 'PubChem CID ' . $cid_int . ' ';
- }
-
- $html .= '
';
- $html .= '
';
- }
-
- // Detailed sections.
- $detail_fields = array(
- 'psa_mechanism' => 'Mechanism of Action',
- 'psa_research_apps' => 'Research Applications',
- 'psa_safety_profile' => 'Safety & Side Effects',
- 'psa_dosage_info' => 'Dosage Information',
- 'psa_references' => 'References',
- );
-
- foreach ( $detail_fields as $key => $label ) {
- $value = get_post_meta( $post_id, $key, true );
- if ( ! empty( $value ) ) {
- $html .= '
';
- $html .= '
' . esc_html( $label ) . ' ';
- $html .= '
' . wp_kses_post( wpautop( $value ) ) . '
';
- $html .= '
';
- }
- }
-
- // Source badge.
- if ( $source && 'pending' !== $source ) {
- $badges = array(
- 'ai-generated' => array(
- 'label' => 'AI-Generated Content',
- 'class' => 'psa-badge-ai',
- ),
- 'pubchem' => array(
- 'label' => 'PubChem Verified',
- 'class' => 'psa-badge-verified',
- ),
- 'manual' => array(
- 'label' => 'Manually Curated',
- 'class' => 'psa-badge-manual',
- ),
- );
- $badge = $badges[ $source ] ?? $badges['ai-generated'];
-
- $html .= '
';
- $html .= esc_html( $badge['label'] );
- if ( 'ai-generated' === $source ) {
- $html .= ' — This entry was automatically generated and may contain inaccuracies. Please verify critical information with primary sources.';
- }
- $html .= '
';
- }
-
- $html .= '
';
-
- return $content . $html;
- }
-}
diff --git a/peptide-search-ai.php b/peptide-search-ai.php
deleted file mode 100755
index b3db80d..0000000
--- a/peptide-search-ai.php
+++ /dev/null
@@ -1,197 +0,0 @@
-post_content, 'peptide_search' ) && ! is_singular( 'peptide' ) ) {
- return;
- }
-
- psa_enqueue_frontend_assets();
-}
-add_action( 'wp_enqueue_scripts', 'psa_maybe_enqueue_assets' );
-
-/**
- * Shared helper to enqueue CSS, JS, and localize script.
- */
-function psa_enqueue_frontend_assets() {
- wp_enqueue_style(
- 'psa-styles',
- PSA_PLUGIN_URL . 'assets/css/peptide-search.css',
- array(),
- PSA_VERSION
- );
-
- wp_enqueue_script(
- 'psa-search',
- PSA_PLUGIN_URL . 'assets/js/peptide-search.js',
- array( 'jquery' ),
- PSA_VERSION,
- true
- );
-
- wp_localize_script(
- 'psa-search',
- 'psaAjax',
- array(
- 'ajaxurl' => admin_url( 'admin-ajax.php' ),
- 'nonce' => wp_create_nonce( 'psa_search_nonce' ),
- )
- );
-}
-
-/**
- * On the KB page, hide Echo KB search and inject ours via wp_footer.
- */
-function psa_replace_kb_search() {
- if ( ! psa_is_kb_page() ) {
- return;
- }
-
- $search_html = PSA_Search::render_search_form(
- array(
- 'placeholder' => 'Search for a peptide (e.g., BPC-157, Thymosin Beta-4)...',
- )
- );
- $encoded = wp_json_encode( $search_html );
-
- echo '';
-
- echo '';
-}
-add_action( 'wp_footer', 'psa_replace_kb_search' );
-
-function psa_activate() {
- PSA_Post_Type::register_peptide_post_type();
- flush_rewrite_rules();
-}
-register_activation_hook( __FILE__, 'psa_activate' );
-
-function psa_deactivate() {
- flush_rewrite_rules();
-}
-register_deactivation_hook( __FILE__, 'psa_deactivate' );
-
-function psa_get_client_ip() {
- $headers = array(
- 'HTTP_CF_CONNECTING_IP',
- 'REMOTE_ADDR',
- );
- foreach ( $headers as $header ) {
- if ( ! empty( $_SERVER[ $header ] ) ) {
- $ip = explode( ',', sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) ) );
- $ip = trim( $ip[0] );
- if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
- return $ip;
- }
- }
- }
- return '0.0.0.0';
-}
-
-function psa_get_admin_user_id() {
- $admins = get_users(
- array(
- 'role' => 'administrator',
- 'number' => 1,
- 'fields' => 'ID',
- )
- );
- return ! empty( $admins ) ? (int) $admins[0] : 0;
-}
diff --git a/readme.txt b/readme.txt
deleted file mode 100755
index 565847d..0000000
--- a/readme.txt
+++ /dev/null
@@ -1,66 +0,0 @@
-=== Peptide Search AI ===
-Contributors: terence
-Tags: peptide, database, search, AI, science, research, openrouter
-Requires at least: 5.8
-Tested up to: 6.7
-Requires PHP: 7.4
-Stable tag: 4.0.0
-License: GPLv2 or later
-
-Searchable peptide database with AI-powered auto-population. Visitors search for peptides — if one doesn't exist, it's automatically validated and created with comprehensive AI-generated scientific content in the background.
-
-== Description ==
-
-Peptide Search AI turns your WordPress site into an intelligent, self-growing peptide database.
-
-**How it works:**
-
-1. Visitors search for a peptide using the search form.
-2. If found, results appear instantly with molecular data.
-3. If not found, the plugin automatically validates the name using a lightweight AI call, creates a placeholder, and queues comprehensive content generation in the background via WP-Cron.
-4. The visitor sees a "currently being added" message and can check back shortly.
-5. Generated entries include: amino acid sequence, molecular weight/formula, mechanism of action, research applications, safety profile, dosage info, and references.
-6. PubChem cross-referencing validates molecular data when available.
-
-**Features:**
-
-* AJAX search with instant results
-* Automatic peptide name validation (lightweight AI check before full generation)
-* Background content generation via WP-Cron (non-blocking)
-* Custom Post Type with structured scientific fields
-* AI content generation via OpenRouter — access hundreds of models (Gemini, Llama, DeepSeek, Mistral, Qwen, and more)
-* Separate model configuration for validation (cheap/fast) and generation (full)
-* PubChem PUG REST API integration for verified molecular data
-* Rate limiting (10 requests/IP/hour) to prevent abuse
-* Transient-based caching for validation and PubChem lookups
-* Admin settings for API key, models, auto-publish behavior, PubChem toggle
-* REST API endpoint for headless/external access (read-only)
-* Conditional asset loading (only on pages with the shortcode or peptide singles)
-* Source badges (AI-Generated / PubChem Verified / Manually Curated)
-* Mobile-responsive design
-
-== Installation ==
-
-1. Upload the `peptide-search-ai` folder to `/wp-content/plugins/`
-2. Activate the plugin through the Plugins menu
-3. Go to Settings > Peptide Search AI
-4. Enter your OpenRouter API key (get one at openrouter.ai/settings/keys)
-5. Optionally set separate models for validation and generation
-6. Configure auto-publish and PubChem settings
-7. Add `[peptide_search]` to any page or post
-
-== Shortcode ==
-
-`[peptide_search]` — Displays the peptide search form.
-
-Optional attributes:
-* `placeholder` — Custom placeholder text for the search input.
-
-Example: `[peptide_search placeholder="Search our peptide database..."]`
-
-== REST API ==
-
-Search endpoint: `GET /wp-json/peptides/v1/search?q=BPC-157`
-
-Returns JSON with matching published peptides, their molecular data, and permalink URLs. This endpoint is read-only and does not trigger auto-generation.
-
diff --git a/uninstall.php b/uninstall.php
deleted file mode 100755
index 207e5f6..0000000
--- a/uninstall.php
+++ /dev/null
@@ -1,39 +0,0 @@
-get_col(
- "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'peptide'"
-);
-
-if ( ! empty( $peptide_ids ) ) {
- foreach ( $peptide_ids as $post_id ) {
- wp_delete_post( (int) $post_id, true ); // Force delete, bypass trash.
- }
-}
-
-// 2. Delete plugin options.
-delete_option( 'psa_settings' );
-
-// 3. Delete all plugin transients (validation cache, rate limits).
-$wpdb->query(
- "DELETE FROM {$wpdb->options}
- WHERE option_name LIKE '_transient_psa_%'
- OR option_name LIKE '_transient_timeout_psa_%'"
-);
-
-// 4. Flush rewrite rules to clean up the peptide post type permalinks.
-flush_rewrite_rules();