Skip to content
Open
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
41 changes: 40 additions & 1 deletion apps/web/app/api/assessment/interview/save-response/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { detectFollowUpNeeded } from '@/lib/claude/client';
import { detectFollowUpNeeded, generateMemoryScaffolds } from '@/lib/claude/client';
import { updateAssessment, insertInterviewResponse, getInterviewResponseCount } from '@/lib/supabase-server';
import { TOTAL_INTERVIEW_QUESTIONS } from '@/lib/constants';

Expand Down Expand Up @@ -36,6 +36,44 @@ export async function POST(request: NextRequest) {
console.warn('Follow-up detection failed, continuing without:', error);
}

// Check if memory scaffolding is needed
let memoryScaffolds: string[] | null = null;
const memoryTriggerPhrases = [
"i don't remember",
"i can't recall",
"i don't know",
"no memory",
"can't remember",
"don't recall",
"fuzzy memory",
"unclear",
"not sure about that"
];
Comment on lines +41 to +51

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): The memory trigger phrase matching is very broad and may cause false positives.

Generic phrases like "unclear" or "I don't know" can occur in many non-memory contexts (e.g., uncertainty about a diagnosis), so the simple includes check on a lowercased response is likely to over-trigger memory scaffolding. Consider narrowing these to more explicitly memory-related phrases (e.g., requiring "remember"/"recall" nearby) or adding word-boundary/regex checks to reduce false positives.

Suggested change
const memoryTriggerPhrases = [
"i don't remember",
"i can't recall",
"i don't know",
"no memory",
"can't remember",
"don't recall",
"fuzzy memory",
"unclear",
"not sure about that"
];
const memoryTriggerPhrases = [
"i don't remember",
"i can't remember",
"i can't recall",
"i have no memory of that",
"i don't have any memory of that",
"my memory is fuzzy",
"my memory is unclear",
"my memory is blank"
];


const needsMemoryHelp = memoryTriggerPhrases.some(phrase =>
response.toLowerCase().includes(phrase)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Validate response as a string before calling .toLowerCase() to avoid runtime 500s on malformed payloads.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/app/api/assessment/interview/save-response/route.ts, line 54:

<comment>Validate `response` as a string before calling `.toLowerCase()` to avoid runtime 500s on malformed payloads.</comment>

<file context>
@@ -36,6 +36,44 @@ export async function POST(request: NextRequest) {
+    ];
+
+    const needsMemoryHelp = memoryTriggerPhrases.some(phrase =>
+      response.toLowerCase().includes(phrase)
+    );
+
</file context>

);

