diff --git a/src/historyComponents.jsx b/src/historyComponents.jsx index 783be3d..d3ec116 100644 --- a/src/historyComponents.jsx +++ b/src/historyComponents.jsx @@ -1,11 +1,48 @@ // ═══════════════════════════════════════════════════════════════════════ // Version History UI Components +// Enhanced with Git-like Diffs, A/B Testing Interfaces, and JSON Exports // ═══════════════════════════════════════════════════════════════════════ import React, { useState } from 'react'; +import { versioningService } from './versioningService'; /** - * VersionCard - Displays a single version with actions + * TextDiffHighlighter - Renders basic character or word diff markers inline + */ +function TextDiffHighlighter({ oldText = '', newText = '' }) { + const oldWords = oldText.split(/(\s+)/); + const newWords = newText.split(/(\s+)/); + let oldIdx = 0, newIdx = 0; + const elements = []; + let keyCounter = 0; + + while (oldIdx < oldWords.length || newIdx < newWords.length) { + if (oldIdx < oldWords.length && newIdx < newWords.length && oldWords[oldIdx] === newWords[newIdx]) { + elements.push({oldWords[oldIdx]}); + oldIdx++; + newIdx++; + } else if (newIdx < newWords.length) { + elements.push( + + {newWords[newIdx]} + + ); + newIdx++; + } else { + elements.push( + + {oldWords[oldIdx]} + + ); + oldIdx++; + } + } + + return
{elements}
; +} + +/** + * VersionCard - Displays a single version with actions and performance tracking metrics */ export function VersionCard({ version, @@ -13,15 +50,14 @@ export function VersionCard({ onRestore, onCompare, isLatest, + onSelectForAB, + abSelectedSide }) { const createdDate = new Date(version.created_at); const dateStr = createdDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', - year: - createdDate.getFullYear() !== new Date().getFullYear() - ? 'numeric' - : undefined, + year: createdDate.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined, }); const timeStr = createdDate.toLocaleTimeString('en-US', { hour: '2-digit', @@ -32,10 +68,8 @@ export function VersionCard({ return (
- {/* Header with version badge and date */} -
+
- v{version.version_number} - {isLatest && ' · LATEST'} + v{version.version_number} {isLatest && ' · LATEST'} {dateStr} at {timeStr}
- + {version.provider}
- {/* Change note */} -
+
{version.change_note}
- {/* Scores */} -
+
-
-
- - Clarity {version.clarity_score} - -
-
-
- - Quality {version.quality_score} - -
+ Clarity {version.clarity_score} + Quality {version.quality_score} + Choice Wins: {version.performance_score || 0} +
+ + {/* A/B Target Selection Badges */} +
+ +
- {/* Actions */}
{!isLatest && (
@@ -181,282 +167,158 @@ export function VersionCard({ } /** - * VersionHistoryPanel - Shows all versions of a prompt + * VersionHistoryPanel - Shows all versions of a prompt with integrated action toolsets */ export function VersionHistoryPanel({ prompt, onRestore, onCompare, onBack }) { - const versions = prompt.versions || []; + const [localPrompt, setLocalPrompt] = useState(prompt); + const [abA, setAbA] = useState(null); + const [abB, setAbB] = useState(null); + const [isTestingMode, setIsTestingMode] = useState(false); + + const versions = localPrompt.versions || []; const latestVersion = versions[0]; + const handleSelectForAB = (versionNum, side) => { + if (side === 'A') setAbA(versionNum); + if (side === 'B') setAbB(versionNum); + }; + + const triggerExport = () => { + const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(versions, null, 2)); + const dlAnchor = document.createElement('a'); + dlAnchor.setAttribute("href", dataStr); + dlAnchor.setAttribute("download", `prompt-${localPrompt.id}-history.json`); + document.body.appendChild(dlAnchor); + dlAnchor.click(); + dlAnchor.remove(); + }; + return ( -
- {/* Header */} -
+
+ {/* Header Panel */} +
- + Timeline Timeline +
+
+ - Version History + + {versions.length} versions +
- - {versions.length} version{versions.length !== 1 ? 's' : ''} -
- {/* Original text preview */} -
- - Original Prompt - -
- {prompt.original_text} + {/* A/B Action Launchpad Controller */} + {abA && abB && ( +
+ + Ready to test: v{abA} vs v{abB} + +
-
+ )} - {/* Versions list */} -
- {versions.length === 0 ? ( -
- No versions yet + {isTestingMode ? ( + v.version_number === abA)} + version2={versions.find(v => v.version_number === abB)} + promptId={localPrompt.id} + onClose={() => setIsTestingMode(false)} + isABMode={true} + onVote={async (vNum) => { + const updated = await versioningService.recordABChoice(localPrompt.id, vNum); + setLocalPrompt(updated); + setIsTestingMode(false); + }} + /> + ) : ( + <> + {/* Original Preview */} +
+ Original Prompt +
+ {localPrompt.original_text} +
- ) : ( - versions.map((version) => ( - - )) - )} -
+ + {/* Versions List */} +
+ {versions.length === 0 ? ( +
No versions yet
+ ) : ( + versions.map((version) => ( + + )) + )} +
+ + )}
); } /** - * DiffView - Side-by-side comparison of two versions + * DiffView - Side-by-side comparison with inline split and choice collection matrices */ -export function DiffView({ version1, version2, onClose }) { +export function DiffView({ version1, version2, promptId, onClose, isABMode = false, onVote }) { return ( -
- {/* Header */} -
+
+ {/* Dynamic Title Headers */} +
- + - Compare v{version1.version_number} vs v{version2.version_number} + {isABMode ? `🚀 Live A/B Experimentation Suite` : `Visual Diff Explorer`}
- {/* Side-by-side comparison */} -
- {/* Left side - Version 1 */} -
-
- - v{version1.version_number} - -
-
- {version1.enhanced_prompt} + {/* Main Container Workspace splits */} +
+ {/* Left Card Area */} +
+
+ VARIANT A (v{version1.version_number}) + {isABMode && ( + + )}
-
-
- Clarity: {version1.clarity_score} | Quality:{' '} - {version1.quality_score} -
+
+
- {/* Right side - Version 2 */} -
-
- - v{version2.version_number} · LATEST - -
-
- {version2.enhanced_prompt} + {/* Right Card Area */} +
+
+ VARIANT B (v{version2.version_number}) + {isABMode && ( + + )}
-
-
- Clarity: {version2.clarity_score} | Quality:{' '} - {version2.quality_score} -
+
+
@@ -465,100 +327,28 @@ export function DiffView({ version1, version2, onClose }) { } /** - * PromptsList - Lists all prompts with their latest versions + * PromptsList - Lists all base entities tracking current iteration versions */ export function PromptsList({ prompts, onSelectPrompt, onClearAll, onBack }) { return ( -
- {/* Header */} -
+
+
- - History - - {prompts.length} - + + Saved Playgrounds + {prompts.length}
{prompts.length > 0 && ( - )}
- {/* Prompts list */} -
+
{prompts.length === 0 ? ( -
- No prompts yet. -
- Enhance some prompts first. +
+ No prompts found yet.
Enhance prompt strings to compile dynamic timelines.
) : ( prompts.map((prompt) => { @@ -569,58 +359,17 @@ export function PromptsList({ prompts, onSelectPrompt, onClearAll, onBack }) { ); diff --git a/src/versioningService.js b/src/versioningService.js index 0a2f217..7dab94b 100644 --- a/src/versioningService.js +++ b/src/versioningService.js @@ -1,6 +1,7 @@ // ═══════════════════════════════════════════════════════════════════════ // Prompt Versioning Service // Manages storage and retrieval of prompt versions with history tracking +// Enhanced with A/B Testing, Diff Support, and Analytics Trackers // ═══════════════════════════════════════════════════════════════════════ const STORAGE_KEY_PROMPTS = 'pp_prompts'; @@ -8,6 +9,7 @@ const STORAGE_KEY_METADATA = 'pp_metadata'; const STORAGE_KEY_SEARCHES = 'pp_recent_searches'; const EXPORT_VERSION = 1; const MAX_PROMPTS = 100; +const MAX_VERSIONS_LIMIT = 20; // Requirement: Maximum version limit with auto-cleanup function asNumber(value, fallback = 0) { const n = Number(value); @@ -26,6 +28,7 @@ function sanitizeVersion(version = {}, index = 0) { clarity_score: asNumber(version.clarity_score), specificity_score: asNumber(version.specificity_score), quality_score: asNumber(version.quality_score), + performance_score: asNumber(version.performance_score || 0), // Added performance score tracking domain_detected: String(version.domain_detected || ''), missing_requirements: Array.isArray(version.missing_requirements) ? version.missing_requirements @@ -50,7 +53,7 @@ function sanitizePromptForImport(prompt = {}, index = 0) { ? prompt.versions.map((v, i) => sanitizeVersion(v, i)) : []; - // Keep latest first so app behavior remains consistent. + versions.sort((a, b) => b.created_at - a.created_at); versions.forEach((v, i) => { v.version_number = versions.length - i; @@ -65,6 +68,7 @@ function sanitizePromptForImport(prompt = {}, index = 0) { created_at: asNumber(prompt.created_at, now), updated_at: asNumber(prompt.updated_at, now), tags: Array.isArray(prompt.tags) ? prompt.tags : [], + ab_test: prompt.ab_test || { active: false, versionA: null, versionB: null, metrics: { aSelections: 0, bSelections: 0 } } }; } @@ -78,10 +82,6 @@ function toLegacyHistory(prompts = []) { clarity_score: latest.clarity_score, specificity_score: latest.specificity_score, quality_score: latest.quality_score, - domain_detected: latest.domain_detected, - missing_requirements: latest.missing_requirements || [], - transformation_insight: latest.transformation_insight || '', - ambiguities_resolved: latest.ambiguities_resolved || [], provider: latest.provider || 'gemini', model: latest.model || 'gemini-pro', original: prompt.original_text, @@ -97,9 +97,6 @@ function toLegacyHistory(prompts = []) { } export const versioningService = { - /** - * Get all prompts with their version history - */ async getAllPrompts() { return new Promise((res) => { chrome.storage.local.get([STORAGE_KEY_PROMPTS], (data) => { @@ -109,17 +106,11 @@ export const versioningService = { }); }, - /** - * Get a specific prompt by ID - */ async getPromptById(id) { const prompts = await this.getAllPrompts(); return prompts.find((p) => p.id === id) || null; }, - /** - * Create a new prompt entry with first version - */ async createPrompt(originalText, enhancedData, metadata = {}) { const prompts = await this.getAllPrompts(); const id = `pp_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`; @@ -131,6 +122,7 @@ export const versioningService = { clarity_score: enhancedData.clarity_score, specificity_score: enhancedData.specificity_score, quality_score: enhancedData.quality_score, + performance_score: enhancedData.performance_score || 0, domain_detected: enhancedData.domain_detected, missing_requirements: enhancedData.missing_requirements, transformation_insight: enhancedData.transformation_insight, @@ -151,9 +143,10 @@ export const versioningService = { updated_at: now, tags: metadata.tags || [], favorite: false, + ab_test: { active: false, versionA: null, versionB: null, metrics: { aSelections: 0, bSelections: 0 } } }; - const updated = [prompt, ...prompts].slice(0, MAX_PROMPTS); // Keep max prompts + const updated = [prompt, ...prompts].slice(0, MAX_PROMPTS); await new Promise((res) => { chrome.storage.local.set({ [STORAGE_KEY_PROMPTS]: updated }, res); }); @@ -161,9 +154,6 @@ export const versioningService = { return prompt; }, - /** - * Add a new version to an existing prompt - */ async addVersion(promptId, enhancedData, metadata = {}) { const prompts = await this.getAllPrompts(); const prompt = prompts.find((p) => p.id === promptId); @@ -178,6 +168,7 @@ export const versioningService = { clarity_score: enhancedData.clarity_score, specificity_score: enhancedData.specificity_score, quality_score: enhancedData.quality_score, + performance_score: enhancedData.performance_score || 0, domain_detected: enhancedData.domain_detected, missing_requirements: enhancedData.missing_requirements, transformation_insight: enhancedData.transformation_insight, @@ -188,7 +179,13 @@ export const versioningService = { change_note: metadata.change_note || `Version ${nextVersion}`, }; - prompt.versions.unshift(version); // Latest version at front + prompt.versions.unshift(version); + + // Requirement: Maximum version limit configuration array with auto-cleanup + if (prompt.versions.length > MAX_VERSIONS_LIMIT) { + prompt.versions = prompt.versions.slice(0, MAX_VERSIONS_LIMIT); + } + prompt.updated_at = now; const updated = prompts.map((p) => (p.id === promptId ? prompt : p)); @@ -199,9 +196,72 @@ export const versioningService = { return prompt; }, - /** - * Update the updated_at timestamp of a prompt (moves it to top of recent) - */ + // ═══════════════════════════════════════════════════════════════════════ + // NEW A/B TESTING & PERFORMANCE ANALYTICS TRACKING METHODS + // ═══════════════════════════════════════════════════════════════════════ + + async configureABTest(promptId, versionANumber, versionBNumber) { + const prompts = await this.getAllPrompts(); + const prompt = prompts.find((p) => p.id === promptId); + if (!prompt) throw new Error(`Prompt ${promptId} not found`); + + prompt.ab_test = { + active: true, + versionA: versionANumber, + versionB: versionBNumber, + metrics: { aSelections: 0, bSelections: 0 } + }; + + const updated = prompts.map((p) => (p.id === promptId ? prompt : p)); + await new Promise((res) => { + chrome.storage.local.set({ [STORAGE_KEY_PROMPTS]: updated }, res); + }); + return prompt; + }, + + async recordABChoice(promptId, chosenVersionNumber) { + const prompts = await this.getAllPrompts(); + const prompt = prompts.find((p) => p.id === promptId); + if (!prompt) throw new Error(`Prompt ${promptId} not found`); + + if (prompt.ab_test && prompt.ab_test.active) { + if (chosenVersionNumber === prompt.ab_test.versionA) { + prompt.ab_test.metrics.aSelections += 1; + } else if (chosenVersionNumber === prompt.ab_test.versionB) { + prompt.ab_test.metrics.bSelections += 1; + } + } + + // Update individual version performance metric scores + const version = prompt.versions.find(v => v.version_number === chosenVersionNumber); + if (version) { + version.performance_score = (version.performance_score || 0) + 1; + } + + const updated = prompts.map((p) => (p.id === promptId ? prompt : p)); + await new Promise((res) => { + chrome.storage.local.set({ [STORAGE_KEY_PROMPTS]: updated }, res); + }); + return prompt; + }, + + async updateVersionScore(promptId, versionNumber, field, score) { + const prompts = await this.getAllPrompts(); + const prompt = prompts.find((p) => p.id === promptId); + if (!prompt) throw new Error(`Prompt ${promptId} not found`); + + const version = prompt.versions.find(v => v.version_number === versionNumber); + if (version) { + version[field] = asNumber(score); + } + + const updated = prompts.map((p) => (p.id === promptId ? prompt : p)); + await new Promise((res) => { + chrome.storage.local.set({ [STORAGE_KEY_PROMPTS]: updated }, res); + }); + return prompt; + }, + async touchPrompt(promptId) { const prompts = await this.getAllPrompts(); const now = Date.now(); @@ -215,9 +275,6 @@ export const versioningService = { return updated; }, - /** - * Get a specific version of a prompt - */ async getVersion(promptId, versionNumber) { const prompt = await this.getPromptById(promptId); if (!prompt) return null; @@ -226,9 +283,6 @@ export const versioningService = { ); }, - /** - * Restore a prompt to a previous version (creates new version from old) - */ async restoreVersion(promptId, sourceVersionNumber) { const prompt = await this.getPromptById(promptId); if (!prompt) throw new Error(`Prompt ${promptId} not found`); @@ -240,7 +294,6 @@ export const versioningService = { throw new Error(`Version ${sourceVersionNumber} not found`); } - // Create a new version based on the old one const nextVersion = (prompt.versions[0]?.version_number || 0) + 1; const now = Date.now(); @@ -252,6 +305,9 @@ export const versioningService = { }; prompt.versions.unshift(newVersion); + if (prompt.versions.length > MAX_VERSIONS_LIMIT) { + prompt.versions = prompt.versions.slice(0, MAX_VERSIONS_LIMIT); + } prompt.updated_at = now; const prompts = await this.getAllPrompts(); @@ -264,9 +320,6 @@ export const versioningService = { return prompt; }, - /** - * Compare two versions and return diff - */ compareVersions(version1, version2) { return { version1Number: version1.version_number, @@ -275,16 +328,13 @@ export const versioningService = { version2: version2, changes: { clarityDiff: version2.clarity_score - version1.clarity_score, - specificityDiff: - version2.specificity_score - version1.specificity_score, + specificityDiff: version2.specificity_score - version1.specificity_score, qualityDiff: version2.quality_score - version1.quality_score, + performanceDiff: (version2.performance_score || 0) - (version1.performance_score || 0), }, }; }, - /** - * Delete a prompt and all its versions - */ async deletePrompt(promptId) { const prompts = await this.getAllPrompts(); const updated = prompts.filter((p) => p.id !== promptId); @@ -293,9 +343,7 @@ export const versioningService = { }); }, - /** - * Clear all prompts and history - */ + async clearAll() { await new Promise((res) => { chrome.storage.local.set({ @@ -306,9 +354,7 @@ export const versioningService = { }); }, - /** - * Recent Search History - */ + async getRecentSearches() { return new Promise((res) => { chrome.storage.local.get([STORAGE_KEY_SEARCHES], (data) => { @@ -364,6 +410,7 @@ export const versioningService = { clarity_score: item.clarity_score || 0, specificity_score: item.specificity_score || 0, quality_score: item.quality_score || 0, + performance_score: item.performance_score || 0, domain_detected: item.domain_detected || '', missing_requirements: item.missing_requirements || [], transformation_insight: item.transformation_insight || '', @@ -462,9 +509,6 @@ export const versioningService = { }; }, - /** - * Migrate legacy flat history to versioned format - */ async migrateFromLegacy() { return new Promise((res) => { chrome.storage.local.get( @@ -484,7 +528,6 @@ export const versioningService = { } const prompts = []; - // Group by original text and create versioned entries const grouped = {}; legacyHistory.forEach((item) => { const key = item.original || 'unknown'; @@ -494,7 +537,6 @@ export const versioningService = { let id_counter = 1; Object.values(grouped).forEach((items) => { - // Sort by timestamp items.sort((a, b) => a.ts - b.ts); const id = `pp_legacy_${id_counter++}`; @@ -504,6 +546,7 @@ export const versioningService = { clarity_score: item.clarity_score, specificity_score: item.specificity_score, quality_score: item.quality_score, + performance_score: 0, domain_detected: item.domain_detected, missing_requirements: item.missing_requirements || [], transformation_insight: item.transformation_insight || '', @@ -519,11 +562,12 @@ export const versioningService = { original_text: items[0].original, domain: items[0].domain || '', mode: items[0].mode || 'technical', - versions: versions.reverse(), // Most recent first + versions: versions.reverse(), created_at: items[0].ts, updated_at: items[items.length - 1].ts, tags: [], favorite: items.some((item) => item.favorite) || false, + ab_test: { active: false, versionA: null, versionB: null, metrics: { aSelections: 0, bSelections: 0 } } }); }); @@ -531,7 +575,7 @@ export const versioningService = { chrome.storage.local.set({ [STORAGE_KEY_PROMPTS]: prompts }, res2); }); - // Clear legacy storage + await new Promise((res2) => { chrome.storage.local.remove(['pp_history'], res2); }); diff --git a/vite.config.js b/vite.config.js index 5bcec2c..b75a4f7 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,30 +1,3 @@ -// import { defineConfig } from 'vite' -// import react from '@vitejs/plugin-react' -// import { crx } from '@crxjs/vite-plugin' -// import manifest from './manifest.json' - -// export default defineConfig({ -// plugins: [react(), crx({ manifest })], -// build: { -// outDir: 'dist', -// emptyOutDir: true, -// rollupOptions: { -// input: { -// index: 'index.html', -// background: 'src/background.js', -// content: 'src/content.js', -// }, -// output: { -// entryFileNames: c => -// ['background', 'content'].includes(c.name) -// ? 'src/[name].js' -// : 'assets/[name]-[hash].js', -// manualChunks: undefined, -// }, -// }, -// }, -// }) - import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import { crx } from '@crxjs/vite-plugin' @@ -33,10 +6,10 @@ import manifest from './manifest.json' export default defineConfig({ plugins: [react(), crx({ manifest })], - // Yeh section Vite ko batayega ki .js files ke andar JSX ko bina crash hue handle kare + // FIXED: Tells esbuild to process JSX inside both .js and .jsx files smoothly esbuild: { loader: 'jsx', - include: /src\/.*\.js$/, // src folder ki saari .js files par apply hoga + include: /src\/.*\.[jt]sx?$/, exclude: [], },