Add manual AI analysis trigger for existing feedback

Implement dashboard functionality to manually trigger AI analysis for
feedback that was submitted before Phase 4 or failed analysis. Adds
detection mechanism to identify which feedback has been analyzed.

Features:
- Manual analysis trigger route: POST /feedback/{id}/analyze
- Detection of whether feedback has been analyzed (analysis.md exists)
- Dashboard UI button showing "Analyze" or "Re-analyze"
- Visual feedback for feedback without text content
- Comprehensive error handling and flash messages

Implementation:
- app/routes/dashboard.py: Added trigger_analysis() route handler
- app/routes/dashboard.py: Updated detail() to pass analysis status
- app/services/feedback_storage.py: Added has_analysis() helper method
- app/templates/dashboard/detail.html: Added analyze button UI
- tests/contract/test_dashboard_routes.py: Added 3 new contract tests

Testing:
- test_post_trigger_analysis_success: Successful manual analysis
- test_post_trigger_analysis_no_content: Reject empty content
- test_post_trigger_analysis_unauthenticated: Auth required
- All 49 tests passing (1 skipped)

User Experience:
- Green box with "Analyze" button for unanalyzed feedback
- Blue box with "Re-analyze" button for already analyzed feedback
- Red box with info message for feedback without text content
- Flash messages show success/error after analysis

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-16 21:26:25 +02:00
co-authored by Claude
parent 1bb117fd98
commit 94c6187bb2
4 changed files with 245 additions and 2 deletions
+92 -2
View File
@@ -1,7 +1,8 @@
"""Dashboard routes - product owner feedback management"""
from flask import Blueprint, render_template, request, redirect, url_for, flash, send_file, abort
from flask import Blueprint, render_template, request, redirect, url_for, flash, send_file, abort, current_app
from flask_login import login_required, current_user
from app.services.feedback_storage import FeedbackStorageService
from app.services.ai_analyzer import ClaudeAnalyzer
from app.models.product import Product
import os
import mimetypes
@@ -140,10 +141,18 @@ def detail(feedback_id):
# Load product info
product = Product.get_by_id(actual_product_id)
# Check if analysis exists
has_analysis = FeedbackStorageService.has_analysis(actual_product_id, feedback_id)
# Check if feedback can be analyzed (has text content)
can_analyze = bool(feedback_data.get('content'))
return render_template(
'dashboard/detail.html',
feedback=feedback_data,
product=product
product=product,
has_analysis=has_analysis,
can_analyze=can_analyze
)
@@ -195,6 +204,87 @@ def update_status(feedback_id):
return redirect(url_for('dashboard.detail', feedback_id=feedback_id))
@bp.route('/feedback/<feedback_id>/analyze', methods=['POST'])
@login_required
def trigger_analysis(feedback_id):
"""Manually trigger AI analysis for feedback
Args:
feedback_id: Feedback ID to analyze
Returns:
Redirect to detail page with flash message
"""
# Find feedback globally first
all_products = Product.get_all()
actual_product_id = None
feedback_data = None
for product in all_products:
feedback_data = FeedbackStorageService.load_feedback_detail(product.product_id, feedback_id)
if feedback_data:
actual_product_id = product.product_id
break
# If not found globally, return 404
if not actual_product_id or not feedback_data:
abort(404)
# Check if user has access to this product
if not check_product_access(actual_product_id):
abort(403)
# Check if feedback has text content
if not feedback_data.get('content'):
flash('Cannot analyze feedback without text content', 'error')
return redirect(url_for('dashboard.detail', feedback_id=feedback_id))
# Get product info for owner language
product = Product.get_by_id(actual_product_id)
try:
# Update status to "analyzing"
FeedbackStorageService.update_feedback_status_by_id(
actual_product_id, feedback_id, 'analyzing'
)
# Get API key from environment
api_key = os.getenv('ANTHROPIC_API_KEY')
if not api_key:
raise Exception("ANTHROPIC_API_KEY not configured")
# Initialize analyzer
analyzer = ClaudeAnalyzer(api_key=api_key)
# Analyze feedback
result = analyzer.analyze_feedback(
feedback_text=feedback_data['content'],
target_language=product.owner_language,
product_id=actual_product_id
)
# Save analysis results
FeedbackStorageService.save_analysis(actual_product_id, feedback_id, result)
# Update status to "analyzed"
FeedbackStorageService.update_feedback_status_by_id(
actual_product_id, feedback_id, 'analyzed'
)
flash('Analysis completed successfully', 'success')
except Exception as e:
# Update status to "analysis_failed" on error
FeedbackStorageService.update_feedback_status_by_id(
actual_product_id, feedback_id, 'analysis_failed'
)
current_app.logger.error(f"Manual analysis failed for feedback {feedback_id}: {e}")
flash(f'Analysis failed: {str(e)}', 'error')
return redirect(url_for('dashboard.detail', feedback_id=feedback_id))
@bp.route('/feedback/<feedback_id>/attachment/<filename>')
@login_required
def download_attachment(feedback_id, filename):
+17
View File
@@ -529,3 +529,20 @@ class FeedbackStorageService:
"""
# Use the raw analysis from Claude, which is already formatted
return analysis_result.raw_analysis
@staticmethod
def has_analysis(product_id, feedback_id):
"""Check if feedback has been analyzed
Args:
product_id: Product ID
feedback_id: Feedback ID
Returns:
bool: True if analysis.md exists, False otherwise
"""
data_dir = current_app.config['DATA_DIR']
analysis_file = os.path.join(
data_dir, 'products', product_id, 'feedback', feedback_id, 'analysis.md'
)
return os.path.exists(analysis_file)
+24
View File
@@ -67,6 +67,30 @@
</form>
</div>
<!-- AI Analysis Trigger -->
{% if can_analyze %}
<div style="margin: 20px 0; padding: 15px; {% if has_analysis %}background: #d1ecf1;{% else %}background: #d4edda;{% endif %} border-radius: 5px;">
<form method="post" action="{{ url_for('dashboard.trigger_analysis', feedback_id=feedback.feedback_id) }}" style="display: flex; align-items: center; gap: 10px;">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
{% if has_analysis %}
<span style="font-size: 1.2em;">🔄</span>
<label><strong>Re-run AI Analysis:</strong> This feedback has been analyzed. Click to re-analyze.</label>
{% else %}
<span style="font-size: 1.2em;">🤖</span>
<label><strong>Run AI Analysis:</strong> This feedback has not been analyzed yet.</label>
{% endif %}
<button type="submit" style="padding: 5px 15px; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer;">
{% if has_analysis %}Re-analyze{% else %}Analyze{% endif %}
</button>
</form>
</div>
{% elif not feedback.content %}
<div style="margin: 20px 0; padding: 15px; background: #f8d7da; border-radius: 5px; color: #721c24;">
<span style="font-size: 1.2em;"></span>
<strong>Cannot analyze:</strong> This feedback has no text content (attachments only).
</div>
{% endif %}
<!-- Original Content -->
<div style="margin: 30px 0;">
<h2>Original Feedback</h2>