mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-02 02:19:43 +00:00
feat(admin): add NSFW flag to packs, remove intensity
Re-add is_explicit toggle on pack and dare pack forms labeled as "NSFW". Show NSFW badge on pack cards and user enabled packs. Remove all intensity/comfort level references from admin UI including form fields, analytics charts, review dialog columns, AI generation prompts/scoring, cherry-pick balancing, and settings toggles. The database columns remain unchanged.
This commit is contained in:
1 parent
14b0199625
commit
e380c0a31c
32 files changed
+948
-1075
No files matched your search
@@ -10,7 +10,7 @@ import type { TextAnalysis } from '../types';
|
||||
* Analyze question text and suggest improvements
|
||||
*/
|
||||
export async function analyzeQuestionText(
|
||||
questions: { id: string; text: string; partner_text?: string | null; intensity?: number }[],
|
||||
questions: { id: string; text: string; partner_text?: string | null }[],
|
||||
isExplicit: boolean = false
|
||||
): Promise<TextAnalysis[]> {
|
||||
const openai = getOpenAI();
|
||||
@@ -19,7 +19,6 @@ export async function analyzeQuestionText(
|
||||
id: q.id,
|
||||
text: q.text,
|
||||
partner_text: q.partner_text,
|
||||
intensity: q.intensity,
|
||||
}));
|
||||
|
||||
const toneInstruction = isExplicit
|
||||
@@ -83,17 +82,6 @@ Cards should be "Proposals" for specific actions, NOT interview questions.
|
||||
8. Skip questions that are ALREADY well-phrased
|
||||
</rules>
|
||||
|
||||
<intensity_guide>
|
||||
Check if current intensity matches the activity:
|
||||
1 (Gentle): Emotional bonding, non-sexual (cooking, cuddling, foot massage)
|
||||
2 (Warm): Romantic, affectionate touch (slow dance, sensual massage, kissing)
|
||||
3 (Playful): Light sexual exploration (oral, mutual masturbation, light roleplay)
|
||||
4 (Steamy): Explicit sex, moderate adventure (intercourse, light bondage, anal play)
|
||||
5 (Intense): Advanced/BDSM/extreme (impact play, power dynamics, taboo kinks)
|
||||
|
||||
If intensity seems wrong, include suggested_intensity and intensity_reason.
|
||||
</intensity_guide>
|
||||
|
||||
<questions_to_analyze>
|
||||
${JSON.stringify(simplifiedQuestions)}
|
||||
</questions_to_analyze>
|
||||
@@ -105,14 +93,12 @@ ${JSON.stringify(simplifiedQuestions)}
|
||||
"id": string, // Question ID
|
||||
"suggested_text": string, // Improved text (short, direct)
|
||||
"suggested_partner_text": string|null, // Improved partner_text or null if symmetric
|
||||
"reason": string, // Brief explanation of text improvement
|
||||
"suggested_intensity": number|null, // 1-5 if intensity seems wrong, null if correct
|
||||
"intensity_reason": string|null // Why intensity should change (if applicable)
|
||||
"reason": string // Brief explanation of text improvement
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Only include questions that need text improvement OR intensity adjustment.
|
||||
Only include questions that need text improvement.
|
||||
</output_format>`;
|
||||
|
||||
const systemMessage = isExplicit
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// =============================================================================
|
||||
// AI Configuration Constants
|
||||
// Council config, tone levels, intensity levels, and prompt guides
|
||||
// Council config, tone levels, and prompt guides
|
||||
// =============================================================================
|
||||
|
||||
import { getCachedAiConfig, preloadAiConfig, type AiConfig } from '@/hooks/useAiConfig';
|
||||
@@ -33,7 +33,6 @@ export function getCouncilConfig(): CouncilConfig {
|
||||
reviewerModel: remoteConfig.council_reviewer_model || 'google/gemini-pro-1.5',
|
||||
reviewerTemperature: remoteConfig.council_reviewer_temperature ?? 0.3,
|
||||
selectionMode: remoteConfig.council_selection_mode || 'whole_set',
|
||||
cherryPickEnsureIntensityDistribution: remoteConfig.cherry_pick_ensure_intensity_distribution ?? true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,7 +43,6 @@ export function getCouncilConfig(): CouncilConfig {
|
||||
reviewerModel: import.meta.env.VITE_COUNCIL_REVIEWER_MODEL || 'google/gemini-pro-1.5',
|
||||
reviewerTemperature: 0.3,
|
||||
selectionMode: 'whole_set',
|
||||
cherryPickEnsureIntensityDistribution: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -69,67 +67,6 @@ export const TONE_LEVELS = [
|
||||
{ level: 5, label: 'Intense', description: 'Advanced/BDSM/Extreme exploration' },
|
||||
] as const;
|
||||
|
||||
// =============================================================================
|
||||
// INTENSITY LEVELS
|
||||
// For UI display - consistent with AI grading
|
||||
// =============================================================================
|
||||
|
||||
export const INTENSITY_LEVELS = [
|
||||
{ level: 1, label: 'Gentle', description: 'Pure emotional connection & non-sexual bonding', color: 'bg-green-500' },
|
||||
{ level: 2, label: 'Warm', description: 'Romantic atmosphere & affectionate touch', color: 'bg-lime-500' },
|
||||
{ level: 3, label: 'Playful', description: 'Light sexual exploration & sensual discovery', color: 'bg-yellow-500' },
|
||||
{ level: 4, label: 'Steamy', description: 'Explicit sexual activities & moderate adventure', color: 'bg-orange-500' },
|
||||
{ level: 5, label: 'Intense', description: 'Advanced/BDSM/Extreme exploration', color: 'bg-red-500' },
|
||||
] as const;
|
||||
|
||||
// =============================================================================
|
||||
// INTENSITY GRADING GUIDE
|
||||
// Full version for detailed AI prompts
|
||||
// =============================================================================
|
||||
|
||||
export const INTENSITY_GUIDE = `
|
||||
INTENSITY GRADING GUIDE - Use this to assign intensity levels consistently:
|
||||
|
||||
Level 1 - GENTLE (Pure emotional connection & non-sexual bonding):
|
||||
- Focus: Emotional safety, friendship-based activities, quality time
|
||||
- Examples: Cook together, take a walk holding hands, give non-sexual foot massages, watch a movie cuddling
|
||||
- Emotional bonding without sexual undertones
|
||||
|
||||
Level 2 - WARM (Romantic atmosphere & affectionate touch):
|
||||
- Focus: Creating romantic moments, sensual but non-sexual touch, building anticipation
|
||||
- Examples: Slow dance in the living room, candlelit bath together (non-sexual), extended kissing sessions, sensual massage with oils
|
||||
- Romantic intimacy building up to arousal
|
||||
|
||||
Level 3 - PLAYFUL (Light sexual exploration & sensual discovery):
|
||||
- Focus: Sexual touch without penetration, playful experimentation, building arousal
|
||||
- Examples: Mutual masturbation, oral sex, light roleplay (doctor/patient), using basic toys together
|
||||
- Fun and exploratory sexual contact
|
||||
|
||||
Level 4 - STEAMY (Explicit sexual activities & moderate adventure):
|
||||
- Focus: Full sexual intercourse with adventurous elements, moderate BDSM/kink
|
||||
- Examples: Sex in different locations/positions, light bondage, anal play with toys, recording consensual intimate moments
|
||||
- Passionate and adventurous sexual connection
|
||||
|
||||
Level 5 - INTENSE (Advanced/BDSM/Extreme exploration):
|
||||
- Focus: Edge play, power dynamics, intense physical experiences
|
||||
- Examples: Heavy impact play, advanced bondage, consensual non-consent scenes, multi-partner activities
|
||||
- Very intense, taboo kinks, and extreme exploration
|
||||
`;
|
||||
|
||||
// =============================================================================
|
||||
// INTENSITY GUIDE (SHORT)
|
||||
// Condensed version for prompts to save tokens
|
||||
// =============================================================================
|
||||
|
||||
export const INTENSITY_GUIDE_SHORT = `
|
||||
INTENSITY LEVELS:
|
||||
1 (Gentle): Pure emotional connection, non-sexual bonding (cooking, cuddling, foot massages)
|
||||
2 (Warm): Romantic atmosphere, affectionate touch (slow dance, sensual massage, kissing)
|
||||
3 (Playful): Light sexual exploration, sensual discovery (oral, mutual masturbation, light roleplay)
|
||||
4 (Steamy): Explicit sexual activities, moderate adventure (sex positions, light bondage, anal play)
|
||||
5 (Intense): Advanced/BDSM/Extreme exploration (impact play, power dynamics, taboo kinks)
|
||||
`;
|
||||
|
||||
// =============================================================================
|
||||
// REVIEW GUIDELINES
|
||||
// Used by council reviewer for quality assessment
|
||||
@@ -155,32 +92,25 @@ REVIEW CRITERIA - Score each 1-10:
|
||||
- Partner text (if present) clearly describes receiver's experience
|
||||
- No confusing or ambiguous phrasing
|
||||
|
||||
4. INTENSITY ACCURACY:
|
||||
Level 1 (Gentle): Pure emotional connection, non-sexual bonding
|
||||
Level 2 (Warm): Romantic atmosphere, affectionate touch
|
||||
Level 3 (Playful): Light sexual exploration, sensual discovery (oral, toys)
|
||||
Level 4 (Steamy): Explicit sexual activities, moderate adventure (intercourse, light bondage)
|
||||
Level 5 (Intense): Advanced/BDSM/Extreme exploration (impact play, power dynamics)
|
||||
|
||||
5. ANATOMICAL CONSISTENCY:
|
||||
4. ANATOMICAL CONSISTENCY:
|
||||
- No mixed male/female anatomy in alternatives
|
||||
- BAD: "Finger or give your partner a handjob"
|
||||
- GOOD: Pick one activity
|
||||
|
||||
6. PARTNER TEXT QUALITY (if applicable):
|
||||
5. PARTNER TEXT QUALITY (if applicable):
|
||||
- Engaging and enticing, not clinical
|
||||
- BAD: "Receive oral from your partner"
|
||||
- GOOD: "Let your partner pleasure you with their mouth"
|
||||
- When initiator causes a response, frame as allowing: "Let your partner make you moan"
|
||||
|
||||
7. COUPLE TARGETING (flag if incorrect):
|
||||
6. COUPLE TARGETING (flag if incorrect):
|
||||
- Default: ALL couples (null) unless explicit anatomical requirement
|
||||
- Restrict to ['male+male', 'female+male'] ONLY if activity requires penis
|
||||
- Restrict to ['female+male', 'female+female'] ONLY if activity requires vagina
|
||||
- Sex toys are GENDER-NEUTRAL (vibrators, dildos, plugs work for anyone)
|
||||
- Flag if targeting seems too restrictive or missing needed restrictions
|
||||
|
||||
8. INITIATOR TARGETING (for asymmetric questions only):
|
||||
7. INITIATOR TARGETING (for asymmetric questions only):
|
||||
- Default: null (anyone can initiate) unless explicit anatomical requirement
|
||||
- Set initiator based on WHO DOES the action in "text" field
|
||||
- "Swallow your partner's cum" -> initiator: female (in M+F, receiver of cum)
|
||||
|
||||
@@ -14,7 +14,6 @@ import type {
|
||||
PooledQuestion,
|
||||
CherryPickEvaluation,
|
||||
CherryPickResult,
|
||||
CouncilConfig,
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
@@ -34,7 +33,6 @@ export async function evaluateQuestionPool(
|
||||
index: i,
|
||||
text: q.text,
|
||||
partner_text: q.partner_text || null,
|
||||
intensity: q.intensity,
|
||||
generatorIndex: q.sourceGeneratorIndex,
|
||||
}));
|
||||
|
||||
@@ -69,11 +67,10 @@ ${REVIEW_GUIDELINES}
|
||||
|
||||
<scoring_weights>
|
||||
Your scores will be weighted as follows for final selection:
|
||||
- Guideline Compliance: 25%
|
||||
- Creativity: 20%
|
||||
- Clarity: 20%
|
||||
- Guideline Compliance: 30%
|
||||
- Creativity: 25%
|
||||
- Clarity: 25%
|
||||
- Uniqueness: 20%
|
||||
- Intensity Accuracy: 15%
|
||||
|
||||
Calibrate your scores knowing these weights. A low compliance score has the most impact.
|
||||
</scoring_weights>
|
||||
@@ -86,8 +83,7 @@ Two questions are duplicates if they:
|
||||
|
||||
For duplicates, mark the WORSE version as duplicate and keep the version with:
|
||||
1. Better phrasing/creativity
|
||||
2. More appropriate intensity grading
|
||||
3. Better partner_text (if applicable)
|
||||
2. Better partner_text (if applicable)
|
||||
</duplicate_detection>
|
||||
|
||||
<verdict_rules>
|
||||
@@ -110,7 +106,6 @@ ${JSON.stringify(questionsForEval, null, 2)}
|
||||
"guidelineCompliance": 1-10,
|
||||
"creativity": 1-10,
|
||||
"clarity": 1-10,
|
||||
"intensityAccuracy": 1-10,
|
||||
"uniqueness": 1-10
|
||||
},
|
||||
"isDuplicate": boolean,
|
||||
@@ -151,16 +146,12 @@ Evaluate ALL ${pooledQuestions.length} questions.
|
||||
}
|
||||
|
||||
/**
|
||||
* Select top N questions from evaluations, optionally balancing intensity levels
|
||||
* Select top N questions from evaluations
|
||||
*/
|
||||
export function selectTopQuestions(
|
||||
pooledQuestions: PooledQuestion[],
|
||||
evaluations: CherryPickEvaluation[],
|
||||
requestedCount: number,
|
||||
options: {
|
||||
ensureIntensityDistribution: boolean;
|
||||
requestedIntensity?: number;
|
||||
}
|
||||
requestedCount: number
|
||||
): CherryPickResult {
|
||||
// Step 1: Filter out duplicates (keep only the original, not the duplicate)
|
||||
const duplicateIndices = new Set<number>();
|
||||
@@ -178,74 +169,25 @@ export function selectTopQuestions(
|
||||
question: pooledQuestions[e.questionIndex],
|
||||
// Weighted overall score
|
||||
weightedScore: (
|
||||
(e.scores.guidelineCompliance * 0.25) +
|
||||
(e.scores.creativity * 0.20) +
|
||||
(e.scores.clarity * 0.20) +
|
||||
(e.scores.intensityAccuracy * 0.15) +
|
||||
(e.scores.guidelineCompliance * 0.30) +
|
||||
(e.scores.creativity * 0.25) +
|
||||
(e.scores.clarity * 0.25) +
|
||||
(e.scores.uniqueness * 0.20)
|
||||
) * 10,
|
||||
}))
|
||||
.sort((a, b) => b.weightedScore - a.weightedScore);
|
||||
|
||||
let selected: typeof scoredQuestions = [];
|
||||
|
||||
// Step 3: Select questions
|
||||
if (options.requestedIntensity) {
|
||||
// If specific intensity requested, just take top N of that intensity
|
||||
const matchingIntensity = scoredQuestions.filter(
|
||||
sq => sq.question.intensity === options.requestedIntensity
|
||||
);
|
||||
selected = matchingIntensity.slice(0, requestedCount);
|
||||
} else if (options.ensureIntensityDistribution) {
|
||||
// Balance across intensity levels
|
||||
const perIntensity = Math.ceil(requestedCount / 5);
|
||||
const byIntensity: Record<number, typeof scoredQuestions> = { 1: [], 2: [], 3: [], 4: [], 5: [] };
|
||||
|
||||
for (const sq of scoredQuestions) {
|
||||
const intensity = sq.question.intensity;
|
||||
if (intensity >= 1 && intensity <= 5) {
|
||||
byIntensity[intensity].push(sq);
|
||||
}
|
||||
}
|
||||
|
||||
// Take top N from each intensity level
|
||||
for (let intensity = 1; intensity <= 5; intensity++) {
|
||||
const toTake = byIntensity[intensity].slice(0, perIntensity);
|
||||
selected.push(...toTake);
|
||||
}
|
||||
|
||||
// If we still need more, fill from remaining top-scored questions
|
||||
if (selected.length < requestedCount) {
|
||||
const selectedIndices = new Set(selected.map(s => s.evaluation.questionIndex));
|
||||
const remaining = scoredQuestions.filter(
|
||||
sq => !selectedIndices.has(sq.evaluation.questionIndex)
|
||||
);
|
||||
selected.push(...remaining.slice(0, requestedCount - selected.length));
|
||||
}
|
||||
|
||||
// Sort final selection by score
|
||||
selected.sort((a, b) => b.weightedScore - a.weightedScore);
|
||||
selected = selected.slice(0, requestedCount);
|
||||
} else {
|
||||
// Simple top N by score
|
||||
selected = scoredQuestions.slice(0, requestedCount);
|
||||
}
|
||||
// Step 3: Select top N by score
|
||||
const selected = scoredQuestions.slice(0, requestedCount);
|
||||
|
||||
// Step 4: Build result
|
||||
const selectedQuestions = selected.map(s => s.question);
|
||||
|
||||
// Calculate intensity distribution
|
||||
const intensityDistribution: Record<number, number> = {};
|
||||
for (const q of selectedQuestions) {
|
||||
intensityDistribution[q.intensity] = (intensityDistribution[q.intensity] || 0) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
selectedQuestions,
|
||||
evaluations,
|
||||
poolSize: pooledQuestions.length,
|
||||
duplicatesRemoved: duplicateIndices.size,
|
||||
intensityDistribution,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -255,8 +197,7 @@ export function selectTopQuestions(
|
||||
export async function performCherryPickSelection(
|
||||
candidates: GenerationCandidate[],
|
||||
packContext: { name: string; description?: string | null; isExplicit: boolean; tone: ToneLevel },
|
||||
requestedCount: number,
|
||||
config: CouncilConfig
|
||||
requestedCount: number
|
||||
): Promise<{
|
||||
questions: GeneratedQuestion[];
|
||||
reviews: QuestionReview[];
|
||||
@@ -280,10 +221,7 @@ export async function performCherryPickSelection(
|
||||
const evaluations = await evaluateQuestionPool(pooledQuestions, packContext, requestedCount);
|
||||
|
||||
// Step 3: Select top questions
|
||||
const cherryPickResult = selectTopQuestions(pooledQuestions, evaluations, requestedCount, {
|
||||
ensureIntensityDistribution: config.cherryPickEnsureIntensityDistribution,
|
||||
requestedIntensity: undefined,
|
||||
});
|
||||
const cherryPickResult = selectTopQuestions(pooledQuestions, evaluations, requestedCount);
|
||||
|
||||
// Step 4: Convert to standard review format for UI compatibility
|
||||
const reviews: QuestionReview[] = cherryPickResult.selectedQuestions.map((q, i) => {
|
||||
@@ -302,7 +240,6 @@ export async function performCherryPickSelection(
|
||||
guidelineCompliance: 7,
|
||||
creativity: 7,
|
||||
clarity: 7,
|
||||
intensityAccuracy: 7,
|
||||
anatomicalConsistency: 10,
|
||||
partnerTextQuality: 10,
|
||||
coupleTargeting: 10,
|
||||
@@ -324,7 +261,6 @@ export async function performCherryPickSelection(
|
||||
guidelineCompliance: evaluation.scores.guidelineCompliance,
|
||||
creativity: evaluation.scores.creativity,
|
||||
clarity: evaluation.scores.clarity,
|
||||
intensityAccuracy: evaluation.scores.intensityAccuracy,
|
||||
// Cherry-pick doesn't evaluate these, default to passing
|
||||
anatomicalConsistency: 10,
|
||||
partnerTextQuality: 10,
|
||||
@@ -346,8 +282,7 @@ export async function performCherryPickSelection(
|
||||
totalScore += review.scores.guidelineCompliance || 0;
|
||||
totalScore += review.scores.creativity || 0;
|
||||
totalScore += review.scores.clarity || 0;
|
||||
totalScore += review.scores.intensityAccuracy || 0;
|
||||
scoreCount += 4;
|
||||
scoreCount += 3;
|
||||
}
|
||||
}
|
||||
const overallQuality = scoreCount > 0 ? Math.round((totalScore / scoreCount) * 10) / 10 : 0;
|
||||
|
||||
@@ -24,7 +24,6 @@ import type {
|
||||
export async function generateQuestionsWithCouncil(
|
||||
packName: string,
|
||||
count: number = 10,
|
||||
intensity?: number,
|
||||
tone: ToneLevel = 3,
|
||||
packDescription?: string,
|
||||
existingQuestions?: string[],
|
||||
@@ -90,7 +89,6 @@ export async function generateQuestionsWithCouncil(
|
||||
const questions = await generateQuestions(
|
||||
packName,
|
||||
count,
|
||||
intensity,
|
||||
tone,
|
||||
packDescription,
|
||||
existingQuestions,
|
||||
@@ -138,7 +136,6 @@ export async function generateQuestionsWithCouncil(
|
||||
gen.model,
|
||||
packName,
|
||||
count,
|
||||
intensity,
|
||||
tone,
|
||||
packDescription,
|
||||
existingQuestions,
|
||||
@@ -191,8 +188,7 @@ export async function generateQuestionsWithCouncil(
|
||||
const cherryPickResult = await performCherryPickSelection(
|
||||
successfulCandidates,
|
||||
packContext,
|
||||
count,
|
||||
config
|
||||
count
|
||||
);
|
||||
const reviewTime = Date.now() - startReview;
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// =============================================================================
|
||||
// Question Generation Functions
|
||||
// Generate questions for packs with various tone/intensity settings
|
||||
// Generate questions for packs with various tone settings
|
||||
// =============================================================================
|
||||
|
||||
import { getOpenAI, getModel, getTemperature } from '../client';
|
||||
import { INTENSITY_GUIDE_SHORT, TONE_INSTRUCTIONS, SYSTEM_MESSAGES, CORE_LANGUAGE_RULES } from '../config';
|
||||
import { TONE_INSTRUCTIONS, SYSTEM_MESSAGES, CORE_LANGUAGE_RULES } from '../config';
|
||||
import type { GeneratedQuestion, ToneLevel } from '../types';
|
||||
|
||||
/**
|
||||
@@ -13,7 +13,6 @@ import type { GeneratedQuestion, ToneLevel } from '../types';
|
||||
function buildQuestionPrompt(
|
||||
packName: string,
|
||||
count: number,
|
||||
intensity: number | undefined,
|
||||
tone: ToneLevel,
|
||||
packDescription?: string,
|
||||
existingQuestions?: string[],
|
||||
@@ -38,12 +37,6 @@ function buildQuestionPrompt(
|
||||
? `\n\n<user_guidance>\n${sanitizedInspiration}\n</user_guidance>\n\nIncorporate the user guidance above into your generation where appropriate. Use it to inform the theme and style of questions.`
|
||||
: '';
|
||||
|
||||
const intensityInstruction = tone === 1
|
||||
? 'All questions should be intensity level 1 (non-sexual bonding).'
|
||||
: intensity
|
||||
? `All questions should be at intensity level ${intensity}.`
|
||||
: 'Vary the intensity levels appropriate for the tone.';
|
||||
|
||||
const toneInstruction = TONE_INSTRUCTIONS[tone];
|
||||
|
||||
// Examples based on tone level
|
||||
@@ -90,12 +83,7 @@ Generate ${count} unique questions for a couples' intimacy question pack called
|
||||
</task>
|
||||
${descriptionContext}${inspirationInstruction}${existingQuestionsContext}
|
||||
|
||||
<intensity_guide>
|
||||
${INTENSITY_GUIDE_SHORT}
|
||||
</intensity_guide>
|
||||
|
||||
<instructions>
|
||||
${intensityInstruction}
|
||||
${toneInstruction}${explicitWarning}${crudeLangInstruction}
|
||||
</instructions>
|
||||
|
||||
@@ -147,7 +135,6 @@ Return a JSON object with this exact structure:
|
||||
{
|
||||
"text": string, // REQUIRED: 5-12 words, doer's perspective using "your partner"
|
||||
"partner_text": string|null, // REQUIRED for asymmetric, null for symmetric
|
||||
"intensity": 1-5, // REQUIRED: must match the intensity level guidelines
|
||||
"requires_props": string[]|null, // Optional: items needed (e.g., ["blindfold", "massage oil"])
|
||||
"inverse_pair_id": string|null, // REQUIRED for asymmetric pairs, null for symmetric
|
||||
"location_type": "home"|"public"|"outdoors"|"travel"|"anywhere", // Optional
|
||||
@@ -164,7 +151,6 @@ Return a JSON object with this exact structure:
|
||||
export async function generateQuestions(
|
||||
packName: string,
|
||||
count: number = 10,
|
||||
intensity?: number,
|
||||
tone: ToneLevel = 3,
|
||||
packDescription?: string,
|
||||
existingQuestions?: string[],
|
||||
@@ -176,7 +162,6 @@ export async function generateQuestions(
|
||||
const prompt = buildQuestionPrompt(
|
||||
packName,
|
||||
count,
|
||||
intensity,
|
||||
tone,
|
||||
packDescription,
|
||||
existingQuestions,
|
||||
@@ -212,7 +197,6 @@ export async function generateQuestionsWithModel(
|
||||
model: string,
|
||||
packName: string,
|
||||
count: number,
|
||||
intensity: number | undefined,
|
||||
tone: ToneLevel,
|
||||
packDescription: string | undefined,
|
||||
existingQuestions: string[] | undefined,
|
||||
@@ -225,7 +209,6 @@ export async function generateQuestionsWithModel(
|
||||
const prompt = buildQuestionPrompt(
|
||||
packName,
|
||||
count,
|
||||
intensity,
|
||||
tone,
|
||||
packDescription,
|
||||
existingQuestions,
|
||||
|
||||
@@ -41,9 +41,6 @@ export {
|
||||
getCouncilConfig,
|
||||
initializeAiConfig,
|
||||
TONE_LEVELS,
|
||||
INTENSITY_LEVELS,
|
||||
INTENSITY_GUIDE,
|
||||
INTENSITY_GUIDE_SHORT,
|
||||
REVIEW_GUIDELINES,
|
||||
TONE_INSTRUCTIONS,
|
||||
SYSTEM_MESSAGES,
|
||||
|
||||
@@ -36,7 +36,6 @@ export async function reviewGeneratedQuestions(
|
||||
index: i,
|
||||
text: q.text,
|
||||
partner_text: q.partner_text || null,
|
||||
intensity: q.intensity,
|
||||
location_type: q.location_type,
|
||||
effort_level: q.effort_level,
|
||||
// Include targeting for review (may not always be present)
|
||||
@@ -64,7 +63,7 @@ ${REVIEW_GUIDELINES}
|
||||
<verdict_rules>
|
||||
- PASS: All scores >= 7, no major issues
|
||||
- FLAG: Any score 5-7, or minor issues worth noting (admin should review but can use)
|
||||
- REJECT: Any score < 5, or major violations (mixed anatomy, wrong intensity, severe guideline violation)
|
||||
- REJECT: Any score < 5, or major violations (mixed anatomy, severe guideline violation)
|
||||
</verdict_rules>
|
||||
|
||||
<questions_to_review>
|
||||
@@ -83,13 +82,11 @@ ${JSON.stringify(questionsForReview, null, 2)}
|
||||
"guidelineCompliance": 1-10, // Criterion 1
|
||||
"creativity": 1-10, // Criterion 2
|
||||
"clarity": 1-10, // Criterion 3
|
||||
"intensityAccuracy": 1-10, // Criterion 4
|
||||
"anatomicalConsistency": 1-10, // Criterion 5
|
||||
"partnerTextQuality": 1-10, // Criterion 6 (10 if no partner_text needed)
|
||||
"coupleTargeting": 1-10, // Criterion 7
|
||||
"initiatorTargeting": 1-10 // Criterion 8 (10 if symmetric)
|
||||
"anatomicalConsistency": 1-10, // Criterion 4
|
||||
"partnerTextQuality": 1-10, // Criterion 5 (10 if no partner_text needed)
|
||||
"coupleTargeting": 1-10, // Criterion 6
|
||||
"initiatorTargeting": 1-10 // Criterion 7 (10 if symmetric)
|
||||
},
|
||||
"intensitySuggestion": number|null, // Suggested intensity (1-5) if current seems wrong, null if correct
|
||||
"targetingSuggestions": { // Only include if targeting needs changes
|
||||
"suggestedCoupleTargets": string[]|null, // ["male+male", "female+male", "female+female"] or subset
|
||||
"suggestedInitiator": string[]|null, // ["male", "female"] or null for any
|
||||
@@ -99,7 +96,7 @@ ${JSON.stringify(questionsForReview, null, 2)}
|
||||
]
|
||||
}
|
||||
|
||||
Review ALL questions against ALL 8 criteria. Be thorough but fair - only flag/reject for genuine issues.
|
||||
Review ALL questions against ALL 7 criteria. Be thorough but fair - only flag/reject for genuine issues.
|
||||
</output_format>`;
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
@@ -132,18 +129,17 @@ Review ALL questions against ALL 8 criteria. Be thorough but fair - only flag/re
|
||||
const flagged = reviews.filter(r => r.verdict === 'flag').length;
|
||||
const rejected = reviews.filter(r => r.verdict === 'reject').length;
|
||||
|
||||
// Calculate overall quality as weighted average of all 8 scores
|
||||
// Weights: guidelineCompliance 20%, creativity 15%, clarity 15%, intensityAccuracy 15%,
|
||||
// anatomicalConsistency 15%, partnerTextQuality 10%, coupleTargeting 5%, initiatorTargeting 5%
|
||||
// Calculate overall quality as weighted average of all 7 scores
|
||||
// Weights: guidelineCompliance 25%, creativity 18%, clarity 18%,
|
||||
// anatomicalConsistency 17%, partnerTextQuality 12%, coupleTargeting 5%, initiatorTargeting 5%
|
||||
let totalWeightedScore = 0;
|
||||
let totalWeight = 0;
|
||||
const weights = {
|
||||
guidelineCompliance: 0.20,
|
||||
creativity: 0.15,
|
||||
clarity: 0.15,
|
||||
intensityAccuracy: 0.15,
|
||||
anatomicalConsistency: 0.15,
|
||||
partnerTextQuality: 0.10,
|
||||
guidelineCompliance: 0.25,
|
||||
creativity: 0.18,
|
||||
clarity: 0.18,
|
||||
anatomicalConsistency: 0.17,
|
||||
partnerTextQuality: 0.12,
|
||||
coupleTargeting: 0.05,
|
||||
initiatorTargeting: 0.05,
|
||||
};
|
||||
@@ -200,7 +196,6 @@ export async function selectBestGeneration(
|
||||
index: qi,
|
||||
text: q.text,
|
||||
partner_text: q.partner_text || null,
|
||||
intensity: q.intensity,
|
||||
allowed_couple_genders: (q as { allowed_couple_genders?: string[] | null }).allowed_couple_genders || null,
|
||||
target_user_genders: (q as { target_user_genders?: string[] | null }).target_user_genders || null,
|
||||
})),
|
||||
@@ -235,9 +230,8 @@ Ranked by importance (approximate weights):
|
||||
1. Overall quality and creativity of questions (30%)
|
||||
2. Variety and uniqueness within the set (20%)
|
||||
3. Adherence to pack theme and tone (20%)
|
||||
4. Proper intensity grading (15%)
|
||||
5. Correct use of "your partner" language (10%)
|
||||
6. Quality of partner_text for asymmetric questions (5%)
|
||||
4. Correct use of "your partner" language (15%)
|
||||
5. Quality of partner_text for asymmetric questions (10%)
|
||||
</selection_criteria>
|
||||
|
||||
<output_format>
|
||||
@@ -254,13 +248,11 @@ Ranked by importance (approximate weights):
|
||||
"guidelineCompliance": 1-10,
|
||||
"creativity": 1-10,
|
||||
"clarity": 1-10,
|
||||
"intensityAccuracy": 1-10,
|
||||
"anatomicalConsistency": 1-10,
|
||||
"partnerTextQuality": 1-10,
|
||||
"coupleTargeting": 1-10,
|
||||
"initiatorTargeting": 1-10
|
||||
},
|
||||
"intensitySuggestion": number|null,
|
||||
"targetingSuggestions": {
|
||||
"suggestedCoupleTargets": string[]|null,
|
||||
"suggestedInitiator": string[]|null,
|
||||
@@ -270,7 +262,7 @@ Ranked by importance (approximate weights):
|
||||
]
|
||||
}
|
||||
|
||||
Select the best set and review ALL questions in that set against ALL 8 criteria.
|
||||
Select the best set and review ALL questions in that set against ALL 7 criteria.
|
||||
</output_format>`;
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
@@ -300,7 +292,7 @@ Select the best set and review ALL questions in that set against ALL 8 criteria.
|
||||
const reviews: QuestionReview[] = parsed.reviews || [];
|
||||
const reasoning = parsed.reasoning || 'No reasoning provided';
|
||||
|
||||
// Calculate summary with weighted average of all 8 scores
|
||||
// Calculate summary with weighted average of all 7 scores
|
||||
const passed = reviews.filter(r => r.verdict === 'pass').length;
|
||||
const flagged = reviews.filter(r => r.verdict === 'flag').length;
|
||||
const rejected = reviews.filter(r => r.verdict === 'reject').length;
|
||||
@@ -308,12 +300,11 @@ Select the best set and review ALL questions in that set against ALL 8 criteria.
|
||||
let totalWeightedScore = 0;
|
||||
let totalWeight = 0;
|
||||
const weights = {
|
||||
guidelineCompliance: 0.20,
|
||||
creativity: 0.15,
|
||||
clarity: 0.15,
|
||||
intensityAccuracy: 0.15,
|
||||
anatomicalConsistency: 0.15,
|
||||
partnerTextQuality: 0.10,
|
||||
guidelineCompliance: 0.25,
|
||||
creativity: 0.18,
|
||||
clarity: 0.18,
|
||||
anatomicalConsistency: 0.17,
|
||||
partnerTextQuality: 0.12,
|
||||
coupleTargeting: 0.05,
|
||||
initiatorTargeting: 0.05,
|
||||
};
|
||||
|
||||
@@ -23,7 +23,6 @@ export interface CouncilConfig {
|
||||
reviewerModel: string;
|
||||
reviewerTemperature?: number;
|
||||
selectionMode: CouncilSelectionMode;
|
||||
cherryPickEnsureIntensityDistribution: boolean;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -71,7 +70,6 @@ export interface QuestionReviewScores {
|
||||
guidelineCompliance: number;
|
||||
creativity: number;
|
||||
clarity: number;
|
||||
intensityAccuracy: number;
|
||||
anatomicalConsistency: number;
|
||||
partnerTextQuality: number;
|
||||
coupleTargeting: number;
|
||||
@@ -85,7 +83,6 @@ export interface QuestionReview {
|
||||
suggestions?: string;
|
||||
scores: QuestionReviewScores;
|
||||
// Additional detailed feedback
|
||||
intensitySuggestion?: number | null; // Suggested intensity if current seems wrong
|
||||
targetingSuggestions?: {
|
||||
suggestedCoupleTargets?: string[] | null;
|
||||
suggestedInitiator?: string[] | null;
|
||||
@@ -173,7 +170,6 @@ export interface CherryPickEvaluation {
|
||||
guidelineCompliance: number;
|
||||
creativity: number;
|
||||
clarity: number;
|
||||
intensityAccuracy: number;
|
||||
uniqueness: number;
|
||||
};
|
||||
isDuplicate: boolean;
|
||||
@@ -187,7 +183,6 @@ export interface CherryPickResult {
|
||||
evaluations: CherryPickEvaluation[];
|
||||
poolSize: number;
|
||||
duplicatesRemoved: number;
|
||||
intensityDistribution: Record<number, number>;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -206,9 +201,6 @@ export interface TextAnalysis {
|
||||
suggested_text: string;
|
||||
suggested_partner_text: string | null;
|
||||
reason: string;
|
||||
// Intensity suggestion (only if AI thinks current is wrong)
|
||||
suggested_intensity?: number | null;
|
||||
intensity_reason?: string | null;
|
||||
}
|
||||
|
||||
export interface DeletionAnalysis {
|
||||
|
||||
Reference in new issue
Block a user