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) => (
-
- ))}
-
-
-
-
-
-
- {INTENSITY_LEVELS.map((intensity) => (
-
- ))}
-
-
-
Partner Text
Couples
Initiator
-
Intensity
Actions
@@ -1124,14 +1101,6 @@ export function QuestionsPage() {
Any
)}
-
-
- {getIntensity(question.intensity).label}
-
-