Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions app/api/follow-up-questions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,53 @@ export async function POST(request: Request) {
const body = await request.json();

const generateQuestions = async () => {
const response = await callAnthropic(generateFollowUpPrompt(body));
const response = await callAnthropic(generateFollowUpPrompt(body), {
tools: [
{
name: 'json',
description: 'Respond with a JSON object',
input_schema: {
type: 'object',
properties: {
questions: {
type: 'array',
description: 'A list of questions to ask a user regarding their content submission.',
items: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'A unique identifier for the question.',
},
question: {
type: 'string',
description: 'The exact question to be presented to the user.',
},
context: {
type: 'string',
description: 'Helpful context explaining why the question is being asked.',
},
reason: {
type: 'string',
description: 'A keyword explaining the purpose, e.g., "essential" or "verification".',
},
},
required: ['id', 'question', 'context', 'reason'],
},
},
},
required: ['questions'],
additionalProperties: false,
},
},
],
tool_choice: { type: 'tool', name: 'json' },
});
return parseAIJson(response);
};

const questions = await retryWithDelay(generateQuestions);
const questionsResponse = await retryWithDelay(generateQuestions);
const questions = questionsResponse.questions || [];

rollbar.info('FollowUpQuestions: Successfully generated follow-up questions', {
questionCount: questions.length,
Expand Down
20 changes: 20 additions & 0 deletions app/api/generate-letter-other-platform/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,27 @@ export async function POST(request: Request) {
name: 'web_search',
max_uses: 10,
},
// currently multiple tools cannot be used at the same time - web search should be prioritised
// {
// name: 'json',
// description: 'Respond with a JSON object',
// input_schema: {
// type: 'object',
// properties: {
// subject: {
// type: 'string',
// },
// body: {
// type: 'string',
// },
// },
// required: ['body', 'subject'],
// additionalProperties: false,
// },
// },
],
});

return parseAIJson(response);
};

Expand All @@ -35,6 +54,7 @@ export async function POST(request: Request) {
error: error.message,
stack: error.stack,
});

