mastodon-collector/app/templates/analysis.html

735 lines
19 KiB
HTML
Raw Normal View History

{% extends "base.html" %}
{% block title %}Toxicity Analysis Dashboard{% endblock %}
{% block extra_css %}
<style>
/* Color scheme */
:root {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--nav-bg: #0f3460;
--text-primary: #e0e0e0;
--text-secondary: #b0b0b0;
--accent: #00b4d8;
--danger: #e74c3c;
--warning: #f39c12;
--success: #27ae60;
--category-1: #00b4d8;
--category-2: #e67e22;
--category-3: #9b59b6;
--category-4: #1abc9c;
--category-5: #e74c3c;
--category-6: #f39c12;
--category-7: #3498db;
--category-8: #2ecc71;
}
/* Layout */
.page-header {
margin-bottom: 2rem;
}
.page-header h1 {
font-size: 2.5rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.page-header .subtitle {
font-size: 1rem;
color: var(--text-secondary);
}
/* Grid layout */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
/* Stat cards */
.stat-card {
background-color: var(--bg-secondary);
border-radius: 8px;
padding: 1.5rem;
border-left: 4px solid var(--accent);
display: flex;
flex-direction: column;
justify-content: space-between;
}
.stat-card.danger {
border-left-color: var(--danger);
}
.stat-card.warning {
border-left-color: var(--warning);
}
.stat-card-label {
font-size: 0.875rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 0.75rem;
}
.stat-card-value {
font-size: 1.75rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.stat-card-detail {
font-size: 0.875rem;
color: var(--text-secondary);
}
/* Percentage bar */
.percentage-bar {
width: 100%;
height: 8px;
background-color: rgba(224, 224, 224, 0.1);
border-radius: 4px;
overflow: hidden;
margin-top: 0.75rem;
}
.percentage-bar-fill {
height: 100%;
background-color: var(--accent);
border-radius: 4px;
transition: width 0.3s ease;
}
.stat-card.danger .percentage-bar-fill {
background-color: var(--danger);
}
/* Cards */
.card {
background-color: var(--bg-secondary);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 2rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 1.5rem;
border-bottom: 2px solid rgba(0, 180, 216, 0.2);
padding-bottom: 0.75rem;
}
/* Chart containers */
.chart-container {
position: relative;
height: 400px;
width: 100%;
}
.chart-container.horizontal {
height: 300px;
}
/* Links section */
.quick-links {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.quick-link {
display: inline-flex;
align-items: center;
padding: 0.75rem 1.25rem;
background-color: rgba(0, 180, 216, 0.1);
border: 1px solid var(--accent);
border-radius: 6px;
color: var(--accent);
text-decoration: none;
font-weight: 500;
transition: all 0.3s ease;
}
.quick-link:hover {
background-color: var(--accent);
color: var(--bg-primary);
}
.quick-link::after {
content: " →";
margin-left: 0.5rem;
}
/* Runs table */
.runs-table {
width: 100%;
border-collapse: collapse;
}
.runs-table thead {
background-color: rgba(0, 180, 216, 0.1);
border-bottom: 2px solid var(--accent);
}
.runs-table th {
padding: 1rem;
text-align: left;
font-weight: 600;
color: var(--text-primary);
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.runs-table td {
padding: 0.75rem 1rem;
border-bottom: 1px solid rgba(224, 224, 224, 0.1);
color: var(--text-secondary);
font-size: 0.875rem;
}
.runs-table tbody tr:hover {
background-color: rgba(0, 180, 216, 0.05);
}
/* Status badge */
.status-badge {
display: inline-block;
padding: 0.35rem 0.75rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-badge.completed {
background-color: rgba(39, 174, 96, 0.2);
color: var(--success);
}
.status-badge.in-progress {
background-color: rgba(243, 156, 18, 0.2);
color: var(--warning);
}
.status-badge.failed {
background-color: rgba(231, 76, 60, 0.2);
color: var(--danger);
}
/* Responsive */
@media (max-width: 768px) {
.page-header h1 {
font-size: 1.75rem;
}
.stats-grid {
grid-template-columns: 1fr;
}
.chart-container {
height: 300px;
}
.quick-links {
flex-direction: column;
}
.quick-link {
width: 100%;
justify-content: center;
}
.runs-table th,
.runs-table td {
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
}
}
/* Empty state */
.empty-state {
text-align: center;
padding: 2rem;
color: var(--text-secondary);
}
.empty-state p {
margin: 0;
}
/* Number formatting */
.number-highlight {
color: var(--accent);
font-weight: 600;
}
.percentage-highlight {
color: var(--warning);
font-weight: 600;
}
.percentage-highlight.high {
color: var(--danger);
}
</style>
{% endblock %}
{% block content %}
<div class="page-header">
<h1>Toxicity Analysis</h1>
<p class="subtitle">
{{ stats.total_scored_posts | format_number }} / {{ stats.total_posts | format_number }} posts scored
<span style="margin: 0 0.5rem;"></span>
{{ stats.total_scored_mentions | format_number }} / {{ stats.total_mentions | format_number }} mentions scored
</p>
</div>
<!-- Stats Grid -->
<div class="stats-grid">
<!-- Total Scored Card -->
<div class="stat-card">
<div>
<div class="stat-card-label">Total Scored</div>
<div class="stat-card-value">{{ (stats.total_scored_posts + stats.total_scored_mentions) | format_number }}</div>
</div>
<div class="stat-card-detail">
{{ stats.total_scored_posts | format_number }} posts
<span style="color: var(--text-secondary); margin: 0 0.25rem;">+</span>
{{ stats.total_scored_mentions | format_number }} mentions
</div>
</div>
<!-- Flagged Posts Card -->
<div class="stat-card {% if stats.flagged_posts > 0 and (stats.flagged_posts / (stats.total_scored_posts or 1)) > 0.05 %}danger{% endif %}">
<div>
<div class="stat-card-label">Flagged Posts</div>
<div class="stat-card-value">{{ stats.flagged_posts | format_number }}</div>
</div>
<div class="stat-card-detail">
<span class="{% if stats.flagged_posts > 0 and (stats.flagged_posts / (stats.total_scored_posts or 1)) > 0.05 %}percentage-highlight high{% else %}percentage-highlight{% endif %}">
{{ "%.2f" | format(100.0 * stats.flagged_posts / (stats.total_scored_posts or 1)) }}%
</span>
of scored posts
</div>
<div class="percentage-bar">
<div class="percentage-bar-fill" style="width: {{ 100.0 * stats.flagged_posts / (stats.total_scored_posts or 1) }}%"></div>
</div>
</div>
<!-- Flagged Mentions Card -->
<div class="stat-card">
<div>
<div class="stat-card-label">Flagged Mentions</div>
<div class="stat-card-value">{{ stats.flagged_mentions | format_number }}</div>
</div>
<div class="stat-card-detail">
<span class="percentage-highlight">
{{ "%.2f" | format(100.0 * stats.flagged_mentions / (stats.total_scored_mentions or 1)) }}%
</span>
of scored mentions
</div>
<div class="percentage-bar">
<div class="percentage-bar-fill" style="width: {{ 100.0 * stats.flagged_mentions / (stats.total_scored_mentions or 1) }}%"></div>
</div>
</div>
<!-- Avg Toxicity Card -->
<div class="stat-card">
<div>
<div class="stat-card-label">Average Toxicity</div>
<div class="stat-card-value">{{ "%.1f" | format(100.0 * ((stats.avg_toxicity_posts + stats.avg_toxicity_mentions) / 2.0)) }}%</div>
</div>
<div class="stat-card-detail">
Posts: {{ "%.2f" | format(100.0 * stats.avg_toxicity_posts) }}%
<span style="color: var(--text-secondary); margin: 0 0.25rem;"></span>
Mentions: {{ "%.2f" | format(100.0 * stats.avg_toxicity_mentions) }}%
</div>
<div class="percentage-bar">
<div class="percentage-bar-fill" style="width: {{ 100.0 * ((stats.avg_toxicity_posts + stats.avg_toxicity_mentions) / 2.0) }}%"></div>
</div>
</div>
</div>
<!-- Trend Chart -->
<div class="card">
<div class="card-title">Toxicity Trends Over Time</div>
<div class="chart-container">
<canvas id="trendChart"></canvas>
</div>
</div>
<!-- Category Breakdown -->
<div class="card">
<div class="card-title">Toxicity by Category</div>
<div class="chart-container horizontal">
<canvas id="categoriesChart"></canvas>
</div>
</div>
<!-- Recent Analysis Runs -->
<div class="card">
<div class="card-title">Recent Analysis Runs</div>
{% if runs %}
<div style="overflow-x: auto;">
<table class="runs-table">
<thead>
<tr>
<th>Started</th>
<th>Duration</th>
<th>Posts Scored</th>
<th>Mentions Scored</th>
<th>Errors</th>
<th>Cost</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for run in runs[:5] %}
<tr>
<td>{{ run.started_at | time_ago }}</td>
<td>{% if run.duration_secs is not none %}{{ "%.0f" | format(run.duration_secs | float) }}s{% else %}—{% endif %}</td>
<td>{{ run.posts_scored | format_number }}</td>
<td>{{ run.mentions_scored | format_number }}</td>
<td>{{ run.errors }}</td>
<td>${{ "%.4f" | format(run.cost_usd | default(0) | float) }}</td>
<td>
<span class="status-badge {% if run.status == 'completed' %}completed{% elif run.status == 'in_progress' %}in-progress{% elif run.status == 'failed' %}failed{% endif %}">
{{ run.status }}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="empty-state">
<p>No analysis runs yet. Start a new analysis to see results here.</p>
</div>
{% endif %}
</div>
<!-- Quick Links -->
<div style="margin-top: 2rem;">
<div class="quick-links">
<a href="{{ url_for('analysis.flagged') }}" class="quick-link">View Flagged Content</a>
<a href="{{ url_for('analysis.accounts') }}" class="quick-link">Account Breakdown</a>
</div>
</div>
<!-- Chart.js Script -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<script>
// Chart color scheme
const chartColors = {
accent: '#00b4d8',
orange: '#e67e22',
gridLine: '#2a2a3e',
text: '#e0e0e0',
categories: [
'#00b4d8', // 1
'#e67e22', // 2
'#9b59b6', // 3
'#1abc9c', // 4
'#e74c3c', // 5
'#f39c12', // 6
'#3498db', // 7
'#2ecc71' // 8
]
};
// Trend Chart
{% if trend_json %}
const trendData = {{ trend_json | safe }};
const trendCtx = document.getElementById('trendChart').getContext('2d');
const trendChart = new Chart(trendCtx, {
type: 'line',
data: {
labels: trendData.map(d => d.week),
datasets: [
{
label: 'Avg Post Toxicity',
data: trendData.map(d => d.avg_post_toxicity),
borderColor: chartColors.accent,
backgroundColor: 'rgba(0, 180, 216, 0.05)',
borderWidth: 2,
tension: 0.4,
fill: true,
yAxisID: 'y',
pointBackgroundColor: chartColors.accent,
pointBorderColor: '#1a1a2e',
pointBorderWidth: 2,
pointRadius: 5,
pointHoverRadius: 7
},
{
label: 'Avg Mention Toxicity',
data: trendData.map(d => d.avg_mention_toxicity),
borderColor: chartColors.orange,
backgroundColor: 'rgba(230, 126, 34, 0.05)',
borderWidth: 2,
tension: 0.4,
fill: true,
yAxisID: 'y',
pointBackgroundColor: chartColors.orange,
pointBorderColor: '#1a1a2e',
pointBorderWidth: 2,
pointRadius: 5,
pointHoverRadius: 7
},
{
label: 'Flagged Posts',
data: trendData.map(d => d.flagged_posts),
type: 'bar',
borderColor: 'rgba(231, 76, 60, 0.5)',
backgroundColor: 'rgba(231, 76, 60, 0.2)',
yAxisID: 'y1',
borderWidth: 1,
barThickness: 8,
categoryPercentage: 0.8,
maxBarThickness: 15
},
{
label: 'Flagged Mentions',
data: trendData.map(d => d.flagged_mentions),
type: 'bar',
borderColor: 'rgba(243, 156, 18, 0.5)',
backgroundColor: 'rgba(243, 156, 18, 0.2)',
yAxisID: 'y1',
borderWidth: 1,
barThickness: 8,
categoryPercentage: 0.8,
maxBarThickness: 15
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
},
plugins: {
legend: {
display: true,
labels: {
color: chartColors.text,
usePointStyle: true,
padding: 15,
font: {
size: 12
}
}
},
tooltip: {
backgroundColor: '#0f3460',
titleColor: chartColors.text,
bodyColor: chartColors.text,
borderColor: chartColors.accent,
borderWidth: 1,
padding: 10,
displayColors: true
}
},
scales: {
x: {
grid: {
color: chartColors.gridLine,
drawBorder: false
},
ticks: {
color: chartColors.text,
font: {
size: 11
}
}
},
y: {
type: 'linear',
display: true,
position: 'left',
min: 0,
max: 1,
ticks: {
color: chartColors.text,
font: {
size: 11
},
callback: function(value) {
return (value * 100).toFixed(0) + '%';
}
},
grid: {
color: chartColors.gridLine,
drawBorder: false
},
title: {
display: true,
text: 'Toxicity Score',
color: chartColors.text,
font: {
size: 12,
weight: 'bold'
}
}
},
y1: {
type: 'linear',
display: true,
position: 'right',
grid: {
drawOnChartArea: false
},
ticks: {
color: chartColors.text,
font: {
size: 11
}
},
title: {
display: true,
text: 'Flagged Count',
color: chartColors.text,
font: {
size: 12,
weight: 'bold'
}
}
}
}
}
});
{% endif %}
// Category Chart
{% if categories_json %}
const categoriesData = {{ categories_json | safe }};
const categoryNames = {{ categories | tojson | safe }};
// Calculate dynamic max for better readability with smart scaling
const categoryValues = categoryNames.map(cat => categoriesData[cat] || 0);
const maxCategoryValue = Math.max(...categoryValues);
// Smart scaling: round up to next nice number with 20% padding
let dynamicMax;
const paddedMax = maxCategoryValue * 1.2;
if (paddedMax <= 0.01) {
dynamicMax = 0.01; // 1%
} else if (paddedMax <= 0.02) {
dynamicMax = 0.02; // 2%
} else if (paddedMax <= 0.05) {
dynamicMax = 0.05; // 5%
} else if (paddedMax <= 0.1) {
dynamicMax = 0.1; // 10%
} else if (paddedMax <= 0.2) {
dynamicMax = 0.2; // 20%
} else if (paddedMax <= 0.5) {
dynamicMax = 0.5; // 50%
} else {
dynamicMax = 1.0; // 100%
}
const categoriesCtx = document.getElementById('categoriesChart').getContext('2d');
const categoriesChart = new Chart(categoriesCtx, {
type: 'bar',
data: {
labels: categoryNames,
datasets: [
{
label: 'Average Toxicity Score',
data: categoryValues,
backgroundColor: chartColors.categories,
borderColor: chartColors.categories.map(c => c.replace('0.', '1.')),
borderWidth: 1.5,
borderRadius: 4
}
]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: '#0f3460',
titleColor: chartColors.text,
bodyColor: chartColors.text,
borderColor: chartColors.accent,
borderWidth: 1,
padding: 10,
callbacks: {
label: function(context) {
return 'Score: ' + (context.parsed.x * 100).toFixed(2) + '%';
}
}
}
},
scales: {
x: {
min: 0,
max: dynamicMax,
grid: {
color: chartColors.gridLine,
drawBorder: false
},
ticks: {
color: chartColors.text,
font: {
size: 11
},
callback: function(value) {
// Use more decimal places for small values
if (dynamicMax <= 0.01) {
return (value * 100).toFixed(2) + '%';
} else if (dynamicMax <= 0.1) {
return (value * 100).toFixed(1) + '%';
} else {
return (value * 100).toFixed(0) + '%';
}
}
},
title: {
display: true,
text: 'Average Toxicity',
color: chartColors.text,
font: {
size: 12,
weight: 'bold'
}
}
},
y: {
grid: {
drawOnChartArea: false,
drawBorder: false
},
ticks: {
color: chartColors.text,
font: {
size: 11
}
}
}
}
}
});
{% endif %}
</script>
{% endblock %}