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
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user