claude-code-tamagotchi
Version:
A virtual pet that lives in your Claude Code statusline
301 lines (264 loc) • 9.1 kB
text/typescript
import { TranscriptAnalyzer } from './TranscriptAnalyzer';
import { PetState } from '../StateManager';
import { config } from '../../utils/config';
import { FeedbackConfig, Feedback } from './types';
export class FeedbackSystem {
private analyzer: TranscriptAnalyzer | null = null;
private initialized: boolean = false;
private currentSessionId?: string;
constructor() {
if (config.feedbackEnabled) {
this.debug('Feedback system enabled, initializing...');
this.initialize();
} else {
this.debug('Feedback system disabled');
}
}
private initialize(): void {
const feedbackConfig: FeedbackConfig = {
enabled: config.feedbackEnabled,
mode: config.feedbackMode,
checkInterval: config.feedbackCheckInterval,
batchSize: config.feedbackBatchSize,
minMessages: config.feedbackMinMessages,
staleLockTime: config.feedbackStaleLockTime,
dbPath: config.feedbackDbPath,
dbMaxSize: config.feedbackDbMaxSize,
groqApiKey: config.groqApiKey,
groqModel: config.groqModel,
groqTimeout: config.groqTimeout,
groqMaxRetries: config.groqMaxRetries
};
this.analyzer = new TranscriptAnalyzer(feedbackConfig);
this.initialized = true;
}
/**
* Process feedback for the current update
*/
processFeedback(
state: PetState,
transcriptPath?: string,
sessionId?: string
): void {
// Store session ID for later use
this.currentSessionId = sessionId;
if (!this.initialized || !this.analyzer || !transcriptPath || !sessionId) {
this.debug('Feedback not processed - missing requirements');
return;
}
this.debug(`Processing feedback for session ${sessionId}`);
// Quick analysis (< 10ms)
const analysis = this.analyzer.quickAnalyze(transcriptPath, sessionId);
// Update behavior score
state.claudeBehaviorScore = analysis.behaviorScore;
state.recentViolations = analysis.violationCount;
// Update mood based on violations
this.updateMoodFromFeedback(state, analysis.violationCount, analysis.behaviorScore);
// Store feedback for display
if (analysis.cachedFeedback) {
this.debug(`Applying feedback: ${analysis.cachedFeedback.feedback_type}/${analysis.cachedFeedback.severity}`);
this.applyFeedback(state, analysis.cachedFeedback);
}
// Spawn background worker if needed
if (analysis.shouldSpawn) {
this.debug('Spawning background worker with pet state');
this.analyzer.spawnAnalysisWorker(transcriptPath, sessionId, state);
}
}
private debug(message: string, error?: Error): void {
if (process.env.PET_FEEDBACK_DEBUG === 'true') {
const timestamp = new Date().toISOString();
const errorInfo = error ? ` - Error: ${error.message}` : '';
const logMessage = `[${timestamp}] [FeedbackSystem] ${message}${errorInfo}\n`;
// Log to console if debug mode
if (process.env.DEBUG_MODE === 'true') {
console.error(logMessage.trim());
}
// Log to file if specified
const logDir = process.env.PET_FEEDBACK_LOG_DIR;
if (logDir) {
try {
const fs = require('fs');
const path = require('path');
// Create log directory if it doesn't exist
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
const logFile = path.join(logDir, 'feedback-system.log');
fs.appendFileSync(logFile, logMessage);
} catch (error) {
// Log error to stderr if file logging fails
console.error(`Failed to write to log file: ${error}`);
}
}
}
}
/**
* Update mood based on feedback
*/
private updateMoodFromFeedback(
state: PetState,
violationCount: number,
behaviorScore: number
): void {
// Don't update mood if system is disabled
if (!this.initialized) {
return;
}
// Simplified mood system based on behavior score
if (behaviorScore >= 85) {
state.currentMood = 'happy';
} else if (behaviorScore >= 70) {
state.currentMood = 'normal';
} else if (behaviorScore >= 50) {
state.currentMood = 'concerned';
} else {
state.currentMood = 'annoyed';
}
// Override with special moods for edge cases
if (behaviorScore >= 95 && violationCount === 0) {
state.currentMood = 'proud';
} else if (violationCount >= 5) {
state.currentMood = 'frustrated';
}
}
/**
* Gradually improve mood
*/
private decayMood(state: PetState): void {
const moodProgression = {
'frustrated': 'annoyed',
'annoyed': 'concerned',
'concerned': 'normal',
'normal': 'happy'
};
const nextMood = moodProgression[state.currentMood as keyof typeof moodProgression];
if (nextMood) {
state.currentMood = nextMood as any;
}
}
/**
* Apply feedback to state
*/
private applyFeedback(state: PetState, feedback: Feedback): void {
// Store in history
state.feedbackHistory.push({
type: feedback.feedback_type,
severity: feedback.severity,
remark: feedback.remark || '',
timestamp: feedback.created_at
});
// Keep only recent history
if (state.feedbackHistory.length > 20) {
state.feedbackHistory.shift();
}
// Track thought history - add the funny_observation to thoughtHistory (what users see)
if (feedback.funny_observation) {
// Add to thought history
if (!state.thoughtHistory) {
state.thoughtHistory = [];
}
// Simple duplicate check - exact match only
const isDuplicate = state.thoughtHistory.some(
existing => existing.toLowerCase() === feedback.funny_observation.toLowerCase()
);
// Only add if not duplicate
if (!isDuplicate) {
state.thoughtHistory.push(feedback.funny_observation);
// Keep only last 10 unique thoughts for avoiding repetition
if (state.thoughtHistory.length > 10) {
state.thoughtHistory.shift();
}
}
}
// Set current feedback for display
state.currentFeedback = {
icon: feedback.icon || this.getIconForFeedback(feedback),
remark: feedback.funny_observation || '', // Show funny_observation to users, not the internal remark
timestamp: Date.now()
};
// Apply happiness changes
if (feedback.severity === 'good') {
state.happiness = Math.min(100, state.happiness + 5);
} else if (feedback.severity === 'problematic') {
state.happiness = Math.max(0, state.happiness - 5);
} else if (feedback.severity === 'annoying') {
state.happiness = Math.max(0, state.happiness - 2);
}
}
/**
* Get icon for feedback type
*/
private getIconForFeedback(feedback: Feedback): string {
if (feedback.severity === 'problematic') {
return '⚠️';
} else if (feedback.severity === 'annoying') {
return '💭';
} else if (feedback.severity === 'good') {
return '✨';
} else {
return '👀';
}
}
/**
* Get feedback thought for display
*/
getFeedbackThought(state: PetState): string | null {
// Return null if system is not initialized (disabled)
if (!this.initialized || !this.analyzer) {
return null;
}
// Always try to get the latest feedback from the database
if (this.analyzer && this.currentSessionId) {
const latestFeedback = this.analyzer.getCachedFeedback(this.currentSessionId);
if (latestFeedback && latestFeedback.length > 0) {
const mostRecent = latestFeedback[0]; // Already sorted by recency
if (mostRecent.funny_observation) {
// Update state with the latest
state.currentFeedback = {
icon: mostRecent.icon || this.getIconForFeedback(mostRecent.severity),
remark: mostRecent.funny_observation,
timestamp: Date.now()
};
// Mark as shown so we don't repeat it
if (mostRecent.id) {
this.analyzer.markFeedbackShown([mostRecent.id]);
}
return mostRecent.funny_observation;
}
}
}
// Fall back to current feedback if nothing new
if (!state.currentFeedback) {
return null;
}
// Keep feedback visible longer (5 minutes)
const age = Date.now() - state.currentFeedback.timestamp;
if (age > 300000) {
state.currentFeedback = undefined;
return null;
}
return state.currentFeedback.remark;
}
/**
* Get feedback icon for display
*/
getFeedbackIcon(state: PetState): string | null {
// Return null if system is not initialized (disabled)
if (!this.initialized) {
return null;
}
if (!state.currentFeedback) {
return null;
}
return state.currentFeedback.icon;
}
/**
* Cleanup
*/
close(): void {
if (this.analyzer) {
this.analyzer.close();
}
}
}