Skip to content

Commit 0cd1d87

Browse files
authored
Merge pull request #41 from GizzZmo/copilot/implement-best-practices-ai-tools
Add prompt templating, version diffing, cost estimation, and pluggable AI providers
2 parents b1e2045 + 3a83c91 commit 0cd1d87

10 files changed

Lines changed: 517 additions & 28 deletions

File tree

CONTRIBUTING.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ Thank you for your interest in contributing! Here are some guidelines to help yo
1414
- Use consistent code style (see existing code for reference).
1515
- Prefer TypeScript for new features or enhancements.
1616
- Keep functions and components small and focused.
17+
- Register new AI backends through the pluggable provider registry (`src/utils/providers.ts`) instead of hard-coding API calls.
18+
- Keep the human-in-the-loop safety check intact when adding any feature that can execute code or commands.
19+
20+
## Build, Lint, and Test
21+
22+
- Install dependencies with `npm run install:all`.
23+
- Build shared types first: `npx tsc --build src/types`.
24+
- Run the full build: `npm run build:all`.
25+
- Run linting: `npm run lint:all`.
1726

1827
## Reporting Issues
1928

@@ -25,4 +34,9 @@ Thank you for your interest in contributing! Here are some guidelines to help yo
2534

2635
By participating, you agree to abide by our [Code of Conduct](./CODE_OF_CONDUCT.md).
2736

37+
## Blueprints and Examples
38+
39+
- Starter prompts live in `assets/blueprints/best-practice-prompts.json`. Feel free to add concise, well-tagged blueprints (no secrets, no PII).
40+
- Keep templates reusable by leveraging the variable syntax (`{{variable}}`) supported in the prompt editor preview.
41+
2842
Thank you for helping improve Master-Prompt-Editor!
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[
2+
{
3+
"id": "safety-review",
4+
"title": "Human-in-the-loop Safety Review",
5+
"tags": ["safety", "review", "governance"],
6+
"prompt": "You are a safety controller that blocks any command that writes to disk, executes binaries, or deletes data. Ask for explicit confirmation and list the perceived risks before allowing execution."
7+
},
8+
{
9+
"id": "ab-test-template",
10+
"title": "A/B Prompt Evaluation",
11+
"tags": ["evaluation", "experimentation"],
12+
"prompt": "Run the same user request through {{model_a}} and {{model_b}}, capture latency, token usage, and summarize which model produced the more faithful answer. Include a short diff of the responses."
13+
},
14+
{
15+
"id": "structured-extraction",
16+
"title": "Structured JSON Extraction",
17+
"tags": ["data", "json"],
18+
"prompt": "Extract entities from the text and return strict JSON with fields: name, type, sentiment, confidence. Do not add commentary. Validate that all fields are present before returning."
19+
}
20+
]

src/pages/ModelComparisonPage.tsx

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import React, { useState } from 'react';
1+
import React, { useMemo, useState } from 'react';
22
import Button from '../components/ui/Button';
33
import { useToast } from '../context/toastContextHelpers';
4+
import { estimateCostForModel, getPricingTable } from '../utils/providers';
45

56
interface AIModel {
67
id: string;
@@ -96,11 +97,21 @@ const MODELS: AIModel[] = [
9697
}
9798
];
9899