const { error: errorMessage, status } = handleApiError(
error,
'/api/generate-letter-other-platform',
Expand Down
23 changes: 22 additions & 1 deletion app/api/generate-letter/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,28 @@ export async function POST(request: Request) {
const body = await request.json();

const generateLetter = async () => {
const response = await callAnthropic(generateLetterPrompt(body));
const response = await callAnthropic(generateLetterPrompt(body), {
tools: [
{
name: 'json',
description: 'Respond with a JSON object',
input_schema: {
type: 'object',
properties: {
subject: {
type: 'string',
},
body: {
type: 'string',
},
},
required: ['body', 'subject'],
additionalProperties: false,
},
},
],
tool_choice: { type: 'tool', name: 'json' },
});
return parseAIJson(response);
};

Expand Down
6 changes: 3 additions & 3 deletions app/letter-generator/components/error-message.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AlertCircle, RefreshCw } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { AlertCircle, RefreshCw } from 'lucide-react';

interface ErrorMessageProps {
message: string;
Expand All @@ -11,8 +11,8 @@ export function ErrorMessage({ message, onRetry }: ErrorMessageProps) {
<div className="flex flex-col items-center justify-center py-8 sm:py-12">
<div className="bg-destructive/10 rounded-xl p-6 max-w-xl text-center">
<AlertCircle className="w-8 h-8 mx-auto mb-4 text-destructive" />
<h3 className="text-lg font-medium mb-2">Letter Generation Failed</h3>
<p className="text-muted-foreground mb-6">{message}</p>
<h3 className="text-lg font-medium mb-2">Letter generation failed</h3>
<p className="text-muted-foreground mb-6">There was an issue generating your letter. Please try again.</p>
<Button
onClick={onRetry}
className="pill bg-primary text-white hover:opacity-90"
Expand Down
59 changes: 39 additions & 20 deletions app/letter-generator/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button';
import { generateLetter } from '@/lib/ai/generate-letter';
import { analytics } from '@/lib/analytics';
import { GA_EVENTS } from '@/lib/constants/analytics';
import { PLATFORM_NAMES, PlatformId } from '@/lib/constants/platforms';
import { PlatformId } from '@/lib/constants/platforms';
import { PlatformInfo, useFormContext } from '@/lib/context/FormContext';
import { GeneratedLetter } from '@/types/letter';
import { motion } from 'framer-motion';
Expand All @@ -20,6 +20,7 @@ import { ProcessExplanation } from './components/process-explanation';
import { ProgressBar } from './components/progress-bar';
import { RemovalProcess } from './components/removal-process';
import { ReportingDetails } from './components/reporting-details';
import { ErrorMessage } from './components/error-message';

type Step =
| 'process-explanation'
Expand Down Expand Up @@ -60,12 +61,12 @@ const stepDescriptions: Record<Step, string> = {
export default function LetterGenerator() {
const router = useRouter();
const [currentStep, setCurrentStep] = useState<Step>('process-explanation');
const [isLoading, setIsLoading] = useState(false);
const { formState, updateCompleteFormData } = useFormContext();
const [hasGeneratedLetter, setHasGeneratedLetter] = useState(false);
const [generatedLetter, setGeneratedLetter] = useState<GeneratedLetter | null>(null);
const [redactedLetter, setRedactedLetter] = useState<GeneratedLetter | null>(null);
const [isRegenerating, setIsRegenerating] = useState(false);
const [generationError, setGenerationError] = useState<string | null>(null);

// Scroll to top of main content when step changes
useEffect(() => {
Expand All @@ -80,7 +81,7 @@ export default function LetterGenerator() {
if (!formState.completeFormData) return;

try {
setIsLoading(true);
setGenerationError(null);
const letters = await generateLetter(formState.completeFormData);
setGeneratedLetter(letters.finalLetter);
setRedactedLetter(letters.redactedLetter);
Expand All @@ -97,17 +98,25 @@ export default function LetterGenerator() {
}
setIsRegenerating(false);
} catch (error) {
console.error('Error generating letter:', error);
const errorMessage = error instanceof Error ? error.message : 'An unexpected error occurred while generating your letter. Please try again.';
setGenerationError(errorMessage);
setIsRegenerating(false);

analytics.trackError(
'letter_generation',
error instanceof Error ? error.message : 'Unknown error',
errorMessage,
'LetterGenerator',
);
} finally {
setIsLoading(false);
}
}, [formState.completeFormData, isRegenerating]);

const handleRetryGeneration = useCallback(() => {
setGenerationError(null);
setGeneratedLetter(null);
setRedactedLetter(null);
generateLetterContent();
}, [generateLetterContent]);

const getStepOrder = (): Step[] => {
// Skip removal process for custom platforms
if (formState.platformInfo?.isCustom) {
Expand Down Expand Up @@ -291,19 +300,28 @@ export default function LetterGenerator() {
)}

{(currentStep === 'generation' || isRegenerating) && !generatedLetter && (
<div className="flex flex-col items-center justify-center py-8 sm:py-12">
<div className="bg-accent-light/50 rounded-xl p-6 max-w-xl text-center">
<Loader2 className="w-8 h-8 mx-auto mb-4 animate-spin text-primary" />
<h3 className="text-lg font-medium mb-2">
{isRegenerating ? 'Regenerating your letter' : 'Creating your letter'}
</h3>
<p className="text-muted-foreground">
We're using AI to craft a professionally-written takedown request based on your
responses, ensuring it aligns with {platformName}'s content policies and removal
processes. <strong>This can take up to a minute.</strong>
</p>
</div>
</div>
<>
{generationError ? (
<ErrorMessage
message={generationError}
onRetry={handleRetryGeneration}
/>
) : (
<div className="flex flex-col items-center justify-center py-8 sm:py-12">
<div className="bg-accent-light/50 rounded-xl p-6 max-w-xl text-center">
<Loader2 className="w-8 h-8 mx-auto mb-4 animate-spin text-primary" />
<h3 className="text-lg font-medium mb-2">
{isRegenerating ? 'Regenerating your letter' : 'Creating your letter'}
</h3>
<p className="text-muted-foreground">
We're using AI to craft a professionally-written takedown request based on your
responses, ensuring it aligns with {platformName}'s content policies and removal
processes. <strong>This can take up to a minute.</strong>
</p>
</div>
</div>
)}
</>
)}

{currentStep === 'review' &&
Expand All @@ -318,6 +336,7 @@ export default function LetterGenerator() {
setIsRegenerating(true);
setGeneratedLetter(null);
setRedactedLetter(null);
setGenerationError(null);
await generateLetterContent();
}}
onComplete={() => {
Expand Down
10 changes: 9 additions & 1 deletion lib/ai/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,16 @@ export async function callAnthropic(
],
...config,
});

if(config.tool_choice?.type === 'tool' && config.tool_choice.name === 'json') {
const toolCall = response.content.find(block => block.type === 'tool_use');

if (toolCall && toolCall.name === 'json') {
// Extract the 'input' object. This is the final structured JSON
return JSON.stringify(toolCall.input);
}
}

// console.log(response);
const responseText = response?.content?.filter((c) => c.type === 'text')[0]?.text;

//@ts-ignore
Expand Down
53 changes: 24 additions & 29 deletions lib/ai/generate-letter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { MAX_RETRIES, RETRY_DELAY, STATIC_NEXT_STEPS } from '../constants/ai';
import { PlatformId } from '../constants/platforms';
import { QualityCheckResponse } from '../prompts/letter-quality-check';
import { serverInstance as rollbar } from '../rollbar';
import { retryWithDelay } from '../utils';
import { cleanupSanitizationMap, desanitizeLetter, sanitizeFormData } from './sanitization';

interface GeneratedLetterResponse {
Expand Down Expand Up @@ -32,43 +31,39 @@ export async function generateLetter(formData: LetterRequest): Promise<Generated
hasFollowUp: !!sanitizedData.followUp,
});

const response: { subject: string; body: string } = await retryWithDelay(async () => {
const endpoint = isOtherPlatform ? '/api/generate-letter-other-platform' : '/api/generate-letter';

const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sanitizedData),
});
const endpoint = isOtherPlatform ? '/api/generate-letter-other-platform' : '/api/generate-letter';

const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sanitizedData),
});

if (!res.ok) {
const error = await res.json();
throw new Error(error.message || 'Failed to generate letter');
}
if (!res.ok) {
const errorResponse = await res.json().catch(() => ({ error: 'Failed to generate letter' }));
throw new Error(errorResponse.error || `Failed to generate letter (${res.status})`);
}

return res.json();
});
const response: { subject: string; body: string } = await res.json();

