Clean up project root - remove 51 obsolete files
Deleted: - 26 old markdown summary/documentation files - 16 debug/test Python scripts (debug_*, test_*, diagnose_*) - 10 untracked JSON files from api_response_samples - terms-of-usage.md, pane_not_found.png Also includes pending web app changes: - Jobs management UI (JobsView, Sidebar components) - API routes for job streaming and comparison - Enhanced ReviewAnalytics and ScraperTest components Final clean structure: ├── api_server_production.py (main entry) ├── modules/ (core Python) ├── web/ (Next.js frontend) ├── tests/ (test suite) ├── docs/ (documentation) └── examples/ (usage examples) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
// Analytics utility functions
|
||||
|
||||
export interface OwnerResponse {
|
||||
text: string;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
export interface Review {
|
||||
author: string;
|
||||
rating: number;
|
||||
@@ -8,6 +13,8 @@ export interface Review {
|
||||
avatar_url: string | null;
|
||||
profile_url: string | null;
|
||||
review_id: string;
|
||||
owner_response?: OwnerResponse | null;
|
||||
photo_urls?: string[] | null;
|
||||
// Derived fields (computed on load)
|
||||
parsedDate?: Date;
|
||||
dateCategory?: 'recent' | 'month' | 'year' | 'older'; // Time range category
|
||||
@@ -22,6 +29,7 @@ export interface TimelineDataPoint {
|
||||
date: string;
|
||||
rating: number;
|
||||
rollingAvg: number;
|
||||
count: number; // Number of reviews in this period
|
||||
}
|
||||
|
||||
export interface ReviewStats {
|
||||
@@ -37,6 +45,21 @@ export interface ReviewStats {
|
||||
negativeReviews: number;
|
||||
responseRate: number;
|
||||
averageResponseTime: string;
|
||||
// Response breakdown
|
||||
responseBreakdown: { answered: number; notAnswered: number };
|
||||
// New trend metrics
|
||||
ratingTrend: {
|
||||
recentAvg: number;
|
||||
olderAvg: number;
|
||||
change: number; // positive = improvement, negative = decline
|
||||
periodLabel: string;
|
||||
};
|
||||
reviewVelocity: {
|
||||
recentCount: number;
|
||||
olderCount: number;
|
||||
changePercent: number; // positive = more reviews, negative = fewer
|
||||
periodLabel: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function calculateReviewStats(reviews: Review[]): ReviewStats {
|
||||
@@ -55,19 +78,21 @@ export function calculateReviewStats(reviews: Review[]): ReviewStats {
|
||||
const totalReviews = reviews.length;
|
||||
|
||||
// Average rating
|
||||
const averageRating = reviews.reduce((sum, r) => sum + r.rating, 0) / totalReviews;
|
||||
const averageRating = totalReviews > 0
|
||||
? reviews.reduce((sum, r) => sum + r.rating, 0) / totalReviews
|
||||
: 0;
|
||||
|
||||
// Sentiment score (% of 4-5 star reviews)
|
||||
const positiveReviews = reviews.filter(r => r.rating >= 4).length;
|
||||
const sentimentScore = (positiveReviews / totalReviews) * 100;
|
||||
const sentimentScore = totalReviews > 0 ? (positiveReviews / totalReviews) * 100 : 0;
|
||||
|
||||
// Photo count (reviews with avatars as proxy)
|
||||
const photoCount = reviews.filter(r => r.avatar_url).length;
|
||||
// Photo count (reviews with actual photos attached)
|
||||
const photoCount = reviews.filter(r => r.photo_urls && r.photo_urls.length > 0).length;
|
||||
|
||||
// Average review length
|
||||
const avgReviewLength = Math.round(
|
||||
reviews.reduce((sum, r) => sum + (r.text?.split(' ').length || 0), 0) / totalReviews
|
||||
);
|
||||
const avgReviewLength = totalReviews > 0
|
||||
? Math.round(reviews.reduce((sum, r) => sum + (r.text?.split(' ').length || 0), 0) / totalReviews)
|
||||
: 0;
|
||||
|
||||
// Recent reviews (last 30 days - simplified check)
|
||||
const recentReviews = reviews.filter(r => {
|
||||
@@ -122,11 +147,50 @@ export function calculateReviewStats(reviews: Review[]): ReviewStats {
|
||||
// Negative reviews count
|
||||
const negativeReviews = reviews.filter(r => r.rating <= 2).length;
|
||||
|
||||
// Response rate (placeholder - would need owner_response field)
|
||||
const responseRate = 0; // TODO: Calculate when owner responses are available
|
||||
// Response breakdown - count answered vs not answered reviews
|
||||
const answeredReviews = reviews.filter(r => r.owner_response?.text).length;
|
||||
const responseBreakdown = {
|
||||
answered: answeredReviews,
|
||||
notAnswered: totalReviews - answeredReviews,
|
||||
};
|
||||
|
||||
// Average response time (placeholder)
|
||||
const averageResponseTime = 'N/A'; // TODO: Calculate when response data is available
|
||||
// Response rate calculated from actual data
|
||||
const responseRate = totalReviews > 0 ? (answeredReviews / totalReviews) * 100 : 0;
|
||||
|
||||
// Average response time (placeholder - would need response timestamps)
|
||||
const averageResponseTime = 'N/A'; // TODO: Calculate when response timestamps are available
|
||||
|
||||
// Rating Trend - compare recent 3 months vs previous 3 months
|
||||
const now = new Date();
|
||||
const threeMonthsAgo = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000);
|
||||
const sixMonthsAgo = new Date(now.getTime() - 180 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const recentReviewsForTrend = reviews.filter(r => r.centerDate && r.centerDate >= threeMonthsAgo);
|
||||
const olderReviewsForTrend = reviews.filter(r => r.centerDate && r.centerDate < threeMonthsAgo && r.centerDate >= sixMonthsAgo);
|
||||
|
||||
const recentAvg = recentReviewsForTrend.length > 0
|
||||
? recentReviewsForTrend.reduce((sum, r) => sum + r.rating, 0) / recentReviewsForTrend.length
|
||||
: 0;
|
||||
const olderAvg = olderReviewsForTrend.length > 0
|
||||
? olderReviewsForTrend.reduce((sum, r) => sum + r.rating, 0) / olderReviewsForTrend.length
|
||||
: 0;
|
||||
|
||||
const ratingTrend = {
|
||||
recentAvg: Math.round(recentAvg * 10) / 10,
|
||||
olderAvg: Math.round(olderAvg * 10) / 10,
|
||||
change: Math.round((recentAvg - olderAvg) * 10) / 10,
|
||||
periodLabel: 'last 3 months vs previous 3 months',
|
||||
};
|
||||
|
||||
// Review Velocity - compare recent 3 months vs previous 3 months
|
||||
const reviewVelocity = {
|
||||
recentCount: recentReviewsForTrend.length,
|
||||
olderCount: olderReviewsForTrend.length,
|
||||
changePercent: olderReviewsForTrend.length > 0
|
||||
? Math.round(((recentReviewsForTrend.length - olderReviewsForTrend.length) / olderReviewsForTrend.length) * 100)
|
||||
: (recentReviewsForTrend.length > 0 ? 100 : 0),
|
||||
periodLabel: 'last 3 months vs previous 3 months',
|
||||
};
|
||||
|
||||
return {
|
||||
totalReviews,
|
||||
@@ -141,6 +205,9 @@ export function calculateReviewStats(reviews: Review[]): ReviewStats {
|
||||
negativeReviews,
|
||||
responseRate,
|
||||
averageResponseTime,
|
||||
responseBreakdown,
|
||||
ratingTrend,
|
||||
reviewVelocity,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -367,9 +434,10 @@ export function calculateTimelineData(reviews: Review[]): TimelineDataPoint[] {
|
||||
|
||||
// Group by month
|
||||
const monthlyData: Record<string, { ratings: number[]; date: Date }> = {};
|
||||
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
|
||||
sortedReviews.forEach(review => {
|
||||
const monthKey = `${review.parsedDate.getFullYear()}-${String(review.parsedDate.getMonth() + 1).padStart(2, '0')}`;
|
||||
const monthKey = `${monthNames[review.parsedDate.getMonth()]} ${review.parsedDate.getFullYear()}`;
|
||||
|
||||
if (!monthlyData[monthKey]) {
|
||||
monthlyData[monthKey] = { ratings: [], date: review.parsedDate };
|
||||
@@ -383,8 +451,17 @@ export function calculateTimelineData(reviews: Review[]): TimelineDataPoint[] {
|
||||
date: monthKey,
|
||||
rating: data.ratings.reduce((a, b) => a + b, 0) / data.ratings.length,
|
||||
rollingAvg: 0, // Will calculate below
|
||||
count: data.ratings.length, // Number of reviews this month
|
||||
}))
|
||||
.sort((a, b) => a.date.localeCompare(b.date));
|
||||
.sort((a, b) => {
|
||||
// Parse "Mon YYYY" format for sorting
|
||||
const parseMonthYear = (d: string) => {
|
||||
const [month, year] = d.split(' ');
|
||||
const monthIndex = monthNames.indexOf(month);
|
||||
return new Date(parseInt(year), monthIndex, 1).getTime();
|
||||
};
|
||||
return parseMonthYear(a.date) - parseMonthYear(b.date);
|
||||
});
|
||||
|
||||
// Calculate 3-month rolling average
|
||||
dataPoints.forEach((point, idx) => {
|
||||
|
||||
Reference in New Issue
Block a user