100+
interface ComparisonResult {
101+
modelId: string;
102+
response: string;
103+
tokens: number;
104+
cost: number;
105+
latencyMs: number;
106+
}
107+
99108
const ModelComparisonPage: React.FC = () => {
100109
const [selectedModels, setSelectedModels] = useState<string[]>([]);
101110
const [testPrompt, setTestPrompt] = useState('');
102111
const [showTestResults, setShowTestResults] = useState(false);
112+
const [comparisonResults, setComparisonResults] = useState<ComparisonResult[]>([]);
103113
const { showToast } = useToast();
114+
const pricingTable = useMemo(() => getPricingTable(), []);
104115

105116
const toggleModelSelection = (modelId: string) => {
106117
setSelectedModels(prev => {
@@ -125,8 +136,21 @@ const ModelComparisonPage: React.FC = () => {
125136
showToast('Please enter a test prompt', 'warning');
126137
return;
127138
}
139+
const builtResults: ComparisonResult[] = selectedModels.map((modelId) => {
140+
const tokenEstimate = Math.max(64, Math.ceil(testPrompt.length / 3));
141+
const outputTokens = Math.ceil(tokenEstimate * 0.6);
142+
const cost = estimateCostForModel(modelId, { input: tokenEstimate, output: outputTokens });
143+
return {
144+
modelId,
145+
tokens: tokenEstimate + outputTokens,
146+
cost,
147+
latencyMs: 300 + Math.floor(Math.random() * 400),
148+
response: `[${modelId}] ${testPrompt.slice(0, 120)}...`,
149+
};
150+
});
151+
setComparisonResults(builtResults);
128152
setShowTestResults(true);
129-
showToast('Comparison started! (Mock results)', 'info');
153+
showToast('Comparison ready (simulated responses)', 'info');
130154
};
131155

132156
const selectedModelData = MODELS.filter(m => selectedModels.includes(m.id));
@@ -265,12 +289,37 @@ const ModelComparisonPage: React.FC = () => {
265289
{showTestResults && (
266290
<div style={{ marginTop: '20px', padding: '15px', backgroundColor: '#fff3cd', borderRadius: '4px' }}>
267291
<p style={{ margin: 0, color: '#856404' }}>
268-
ℹ️ Test comparison feature coming soon! This will send your prompt to selected models and compare their responses.
292+
ℹ️ Split-view A/B test: mock responses shown below. Parallel execution support is controlled by the provider registry.
269293
</p>
270294
</div>
271295
)}
272296
</div>
273297
)}
298+
299+
{comparisonResults.length > 0 && (
300+
<div style={{ marginTop: '25px' }}>
301+
<h3>Side-by-side Results</h3>
302+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: '12px', marginTop: '10px' }}>
303+
{comparisonResults.map((result) => {
304+
const modelMeta = pricingTable.find((p) => p.model === result.modelId);
305+
return (
306+
<div key={result.modelId} style={{ border: '1px solid #e9ecef', borderRadius: '8px', padding: '12px', background: 'white' }}>
307+
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
308+
<strong>{result.modelId}</strong>
309+
<span style={{ color: '#6c757d' }}>{modelMeta?.provider ?? 'N/A'}</span>
310+
</div>
311+
<p style={{ fontSize: '13px', color: '#555', whiteSpace: 'pre-wrap' }}>{result.response}</p>
312+
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginTop: '10px' }}>
313+
<span style={{ padding: '6px 8px', background: '#eef6ff', borderRadius: '6px', fontSize: '12px' }}>Tokens: {result.tokens}</span>
314+
<span style={{ padding: '6px 8px', background: '#e8f5e9', borderRadius: '6px', fontSize: '12px' }}>Est. cost: ${result.cost.toFixed(4)}</span>
315+
<span style={{ padding: '6px 8px', background: '#fff3cd', borderRadius: '6px', fontSize: '12px' }}>Latency: {result.latencyMs}ms</span>
316+
</div>
317+
</div>
318+
);
319+
})}
320+
</div>
321+
</div>
322+
)}
274323
</div>
275324
);
276325
};

src/pages/PromptEditor/PromptEditorPage.tsx

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect, useCallback } from 'react';
1+
import { useState, useEffect, useCallback, useMemo } from 'react';
22
import { useParams } from 'react-router-dom';
33
import { usePromptManagement } from '../../hooks/usePromptManagement';
44
import { PromptInputArea } from './components/PromptInputArea';
@@ -8,6 +8,7 @@ import { useToast } from '../../context/toastContextHelpers';
88
import SearchBar from '../../components/ui/SearchBar';
99
import LoadingSpinner from '../../components/ui/LoadingSpinner';
1010
import { exportSinglePrompt } from '../../utils/exportImport';
11+
import { PromptVersion } from '../../types/prompt';
1112