if (needsMemoryHelp) {
try {
// Determine age group based on question content (simplified logic)
let ageGroup: 'early' | 'school' | 'adolescent' | 'general' = 'general';
const questionLower = (questionText || '').toLowerCase();

if (questionLower.includes('child') || questionLower.includes('early') || questionLower.includes('preschool')) {
ageGroup = 'early';
} else if (questionLower.includes('school') || questionLower.includes('elementary') || questionLower.includes('middle')) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: "high school" is shadowed by the earlier "school" check, causing incorrect age-group selection.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/app/api/assessment/interview/save-response/route.ts, line 65:

<comment>`"high school"` is shadowed by the earlier `"school"` check, causing incorrect age-group selection.</comment>

<file context>
@@ -36,6 +36,44 @@ export async function POST(request: NextRequest) {
+
+        if (questionLower.includes('child') || questionLower.includes('early') || questionLower.includes('preschool')) {
+          ageGroup = 'early';
+        } else if (questionLower.includes('school') || questionLower.includes('elementary') || questionLower.includes('middle')) {
+          ageGroup = 'school';
+        } else if (questionLower.includes('teen') || questionLower.includes('adolescent') || questionLower.includes('high school')) {
</file context>
Suggested change
} else if (questionLower.includes('school') || questionLower.includes('elementary') || questionLower.includes('middle')) {
} else if ((questionLower.includes('school') && !questionLower.includes('high school')) || questionLower.includes('elementary') || questionLower.includes('middle')) {

ageGroup = 'school';
} else if (questionLower.includes('teen') || questionLower.includes('adolescent') || questionLower.includes('high school')) {
ageGroup = 'adolescent';
}

memoryScaffolds = await generateMemoryScaffolds(ageGroup);
} catch (error) {
console.warn('Memory scaffolding failed, continuing without:', error);
}
}

// Get current response count BEFORE inserting the new one
const previousCount = await getInterviewResponseCount(assessmentId);

Expand Down Expand Up @@ -66,6 +104,7 @@ export async function POST(request: NextRequest) {
success: true,
assessment,
followUpQuestion: shouldReturnFollowUp ? followUpResult.followUpQuestion || null : null,
memoryScaffolds: memoryScaffolds || null,
});
} catch (err) {
console.error('Interview save error:', err);
Expand Down
149 changes: 95 additions & 54 deletions apps/web/lib/claude/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Anthropic from '@anthropic-ai/sdk';
import { INTERVIEW_SYSTEM_PROMPTS, getFallbackQuestionsForDomain, getMemoryScaffoldsForAgeGroup } from './prompts';

let anthropicClient: Anthropic | null = null;

Expand All @@ -24,27 +25,17 @@ export async function generateInterviewQuestion(
): Promise<string> {
const client = getAnthropicClient();

const systemPrompt = `You are an empathic clinical interviewer specializing in ADHD assessment.
Your role is to ask thoughtful, open-ended questions that help gather detailed information about the person's experiences.
const systemPrompt = INTERVIEW_SYSTEM_PROMPTS.interviewer;

Guidelines:
- Ask one question at a time
- Questions should be conversational and non-judgmental
- Focus on gathering specific examples and experiences
- Avoid yes/no questions when possible
- Keep questions under 150 characters
- Be supportive and understanding in tone
const userPrompt = `CONTEXT: ADHD assessment interview in "${section}" domain
PREVIOUS RESPONSES: ${previousResponses.join('; ')}

Current context: This is for an ADHD assessment interview in the "${section}" section.`;

const userPrompt = `Based on these previous responses: ${previousResponses.join('; ')}

Generate the next most appropriate interview question for the "${section}" section.`;
Generate the next most appropriate interview question.`;

try {
const response = await client.messages.create({
model: 'claude-3-5-sonnet-20241022',
Comment on lines 35 to 37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): generateMemoryScaffolds uses an undefined userPrompt and ignores contextPrompt.

In generateMemoryScaffolds, messages uses content: userPrompt, but userPrompt isn’t defined in this scope, which will cause a runtime ReferenceError and leaves contextPrompt unused. You likely meant to use:

messages: [{ role: 'user', content: contextPrompt }],

so the model receives the intended age-group-specific context and the code doesn’t crash.

max_tokens: 150,
max_tokens: 120,
system: systemPrompt,
messages: [{ role: 'user', content: userPrompt }],
temperature: 0.7,
Expand All @@ -58,6 +49,14 @@ Generate the next most appropriate interview question for the "${section}" secti
throw new Error('No question generated');
}

return question;
} catch (error) {
console.error('Claude API error:', error);
// Enhanced fallback using domain-specific question banks
return getEnhancedFallbackQuestion(section);
}
}

return question;
} catch (error) {
console.error('Claude API error:', error);
Expand All @@ -72,28 +71,17 @@ export async function detectFollowUpNeeded(
): Promise<{ needsFollowUp: boolean; followUpQuestion?: string | null }> {
const client = getAnthropicClient();

const systemPrompt = `You are analyzing interview responses for ADHD assessment.
Determine if the response is vague, incomplete, or would benefit from a follow-up question.

Return a JSON object with:
- needsFollowUp: boolean
- followUpQuestion: string (only if needsFollowUp is true, max 100 characters)

Guidelines:
- Mark as needing follow-up if response is very brief (< 20 words)
- Mark as needing follow-up if response lacks specific examples
- Mark as needing follow-up if response seems incomplete
- Follow-up questions should be specific and probing`;
const systemPrompt = INTERVIEW_SYSTEM_PROMPTS.followUpDetector;

const userPrompt = `Question: ${question}
Response: ${response}
const userPrompt = `QUESTION: ${question}
RESPONSE: ${response}

Analyze if this response needs a follow-up question:`;
DETERMINE if follow-up needed:`;

try {
const response = await client.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 200,
max_tokens: 150,
system: systemPrompt,
messages: [{ role: 'user', content: userPrompt }],
temperature: 0.3,
Expand All @@ -106,12 +94,39 @@ Analyze if this response needs a follow-up question:`;
try {
const result = JSON.parse(content);

// Validate the result structure
if (typeof result !== 'object' || result === null) {
console.error('Claude response is not a valid object');
// Enhanced validation with better error handling
if (!result || typeof result !== 'object') {
console.error('Invalid Claude response structure');
return { needsFollowUp: false };
}

if (typeof result.needsFollowUp !== 'boolean') {
console.error('Missing or invalid needsFollowUp boolean');
return { needsFollowUp: false };
}

if (result.needsFollowUp) {
if (typeof result.followUpQuestion !== 'string') {
console.error('Follow-up needed but question missing');
return { needsFollowUp: false };
}
if (result.followUpQuestion.length > 80) {
console.warn('Follow-up question truncated to 80 chars');
result.followUpQuestion = result.followUpQuestion.substring(0, 80);
}
}

return result;
} catch (parseError) {
console.error('JSON parse error in follow-up detection:', parseError);
return { needsFollowUp: false };
}
} catch (error) {
console.error('Claude API error in follow-up detection:', error);
return { needsFollowUp: false };
}
}

if (typeof result.needsFollowUp !== 'boolean') {
console.error('Claude response missing valid needsFollowUp boolean');
return { needsFollowUp: false };
Expand Down Expand Up @@ -139,25 +154,51 @@ Analyze if this response needs a follow-up question:`;
}
}

function getFallbackQuestion(section: string): string {
const fallbacks: Record<string, string[]> = {
'attention': [
'Can you tell me about a specific time when you struggled to maintain focus?',
'How does difficulty concentrating affect your daily activities?',
'What strategies have you tried to improve your focus?'
],
'hyperactivity': [
'How do you handle feelings of restlessness?',
'Can you describe situations where you feel the need to move constantly?',
'How does physical restlessness impact your work or relationships?'
],
'impulsivity': [
'Tell me about a time when acting quickly led to unintended consequences.',
'How do you manage impulsive decisions?',
'What situations tend to trigger impulsive behavior for you?'
]
};

const questions = fallbacks[section] || fallbacks['attention'];
// Enhanced fallback question selection using comprehensive question banks
function getEnhancedFallbackQuestion(section: string): string {
const questions = getFallbackQuestionsForDomain(section.toLowerCase());
return questions[Math.floor(Math.random() * questions.length)];
}

// Memory scaffolding for patients who express difficulty remembering
export async function generateMemoryScaffolds(ageGroup: 'early' | 'school' | 'adolescent' | 'general' = 'general'): Promise<string[]> {
const client = getAnthropicClient();

const systemPrompt = INTERVIEW_SYSTEM_PROMPTS.memoryScaffolding;

const scaffolds = getMemoryScaffoldsForAgeGroup(ageGroup);
const contextPrompt = `AGE GROUP: ${ageGroup} childhood
AVAILABLE SCAFFOLDS: ${scaffolds.slice(0, 5).join('; ')}

Generate 2-3 contextual memory prompts for ADHD recall:`;

try {
const response = await client.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 200,
system: systemPrompt,
messages: [{ role: 'user', content: userPrompt }],
temperature: 0.6,
Comment on lines +169 to +181

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Bug: userPrompt is undefined; should be contextPrompt.

Line 170 declares contextPrompt but line 180 references userPrompt, which is undefined. This will cause a runtime error when calling the Claude API.

🐛 Fix variable name mismatch
   const scaffolds = getMemoryScaffoldsForAgeGroup(ageGroup);
   const contextPrompt = `AGE GROUP: ${ageGroup} childhood
 AVAILABLE SCAFFOLDS: ${scaffolds.slice(0, 5).join('; ')}
 
 Generate 2-3 contextual memory prompts for ADHD recall:`;
 
   try {
     const response = await client.messages.create({
       model: 'claude-3-5-sonnet-20241022',
       max_tokens: 200,
       system: systemPrompt,
-      messages: [{ role: 'user', content: userPrompt }],
+      messages: [{ role: 'user', content: contextPrompt }],
       temperature: 0.6,
     });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const scaffolds = getMemoryScaffoldsForAgeGroup(ageGroup);
const contextPrompt = `AGE GROUP: ${ageGroup} childhood
AVAILABLE SCAFFOLDS: ${scaffolds.slice(0, 5).join('; ')}
Generate 2-3 contextual memory prompts for ADHD recall:`;
try {
const response = await client.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 200,
system: systemPrompt,
messages: [{ role: 'user', content: userPrompt }],
temperature: 0.6,
const scaffolds = getMemoryScaffoldsForAgeGroup(ageGroup);
const contextPrompt = `AGE GROUP: ${ageGroup} childhood
AVAILABLE SCAFFOLDS: ${scaffolds.slice(0, 5).join('; ')}
Generate 2-3 contextual memory prompts for ADHD recall:`;
try {
const response = await client.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 200,
system: systemPrompt,
messages: [{ role: 'user', content: contextPrompt }],
temperature: 0.6,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/claude/client.ts` around lines 169 - 181, The code passes an
undefined variable userPrompt to client.messages.create causing a runtime error;
update the messages payload in the Claude call (client.messages.create) to use
the correctly defined contextPrompt (messages: [{ role: 'user', content:
contextPrompt }]) so the API receives the intended prompt, and verify there are
no other references to userPrompt in this function.

});

const content = response.content[0]?.type === 'text'
? response.content[0].text.trim()
: '';

try {
const result = JSON.parse(content);
if (result.scaffolds && Array.isArray(result.scaffolds) && result.scaffolds.length >= 2) {
return result.scaffolds.slice(0, 3); // Limit to 3 scaffolds
}
Comment on lines +188 to +192

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Validate that scaffold array elements are strings before returning.

The parsed result.scaffolds is assumed to contain strings, but the Claude response could include non-string elements. Consider adding type validation to prevent downstream issues.

♻️ Add element type validation
     try {
       const result = JSON.parse(content);
-      if (result.scaffolds && Array.isArray(result.scaffolds) && result.scaffolds.length >= 2) {
-        return result.scaffolds.slice(0, 3); // Limit to 3 scaffolds
+      if (result.scaffolds && Array.isArray(result.scaffolds) && result.scaffolds.length >= 2) {
+        const validScaffolds = result.scaffolds
+          .filter((s: unknown): s is string => typeof s === 'string' && s.length > 0)
+          .slice(0, 3);
+        if (validScaffolds.length >= 2) {
+          return validScaffolds;
+        }
       }
     } catch (parseError) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const result = JSON.parse(content);
if (result.scaffolds && Array.isArray(result.scaffolds) && result.scaffolds.length >= 2) {
return result.scaffolds.slice(0, 3); // Limit to 3 scaffolds
}
try {
const result = JSON.parse(content);
if (result.scaffolds && Array.isArray(result.scaffolds) && result.scaffolds.length >= 2) {
const validScaffolds = result.scaffolds
.filter((s: unknown): s is string => typeof s === 'string' && s.length > 0)
.slice(0, 3);
if (validScaffolds.length >= 2) {
return validScaffolds;
}
}
} catch (parseError) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/claude/client.ts` around lines 188 - 192, Parsed
result.scaffolds may contain non-string elements; update the handling in the try
block (where you parse content into result and check result.scaffolds) to filter
and validate elements are strings before returning: replace the current slice
logic with a filter that keeps only typeof === 'string' entries, then limit to 3
(e.g., filter result.scaffolds to strings and then take the first three) and
return that array so downstream code always receives string scaffolds.

} catch (parseError) {
console.error('Failed to parse memory scaffold response:', parseError);
}

// Fallback to static scaffolds
return scaffolds.slice(0, 3);
} catch (error) {
console.error('Claude API error in memory scaffolding:', error);
// Return static scaffolds as fallback
return getMemoryScaffoldsForAgeGroup(ageGroup).slice(0, 3);
}
}
Loading
Loading