- Next.js 14+ with App Router and TypeScript - Tailwind CSS and ShadCN UI styling - Zustand state management - Dexie.js for IndexedDB (local-first data) - Auth.js v5 for authentication - BMAD framework integration Co-Authored-By: Claude <noreply@anthropic.com>
168 lines
4.6 KiB
TypeScript
168 lines
4.6 KiB
TypeScript
/**
|
|
* Intent Detector
|
|
*
|
|
* Classifies user messages as "venting" or "insight" based on keyword patterns.
|
|
* Uses a combination of keyword-based heuristics for fast classification.
|
|
*
|
|
* Venting Indicators:
|
|
* - Negative emotion words (frustrated, stuck, hate, broke)
|
|
* - Problem-focused language (doesn't work, failing, error)
|
|
* - Uncertainty or confusion (don't understand, why does)
|
|
* - Time spent struggling (hours, days, all day)
|
|
*
|
|
* Insight Indicators:
|
|
* - Positive realization words (get, understand, clicked, realized)
|
|
* - Solution-focused language (figured out, solved, fixed)
|
|
* - Teaching/explaining intent (so the trick is, here's what)
|
|
* - Completion or success (finally, working, done)
|
|
*/
|
|
|
|
export type Intent = 'venting' | 'insight';
|
|
|
|
// Venting keyword patterns (more specific to avoid false positives)
|
|
const VENTING_KEYWORDS = [
|
|
'frustrated',
|
|
'stuck',
|
|
'hate',
|
|
'broke',
|
|
'broken',
|
|
"don't understand",
|
|
'doesnt understand',
|
|
'confused',
|
|
'failing',
|
|
'error',
|
|
"won't work",
|
|
'wont work',
|
|
'cant figure',
|
|
"can't figure",
|
|
'struggling',
|
|
'difficult',
|
|
'hard',
|
|
'annoying',
|
|
// Question words (only when at start or with ?)
|
|
'why',
|
|
'how do',
|
|
'help',
|
|
];
|
|
|
|
// Insight keyword patterns (more specific to avoid false positives)
|
|
const INSIGHT_KEYWORDS = [
|
|
'finally get', // More specific than just "get"
|
|
'get it', // Also add "get it" pattern
|
|
'get it now', // "I get it now"
|
|
'understand',
|
|
'clicked',
|
|
'realized',
|
|
'figured it out', // Common phrase: "I figured it out"
|
|
'figured out', // Alternative: "I figured out the bug"
|
|
'solved',
|
|
'fixed',
|
|
'fixed it', // "I fixed it"
|
|
'now working', // More specific than just "working"
|
|
"it's working", // "It's working now"
|
|
'its working',
|
|
'done',
|
|
'finally',
|
|
'solution',
|
|
'found the solution', // "I found the solution"
|
|
'trick is',
|
|
'trick was', // "The trick was to..."
|
|
"here's what",
|
|
'heres what',
|
|
'learned',
|
|
'key is',
|
|
'answer',
|
|
'accomplished',
|
|
'makes sense', // "This makes sense"
|
|
'makes sense now',
|
|
];
|
|
|
|
/**
|
|
* Classifies the intent of a user message as "venting" or "insight".
|
|
* @param input - The user's message text
|
|
* @returns The classified intent ('venting' | 'insight')
|
|
*/
|
|
export function classifyIntent(input: string): Intent {
|
|
if (!input || input.trim().length === 0) {
|
|
return 'venting'; // Default to venting for empty input
|
|
}
|
|
|
|
const normalizedInput = input.toLowerCase().trim();
|
|
|
|
// Count insight indicators (positive patterns) FIRST
|
|
let insightScore = 0;
|
|
for (const keyword of INSIGHT_KEYWORDS) {
|
|
if (normalizedInput.includes(keyword)) {
|
|
insightScore += 1;
|
|
}
|
|
}
|
|
|
|
// Count venting indicators (negative patterns)
|
|
let ventingScore = 0;
|
|
for (const keyword of VENTING_KEYWORDS) {
|
|
if (normalizedInput.includes(keyword)) {
|
|
ventingScore += 1;
|
|
}
|
|
}
|
|
|
|
// Strong insight patterns (solution language, teaching) take ULTIMATE precedence
|
|
// This check happens AFTER scoring but BEFORE time-struggling check
|
|
const strongInsightPatterns = [
|
|
'figured it out', // Common phrase: "I figured it out"
|
|
'figured out', // Alternative: "I figured out the bug"
|
|
'solution is',
|
|
'trick is',
|
|
'key is',
|
|
"here's what",
|
|
'heres what',
|
|
];
|
|
|
|
const hasStrongInsightPattern = strongInsightPatterns.some(pattern =>
|
|
normalizedInput.includes(pattern)
|
|
);
|
|
|
|
// Strong insight patterns override everything else
|
|
// "figured out but it took forever" - still insight because they accomplished it
|
|
if (hasStrongInsightPattern) {
|
|
return 'insight';
|
|
}
|
|
|
|
// Questions are typically venting (seeking help)
|
|
const isQuestion = normalizedInput.includes('?') ||
|
|
normalizedInput.startsWith('why ') ||
|
|
normalizedInput.startsWith('how ') ||
|
|
normalizedInput.startsWith('what ') ||
|
|
normalizedInput.startsWith('when ') ||
|
|
normalizedInput.startsWith('where ');
|
|
|
|
if (isQuestion && insightScore === 0) {
|
|
return 'venting';
|
|
}
|
|
|
|
// Special case: time spent struggling is venting UNLESS there's strong insight pattern
|
|
const timeStrugglingPatterns = [
|
|
'all day',
|
|
'for hours',
|
|
' hours ',
|
|
'days',
|
|
'forever',
|
|
];
|
|
|
|
const hasTimeStrugglingPattern = timeStrugglingPatterns.some(pattern =>
|
|
normalizedInput.includes(pattern)
|
|
);
|
|
|
|
if (hasTimeStrugglingPattern && !hasStrongInsightPattern) {
|
|
return 'venting';
|
|
}
|
|
|
|
// Decision logic: insight if insight score is strictly higher than venting score
|
|
// Default to venting for tie or when scores are equal (safer assumption)
|
|
// Also default to venting when there are no clear indicators (both scores are 0)
|
|
if (insightScore > ventingScore && insightScore > 0) {
|
|
return 'insight';
|
|
}
|
|
|
|
return 'venting';
|
|
}
|