let improvedLetter;
const originalLetter = response;

// Perform quality check
const qualityCheckResponse: QualityCheckResponse = await retryWithDelay(async () => {
const res = await fetch('/api/quality-check-letter', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
letter: originalLetter,
formData: sanitizedData,
}),
});
const qualityCheckRes = await fetch('/api/quality-check-letter', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
letter: originalLetter,
formData: sanitizedData,
}),
});

if (!res.ok) {
throw new Error('Quality check request failed');
}
if (!qualityCheckRes.ok) {
throw new Error('Quality check request failed');
}

return res.json();
});
const qualityCheckResponse: QualityCheckResponse = await qualityCheckRes.json();

// If major issues found, retry generation unless on last attempt
if (qualityCheckResponse.issues?.length > 0 && qualityCheckResponse.improvedLetter) {
Expand Down
2 changes: 1 addition & 1 deletion lib/constants/ai.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const MAX_RETRIES = 2;
export const MAX_RETRIES = 1;
export const RETRY_DELAY = 1000; // 1 second delay between retries

// Static next steps that will be used for all letters
Expand Down
1 change: 1 addition & 0 deletions lib/platforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export function getDefaultPlatformEmail(platform: PlatformId): string {
break;
case PlatformId.PORNHUB:
platformChannels = pornhubReportingChannels;
break;
default:
return "Please check the platform's help center for the appropriate contact email";
}
Expand Down
Loading