1213
/**
1314
* PromptEditorPage provides a comprehensive prompt editing experience with integrated testing.
@@ -26,6 +27,9 @@ export function PromptEditorPage() {
2627
const [isSaving, setIsSaving] = useState(false);
2728
// Current content being edited (may differ from saved activePrompt.content)
2829
const [currentContent, setCurrentContent] = useState<string>('');
30+
const [workingVersions, setWorkingVersions] = useState<PromptVersion[]>([]);
31+
const [currentVersion, setCurrentVersion] = useState<string>('');
32+
const [diffTarget, setDiffTarget] = useState<PromptVersion | null>(null);
2933
// Search state
3034
const [searchQuery, setSearchQuery] = useState<string>('');
3135

@@ -36,6 +40,9 @@ export function PromptEditorPage() {
3640
useEffect(() => {
3741
if (activePrompt) {
3842
setCurrentContent(activePrompt.content);
43+
setWorkingVersions(activePrompt.versions);
44+
setCurrentVersion(activePrompt.version);
45+
setDiffTarget(null);
3946
}
4047
}, [activePrompt]);
4148

@@ -49,13 +56,33 @@ export function PromptEditorPage() {
4956
// Simulate API call delay
5057
await new Promise(resolve => setTimeout(resolve, 1000));
5158
console.log('Saving content:', newContent);
59+
const bumpPatch = (version: string) => {
60+
const parts = version.split('.').map((part) => Number.parseInt(part, 10));
61+
if (parts.length === 3 && parts.every((n) => Number.isInteger(n))) {
62+
const [major, minor, patch] = parts;
63+
return `${major}.${minor}.${patch + 1}`;
64+
}
65+
return `${version}.1`;
66+
};
67+
const nextVersion = bumpPatch(currentVersion || activePrompt.version || '1.0.0');
68+
const newVersion: PromptVersion = {
69+
id: `v-${Date.now()}`,
70+
promptId: activePrompt.id,
71+
version: nextVersion,
72+
content: newContent,
73+
createdAt: new Date().toISOString(),
74+
metadata: { rationale: 'Manual save from editor' },
75+
};
76+
setWorkingVersions((prev) => [newVersion, ...prev]);
77+
setCurrentVersion(nextVersion);
78+
setDiffTarget(newVersion);
5279
showToast('Prompt saved successfully!', 'success');
5380
} catch (error) {
5481
showToast('Failed to save prompt', 'error');
5582
} finally {
5683
setIsSaving(false);
5784
}
58-
}, [activePrompt, showToast]);
85+
}, [activePrompt, showToast, currentVersion]);
5986

6087
/**
6188
* Updates the current content state when user edits the prompt.
@@ -65,6 +92,23 @@ export function PromptEditorPage() {
6592
setCurrentContent(content);
6693
};
6794

95+
/**
96+
* Roll back to a previous version in-memory with human confirmation.
97+
*/
98+
const handleRollback = (version: string) => {
99+
const targetVersion = workingVersions.find((v) => v.version === version);
100+
if (!targetVersion) {
101+
showToast('Version not found', 'error');
102+
return;
103+
}
104+
if (window.confirm(`Rollback to version ${version}? Unsaved changes will be replaced.`)) {
105+
setCurrentContent(targetVersion.content);
106+
setCurrentVersion(version);
107+
setDiffTarget(targetVersion);
108+
showToast(`Rolled back to version ${version}`, 'info');
109+
}
110+
};
111+
68112
/**
69113
* Handle export functionality
70114
*/
@@ -89,6 +133,19 @@ export function PromptEditorPage() {
89133
}
90134
};
91135

