Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 18 additions & 17 deletions frontend/src/api/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,10 @@ describe('API Client', () => {
});

describe('submitFeedback', () => {
it('submits feedback successfully', async () => {
it('submits feedback to Web3Forms successfully', async () => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ success: true, message: 'Feedback submitted' }),
json: () => Promise.resolve({ success: true }),
} as Response);

const result = await submitFeedback({
Expand All @@ -139,16 +139,17 @@ describe('API Client', () => {
});

expect(result.success).toBe(true);
expect(result.message).toBe('Feedback submitted');
expect(result.message).toBe('Feedback submitted successfully');

const fetchCall = vi.mocked(global.fetch).mock.calls[0];
expect(fetchCall[0]).toContain('/feedback');
expect(fetchCall[0]).toBe('https://api.web3forms.com/submit');
expect(fetchCall[1]?.headers).toEqual({ 'Content-Type': 'application/json' });

const body = JSON.parse(fetchCall[1]?.body as string);
expect(body.rating).toBe(5);
expect(body.improvements).toEqual(['Translation quality', 'Speed/Performance']);
expect(body.additional_feedback).toBe('Great tool!');
expect(body.access_key).toBeDefined();
expect(body.subject).toContain('Rating 5/5');
expect(body.message).toContain('Translation quality, Speed/Performance');
expect(body.message).toContain('Great tool!');
});

it('submits minimal feedback', async () => {
Expand All @@ -166,35 +167,35 @@ describe('API Client', () => {

const fetchCall = vi.mocked(global.fetch).mock.calls[0];
const body = JSON.parse(fetchCall[1]?.body as string);
expect(body.additional_feedback).toBeUndefined();
expect(body.message).toContain('None selected');
expect(body.message).toContain('None provided');
});

it('handles server errors', async () => {
it('returns success even on Web3Forms error', async () => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: false,
status: 422,
json: () => Promise.resolve({ detail: 'Invalid rating' }),
ok: true,
json: () => Promise.resolve({ success: false, message: 'Error' }),
} as Response);

const result = await submitFeedback({
rating: 5,
improvements: [],
});

expect(result.success).toBe(false);
expect(result.error).toBe('Invalid rating');
// Still shows success to user
expect(result.success).toBe(true);
});

it('handles network errors', async () => {
it('returns success even on network error', async () => {
vi.mocked(global.fetch).mockRejectedValueOnce(new Error('Network failure'));

const result = await submitFeedback({
rating: 4,
improvements: ['User interface'],
});

expect(result.success).toBe(false);
expect(result.error).toBe('Network failure');
// Still shows success to user
expect(result.success).toBe(true);
});
});
});
58 changes: 41 additions & 17 deletions frontend/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,38 +97,62 @@ export function downloadBlob(blob: Blob, filename: string): void {
URL.revokeObjectURL(url);
}

const WEB3FORMS_URL = 'https://api.web3forms.com/submit';
const WEB3FORMS_ACCESS_KEY = '8ed7e53d-d67a-476c-ad63-1160c7681975';

export async function submitFeedback(request: FeedbackRequest): Promise<FeedbackResponse> {
const ratingLabels: Record<number, string> = {
1: 'Very Dissatisfied',
2: 'Dissatisfied',
3: 'Neutral',
4: 'Satisfied',
5: 'Very Satisfied',
};

const ratingLabel = ratingLabels[request.rating] || String(request.rating);
const improvementsText = request.improvements.length > 0
? request.improvements.join(', ')
: 'None selected';

const message = `Rating: ${request.rating}/5 (${ratingLabel})

Areas for Improvement: ${improvementsText}

Additional Feedback: ${request.additionalFeedback || 'None provided'}`;

try {
const response = await fetch(`${API_URL}/feedback`, {
const response = await fetch(WEB3FORMS_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
rating: request.rating,
improvements: request.improvements,
additional_feedback: request.additionalFeedback,
access_key: WEB3FORMS_ACCESS_KEY,
subject: `Rosetta Feedback - Rating ${request.rating}/5 (${ratingLabel})`,
from_name: 'Rosetta Feedback',
message,
}),
});

if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const data = await response.json();

if (data.success) {
return {
success: false,
error: errorData.detail || `Failed with status ${response.status}`,
success: true,
message: 'Feedback submitted successfully',
};
} else {
console.error('[Feedback Error] Web3Forms error:', data);
return {
success: true, // Still show success to user
message: 'Feedback recorded',
};
}

const data = await response.json();
return {
success: true,
message: data.message,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Network error occurred';
console.error('[Feedback Error] Failed to submit feedback:', error);
return {
success: false,
error: errorMessage,
success: true, // Still show success to user
message: 'Feedback recorded',
};
}
}
66 changes: 1 addition & 65 deletions src/rosetta/api/app.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
"""FastAPI application for Rosetta translation service."""

import smtplib
import tempfile
from email.mime.text import MIMEText
from pathlib import Path
from typing import List, Optional
from typing import Optional

from dotenv import load_dotenv
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from openpyxl import load_workbook
from pydantic import BaseModel

from rosetta.services.translation_service import count_cells, translate_file

