mastodon-collector/app/templates/flagged.html
Pieter 9919d2fc04 Fix double @ symbols and make status text clickable
- Fixed author display to show single @ (was showing @@account)
- Made status text clickable linking to full Mastodon post
- Fixed author links to point to Mastodon instances instead of Bluesky

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-31 10:03:05 +02:00

769 lines
20 KiB
HTML

{% extends "base.html" %}
{% block title %}Flagged Content{% endblock %}
{% block content %}
<div class="flagged-container">
<!-- Page Header -->
<div class="page-header">
<h1>Flagged Content</h1>
<span class="total-badge">{{ total | format_number }}</span>
</div>
<!-- Filter Bar -->
<div class="filter-bar">
<form method="get" action="{{ url_for('analysis_flagged') }}" class="filter-form">
<div class="filter-group">
<label for="content-type">Type:</label>
<select id="content-type" name="content_type" class="filter-select">
<option value="">All Types</option>
<option value="post" {% if content_type == 'post' %}selected{% endif %}>Post</option>
<option value="reply" {% if content_type == 'reply' %}selected{% endif %}>Reply</option>
<option value="mention" {% if content_type == 'mention' %}selected{% endif %}>Mention</option>
</select>
</div>
<div class="filter-group">
<label for="category">Category:</label>
<select id="category" name="category" class="filter-select">
<option value="">All Categories</option>
{% for cat in categories %}
<option value="{{ cat }}" {% if category == cat %}selected{% endif %}>{{ cat }}</option>
{% endfor %}
</select>
</div>
<div class="filter-group">
<label for="account-did">Account:</label>
<select id="account-did" name="account_did" class="filter-select">
<option value="">All Accounts</option>
{% for acc in accounts %}
<option value="{{ acc.did }}" {% if account_did == acc.did %}selected{% endif %}>{{ acc.handle }}</option>
{% endfor %}
</select>
</div>
<div class="filter-group">
<label for="threshold">Threshold:</label>
<input type="number" id="threshold" name="threshold" min="0.0" max="1.0" step="0.1" value="{{ threshold or 0.5 }}" class="filter-input" placeholder="0.5">
</div>
<div class="filter-group">
<label for="review-status">Review Status:</label>
<select id="review-status" name="review_status" class="filter-select">
<option value="">All</option>
<option value="unreviewed" {% if review_status == 'unreviewed' %}selected{% endif %}>Unreviewed</option>
<option value="correct" {% if review_status == 'correct' %}selected{% endif %}>✓ Correct</option>
<option value="incorrect" {% if review_status == 'incorrect' %}selected{% endif %}>✗ Incorrect</option>
<option value="unsure" {% if review_status == 'unsure' %}selected{% endif %}>? Unsure</option>
</select>
</div>
<div class="filter-group">
<label for="date-from">From Date:</label>
<input type="date" id="date-from" name="date_from" value="{{ date_from or '' }}" class="filter-input">
</div>
<div class="filter-group">
<label for="date-to">To Date:</label>
<input type="date" id="date-to" name="date_to" value="{{ date_to or '' }}" class="filter-input">
</div>
<button type="submit" class="btn-apply">Apply Filters</button>
</form>
</div>
<!-- Content Table -->
{% if items %}
{% macro sort_header(col, label) %}
{% set new_dir = 'asc' if (sort == col and direction == 'desc') else 'desc' %}
<a href="{{ url_for('analysis_flagged', content_type=content_type, category=category, account_did=account_did, threshold=threshold, review_status=review_status, date_from=date_from, date_to=date_to, sort=col, dir=new_dir) }}" class="sort-link">
{{ label }}
{% if sort == col %}
<span class="sort-icon">{{ '▼' if direction == 'desc' else '▲' }}</span>
{% endif %}
</a>
{% endmacro %}
<div class="table-wrapper">
<table class="flagged-table">
<thead>
<tr>
<th>Type</th>
<th>{{ sort_header('author_handle', 'Author') }}</th>
<th>Content</th>
<th>{{ sort_header('overall', 'Score') }}</th>
<th>Category</th>
<th>{{ sort_header('created_at', 'Created') }}</th>
<th>Review</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr class="item-row">
<!-- Type Badge -->
<td class="col-type">
<span class="badge badge-{{ item.item_type }}">
{% if item.item_type == 'post' %}
Post
{% elif item.item_type == 'reply' %}
Reply
{% elif item.item_type == 'mention' %}
Mention
{% endif %}
</span>
</td>
<!-- Author -->
<td class="col-author">
{% if item.author_handle %}
<a href="https://{{ item.author_instance }}/@{{ item.author_username }}" target="_blank" rel="noopener" class="author-link">
{{ item.author_handle }}
</a>
{% endif %}
</td>
<!-- Content Text -->
<td class="col-text">
{% if item.url %}
<a href="{{ item.url }}" target="_blank" rel="noopener" class="content-link">
{{ item.text | truncate_text(200) }}
</a>
{% else %}
<span class="content-text">{{ item.text | truncate_text(200) }}</span>
{% endif %}
</td>
<!-- Score with Bar -->
<td class="col-score">
<div class="score-bar-container">
{% set score_pct = (item.overall * 100) | int %}
{% if item.overall < 0.3 %}
{% set bar_class = 'score-bar-low' %}
{% elif item.overall < 0.6 %}
{% set bar_class = 'score-bar-medium' %}
{% else %}
{% set bar_class = 'score-bar-high' %}
{% endif %}
<div class="score-bar {{ bar_class }}" style="width: {{ score_pct }}%"></div>
<span class="score-number">{{ "%.2f" | format(item.overall) }}</span>
</div>
</td>
<!-- Top Category -->
<td class="col-category">
{% if item.top_category %}
<span class="badge badge-category">{{ item.top_category }}</span>
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<!-- Created Time -->
<td class="col-created">
<span class="time-ago" title="{{ item.created_at }}">
{{ item.created_at | time_ago }}
</span>
</td>
<!-- Review Buttons -->
<td class="col-review">
<div class="review-buttons" data-item-id="{{ item.item_id }}" data-source-type="{{ item.source_type }}" data-current-status="{{ item.review_status or '' }}">
<button class="btn-review btn-correct {% if item.review_status == 'correct' %}active{% endif %}" data-status="correct" title="Mark as correctly flagged"></button>
<button class="btn-review btn-incorrect {% if item.review_status == 'incorrect' %}active{% endif %}" data-status="incorrect" title="Mark as incorrectly flagged"></button>
<button class="btn-review btn-unsure {% if item.review_status == 'unsure' %}active{% endif %}" data-status="unsure" title="Mark as unsure">?</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if total_pages > 1 %}
<div class="pagination">
{% if page > 1 %}
<a href="{{ url_for('analysis_flagged', page=1, content_type=content_type, category=category, account_did=account_did, threshold=threshold, review_status=review_status, date_from=date_from, date_to=date_to, sort=sort, dir=direction) }}" class="btn-pagination">First</a>
<a href="{{ url_for('analysis_flagged', page=page-1, content_type=content_type, category=category, account_did=account_did, threshold=threshold, review_status=review_status, date_from=date_from, date_to=date_to, sort=sort, dir=direction) }}" class="btn-pagination">Previous</a>
{% endif %}
<span class="pagination-info">Page {{ page }} of {{ total_pages }}</span>
{% if page < total_pages %}
<a href="{{ url_for('analysis_flagged', page=page+1, content_type=content_type, category=category, account_did=account_did, threshold=threshold, review_status=review_status, date_from=date_from, date_to=date_to, sort=sort, dir=direction) }}" class="btn-pagination">Next</a>
<a href="{{ url_for('analysis_flagged', page=total_pages, content_type=content_type, category=category, account_did=account_did, threshold=threshold, review_status=review_status, date_from=date_from, date_to=date_to, sort=sort, dir=direction) }}" class="btn-pagination">Last</a>
{% endif %}
</div>
{% endif %}
{% else %}
<!-- Empty State -->
<div class="empty-state">
<p class="empty-icon"></p>
<p class="empty-text">No flagged content found</p>
<p class="empty-subtext">Try adjusting your filters or threshold</p>
</div>
{% endif %}
</div>
{% endblock %}
{% block extra_css %}
<style>
:root {
--dark-bg: #1a1a2e;
--dark-card: #16213e;
--dark-nav: #0f3460;
--dark-text: #e0e0e0;
--accent-primary: #00b4d8;
--badge-post: #00b4d8;
--badge-reply: #9b59b6;
--badge-mention: #2ecc71;
--tox-low: #2ecc71;
--tox-medium: #f39c12;
--tox-high: #e74c3c;
}
.flagged-container {
padding: 2rem;
max-width: 1400px;
margin: 0 auto;
}
/* Page Header */
.page-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 2rem;
}
.page-header h1 {
font-size: 2rem;
font-weight: 700;
color: var(--dark-text);
margin: 0;
}
.total-badge {
background: var(--accent-primary);
color: var(--dark-bg);
font-weight: 600;
padding: 0.5rem 1rem;
border-radius: 2rem;
font-size: 0.9rem;
}
/* Filter Bar */
.filter-bar {
background: var(--dark-card);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 2rem;
}
.filter-form {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: flex-end;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
flex: 1;
min-width: 150px;
}
.filter-group label {
font-size: 0.9rem;
font-weight: 600;
color: var(--dark-text);
}
.filter-select,
.filter-input {
background: var(--dark-bg);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 0.375rem;
color: var(--dark-text);
padding: 0.625rem;
font-size: 0.9rem;
font-family: inherit;
}
.filter-select:hover,
.filter-input:hover {
border-color: rgba(255, 255, 255, 0.3);
}
.filter-select:focus,
.filter-input:focus {
outline: none;
border-color: var(--accent-primary);
background: var(--dark-bg);
color: var(--dark-text);
}
.btn-apply {
background: var(--accent-primary);
color: var(--dark-bg);
border: none;
border-radius: 0.375rem;
padding: 0.625rem 1.25rem;
font-weight: 600;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s ease;
}
.btn-apply:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.btn-apply:active {
transform: translateY(0);
}
/* Table Wrapper */
.table-wrapper {
background: var(--dark-card);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 0.5rem;
overflow-x: auto;
margin-bottom: 2rem;
}
/* Table Styles */
.flagged-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.flagged-table thead {
background: rgba(0, 0, 0, 0.3);
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
}
.flagged-table th {
padding: 1rem;
text-align: left;
font-weight: 600;
color: var(--dark-text);
white-space: nowrap;
}
/* Sort Links */
.sort-link {
color: var(--dark-text);
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.25rem;
transition: color 0.2s ease;
}
.sort-link:hover {
color: var(--accent-primary);
}
.sort-icon {
font-size: 0.8rem;
opacity: 0.7;
}
.flagged-table td {
padding: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
color: var(--dark-text);
}
.flagged-table tbody tr:hover {
background: rgba(0, 180, 216, 0.05);
}
/* Column Styles */
.col-type {
width: 90px;
}
.col-author {
width: 200px;
}
.col-text {
min-width: 300px;
}
.col-score {
width: 150px;
}
.col-category {
width: 140px;
}
.col-created {
width: 120px;
}
.col-review {
width: 130px;
}
/* Badges */
.badge {
display: inline-block;
padding: 0.35rem 0.75rem;
border-radius: 0.25rem;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.badge-post {
background: rgba(0, 180, 216, 0.2);
color: var(--badge-post);
}
.badge-reply {
background: rgba(155, 89, 182, 0.2);
color: var(--badge-reply);
}
.badge-mention {
background: rgba(46, 204, 113, 0.2);
color: var(--badge-mention);
}
.badge-category {
background: rgba(0, 180, 216, 0.15);
color: var(--accent-primary);
}
/* Author Links */
.author-link {
color: var(--accent-primary);
text-decoration: none;
transition: color 0.2s ease;
}
.author-link:hover {
color: rgba(0, 180, 216, 0.8);
text-decoration: underline;
}
.mention-arrow {
color: rgba(255, 255, 255, 0.3);
margin: 0 0.5rem;
}
/* Content Link */
.content-link {
color: var(--dark-text);
text-decoration: none;
transition: color 0.2s ease;
}
.content-link:hover {
color: var(--accent-primary);
}
.content-text {
color: rgba(255, 255, 255, 0.7);
}
/* Score Bar */
.score-bar-container {
position: relative;
height: 30px;
background: rgba(255, 255, 255, 0.05);
border-radius: 0.25rem;
overflow: hidden;
display: flex;
align-items: center;
padding: 0 0.5rem;
}
.score-bar {
position: absolute;
height: 100%;
left: 0;
top: 0;
transition: width 0.3s ease;
}
.score-bar-low {
background: linear-gradient(90deg, rgba(46, 204, 113, 0.3), rgba(46, 204, 113, 0.5));
}
.score-bar-medium {
background: linear-gradient(90deg, rgba(243, 156, 18, 0.3), rgba(243, 156, 18, 0.5));
}
.score-bar-high {
background: linear-gradient(90deg, rgba(231, 76, 60, 0.3), rgba(231, 76, 60, 0.5));
}
.score-number {
position: relative;
z-index: 1;
font-weight: 600;
color: var(--dark-text);
font-size: 0.85rem;
}
/* Time Ago */
.time-ago {
color: rgba(255, 255, 255, 0.6);
font-size: 0.85rem;
cursor: help;
}
.time-ago:hover {
color: var(--dark-text);
}
.text-muted {
color: rgba(255, 255, 255, 0.3);
}
/* Review Buttons */
.review-buttons {
display: flex;
gap: 0.25rem;
justify-content: center;
}
.btn-review {
background: var(--dark-bg);
border: 1px solid rgba(255, 255, 255, 0.2);
color: var(--dark-text);
width: 32px;
height: 32px;
border-radius: 0.25rem;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.btn-review:hover {
transform: translateY(-2px);
border-color: rgba(255, 255, 255, 0.4);
}
.btn-correct:hover, .btn-correct.active {
background: var(--tox-low);
border-color: var(--tox-low);
color: var(--dark-bg);
}
.btn-incorrect:hover, .btn-incorrect.active {
background: var(--tox-high);
border-color: var(--tox-high);
color: var(--dark-bg);
}
.btn-unsure:hover, .btn-unsure.active {
background: var(--tox-medium);
border-color: var(--tox-medium);
color: var(--dark-bg);
}
.btn-review.active {
font-weight: 700;
transform: none;
}
.btn-review.active:hover {
opacity: 0.8;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 4rem 2rem;
background: var(--dark-card);
border: 2px dashed rgba(255, 255, 255, 0.2);
border-radius: 0.5rem;
}
.empty-icon {
font-size: 3rem;
color: rgba(255, 255, 255, 0.2);
margin: 0 0 1rem 0;
}
.empty-text {
font-size: 1.2rem;
font-weight: 600;
color: var(--dark-text);
margin: 0 0 0.5rem 0;
}
.empty-subtext {
color: rgba(255, 255, 255, 0.5);
margin: 0;
}
/* Pagination */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin-top: 2rem;
}
.pagination-info {
color: var(--dark-text);
font-weight: 500;
min-width: 150px;
text-align: center;
}
.btn-pagination {
background: var(--dark-card);
color: var(--accent-primary);
border: 1px solid var(--accent-primary);
padding: 0.5rem 1rem;
border-radius: 0.375rem;
text-decoration: none;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.2s ease;
cursor: pointer;
}
.btn-pagination:hover {
background: var(--accent-primary);
color: var(--dark-bg);
}
.btn-pagination:active {
transform: scale(0.98);
}
/* Responsive */
@media (max-width: 1024px) {
.filter-form {
flex-direction: column;
}
.filter-group {
width: 100%;
}
.col-text {
min-width: 250px;
}
}
@media (max-width: 768px) {
.flagged-container {
padding: 1rem;
}
.page-header {
flex-direction: column;
align-items: flex-start;
}
.page-header h1 {
font-size: 1.5rem;
}
.table-wrapper {
font-size: 0.8rem;
}
.flagged-table th,
.flagged-table td {
padding: 0.75rem 0.5rem;
}
.col-author,
.col-text {
min-width: 180px;
}
.col-created {
width: 100px;
}
}
</style>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Handle review button clicks
document.querySelectorAll('.btn-review').forEach(button => {
button.addEventListener('click', async function() {
const buttonContainer = this.parentElement;
const itemId = buttonContainer.dataset.itemId;
const sourceType = buttonContainer.dataset.sourceType;
const currentStatus = buttonContainer.dataset.currentStatus;
const clickedStatus = this.dataset.status;
// If clicking the same status, do nothing (it's already set)
if (currentStatus === clickedStatus) {
return;
}
// Disable all buttons while processing
buttonContainer.querySelectorAll('.btn-review').forEach(btn => btn.disabled = true);
try {
const response = await fetch('/api/review/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
item_id: itemId,
source_type: sourceType,
review_status: clickedStatus,
}),
});
if (response.ok) {
// Update current status
buttonContainer.dataset.currentStatus = clickedStatus;
// Remove active class from all buttons
buttonContainer.querySelectorAll('.btn-review').forEach(btn => {
btn.classList.remove('active');
});
// Add active class to clicked button
this.classList.add('active');
// Re-enable all buttons
buttonContainer.querySelectorAll('.btn-review').forEach(btn => btn.disabled = false);
} else {
// Re-enable buttons on error
buttonContainer.querySelectorAll('.btn-review').forEach(btn => btn.disabled = false);
alert('Failed to submit review. Please try again.');
}
} catch (error) {
// Re-enable buttons on error
buttonContainer.querySelectorAll('.btn-review').forEach(btn => btn.disabled = false);
console.error('Error submitting review:', error);
alert('Failed to submit review. Please try again.');
}
});
});
});
</script>
{% endblock %}