Studley AI implements comprehensive content moderation to ensure users create appropriate educational content. The system uses AI-powered policy checks to flag inappropriate topics and maintain a safe learning environment.
Currently, inappropriate attempts are logged but not permanently stored in the database. Consider implementing an inappropriate_attempts table for historical tracking.
CREATE TABLE IF NOT EXISTS moderation_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id TEXT NOT NULL, content_type TEXT NOT NULL, -- 'quiz', 'flashcard', 'study_guide' topic TEXT NOT NULL, flagged_reason TEXT NOT NULL, severity TEXT NOT NULL, -- 'low', 'medium', 'high', 'critical' blocked BOOLEAN DEFAULT true, reviewed BOOLEAN DEFAULT false, reviewer_id TEXT, reviewer_notes TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE);CREATE INDEX idx_moderation_user_id ON moderation_events(user_id);CREATE INDEX idx_moderation_created_at ON moderation_events(created_at DESC);CREATE INDEX idx_moderation_severity ON moderation_events(severity);
CREATE TABLE IF NOT EXISTS bug_reports ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, preferred_name TEXT NOT NULL, severity TEXT NOT NULL CHECK (severity IN ('low', 'medium', 'high', 'critical')), error_code TEXT, has_error_code BOOLEAN DEFAULT true, description TEXT NOT NULL, browser_info TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL, status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'in_progress', 'resolved', 'closed')), admin_notes TEXT);
CREATE TABLE IF NOT EXISTS user_feedback ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, preferred_name TEXT NOT NULL, feedback_type TEXT NOT NULL CHECK (feedback_type IN ('general', 'feature_request', 'improvement', 'other')), message TEXT NOT NULL, rating INTEGER CHECK (rating >= 1 AND rating <= 5), created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL, status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'reviewed', 'implemented', 'closed')), admin_notes TEXT);
-- Get all pending bug reportsSELECT id, preferred_name, severity, description, created_atFROM bug_reportsWHERE status = 'pending'ORDER BY CASE severity WHEN 'critical' THEN 1 WHEN 'high' THEN 2 WHEN 'medium' THEN 3 WHEN 'low' THEN 4 END, created_at DESC;
-- Mark bug report as resolvedUPDATE bug_reportsSET status = 'resolved', admin_notes = 'Fixed in version 2.1.0'WHERE id = 'bug-id-here';-- Mark feedback as reviewedUPDATE user_feedbackSET status = 'reviewed', admin_notes = 'Great suggestion! Added to roadmap.'WHERE id = 'feedback-id-here';
CREATE TABLE quiz_results ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, user_id UUID NOT NULL, quiz_item_id UUID REFERENCES study_items(id) ON DELETE SET NULL, score NUMERIC NOT NULL, total_questions INTEGER NOT NULL, correct_count INTEGER NOT NULL DEFAULT 0, partial_count INTEGER NOT NULL DEFAULT 0, incorrect_count INTEGER NOT NULL DEFAULT 0, accuracy NUMERIC NOT NULL DEFAULT 0, avg_time_per_question NUMERIC NOT NULL DEFAULT 0, best_streak INTEGER NOT NULL DEFAULT 0, answers JSONB, created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW());
Monitor quiz quality:
-- Find quizzes with unusually low scores (potential quality issues)SELECT quiz_item_id, COUNT(*) as attempt_count, AVG(accuracy) as avg_accuracy, AVG(score) as avg_scoreFROM quiz_resultsGROUP BY quiz_item_idHAVING AVG(accuracy) < 0.5 -- Less than 50% average accuracyORDER BY attempt_count DESC;
CREATE TABLE IF NOT EXISTS shared_materials ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), material_id UUID NOT NULL REFERENCES study_items(id) ON DELETE CASCADE, owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, shared_with_user_id UUID REFERENCES users(id) ON DELETE CASCADE, public_token TEXT UNIQUE, public_access_enabled BOOLEAN DEFAULT false, allow_resharing BOOLEAN DEFAULT false, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
Query publicly shared content:
-- Get all publicly accessible shared materialsSELECT sm.id, sm.public_token, u.email as owner_email, sm.created_at, sm.allow_resharingFROM shared_materials smJOIN users u ON sm.owner_id = u.idWHERE sm.public_access_enabled = trueORDER BY sm.created_at DESC;
CREATE TABLE IF NOT EXISTS generation_rate_limits ( id BIGSERIAL PRIMARY KEY, identifier TEXT NOT NULL, -- User ID or IP address created_at TIMESTAMP WITH TIME ZONE DEFAULT now());
Check rate limit violations:
-- Find users exceeding rate limits (more than 50 requests/hour)SELECT identifier, COUNT(*) as request_count, MAX(created_at) as last_requestFROM generation_rate_limitsWHERE created_at > NOW() - INTERVAL '1 hour'GROUP BY identifierHAVING COUNT(*) > 50ORDER BY request_count DESC;