Expand Down Expand Up @@ -179,64 +176,3 @@ async def translate(
finally:
# Cleanup input file (output file cleaned up after response is sent)
input_path.unlink(missing_ok=True)


class FeedbackRequest(BaseModel):
"""Feedback submission request."""

rating: int
improvements: List[str]
additional_feedback: Optional[str] = None


@app.post("/feedback")
async def submit_feedback(feedback: FeedbackRequest) -> dict:
"""Submit user feedback via email.

Sends feedback to the configured email address.
"""
# Build email content
rating_emojis = {1: "Very Dissatisfied", 2: "Dissatisfied", 3: "Neutral", 4: "Satisfied", 5: "Very Satisfied"}
rating_label = rating_emojis.get(feedback.rating, str(feedback.rating))

body = f"""New Rosetta Feedback Received

Rating: {feedback.rating}/5 ({rating_label})

Areas for Improvement:
{chr(10).join(f" - {item}" for item in feedback.improvements) if feedback.improvements else " None selected"}

Additional Feedback:
{feedback.additional_feedback or "None provided"}
"""

try:
# For now, just log the feedback (email sending requires SMTP config)
# In production, configure SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS env vars
import os

smtp_host = os.getenv("SMTP_HOST")
if smtp_host:
msg = MIMEText(body)
msg["Subject"] = f"Rosetta Feedback - Rating {feedback.rating}/5"
msg["From"] = os.getenv("SMTP_FROM", "noreply@rosetta.app")
msg["To"] = "w.elmselmi@gmail.com"

with smtplib.SMTP(smtp_host, int(os.getenv("SMTP_PORT", "587"))) as server:
server.starttls()
smtp_user = os.getenv("SMTP_USER")
smtp_pass = os.getenv("SMTP_PASS")
if smtp_user and smtp_pass:
server.login(smtp_user, smtp_pass)
server.send_message(msg)
else:
# Log feedback when SMTP is not configured
print(f"[Feedback] {body}")

return {"success": True, "message": "Feedback submitted successfully"}

except Exception as e:
# Don't fail the request if email fails - just log it
print(f"[Feedback Error] Failed to send email: {e}")
print(f"[Feedback] {body}")
return {"success": True, "message": "Feedback recorded"}
118 changes: 0 additions & 118 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,121 +290,3 @@ def test_get_sheets_large_file(self, client):
assert "File too large" in response.json()["detail"]


class TestFeedbackEndpoint:
"""Tests for the /feedback endpoint."""

def test_submit_feedback_success(self, client):
"""POST /feedback with valid data should succeed."""
response = client.post(
"/feedback",
json={
"rating": 5,
"improvements": ["Translation quality", "Speed/Performance"],
"additional_feedback": "Great tool!",
},
)

assert response.status_code == 200
data = response.json()
assert data["success"] is True

def test_submit_feedback_minimal(self, client):
"""POST /feedback with only required fields should succeed."""
response = client.post(
"/feedback",
json={
"rating": 3,
"improvements": [],
},
)

assert response.status_code == 200
data = response.json()
assert data["success"] is True

def test_submit_feedback_all_ratings(self, client):
"""POST /feedback should accept all valid rating values."""
for rating in [1, 2, 3, 4, 5]:
response = client.post(
"/feedback",
json={
"rating": rating,
"improvements": ["User interface"],
},
)
assert response.status_code == 200
assert response.json()["success"] is True

def test_submit_feedback_missing_rating(self, client):
"""POST /feedback without rating should return 422."""
response = client.post(
"/feedback",
json={
"improvements": ["Translation quality"],
},
)
assert response.status_code == 422

def test_submit_feedback_missing_improvements(self, client):
"""POST /feedback without improvements should return 422."""
response = client.post(
"/feedback",
json={
"rating": 4,
},
)
assert response.status_code == 422

def test_submit_feedback_invalid_json(self, client):
"""POST /feedback with invalid JSON should return 422."""
response = client.post(
"/feedback",
content="not valid json",
headers={"Content-Type": "application/json"},
)
assert response.status_code == 422

def test_submit_feedback_empty_body(self, client):
"""POST /feedback with empty body should return 422."""
response = client.post(
"/feedback",
json={},
)
assert response.status_code == 422

def test_submit_feedback_with_multiple_improvements(self, client):
"""POST /feedback with multiple improvement areas should succeed."""
response = client.post(
"/feedback",
json={
"rating": 4,
"improvements": [
"Translation quality",
"Speed/Performance",
"User interface",
"Language options",
"File format support",
"Documentation",
],
"additional_feedback": "Comprehensive feedback with all options selected.",
},
)

assert response.status_code == 200
assert response.json()["success"] is True

def test_submit_feedback_long_additional_feedback(self, client):
"""POST /feedback with long additional feedback should succeed."""
long_feedback = "This is a detailed feedback. " * 100 # ~3000 chars

response = client.post(
"/feedback",
json={
"rating": 5,
"improvements": ["Translation quality"],
"additional_feedback": long_feedback,
},
)

assert response.status_code == 200
assert response.json()["success"] is True