diff --git a/CLAUDE.md b/CLAUDE.md index 0e9ff16..d2dd7ce 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -134,6 +134,7 @@ Edge functions in `apps/supabase/functions/` handle complex operations: - `delete-relationship` - Deletes couple data (matches, messages, media) while keeping accounts - `delete-account` - Permanently deletes user account and all associated data (Apple App Store requirement) - `send-notification` - Push notifications for matches +- `send-nudge-notification` - Partner nudge notifications (rate limited to once per 12 hours) - `revenuecat-webhook` - Subscription webhook handler - `sync-subscription` - Syncs RevenueCat subscription status - `redeem-code` - Redeems promotional codes for premium access (website only) @@ -165,6 +166,7 @@ This approach: | `revenuecat-webhook` | false | Webhook signature validation | | `send-notification` | false | Internal trigger (no auth) | | `send-message-notification` | false | Internal trigger (no auth) | +| `send-nudge-notification` | false | Manual via getUser() | ### Database Best Practices - **Always use `.maybeSingle()` instead of `.single()`** when the row might not exist. `.single()` throws an error if 0 or >1 rows are returned. diff --git a/apps/admin/src/components/ai/AiGeneratorDialog.tsx b/apps/admin/src/components/ai/AiGeneratorDialog.tsx index d36df69..bc5b069 100644 --- a/apps/admin/src/components/ai/AiGeneratorDialog.tsx +++ b/apps/admin/src/components/ai/AiGeneratorDialog.tsx @@ -114,7 +114,7 @@ export function AiGeneratorDialog({ context={context} config={generation.config} onCountChange={generation.setCount} - onToneChange={generation.setTone} + onExplicitChange={generation.setExplicit} onCrudeLangChange={generation.setCrudeLang} onInspirationChange={generation.setInspiration} /> diff --git a/apps/admin/src/components/ai/GenerationForm.tsx b/apps/admin/src/components/ai/GenerationForm.tsx index 20c0528..3243666 100644 --- a/apps/admin/src/components/ai/GenerationForm.tsx +++ b/apps/admin/src/components/ai/GenerationForm.tsx @@ -1,9 +1,7 @@ -import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Switch } from '@/components/ui/switch'; -import { TONE_LEVELS, type ToneLevel } from '@/lib/openai'; import type { GeneratorType, GeneratorContext, GenerationConfig } from './hooks/useAiGeneration'; // ============================================================================= @@ -15,7 +13,7 @@ interface GenerationFormProps { context?: GeneratorContext; config: GenerationConfig; onCountChange: (count: number) => void; - onToneChange: (tone: ToneLevel) => void; + onExplicitChange: (isExplicit: boolean) => void; onCrudeLangChange: (crudeLang: boolean) => void; onInspirationChange: (inspiration: string) => void; } @@ -29,12 +27,10 @@ export function GenerationForm({ context, config, onCountChange, - onToneChange, + onExplicitChange, onCrudeLangChange, onInspirationChange, }: GenerationFormProps) { - const isExplicit = config.tone >= 4; - return (
{/* Question Count (questions only) */} @@ -53,42 +49,15 @@ export function GenerationForm({
)} - {/* Intimacy Level Selector (questions only) */} - {type === 'questions' && ( -
- -
- {TONE_LEVELS.map((t) => ( - - ))} -
-

- {TONE_LEVELS.find(t => t.level === config.tone)?.description} -

-
- )} - - {/* Wild/Explicit Toggle (for other generators) */} - {(type === 'category-ideas' || type === 'category-pack-ideas' || type === 'pack') && ( -
- onToneChange(checked ? 5 : 3)} - /> - -
- )} + {/* Explicit Toggle */} +
+ + +
{/* Crude Language Override Toggle */}
diff --git a/apps/admin/src/components/ai/QuestionList.tsx b/apps/admin/src/components/ai/QuestionList.tsx index 973959d..25a30bb 100644 --- a/apps/admin/src/components/ai/QuestionList.tsx +++ b/apps/admin/src/components/ai/QuestionList.tsx @@ -10,7 +10,6 @@ import { Bot, } from 'lucide-react'; import { - INTENSITY_LEVELS, type GeneratedQuestion, type QuestionReview, type CouncilGenerationResult, @@ -199,16 +198,6 @@ function QuestionItem({ )}
- {/* Intensity Badge */} - - {question.intensity} - {INTENSITY_LEVELS[question.intensity - 1]?.label || 'Unknown'} - - {/* Source Generator Badge (cherry-pick mode) */} {selectionMode === 'cherry_pick' && question.sourceGeneratorModel && ( @@ -228,7 +217,7 @@ function QuestionItem({ : 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400' }`} title={review.scores - ? `Scores: ${review.scores.guidelineCompliance}/${review.scores.creativity}/${review.scores.clarity}/${review.scores.intensityAccuracy}` + ? `Scores: ${review.scores.guidelineCompliance}/${review.scores.creativity}/${review.scores.clarity}` : '' } > diff --git a/apps/admin/src/components/ai/ReviewSummary.tsx b/apps/admin/src/components/ai/ReviewSummary.tsx index a16280f..cef67fb 100644 --- a/apps/admin/src/components/ai/ReviewSummary.tsx +++ b/apps/admin/src/components/ai/ReviewSummary.tsx @@ -91,15 +91,6 @@ export function ReviewSummary({

)} - {/* Intensity distribution for cherry-pick */} - {selectionMode === 'cherry_pick' && cherryPickResult && Object.keys(cherryPickResult.intensityDistribution).length > 0 && ( -

- Intensity distribution: {Object.entries(cherryPickResult.intensityDistribution) - .sort(([a], [b]) => Number(a) - Number(b)) - .map(([level, count]) => `L${level}:${count}`) - .join(', ')} -

- )}
)} diff --git a/apps/admin/src/components/ai/hooks/useAiGeneration.ts b/apps/admin/src/components/ai/hooks/useAiGeneration.ts index ba97914..b978f7c 100644 --- a/apps/admin/src/components/ai/hooks/useAiGeneration.ts +++ b/apps/admin/src/components/ai/hooks/useAiGeneration.ts @@ -10,7 +10,6 @@ import { type GeneratedQuestion, type GeneratedCategoryIdea, type GeneratedPackIdea, - type ToneLevel, type QuestionReview, type CouncilGenerationResult, type GenerationCandidate, @@ -38,7 +37,7 @@ export interface GeneratorContext { export interface GenerationConfig { count: number; - tone: ToneLevel; + isExplicit: boolean; crudeLang: boolean; inspiration: string; } @@ -77,7 +76,7 @@ export function useAiGeneration( // Config state const [config, setConfig] = useState({ count: 10, - tone: context?.isExplicit ? 5 : 2, + isExplicit: Boolean(context?.isExplicit), crudeLang: false, inspiration: '', }); @@ -118,14 +117,12 @@ export function useAiGeneration( // Council config const councilConfig = getCouncilConfig(); - // Update tone when context changes + // Update explicit flag when context changes useEffect(() => { - if (type === 'questions') { - setConfig(prev => ({ - ...prev, - tone: context?.isExplicit ? 5 : 2, - })); - } + setConfig(prev => ({ + ...prev, + isExplicit: Boolean(context?.isExplicit), + })); }, [context?.isExplicit, type]); // Config setters @@ -133,8 +130,8 @@ export function useAiGeneration( setConfig(prev => ({ ...prev, count })); }, []); - const setTone = useCallback((tone: ToneLevel) => { - setConfig(prev => ({ ...prev, tone })); + const setExplicit = useCallback((isExplicit: boolean) => { + setConfig(prev => ({ ...prev, isExplicit })); }, []); const setCrudeLang = useCallback((crudeLang: boolean) => { @@ -219,7 +216,7 @@ export function useAiGeneration( setFilter('all'); setSourceModel(null); - const isExplicit = config.tone >= 4; + const isExplicit = config.isExplicit; try { if (type === 'pack') { @@ -253,7 +250,7 @@ export function useAiGeneration( context.packName, config.count, undefined, - config.tone, + isExplicit ? 5 : 2, context.packDescription || undefined, context.existingQuestions, config.crudeLang, @@ -369,7 +366,7 @@ export function useAiGeneration( // Config config, setCount, - setTone, + setExplicit, setCrudeLang, setInspiration, diff --git a/apps/admin/src/components/content/PackFormDialog.tsx b/apps/admin/src/components/content/PackFormDialog.tsx index 3ffa055..3d82d07 100644 --- a/apps/admin/src/components/content/PackFormDialog.tsx +++ b/apps/admin/src/components/content/PackFormDialog.tsx @@ -203,11 +203,12 @@ export function PackFormDialog({ { - setField('is_public', checked); - // Clear scheduled release if making public immediately - if (checked) { - setField('scheduled_release_at', null); - } + const next = { + ...formData, + is_public: checked, + ...(checked ? { scheduled_release_at: null } : {}), + }; + onFormChange(next); }} /> diff --git a/apps/admin/src/pages/content/DaresPage.tsx b/apps/admin/src/pages/content/DaresPage.tsx index 76e25f8..a4b4416 100644 --- a/apps/admin/src/pages/content/DaresPage.tsx +++ b/apps/admin/src/pages/content/DaresPage.tsx @@ -37,7 +37,6 @@ import { Plus, Pencil, Trash2, Zap, Loader2, Clock } from 'lucide-react'; import { Checkbox } from '@/components/ui/checkbox'; import { toast } from 'sonner'; import { IconPreview } from '@/components/ui/icon-picker'; -import { INTENSITY_LEVELS } from '@/lib/openai'; // ============================================================================= // Types @@ -71,8 +70,7 @@ const DURATION_OPTIONS = [ { value: '168', label: '1 week' }, ]; -// Helper to get intensity info by level (1-indexed) -const getIntensity = (level: number) => INTENSITY_LEVELS[level - 1] || INTENSITY_LEVELS[0]; +const getDefaultIntensity = (isExplicit?: boolean) => (isExplicit ? 4 : 2); // Format duration for display const formatDuration = (hours: number | null): string => { @@ -107,7 +105,7 @@ export function DaresPage() { suggested_duration_hours: string; }>({ text: '', - intensity: 1, + intensity: getDefaultIntensity(), suggested_duration_hours: '', }); @@ -169,7 +167,7 @@ export function DaresPage() { const openCreateDialog = () => { setEditingDare(null); - setFormData({ text: '', intensity: 1, suggested_duration_hours: '' }); + setFormData({ text: '', intensity: getDefaultIntensity(pack?.is_explicit), suggested_duration_hours: '' }); setDialogOpen(true); }; @@ -343,25 +341,6 @@ export function DaresPage() { /> -
- -
- {INTENSITY_LEVELS.map((intensity) => ( - - ))} -
-
-
Partner Text Couples Initiator - Intensity Actions @@ -1124,14 +1101,6 @@ export function QuestionsPage() { Any )} - - - {getIntensity(question.intensity).label} - -