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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
463 changes: 463 additions & 0 deletions wp-content/plugins/postsecret-ai/postsecret-ai.php

Large diffs are not rendered by default.

800 changes: 800 additions & 0 deletions wp-content/plugins/postsecret-ai/src/AdminBulkReclassify.php

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion wp-content/plugins/postsecret-ai/src/AdminBulkUpload.php
Original file line number Diff line number Diff line change
Expand Up @@ -668,7 +668,16 @@ function processStep(jobId) {

// Continue if still running
if (response.data.status === 'running' && response.data.has_more) {
setTimeout(() => processStep(jobId), 500);
// Use longer delay if rate limited
const delay = response.data.rate_limited ?
(response.data.retry_after || 2000) :
500;

if (response.data.rate_limited) {
updateLiveStatus('Rate limit reached - pausing briefly...');
}

setTimeout(() => processStep(jobId), delay);
} else if (response.data.status === 'completed') {
updateLiveStatus('Job completed');
}
Expand Down
33 changes: 28 additions & 5 deletions wp-content/plugins/postsecret-ai/src/AdminMetaBox.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ public static function render(\WP_Post $post): void
$topics = get_post_meta($post->ID, '_ps_topics', true) ?: [];
$feelings = get_post_meta($post->ID, '_ps_feelings', true) ?: [];
$meanings = get_post_meta($post->ID, '_ps_meanings', true) ?: [];
$vibe = get_post_meta($post->ID, '_ps_vibe', true) ?: [];
$style = get_post_meta($post->ID, '_ps_style', true) ?: 'unknown';
$locations = get_post_meta($post->ID, '_ps_locations', true) ?: [];
$wisdom = get_post_meta($post->ID, '_ps_wisdom', true) ?: '';
$model = get_post_meta($post->ID, '_ps_model', true);
$pver = get_post_meta($post->ID, '_ps_prompt_version', true);
$when = get_post_meta($post->ID, '_ps_updated_at', true);
Expand Down Expand Up @@ -68,7 +72,7 @@ public static function render(\WP_Post $post): void
echo '<strong>Review:</strong> <span class="psai-pill psai-rv-' . esc_attr($review) . '">' . esc_html($review) . '</span><br>';
echo '<strong>Vetted:</strong> ' . esc_html($vetted) . '</p>';

// Facets
// Text-only facets
if ($topics && is_array($topics)) {
echo '<p><strong>Topics:</strong><br>';
foreach ($topics as $t) echo '<span class="psai-chip psai-chip-topic">' . esc_html($t) . '</span> ';
Expand All @@ -85,10 +89,26 @@ public static function render(\WP_Post $post): void
echo '</p>';
}

// Teaches Wisdom indicator
$teachesWisdom = get_post_meta($post->ID, '_ps_teaches_wisdom', true);
if ($teachesWisdom === '1') {
echo '<p><strong>Teaches Wisdom:</strong> <span class="psai-badge" style="background:#fef3c7;color:#78350f;">✓ Yes</span></p>';
// Image+text facets
if ($vibe && is_array($vibe) && count($vibe) > 0) {
echo '<p><strong>Vibe:</strong><br>';
foreach ($vibe as $v) echo '<span class="psai-chip psai-chip-vibe">' . esc_html($v) . '</span> ';
echo '</p>';
}
if ($style && $style !== 'unknown') {
echo '<p><strong>Style:</strong> <span class="psai-chip psai-chip-style">' . esc_html($style) . '</span></p>';
}
if ($locations && is_array($locations) && count($locations) > 0) {
echo '<p><strong>Locations:</strong><br>';
foreach ($locations as $loc) echo '<span class="psai-chip psai-chip-location">' . esc_html($loc) . '</span> ';
echo '</p>';
}

// Wisdom
if ($wisdom && trim($wisdom) !== '') {
echo '<p><strong>Wisdom:</strong><br>';
echo '<em style="color:#78350f;background:#fef3c7;padding:4px 8px;border-radius:4px;display:inline-block;">' . esc_html($wisdom) . '</em>';
echo '</p>';
}

// Model / prompt / timestamp
Expand Down Expand Up @@ -258,6 +278,9 @@ public static function assets($hook)
.psai-chip-topic{background:#e6f3ff;color:#0c4a6e}
.psai-chip-feeling{background:#fff4e6;color:#78350f}
.psai-chip-meaning{background:#f0fdf4;color:#14532d}
.psai-chip-vibe{background:#f3e8ff;color:#581c87}
.psai-chip-style{background:#fef3e2;color:#78350f}
.psai-chip-location{background:#e0f2fe;color:#0c4a6e}
.psai-dot{display:inline-block;font-weight:700}
.psai-green{color:#008a20}.psai-blue{color:#2271b1}.psai-gray{color:#777}.psai-amber{color:#b95000}.psai-red{color:#b32d2e}
.psai-box details{margin-top:6px}
Expand Down
113 changes: 113 additions & 0 deletions wp-content/plugins/postsecret-ai/src/AdminPromptEditor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

namespace PSAI;

if (!defined('ABSPATH')) exit;

class AdminPromptEditor
{
public static function render()
{
if (!current_user_can('manage_options')) wp_die('Unauthorized', 403);

$opts = get_option(Settings::OPTION, []) ?: [];
$custom = $opts['CUSTOM_PROMPT'] ?? '';
$builtin_version = Prompt::VERSION;
$is_custom = trim($custom) !== '';

echo '<div class="wrap">';
echo '<h1>Prompt Editor</h1>';

// Show success/error messages
if (isset($_GET['psai_prompt_saved'])) {
echo '<div class="notice notice-success is-dismissible"><p>Prompt saved successfully.</p></div>';
}
if (isset($_GET['psai_prompt_reset'])) {
echo '<div class="notice notice-success is-dismissible"><p>Prompt reset to built-in version.</p></div>';
}

// Current status
echo '<div style="background:#f0f6fc;border:1px solid #0969da;border-radius:6px;padding:16px;margin:20px 0;">';
echo '<p style="margin:0;"><strong>Current prompt:</strong> ';
if ($is_custom) {
echo '<span style="color:#cf222e;">Custom prompt</span> (Built-in version: v' . esc_html($builtin_version) . ')';
} else {
echo '<span style="color:#1a7f37;">Built-in prompt v' . esc_html($builtin_version) . '</span>';
}
echo '</p>';
echo '</div>';

// Instructions
echo '<div style="background:#fff8c5;border:1px solid #d4a72c;border-radius:6px;padding:16px;margin:20px 0;">';
echo '<p style="margin:0;"><strong>Instructions:</strong></p>';
echo '<ul style="margin:8px 0 0 0;">';
echo '<li>Edit the prompt below to customize the AI classification behavior</li>';
echo '<li>Leave the textarea empty to use the built-in prompt (recommended)</li>';
echo '<li>Changes take effect immediately for all new classifications</li>';
echo '<li>Use the "Reset to Built-in" button to restore the default prompt</li>';
echo '</ul>';
echo '</div>';

// Editor form
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
wp_nonce_field('psai_save_prompt');
echo '<input type="hidden" name="action" value="psai_save_prompt" />';

echo '<div style="margin:20px 0;">';
echo '<textarea name="psai_custom_prompt" id="psai-prompt-editor" rows="40" style="width:100%;font-family:\'Courier New\',Consolas,Monaco,monospace;font-size:13px;line-height:1.6;padding:12px;border:1px solid #8c8f94;border-radius:4px;resize:vertical;">';
if ($is_custom) {
echo esc_textarea($custom);
} else {
// Show built-in prompt as placeholder for reference
echo esc_textarea(Prompt::TEXT);
}
echo '</textarea>';
echo '</div>';

echo '<div style="display:flex;gap:12px;align-items:center;">';
submit_button('Save Custom Prompt', 'primary', 'submit', false);

// Reset button (separate form)
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:0;" onsubmit="return confirm(\'Reset to built-in prompt? Your custom prompt will be cleared.\');">';
wp_nonce_field('psai_reset_prompt');
echo '<input type="hidden" name="action" value="psai_reset_prompt" />';
submit_button('Reset to Built-in', 'secondary', 'submit', false);
echo '</form>';

// View built-in button
echo '<a href="#" id="psai-view-builtin" class="button" style="text-decoration:none;">View Built-in Prompt</a>';
echo '</div>';

echo '</form>';

// Modal for viewing built-in prompt
echo '<div id="psai-builtin-modal" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:100000;align-items:center;justify-content:center;">';
echo '<div style="background:#fff;border-radius:8px;padding:24px;max-width:90%;max-height:90%;overflow:auto;position:relative;">';
echo '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;border-bottom:1px solid #ddd;padding-bottom:12px;">';
echo '<h2 style="margin:0;">Built-in Prompt (v' . esc_html($builtin_version) . ')</h2>';
echo '<button id="psai-close-modal" style="background:none;border:none;font-size:24px;cursor:pointer;padding:0;line-height:1;">&times;</button>';
echo '</div>';
echo '<pre style="background:#f6f8fa;padding:16px;border-radius:4px;overflow:auto;margin:0;font-family:monospace;font-size:13px;line-height:1.5;white-space:pre-wrap;">';
echo esc_html(Prompt::TEXT);
echo '</pre>';
echo '</div>';
echo '</div>';

// JavaScript for modal
echo '<script>
(function($) {
$("#psai-view-builtin").on("click", function(e) {
e.preventDefault();
$("#psai-builtin-modal").css("display", "flex");
});
$("#psai-close-modal, #psai-builtin-modal").on("click", function(e) {
if (e.target === this) {
$("#psai-builtin-modal").hide();
}
});
})(jQuery);
</script>';

echo '</div>';
}
}
8 changes: 6 additions & 2 deletions wp-content/plugins/postsecret-ai/src/AttachmentSync.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ public static function sync_from_payload(int $front_id, array $payload, ?int $ba
{
$containsPII = (bool)($payload['moderation']['containsPII'] ?? false);

// Combine all facets for caption
// Combine all facets for caption (text-only + image+text)
$topics = is_array($payload['topics'] ?? null) ? $payload['topics'] : [];
$feelings = is_array($payload['feelings'] ?? null) ? $payload['feelings'] : [];
$meanings = is_array($payload['meanings'] ?? null) ? $payload['meanings'] : [];
$allFacets = array_merge($topics, $feelings, $meanings);
$vibe = is_array($payload['vibe'] ?? null) ? $payload['vibe'] : [];
$locations = is_array($payload['locations'] ?? null) ? $payload['locations'] : [];
$allFacets = array_merge($topics, $feelings, $meanings, $vibe, $locations);

$secretDesc = self::clean_str($payload['secretDescription'] ?? '');

Expand Down Expand Up @@ -72,6 +74,8 @@ private static function apply_if_empty(int $att_id, string $alt, string $caption
// DESCRIPTION (objective summary from secretDescription, which is always PII-free by design)
// Note: secretDescription is an AI-generated summary, not raw extracted text, so it's safe even when containsPII=true
$existingDesc = is_object($existingPost) ? trim((string)$existingPost->post_content) : '';
// Decode HTML entities that WordPress may have encoded
$existingDesc = html_entity_decode($existingDesc, ENT_QUOTES | ENT_HTML5, 'UTF-8');
error_log("AttachmentSync: existingDesc='" . substr($existingDesc, 0, 100) . "', force_update=" . ($force_update ? 'true' : 'false') . ", containsPII=" . ($containsPII ? 'true' : 'false') . ", desc='" . substr($desc, 0, 100) . "'");
if (($existingDesc === '' || $force_update) && $desc !== '') {
// Strip dangerous tags but preserve text as-is (no entity encoding)
Expand Down
68 changes: 58 additions & 10 deletions wp-content/plugins/postsecret-ai/src/BulkJobService.php
Original file line number Diff line number Diff line change
Expand Up @@ -464,16 +464,64 @@ public static function process_batch(int $job_id, int $batch_size = 25): array
], ['id' => $item['id']]);
$success_count++;
} else {
$attempts = (int)$item['attempts'] + 1;
$status = $attempts >= 3 ? self::ITEM_QUARANTINED : self::ITEM_ERROR;

$wpdb->update($table_items, [
'status' => $status,
'attempts' => $attempts,
'last_error' => substr($result['error'], 0, 500),
'updated_at' => current_time('mysql'),
], ['id' => $item['id']]);
$fail_count++;
$error_msg = $result['error'] ?? 'Unknown error';

// Check if this is a rate limit error (HTTP 429)
$is_rate_limit = strpos($error_msg, 'HTTP 429') !== false ||
strpos($error_msg, 'rate_limit_exceeded') !== false ||
strpos($error_msg, 'Rate limit') !== false;

if ($is_rate_limit) {
// For rate limits, reset item to pending so it can be retried
$wpdb->update($table_items, [
'status' => self::ITEM_PENDING,
'last_error' => 'Rate limit - will retry',
'updated_at' => current_time('mysql'),
], ['id' => $item['id']]);

// Store rate limit warning on job
$wpdb->update($table_jobs, [
'last_error' => 'Rate limit reached - pausing briefly',
'updated_at' => current_time('mysql'),
], ['id' => $job_id]);

// Update what we've processed so far
$wpdb->query($wpdb->prepare(
"UPDATE {$table_jobs} SET
processed_items = processed_items + %d,
success_count = success_count + %d,
fail_count = fail_count + %d,
updated_at = %s
WHERE id = %d",
$processed,
$success_count,
$fail_count,
current_time('mysql'),
$job_id
));

// Return immediately with retry signal
return [
'success' => true,
'processed' => $processed,
'status' => self::STATUS_RUNNING,
'has_more' => true,
'rate_limited' => true,
'retry_after' => 2000, // 2 second delay before next batch
];
} else {
// Regular error - mark as failed
$attempts = (int)$item['attempts'] + 1;
$status = $attempts >= 3 ? self::ITEM_QUARANTINED : self::ITEM_ERROR;

$wpdb->update($table_items, [
'status' => $status,
'attempts' => $attempts,
'last_error' => substr($error_msg, 0, 500),
'updated_at' => current_time('mysql'),
], ['id' => $item['id']]);
$fail_count++;
}
}

$processed++;
Expand Down
9 changes: 5 additions & 4 deletions wp-content/plugins/postsecret-ai/src/Classifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ public static function classify(string $apiKey, string $model, string $frontUrl,
$seed = isset($opts['SEED']) && $opts['SEED'] !== '' ? (int)$opts['SEED'] : null;

// ── Vision options
$detail = in_array(($opts['VISION_DETAIL'] ?? 'high'), ['low', 'auto', 'high'], true)
? (string)$opts['VISION_DETAIL']
$vision_detail = $opts['VISION_DETAIL'] ?? 'high';
$detail = in_array($vision_detail, ['low', 'auto', 'high'], true)
? (string)$vision_detail
: 'high';

// ── HTTP knobs
Expand All @@ -96,9 +97,9 @@ public static function classify(string $apiKey, string $model, string $frontUrl,
$headers['OpenAI-Project'] = (string)$opts['OPENAI_PROJECT'];
}

// ── Messages
// ── Messages (use custom prompt if configured, else built-in)
$messages = [
['role' => 'system', 'content' => Prompt::TEXT],
['role' => 'system', 'content' => Prompt::get()],
['role' => 'user', 'content' => self::buildVisionContent($frontUrl, $backUrl, $detail)],
];

Expand Down
31 changes: 22 additions & 9 deletions wp-content/plugins/postsecret-ai/src/EmbeddingService.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,13 @@ public static function generate_and_store(int $secret_id, array $payload, string
if (self::qdrant_enabled()) {
$qdrantPayload = [
'status' => 'public', // adjust at query time if needed
'teachesWisdom' => !empty($payload['teachesWisdom']),
'topics' => $payload['topics'] ?? [],
'feelings' => $payload['feelings'] ?? [],
'meanings' => $payload['meanings'] ?? [],
'vibe' => $payload['vibe'] ?? [],
'style' => $payload['style'] ?? 'unknown',
'locations' => $payload['locations'] ?? [],
'wisdom' => !empty($payload['wisdom']) ? (string)$payload['wisdom'] : '',
];
/** @var array $qdrantPayload */
$qdrantPayload = apply_filters('psai/embedding/qdrant-payload', $qdrantPayload, $secret_id, $payload);
Expand Down Expand Up @@ -527,8 +530,7 @@ private static function store_embedding(int $secret_id, string $model, array $em
'topics' => $payload['topics'] ?? [],
'feelings' => $payload['feelings'] ?? [],
'meanings' => $payload['meanings'] ?? [],
'frontText' => $payload['front']['text']['fullText'] ?? null,
'backText' => $payload['back']['text']['fullText'] ?? null,
'vibe' => $payload['vibe'] ?? [],
],
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));

Expand Down Expand Up @@ -622,24 +624,35 @@ private static function build_embedding_input(array $payload): string
$parts[] = 'Secret: ' . self::sanitize_space((string)$payload['secret']);
}

// Add topics
// Add text-only facets
if (!empty($payload['topics']) && is_array($payload['topics'])) {
$parts[] = 'Topics: ' . implode(', ', array_map('self::sanitize_space', $payload['topics']));
}

// Add feelings
if (!empty($payload['feelings']) && is_array($payload['feelings'])) {
$parts[] = 'Feelings: ' . implode(', ', array_map('self::sanitize_space', $payload['feelings']));
}

// Add meanings
if (!empty($payload['meanings']) && is_array($payload['meanings'])) {
$parts[] = 'Meanings: ' . implode(', ', array_map('self::sanitize_space', $payload['meanings']));
}

// Add extracted text if available
if (!empty($payload['text'])) {
$parts[] = 'Text: ' . self::sanitize_space((string)$payload['text']);
// Add image+text facets
if (!empty($payload['vibe']) && is_array($payload['vibe'])) {
$parts[] = 'Vibe: ' . implode(', ', array_map('self::sanitize_space', $payload['vibe']));
}

if (!empty($payload['style']) && $payload['style'] !== 'unknown') {
$parts[] = 'Style: ' . self::sanitize_space((string)$payload['style']);
}

if (!empty($payload['locations']) && is_array($payload['locations'])) {
$parts[] = 'Locations: ' . implode(', ', array_map('self::sanitize_space', $payload['locations']));
}

// Add wisdom if present
if (!empty($payload['wisdom'])) {
$parts[] = 'Wisdom: ' . self::sanitize_space((string)$payload['wisdom']);
}

return implode('. ', $parts);
Expand Down
Loading