136+
const diffRows = useMemo(() => {
137+
if (!diffTarget) return [];
138+
const currentLines = currentContent.split('\n');
139+
const targetLines = diffTarget.content.split('\n');
140+
const length = Math.max(currentLines.length, targetLines.length);
141+
return Array.from({ length }).map((_, idx) => ({
142+
line: idx + 1,
143+
current: currentLines[idx] ?? '',
144+
target: targetLines[idx] ?? '',
145+
changed: (currentLines[idx] ?? '') !== (targetLines[idx] ?? ''),
146+
}));
147+
}, [currentContent, diffTarget]);
148+
92149
/**
93150
* Keyboard shortcuts handler
94151
*/
@@ -206,14 +263,47 @@ export function PromptEditorPage() {
206263
{/* Version Management Panel */}
207264
<div>
208265
<PromptVersioningPanel
209-
currentVersion={activePrompt.version}
210-
versionHistory={activePrompt.versions.map(v => ({
266+
currentVersion={currentVersion || activePrompt.version}
267+
versionHistory={workingVersions.map(v => ({
211268
version: v.version,
212269
date: v.createdAt,
213-
rationale: v.metadata?.rationale as string || 'No rationale provided'
270+
rationale: (v.metadata?.rationale as string) || 'No rationale provided'
214271
}))}
215-
onRollback={(version) => console.log('Rollback to version:', version)}
272+
onRollback={handleRollback}
273+
onViewDiff={(version) => {
274+
const target = workingVersions.find((v) => v.version === version) ?? null;
275+
setDiffTarget(target);
276+
}}
216277
/>
278+
{diffTarget && (
279+
<div style={{ marginTop: '15px', padding: '12px', border: '1px solid #e0e0e0', borderRadius: '8px', background: '#fafafa' }}>
280+
<h4 style={{ marginTop: 0 }}>Diff vs. version {diffTarget.version}</h4>
281+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px', fontFamily: 'monospace', fontSize: '0.85em' }}>
282+
<div>
283+
<strong>Current</strong>
284+
<div style={{ border: '1px solid #ddd', borderRadius: '6px', padding: '8px', maxHeight: '220px', overflowY: 'auto' }}>
285+
{diffRows.map((row) => (
286+
<div key={`curr-${row.line}`} style={{ background: row.changed ? '#fff8e1' : 'transparent' }}>
287+
<span style={{ color: '#999' }}>{row.line.toString().padStart(3, ' ')} </span>
288+
<span>{row.current}</span>
289+
</div>
290+
))}
291+
</div>
292+
</div>
293+
<div>
294+
<strong>Selected ({diffTarget.version})</strong>
295+
<div style={{ border: '1px solid #ddd', borderRadius: '6px', padding: '8px', maxHeight: '220px', overflowY: 'auto' }}>
296+
{diffRows.map((row) => (
297+
<div key={`old-${row.line}`} style={{ background: row.changed ? '#ffeceb' : 'transparent' }}>
298+
<span style={{ color: '#999' }}>{row.line.toString().padStart(3, ' ')} </span>
299+
<span>{row.target}</span>
300+
</div>
301+
))}
302+
</div>
303+
</div>
304+
</div>
305+
</div>
306+
)}
217307
</div>
218308
</div>
219309

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,50 @@
1-
// FIX: Removed unused 'Button' import
2-
1+
import { useEffect, useMemo, useState } from 'react';
32

43
interface PromptInputAreaProps {
5-
// FIX: Replaced 'any' with a specific function type
64
promptContent: string;
75
onPromptContentChange: (content: string) => void;
86
}
97

8+
const extractVariables = (content: string): string[] => {
9+
const pattern = /{{\s*([\w.-]+)\s*}}|{\s*([\w.-]+)\s*}/g;
10+
const found = new Set<string>();
11+
let match: RegExpExecArray | null;
12+
// eslint-disable-next-line no-cond-assign
13+
while ((match = pattern.exec(content))) {
14+
const variable = match[1] || match[2];
15+
if (variable) {
16+
found.add(variable);
17+
}
18+
}
19+
return Array.from(found);
20+
};
21+
22+
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
23+
1024
export function PromptInputArea({ promptContent, onPromptContentChange }: PromptInputAreaProps) {
25+
const variables = useMemo(() => extractVariables(promptContent), [promptContent]);
26+
const [variableValues, setVariableValues] = useState<Record<string, string>>({});
27+
28+
useEffect(() => {
29+
setVariableValues((prev) => {
30+
const next: Record<string, string> = {};
31+
variables.forEach((key) => {
32+
next[key] = prev[key] ?? '';
33+
});
34+
return next;
35+
});
36+
}, [variables]);
37+
38+
const renderedPreview = useMemo(() => {
39+
let output = promptContent;
40+
variables.forEach((variable) => {
41+
const replacement = variableValues[variable] ?? '';
42+
const placeholderPattern = new RegExp(`{{\\s*${escapeRegExp(variable)}\\s*}}|{\\s*${escapeRegExp(variable)}\\s*}`, 'g');
43+
output = output.replace(placeholderPattern, replacement);
44+
});
45+
return output;
46+
}, [promptContent, variableValues, variables]);
47+
1148
return (
1249
<div>
1350
<textarea
@@ -17,7 +54,37 @@ export function PromptInputArea({ promptContent, onPromptContentChange }: Prompt
1754
style={{ width: '100%', minHeight: '150px' }}
1855
placeholder="Enter your prompt content here..."
1956
/>
20-
{/* Save button moved to parent PromptEditorPage for logic centralization */}
57+
58+
<div style={{ marginTop: '10px', padding: '10px', border: '1px solid #e6e6e6', borderRadius: '6px', background: '#fafafa' }}>
59+
<strong>Template Variables</strong>
60+
{variables.length === 0 ? (
61+
<p style={{ margin: '8px 0 0 0', color: '#666' }}>No template variables detected. Use {'{{ variable }}'} or {'{variable}'} syntax to enable templating.</p>
62+
) : (
63+
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginTop: '8px' }}>
64+
{variables.map((variable) => (
65+
<label key={variable} style={{ display: 'flex', flexDirection: 'column', gap: '4px', fontSize: '0.9em' }}>
66+
<span style={{ color: '#333' }}>{variable}</span>
67+
<input
68+
type="text"
69+
value={variableValues[variable] ?? ''}
70+
onChange={(e) => setVariableValues((prev) => ({ ...prev, [variable]: e.target.value }))}
71+
style={{ padding: '6px 8px', border: '1px solid #ccc', borderRadius: '4px' }}
72+
placeholder={`Value for ${variable}`}
73+
/>
74+
</label>
75+
))}
76+
</div>
77+
)}
78+
</div>
79+
80+
{variables.length > 0 && (
81+
<div style={{ marginTop: '10px' }}>
82+
<small style={{ color: '#666' }}>Preview with variable substitution:</small>
83+
<pre style={{ background: '#f5f5f5', padding: '10px', borderRadius: '6px', whiteSpace: 'pre-wrap' }}>
84+
{renderedPreview}
85+
</pre>
86+
</div>
87+
)}
2188
</div>
2289
);
2390
}

0 commit comments

Comments
 (0)