AI Development Team Claude commited on
Commit
5c4bfe8
·
1 Parent(s): b07a1e9

feat(ui): AgentFactory-inspired dark theme redesign

Browse files

Complete UI/UX overhaul with professional dark theme inspired by AgentFactory:

## Theme Changes (custom.css)
- Primary accent: Cyan/Teal (#00d4aa) for robotics feel
- Dark backgrounds: #0f0f14, #18181f (deep charcoal)
- Secondary accent: Electric Blue (#3b82f6)
- Professional typography with Inter font
- Smooth transitions and hover effects
- Custom scrollbar styling
- Print-friendly styles

## Homepage Redesign (index.js + index.module.css)
- Full-height hero section with gradient background
- Grid pattern overlay effect
- Animated humanoid robot with glowing cyan accents
- Professional badge/tag component
- Gradient text for title highlights
- New Stats section (8 Chapters, 16 Weeks, etc.)
- Redesigned highlight cards with hover animations
- Features grid with icons
- CTA section with radial gradient background

## Component Updates
- Reading Progress Bar: Cyan-to-blue gradient with glow
- Chapter Navigation: Glass-morphism dark cards
- Both components with light/dark mode support

## Design Elements
- Deep dark backgrounds (AgentFactory-style)
- Cyan/teal primary accents
- Subtle grid patterns
- Glassmorphism effects
- Smooth hover animations
- Professional typography
- Mobile-responsive design

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

Co-Authored-By: Claude <noreply@anthropic.com>

alembic.ini CHANGED
@@ -1,116 +1,116 @@
1
- # A generic, single database configuration.
2
-
3
- [alembic]
4
- # path to migration scripts
5
- script_location = database/migrations
6
-
7
- # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
8
- # Uncomment the line below if you want the files to be prepended with date and time
9
- # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
10
- # for all available tokens
11
- # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
12
-
13
- # sys.path path, will be prepended to sys.path if present.
14
- # defaults to the current working directory.
15
- prepend_sys_path = .
16
-
17
- # timezone to use when rendering the date within the migration file
18
- # as well as the filename.
19
- # If specified, requires the python>=3.9 or backports.zoneinfo library.
20
- # Any required deps can installed by adding `alembic[tz]` to the pip requirements
21
- # string value is passed to ZoneInfo()
22
- # leave blank for localtime
23
- # timezone =
24
-
25
- # max length of characters to apply to the
26
- # "slug" field
27
- # truncate_slug_length = 40
28
-
29
- # set to 'true' to run the environment during
30
- # the 'revision' command, regardless of autogenerate
31
- # revision_environment = false
32
-
33
- # set to 'true' to allow .pyc and .pyo files without
34
- # a source .py file to be detected as revisions in the
35
- # versions/ directory
36
- # sourceless = false
37
-
38
- # version location specification; This defaults
39
- # to database/migrations/versions. When using multiple version
40
- # directories, initial revisions must be specified with --version-path.
41
- # The path separator used here should be the separator specified by "version_path_separator" below.
42
- # version_locations = %(here)s/bar:%(here)s/bat:database/migrations/versions
43
-
44
- # version path separator; As mentioned above, this is the character used to split
45
- # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
46
- # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
47
- # Valid values for version_path_separator are:
48
- #
49
- # version_path_separator = :
50
- # version_path_separator = ;
51
- # version_path_separator = space
52
- version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
53
-
54
- # set to 'true' to search source files recursively
55
- # in each "version_locations" directory
56
- # new in Alembic version 1.10
57
- # recursive_version_locations = false
58
-
59
- # the output encoding used when revision files
60
- # are written from script.py.mako
61
- # output_encoding = utf-8
62
-
63
- # sqlalchemy.url will be set in env.py from environment variable
64
-
65
-
66
- [post_write_hooks]
67
- # post_write_hooks defines scripts or Python functions that are run
68
- # on newly generated revision scripts. See the documentation for further
69
- # detail and examples
70
-
71
- # format using "black" - use the console_scripts runner, against the "black" entrypoint
72
- # hooks = black
73
- # black.type = console_scripts
74
- # black.entrypoint = black
75
- # black.options = -l 79 REVISION_SCRIPT_FILENAME
76
-
77
- # lint with attempts to fix using "ruff" - use the exec runner, execute a binary
78
- # hooks = ruff
79
- # ruff.type = exec
80
- # ruff.executable = %(here)s/.venv/bin/ruff
81
- # ruff.options = --fix REVISION_SCRIPT_FILENAME
82
-
83
- # Logging configuration
84
- [loggers]
85
- keys = root,sqlalchemy,alembic
86
-
87
- [handlers]
88
- keys = console
89
-
90
- [formatters]
91
- keys = generic
92
-
93
- [logger_root]
94
- level = WARN
95
- handlers = console
96
- qualname =
97
-
98
- [logger_sqlalchemy]
99
- level = WARN
100
- handlers =
101
- qualname = sqlalchemy.engine
102
-
103
- [logger_alembic]
104
- level = INFO
105
- handlers =
106
- qualname = alembic
107
-
108
- [handler_console]
109
- class = StreamHandler
110
- args = (sys.stderr,)
111
- level = NOTSET
112
- formatter = generic
113
-
114
- [formatter_generic]
115
- format = %(levelname)-5.5s [%(name)s] %(message)s
116
- datefmt = %H:%M:%S
 
1
+ # A generic, single database configuration.
2
+
3
+ [alembic]
4
+ # path to migration scripts
5
+ script_location = database/migrations
6
+
7
+ # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
8
+ # Uncomment the line below if you want the files to be prepended with date and time
9
+ # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
10
+ # for all available tokens
11
+ # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
12
+
13
+ # sys.path path, will be prepended to sys.path if present.
14
+ # defaults to the current working directory.
15
+ prepend_sys_path = .
16
+
17
+ # timezone to use when rendering the date within the migration file
18
+ # as well as the filename.
19
+ # If specified, requires the python>=3.9 or backports.zoneinfo library.
20
+ # Any required deps can installed by adding `alembic[tz]` to the pip requirements
21
+ # string value is passed to ZoneInfo()
22
+ # leave blank for localtime
23
+ # timezone =
24
+
25
+ # max length of characters to apply to the
26
+ # "slug" field
27
+ # truncate_slug_length = 40
28
+
29
+ # set to 'true' to run the environment during
30
+ # the 'revision' command, regardless of autogenerate
31
+ # revision_environment = false
32
+
33
+ # set to 'true' to allow .pyc and .pyo files without
34
+ # a source .py file to be detected as revisions in the
35
+ # versions/ directory
36
+ # sourceless = false
37
+
38
+ # version location specification; This defaults
39
+ # to database/migrations/versions. When using multiple version
40
+ # directories, initial revisions must be specified with --version-path.
41
+ # The path separator used here should be the separator specified by "version_path_separator" below.
42
+ # version_locations = %(here)s/bar:%(here)s/bat:database/migrations/versions
43
+
44
+ # version path separator; As mentioned above, this is the character used to split
45
+ # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
46
+ # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
47
+ # Valid values for version_path_separator are:
48
+ #
49
+ # version_path_separator = :
50
+ # version_path_separator = ;
51
+ # version_path_separator = space
52
+ version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
53
+
54
+ # set to 'true' to search source files recursively
55
+ # in each "version_locations" directory
56
+ # new in Alembic version 1.10
57
+ # recursive_version_locations = false
58
+
59
+ # the output encoding used when revision files
60
+ # are written from script.py.mako
61
+ # output_encoding = utf-8
62
+
63
+ # sqlalchemy.url will be set in env.py from environment variable
64
+
65
+
66
+ [post_write_hooks]
67
+ # post_write_hooks defines scripts or Python functions that are run
68
+ # on newly generated revision scripts. See the documentation for further
69
+ # detail and examples
70
+
71
+ # format using "black" - use the console_scripts runner, against the "black" entrypoint
72
+ # hooks = black
73
+ # black.type = console_scripts
74
+ # black.entrypoint = black
75
+ # black.options = -l 79 REVISION_SCRIPT_FILENAME
76
+
77
+ # lint with attempts to fix using "ruff" - use the exec runner, execute a binary
78
+ # hooks = ruff
79
+ # ruff.type = exec
80
+ # ruff.executable = %(here)s/.venv/bin/ruff
81
+ # ruff.options = --fix REVISION_SCRIPT_FILENAME
82
+
83
+ # Logging configuration
84
+ [loggers]
85
+ keys = root,sqlalchemy,alembic
86
+
87
+ [handlers]
88
+ keys = console
89
+
90
+ [formatters]
91
+ keys = generic
92
+
93
+ [logger_root]
94
+ level = WARN
95
+ handlers = console
96
+ qualname =
97
+
98
+ [logger_sqlalchemy]
99
+ level = WARN
100
+ handlers =
101
+ qualname = sqlalchemy.engine
102
+
103
+ [logger_alembic]
104
+ level = INFO
105
+ handlers =
106
+ qualname = alembic
107
+
108
+ [handler_console]
109
+ class = StreamHandler
110
+ args = (sys.stderr,)
111
+ level = NOTSET
112
+ formatter = generic
113
+
114
+ [formatter_generic]
115
+ format = %(levelname)-5.5s [%(name)s] %(message)s
116
+ datefmt = %H:%M:%S
api/feedback.py CHANGED
@@ -1,107 +1,107 @@
1
- """
2
- Translation Feedback API
3
-
4
- Endpoint: POST /api/translate/feedback
5
- Purpose: Allow users to report translation quality issues
6
- """
7
-
8
- from fastapi import APIRouter, Depends, HTTPException, Header
9
- from pydantic import BaseModel, Field
10
- from auth.jwt_utils import get_current_user_id_from_token
11
- from database.db import get_db
12
- from models.translation_feedback import TranslationFeedback
13
- from sqlalchemy import select
14
- from database.models import Translation
15
- import uuid
16
- import logging
17
-
18
- logger = logging.getLogger(__name__)
19
-
20
- router = APIRouter()
21
-
22
-
23
- # JWT verification dependency
24
- def verify_jwt_token(authorization: str = Header(None)) -> dict:
25
- """Verify JWT token from Authorization header"""
26
- if not authorization:
27
- raise HTTPException(status_code=401, detail="Authorization header missing")
28
-
29
- if not authorization.startswith("Bearer "):
30
- raise HTTPException(status_code=401, detail="Invalid authorization format")
31
-
32
- token = authorization[7:] # Remove "Bearer " prefix
33
- user_id = get_current_user_id_from_token(token)
34
-
35
- if not user_id:
36
- raise HTTPException(status_code=401, detail="Invalid or expired token")
37
-
38
- return {"user_id": user_id}
39
-
40
-
41
- class FeedbackRequest(BaseModel):
42
- """Request model for submitting translation feedback"""
43
- translation_id: str = Field(..., description="UUID of the translation being reported")
44
- issue_description: str = Field(..., min_length=10, max_length=2000, description="Description of the translation issue")
45
-
46
-
47
- class FeedbackResponse(BaseModel):
48
- """Response model for feedback submission"""
49
- feedback_id: str
50
- message: str
51
-
52
-
53
- @router.post("/api/translate/feedback", response_model=FeedbackResponse, status_code=201)
54
- def submit_translation_feedback(
55
- request: FeedbackRequest,
56
- user_data: dict = Depends(verify_jwt_token),
57
- db = Depends(get_db)
58
- ):
59
- """
60
- Submit feedback on translation quality
61
-
62
- **Authentication**: JWT token required
63
-
64
- **Request Body**:
65
- - translation_id: UUID of the translation
66
- - issue_description: Description of the issue (10-2000 characters)
67
-
68
- **Response**: 201 Created with feedback_id
69
- """
70
- user_id = user_data["user_id"]
71
-
72
- try:
73
- # Validate translation_id is a valid UUID
74
- translation_uuid = uuid.UUID(request.translation_id)
75
- except ValueError:
76
- raise HTTPException(status_code=400, detail="Invalid translation_id format. Must be a valid UUID.")
77
-
78
- try:
79
- # Optional: Verify translation exists (can skip for better performance)
80
- # result = await db.execute(select(Translation).where(Translation.id == translation_uuid))
81
- # translation = result.scalar_one_or_none()
82
- # if not translation:
83
- # raise HTTPException(status_code=404, detail="Translation not found")
84
-
85
- # Create feedback record
86
- feedback = TranslationFeedback(
87
- id=uuid.uuid4(),
88
- translation_id=translation_uuid,
89
- user_id=uuid.UUID(user_id),
90
- issue_description=request.issue_description.strip()
91
- )
92
-
93
- db.add(feedback)
94
- db.commit()
95
- db.refresh(feedback)
96
-
97
- logger.info(f"Feedback submitted: feedback_id={feedback.id}, translation_id={translation_uuid}, user_id={user_id}")
98
-
99
- return FeedbackResponse(
100
- feedback_id=str(feedback.id),
101
- message="Thank you for your feedback! We'll review the translation quality issue."
102
- )
103
-
104
- except Exception as e:
105
- logger.error(f"Failed to submit feedback: {str(e)}")
106
- db.rollback()
107
- raise HTTPException(status_code=500, detail=f"Failed to submit feedback: {str(e)}")
 
1
+ """
2
+ Translation Feedback API
3
+
4
+ Endpoint: POST /api/translate/feedback
5
+ Purpose: Allow users to report translation quality issues
6
+ """
7
+
8
+ from fastapi import APIRouter, Depends, HTTPException, Header
9
+ from pydantic import BaseModel, Field
10
+ from auth.jwt_utils import get_current_user_id_from_token
11
+ from database.db import get_db
12
+ from models.translation_feedback import TranslationFeedback
13
+ from sqlalchemy import select
14
+ from database.models import Translation
15
+ import uuid
16
+ import logging
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ router = APIRouter()
21
+
22
+
23
+ # JWT verification dependency
24
+ def verify_jwt_token(authorization: str = Header(None)) -> dict:
25
+ """Verify JWT token from Authorization header"""
26
+ if not authorization:
27
+ raise HTTPException(status_code=401, detail="Authorization header missing")
28
+
29
+ if not authorization.startswith("Bearer "):
30
+ raise HTTPException(status_code=401, detail="Invalid authorization format")
31
+
32
+ token = authorization[7:] # Remove "Bearer " prefix
33
+ user_id = get_current_user_id_from_token(token)
34
+
35
+ if not user_id:
36
+ raise HTTPException(status_code=401, detail="Invalid or expired token")
37
+
38
+ return {"user_id": user_id}
39
+
40
+
41
+ class FeedbackRequest(BaseModel):
42
+ """Request model for submitting translation feedback"""
43
+ translation_id: str = Field(..., description="UUID of the translation being reported")
44
+ issue_description: str = Field(..., min_length=10, max_length=2000, description="Description of the translation issue")
45
+
46
+
47
+ class FeedbackResponse(BaseModel):
48
+ """Response model for feedback submission"""
49
+ feedback_id: str
50
+ message: str
51
+
52
+
53
+ @router.post("/api/translate/feedback", response_model=FeedbackResponse, status_code=201)
54
+ def submit_translation_feedback(
55
+ request: FeedbackRequest,
56
+ user_data: dict = Depends(verify_jwt_token),
57
+ db = Depends(get_db)
58
+ ):
59
+ """
60
+ Submit feedback on translation quality
61
+
62
+ **Authentication**: JWT token required
63
+
64
+ **Request Body**:
65
+ - translation_id: UUID of the translation
66
+ - issue_description: Description of the issue (10-2000 characters)
67
+
68
+ **Response**: 201 Created with feedback_id
69
+ """
70
+ user_id = user_data["user_id"]
71
+
72
+ try:
73
+ # Validate translation_id is a valid UUID
74
+ translation_uuid = uuid.UUID(request.translation_id)
75
+ except ValueError:
76
+ raise HTTPException(status_code=400, detail="Invalid translation_id format. Must be a valid UUID.")
77
+
78
+ try:
79
+ # Optional: Verify translation exists (can skip for better performance)
80
+ # result = await db.execute(select(Translation).where(Translation.id == translation_uuid))
81
+ # translation = result.scalar_one_or_none()
82
+ # if not translation:
83
+ # raise HTTPException(status_code=404, detail="Translation not found")
84
+
85
+ # Create feedback record
86
+ feedback = TranslationFeedback(
87
+ id=uuid.uuid4(),
88
+ translation_id=translation_uuid,
89
+ user_id=uuid.UUID(user_id),
90
+ issue_description=request.issue_description.strip()
91
+ )
92
+
93
+ db.add(feedback)
94
+ db.commit()
95
+ db.refresh(feedback)
96
+
97
+ logger.info(f"Feedback submitted: feedback_id={feedback.id}, translation_id={translation_uuid}, user_id={user_id}")
98
+
99
+ return FeedbackResponse(
100
+ feedback_id=str(feedback.id),
101
+ message="Thank you for your feedback! We'll review the translation quality issue."
102
+ )
103
+
104
+ except Exception as e:
105
+ logger.error(f"Failed to submit feedback: {str(e)}")
106
+ db.rollback()
107
+ raise HTTPException(status_code=500, detail=f"Failed to submit feedback: {str(e)}")
cleanup_code.py CHANGED
@@ -1,149 +1,149 @@
1
- #!/usr/bin/env python3
2
- """
3
- Code Cleanup Script for Feature 006: Urdu Translation
4
-
5
- Removes unused imports and organizes code
6
- Run: python3 cleanup_code.py
7
- """
8
-
9
- import os
10
- import re
11
- from pathlib import Path
12
-
13
-
14
- def remove_unused_imports(file_path):
15
- """Remove commented imports and organize imports"""
16
- with open(file_path, 'r', encoding='utf-8') as f:
17
- content = f.read()
18
-
19
- original_content = content
20
- lines = content.split('\n')
21
- cleaned_lines = []
22
- import_section = []
23
- in_import_section = True
24
-
25
- for line in lines:
26
- # Skip empty lines in import section
27
- if in_import_section and line.strip() == '':
28
- continue
29
-
30
- # Collect imports
31
- if in_import_section and (line.startswith('import ') or line.startswith('from ')):
32
- import_section.append(line)
33
- continue
34
-
35
- # End of import section
36
- if in_import_section and not line.startswith('import ') and not line.startswith('from ') and line.strip():
37
- in_import_section = False
38
- # Add organized imports
39
- import_section.sort()
40
- cleaned_lines.extend(import_section)
41
- cleaned_lines.append('')
42
-
43
- cleaned_lines.append(line)
44
-
45
- # Write back if changed
46
- new_content = '\n'.join(cleaned_lines)
47
- if new_content != original_content:
48
- with open(file_path, 'w', encoding='utf-8') as f:
49
- f.write(new_content)
50
- return True
51
- return False
52
-
53
-
54
- def cleanup_trailing_whitespace(file_path):
55
- """Remove trailing whitespace from lines"""
56
- with open(file_path, 'r', encoding='utf-8') as f:
57
- content = f.read()
58
-
59
- lines = content.split('\n')
60
- cleaned_lines = [line.rstrip() for line in lines]
61
- new_content = '\n'.join(cleaned_lines)
62
-
63
- if new_content != content:
64
- with open(file_path, 'w', encoding='utf-8') as f:
65
- f.write(new_content)
66
- return True
67
- return False
68
-
69
-
70
- def check_pep8_line_length(file_path):
71
- """Check for lines exceeding PEP 8 line length (120 chars is acceptable for modern code)"""
72
- issues = []
73
- with open(file_path, 'r', encoding='utf-8') as f:
74
- for i, line in enumerate(f, 1):
75
- if len(line.rstrip()) > 120:
76
- issues.append(f" Line {i}: {len(line.rstrip())} characters")
77
-
78
- return issues
79
-
80
-
81
- def main():
82
- """Main cleanup function"""
83
- backend_dir = Path(__file__).parent
84
-
85
- # Files to cleanup
86
- files_to_check = [
87
- 'api/translation.py',
88
- 'api/feedback.py',
89
- 'services/translation_service.py',
90
- 'services/rate_limiter.py',
91
- 'models/translation_feedback.py',
92
- 'main.py'
93
- ]
94
-
95
- print("🧹 Starting code cleanup...")
96
- print(f"📁 Backend directory: {backend_dir}\n")
97
-
98
- total_files = 0
99
- files_modified = 0
100
-
101
- for file_rel_path in files_to_check:
102
- file_path = backend_dir / file_rel_path
103
-
104
- if not file_path.exists():
105
- print(f"⚠️ {file_rel_path} - NOT FOUND")
106
- continue
107
-
108
- total_files += 1
109
- print(f"🔍 Checking {file_rel_path}...")
110
-
111
- modified = False
112
-
113
- # Cleanup trailing whitespace
114
- if cleanup_trailing_whitespace(file_path):
115
- print(f" ✅ Removed trailing whitespace")
116
- modified = True
117
-
118
- # Check line length
119
- long_lines = check_pep8_line_length(file_path)
120
- if long_lines:
121
- print(f" ⚠️ Found {len(long_lines)} lines exceeding 120 characters:")
122
- for issue in long_lines[:3]: # Show first 3
123
- print(issue)
124
- if len(long_lines) > 3:
125
- print(f" ... and {len(long_lines) - 3} more")
126
-
127
- if modified:
128
- files_modified += 1
129
- else:
130
- print(f" ✓ No changes needed")
131
-
132
- print()
133
-
134
- print("═" * 50)
135
- print(f"✅ Cleanup complete!")
136
- print(f"📊 Files checked: {total_files}")
137
- print(f"✏️ Files modified: {files_modified}")
138
- print("═" * 50)
139
-
140
- # Additional recommendations
141
- print("\n📝 Additional Recommendations:")
142
- print(" 1. Run: pip install black flake8 # Install linters")
143
- print(" 2. Run: black backend/ # Auto-format code")
144
- print(" 3. Run: flake8 backend/ # Check PEP 8 compliance")
145
- print(" 4. Add .flake8 config to exclude tests/ from strict checks")
146
-
147
-
148
- if __name__ == '__main__':
149
- main()
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Code Cleanup Script for Feature 006: Urdu Translation
4
+
5
+ Removes unused imports and organizes code
6
+ Run: python3 cleanup_code.py
7
+ """
8
+
9
+ import os
10
+ import re
11
+ from pathlib import Path
12
+
13
+
14
+ def remove_unused_imports(file_path):
15
+ """Remove commented imports and organize imports"""
16
+ with open(file_path, 'r', encoding='utf-8') as f:
17
+ content = f.read()
18
+
19
+ original_content = content
20
+ lines = content.split('\n')
21
+ cleaned_lines = []
22
+ import_section = []
23
+ in_import_section = True
24
+
25
+ for line in lines:
26
+ # Skip empty lines in import section
27
+ if in_import_section and line.strip() == '':
28
+ continue
29
+
30
+ # Collect imports
31
+ if in_import_section and (line.startswith('import ') or line.startswith('from ')):
32
+ import_section.append(line)
33
+ continue
34
+
35
+ # End of import section
36
+ if in_import_section and not line.startswith('import ') and not line.startswith('from ') and line.strip():
37
+ in_import_section = False
38
+ # Add organized imports
39
+ import_section.sort()
40
+ cleaned_lines.extend(import_section)
41
+ cleaned_lines.append('')
42
+
43
+ cleaned_lines.append(line)
44
+
45
+ # Write back if changed
46
+ new_content = '\n'.join(cleaned_lines)
47
+ if new_content != original_content:
48
+ with open(file_path, 'w', encoding='utf-8') as f:
49
+ f.write(new_content)
50
+ return True
51
+ return False
52
+
53
+
54
+ def cleanup_trailing_whitespace(file_path):
55
+ """Remove trailing whitespace from lines"""
56
+ with open(file_path, 'r', encoding='utf-8') as f:
57
+ content = f.read()
58
+
59
+ lines = content.split('\n')
60
+ cleaned_lines = [line.rstrip() for line in lines]
61
+ new_content = '\n'.join(cleaned_lines)
62
+
63
+ if new_content != content:
64
+ with open(file_path, 'w', encoding='utf-8') as f:
65
+ f.write(new_content)
66
+ return True
67
+ return False
68
+
69
+
70
+ def check_pep8_line_length(file_path):
71
+ """Check for lines exceeding PEP 8 line length (120 chars is acceptable for modern code)"""
72
+ issues = []
73
+ with open(file_path, 'r', encoding='utf-8') as f:
74
+ for i, line in enumerate(f, 1):
75
+ if len(line.rstrip()) > 120:
76
+ issues.append(f" Line {i}: {len(line.rstrip())} characters")
77
+
78
+ return issues
79
+
80
+
81
+ def main():
82
+ """Main cleanup function"""
83
+ backend_dir = Path(__file__).parent
84
+
85
+ # Files to cleanup
86
+ files_to_check = [
87
+ 'api/translation.py',
88
+ 'api/feedback.py',
89
+ 'services/translation_service.py',
90
+ 'services/rate_limiter.py',
91
+ 'models/translation_feedback.py',
92
+ 'main.py'
93
+ ]
94
+
95
+ print("🧹 Starting code cleanup...")
96
+ print(f"📁 Backend directory: {backend_dir}\n")
97
+
98
+ total_files = 0
99
+ files_modified = 0
100
+
101
+ for file_rel_path in files_to_check:
102
+ file_path = backend_dir / file_rel_path
103
+
104
+ if not file_path.exists():
105
+ print(f"⚠️ {file_rel_path} - NOT FOUND")
106
+ continue
107
+
108
+ total_files += 1
109
+ print(f"🔍 Checking {file_rel_path}...")
110
+
111
+ modified = False
112
+
113
+ # Cleanup trailing whitespace
114
+ if cleanup_trailing_whitespace(file_path):
115
+ print(f" ✅ Removed trailing whitespace")
116
+ modified = True
117
+
118
+ # Check line length
119
+ long_lines = check_pep8_line_length(file_path)
120
+ if long_lines:
121
+ print(f" ⚠️ Found {len(long_lines)} lines exceeding 120 characters:")
122
+ for issue in long_lines[:3]: # Show first 3
123
+ print(issue)
124
+ if len(long_lines) > 3:
125
+ print(f" ... and {len(long_lines) - 3} more")
126
+
127
+ if modified:
128
+ files_modified += 1
129
+ else:
130
+ print(f" ✓ No changes needed")
131
+
132
+ print()
133
+
134
+ print("═" * 50)
135
+ print(f"✅ Cleanup complete!")
136
+ print(f"📊 Files checked: {total_files}")
137
+ print(f"✏️ Files modified: {files_modified}")
138
+ print("═" * 50)
139
+
140
+ # Additional recommendations
141
+ print("\n📝 Additional Recommendations:")
142
+ print(" 1. Run: pip install black flake8 # Install linters")
143
+ print(" 2. Run: black backend/ # Auto-format code")
144
+ print(" 3. Run: flake8 backend/ # Check PEP 8 compliance")
145
+ print(" 4. Add .flake8 config to exclude tests/ from strict checks")
146
+
147
+
148
+ if __name__ == '__main__':
149
+ main()
database/migrations/env.py CHANGED
@@ -1,92 +1,92 @@
1
- from logging.config import fileConfig
2
- import os
3
- import sys
4
- from pathlib import Path
5
-
6
- from sqlalchemy import engine_from_config
7
- from sqlalchemy import pool
8
- from dotenv import load_dotenv
9
-
10
- from alembic import context
11
-
12
- # Add parent directory to path to import models
13
- sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
14
-
15
- # Load environment variables
16
- load_dotenv()
17
-
18
- # this is the Alembic Config object, which provides
19
- # access to the values within the .ini file in use.
20
- config = context.config
21
-
22
- # Set sqlalchemy.url from environment variable
23
- config.set_main_option('sqlalchemy.url', os.getenv('NEON_POSTGRES_URL', ''))
24
-
25
- # Interpret the config file for Python logging.
26
- # This line sets up loggers basically.
27
- if config.config_file_name is not None:
28
- fileConfig(config.config_file_name)
29
-
30
- # Import Base and models for autogenerate support
31
- from database.db import Base
32
- from database.models import User, Personalization, Translation # Import all existing models
33
- from models.translation_feedback import TranslationFeedback # Import TranslationFeedback model
34
-
35
- target_metadata = Base.metadata
36
-
37
- # other values from the config, defined by the needs of env.py,
38
- # can be acquired:
39
- # my_important_option = config.get_main_option("my_important_option")
40
- # ... etc.
41
-
42
-
43
- def run_migrations_offline() -> None:
44
- """Run migrations in 'offline' mode.
45
-
46
- This configures the context with just a URL
47
- and not an Engine, though an Engine is acceptable
48
- here as well. By skipping the Engine creation
49
- we don't even need a DBAPI to be available.
50
-
51
- Calls to context.execute() here emit the given string to the
52
- script output.
53
-
54
- """
55
- url = config.get_main_option("sqlalchemy.url")
56
- context.configure(
57
- url=url,
58
- target_metadata=target_metadata,
59
- literal_binds=True,
60
- dialect_opts={"paramstyle": "named"},
61
- )
62
-
63
- with context.begin_transaction():
64
- context.run_migrations()
65
-
66
-
67
- def run_migrations_online() -> None:
68
- """Run migrations in 'online' mode.
69
-
70
- In this scenario we need to create an Engine
71
- and associate a connection with the context.
72
-
73
- """
74
- connectable = engine_from_config(
75
- config.get_section(config.config_ini_section, {}),
76
- prefix="sqlalchemy.",
77
- poolclass=pool.NullPool,
78
- )
79
-
80
- with connectable.connect() as connection:
81
- context.configure(
82
- connection=connection, target_metadata=target_metadata
83
- )
84
-
85
- with context.begin_transaction():
86
- context.run_migrations()
87
-
88
-
89
- if context.is_offline_mode():
90
- run_migrations_offline()
91
- else:
92
- run_migrations_online()
 
1
+ from logging.config import fileConfig
2
+ import os
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from sqlalchemy import engine_from_config
7
+ from sqlalchemy import pool
8
+ from dotenv import load_dotenv
9
+
10
+ from alembic import context
11
+
12
+ # Add parent directory to path to import models
13
+ sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
14
+
15
+ # Load environment variables
16
+ load_dotenv()
17
+
18
+ # this is the Alembic Config object, which provides
19
+ # access to the values within the .ini file in use.
20
+ config = context.config
21
+
22
+ # Set sqlalchemy.url from environment variable
23
+ config.set_main_option('sqlalchemy.url', os.getenv('NEON_POSTGRES_URL', ''))
24
+
25
+ # Interpret the config file for Python logging.
26
+ # This line sets up loggers basically.
27
+ if config.config_file_name is not None:
28
+ fileConfig(config.config_file_name)
29
+
30
+ # Import Base and models for autogenerate support
31
+ from database.db import Base
32
+ from database.models import User, Personalization, Translation # Import all existing models
33
+ from models.translation_feedback import TranslationFeedback # Import TranslationFeedback model
34
+
35
+ target_metadata = Base.metadata
36
+
37
+ # other values from the config, defined by the needs of env.py,
38
+ # can be acquired:
39
+ # my_important_option = config.get_main_option("my_important_option")
40
+ # ... etc.
41
+
42
+
43
+ def run_migrations_offline() -> None:
44
+ """Run migrations in 'offline' mode.
45
+
46
+ This configures the context with just a URL
47
+ and not an Engine, though an Engine is acceptable
48
+ here as well. By skipping the Engine creation
49
+ we don't even need a DBAPI to be available.
50
+
51
+ Calls to context.execute() here emit the given string to the
52
+ script output.
53
+
54
+ """
55
+ url = config.get_main_option("sqlalchemy.url")
56
+ context.configure(
57
+ url=url,
58
+ target_metadata=target_metadata,
59
+ literal_binds=True,
60
+ dialect_opts={"paramstyle": "named"},
61
+ )
62
+
63
+ with context.begin_transaction():
64
+ context.run_migrations()
65
+
66
+
67
+ def run_migrations_online() -> None:
68
+ """Run migrations in 'online' mode.
69
+
70
+ In this scenario we need to create an Engine
71
+ and associate a connection with the context.
72
+
73
+ """
74
+ connectable = engine_from_config(
75
+ config.get_section(config.config_ini_section, {}),
76
+ prefix="sqlalchemy.",
77
+ poolclass=pool.NullPool,
78
+ )
79
+
80
+ with connectable.connect() as connection:
81
+ context.configure(
82
+ connection=connection, target_metadata=target_metadata
83
+ )
84
+
85
+ with context.begin_transaction():
86
+ context.run_migrations()
87
+
88
+
89
+ if context.is_offline_mode():
90
+ run_migrations_offline()
91
+ else:
92
+ run_migrations_online()
database/migrations/script.py.mako CHANGED
@@ -1,26 +1,26 @@
1
- """${message}
2
-
3
- Revision ID: ${up_revision}
4
- Revises: ${down_revision | comma,n}
5
- Create Date: ${create_date}
6
-
7
- """
8
- from typing import Sequence, Union
9
-
10
- from alembic import op
11
- import sqlalchemy as sa
12
- ${imports if imports else ""}
13
-
14
- # revision identifiers, used by Alembic.
15
- revision: str = ${repr(up_revision)}
16
- down_revision: Union[str, None] = ${repr(down_revision)}
17
- branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
18
- depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
19
-
20
-
21
- def upgrade() -> None:
22
- ${upgrades if upgrades else "pass"}
23
-
24
-
25
- def downgrade() -> None:
26
- ${downgrades if downgrades else "pass"}
 
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ ${imports if imports else ""}
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = ${repr(up_revision)}
16
+ down_revision: Union[str, None] = ${repr(down_revision)}
17
+ branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
18
+ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
19
+
20
+
21
+ def upgrade() -> None:
22
+ ${upgrades if upgrades else "pass"}
23
+
24
+
25
+ def downgrade() -> None:
26
+ ${downgrades if downgrades else "pass"}
database/migrations/versions/57c6b0ea13f8_add_translations_table.py CHANGED
@@ -1,54 +1,54 @@
1
- """add translations table
2
-
3
- Revision ID: 57c6b0ea13f8
4
- Revises:
5
- Create Date: 2025-12-24 22:54:29.988202
6
-
7
- """
8
- from typing import Sequence, Union
9
-
10
- from alembic import op
11
- import sqlalchemy as sa
12
-
13
-
14
- # revision identifiers, used by Alembic.
15
- revision: str = '57c6b0ea13f8'
16
- down_revision: Union[str, None] = None
17
- branch_labels: Union[str, Sequence[str], None] = None
18
- depends_on: Union[str, Sequence[str], None] = None
19
-
20
-
21
- def upgrade() -> None:
22
- # ### commands auto generated by Alembic - please adjust! ###
23
- op.create_table('translations',
24
- sa.Column('id', sa.UUID(), nullable=False),
25
- sa.Column('chapter_id', sa.String(length=255), nullable=False),
26
- sa.Column('content_hash', sa.String(length=64), nullable=False),
27
- sa.Column('source_language', sa.String(length=10), nullable=False),
28
- sa.Column('target_language', sa.String(length=10), nullable=False),
29
- sa.Column('original_content', sa.Text(), nullable=False),
30
- sa.Column('translated_content', sa.Text(), nullable=False),
31
- sa.Column('user_id', sa.UUID(), nullable=False),
32
- sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
33
- sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
34
- sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
35
- sa.PrimaryKeyConstraint('id'),
36
- sa.UniqueConstraint('chapter_id', 'content_hash', 'target_language', name='uq_chapter_hash_language')
37
- )
38
- op.create_index('idx_translations_lookup', 'translations', ['chapter_id', 'content_hash', 'target_language'], unique=False)
39
- op.create_index(op.f('ix_translations_chapter_id'), 'translations', ['chapter_id'], unique=False)
40
- op.create_index(op.f('ix_translations_content_hash'), 'translations', ['content_hash'], unique=False)
41
- op.create_index(op.f('ix_translations_target_language'), 'translations', ['target_language'], unique=False)
42
- op.create_index(op.f('ix_translations_user_id'), 'translations', ['user_id'], unique=False)
43
- # ### end Alembic commands ###
44
-
45
-
46
- def downgrade() -> None:
47
- # ### commands auto generated by Alembic - please adjust! ###
48
- op.drop_index(op.f('ix_translations_user_id'), table_name='translations')
49
- op.drop_index(op.f('ix_translations_target_language'), table_name='translations')
50
- op.drop_index(op.f('ix_translations_content_hash'), table_name='translations')
51
- op.drop_index(op.f('ix_translations_chapter_id'), table_name='translations')
52
- op.drop_index('idx_translations_lookup', table_name='translations')
53
- op.drop_table('translations')
54
- # ### end Alembic commands ###
 
1
+ """add translations table
2
+
3
+ Revision ID: 57c6b0ea13f8
4
+ Revises:
5
+ Create Date: 2025-12-24 22:54:29.988202
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = '57c6b0ea13f8'
16
+ down_revision: Union[str, None] = None
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ # ### commands auto generated by Alembic - please adjust! ###
23
+ op.create_table('translations',
24
+ sa.Column('id', sa.UUID(), nullable=False),
25
+ sa.Column('chapter_id', sa.String(length=255), nullable=False),
26
+ sa.Column('content_hash', sa.String(length=64), nullable=False),
27
+ sa.Column('source_language', sa.String(length=10), nullable=False),
28
+ sa.Column('target_language', sa.String(length=10), nullable=False),
29
+ sa.Column('original_content', sa.Text(), nullable=False),
30
+ sa.Column('translated_content', sa.Text(), nullable=False),
31
+ sa.Column('user_id', sa.UUID(), nullable=False),
32
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
33
+ sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
34
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
35
+ sa.PrimaryKeyConstraint('id'),
36
+ sa.UniqueConstraint('chapter_id', 'content_hash', 'target_language', name='uq_chapter_hash_language')
37
+ )
38
+ op.create_index('idx_translations_lookup', 'translations', ['chapter_id', 'content_hash', 'target_language'], unique=False)
39
+ op.create_index(op.f('ix_translations_chapter_id'), 'translations', ['chapter_id'], unique=False)
40
+ op.create_index(op.f('ix_translations_content_hash'), 'translations', ['content_hash'], unique=False)
41
+ op.create_index(op.f('ix_translations_target_language'), 'translations', ['target_language'], unique=False)
42
+ op.create_index(op.f('ix_translations_user_id'), 'translations', ['user_id'], unique=False)
43
+ # ### end Alembic commands ###
44
+
45
+
46
+ def downgrade() -> None:
47
+ # ### commands auto generated by Alembic - please adjust! ###
48
+ op.drop_index(op.f('ix_translations_user_id'), table_name='translations')
49
+ op.drop_index(op.f('ix_translations_target_language'), table_name='translations')
50
+ op.drop_index(op.f('ix_translations_content_hash'), table_name='translations')
51
+ op.drop_index(op.f('ix_translations_chapter_id'), table_name='translations')
52
+ op.drop_index('idx_translations_lookup', table_name='translations')
53
+ op.drop_table('translations')
54
+ # ### end Alembic commands ###
database/migrations/versions/809b34b1c5dc_add_translation_feedback_table.py CHANGED
@@ -1,43 +1,43 @@
1
- """add translation_feedback table
2
-
3
- Revision ID: 809b34b1c5dc
4
- Revises: 57c6b0ea13f8
5
- Create Date: 2025-12-25 01:42:15.691192
6
-
7
- """
8
- from typing import Sequence, Union
9
-
10
- from alembic import op
11
- import sqlalchemy as sa
12
-
13
-
14
- # revision identifiers, used by Alembic.
15
- revision: str = '809b34b1c5dc'
16
- down_revision: Union[str, None] = '57c6b0ea13f8'
17
- branch_labels: Union[str, Sequence[str], None] = None
18
- depends_on: Union[str, Sequence[str], None] = None
19
-
20
-
21
- def upgrade() -> None:
22
- # ### commands auto generated by Alembic - please adjust! ###
23
- op.create_table('translation_feedback',
24
- sa.Column('id', sa.UUID(), nullable=False),
25
- sa.Column('translation_id', sa.UUID(), nullable=False),
26
- sa.Column('user_id', sa.UUID(), nullable=False),
27
- sa.Column('issue_description', sa.Text(), nullable=False),
28
- sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
29
- sa.ForeignKeyConstraint(['translation_id'], ['translations.id'], ondelete='CASCADE'),
30
- sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
31
- sa.PrimaryKeyConstraint('id')
32
- )
33
- op.create_index(op.f('ix_translation_feedback_translation_id'), 'translation_feedback', ['translation_id'], unique=False)
34
- op.create_index(op.f('ix_translation_feedback_user_id'), 'translation_feedback', ['user_id'], unique=False)
35
- # ### end Alembic commands ###
36
-
37
-
38
- def downgrade() -> None:
39
- # ### commands auto generated by Alembic - please adjust! ###
40
- op.drop_index(op.f('ix_translation_feedback_user_id'), table_name='translation_feedback')
41
- op.drop_index(op.f('ix_translation_feedback_translation_id'), table_name='translation_feedback')
42
- op.drop_table('translation_feedback')
43
- # ### end Alembic commands ###
 
1
+ """add translation_feedback table
2
+
3
+ Revision ID: 809b34b1c5dc
4
+ Revises: 57c6b0ea13f8
5
+ Create Date: 2025-12-25 01:42:15.691192
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = '809b34b1c5dc'
16
+ down_revision: Union[str, None] = '57c6b0ea13f8'
17
+ branch_labels: Union[str, Sequence[str], None] = None
18
+ depends_on: Union[str, Sequence[str], None] = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ # ### commands auto generated by Alembic - please adjust! ###
23
+ op.create_table('translation_feedback',
24
+ sa.Column('id', sa.UUID(), nullable=False),
25
+ sa.Column('translation_id', sa.UUID(), nullable=False),
26
+ sa.Column('user_id', sa.UUID(), nullable=False),
27
+ sa.Column('issue_description', sa.Text(), nullable=False),
28
+ sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
29
+ sa.ForeignKeyConstraint(['translation_id'], ['translations.id'], ondelete='CASCADE'),
30
+ sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
31
+ sa.PrimaryKeyConstraint('id')
32
+ )
33
+ op.create_index(op.f('ix_translation_feedback_translation_id'), 'translation_feedback', ['translation_id'], unique=False)
34
+ op.create_index(op.f('ix_translation_feedback_user_id'), 'translation_feedback', ['user_id'], unique=False)
35
+ # ### end Alembic commands ###
36
+
37
+
38
+ def downgrade() -> None:
39
+ # ### commands auto generated by Alembic - please adjust! ###
40
+ op.drop_index(op.f('ix_translation_feedback_user_id'), table_name='translation_feedback')
41
+ op.drop_index(op.f('ix_translation_feedback_translation_id'), table_name='translation_feedback')
42
+ op.drop_table('translation_feedback')
43
+ # ### end Alembic commands ###
models/translation.py CHANGED
@@ -1,34 +1,34 @@
1
- """
2
- Translation model for storing Urdu translations with caching
3
- """
4
- from sqlalchemy import Column, String, Text, DateTime, ForeignKey, UniqueConstraint, Index
5
- from sqlalchemy.dialects.postgresql import UUID
6
- from sqlalchemy.sql import func
7
- import uuid
8
- from database.db import Base
9
-
10
-
11
- class Translation(Base):
12
- __tablename__ = "translations"
13
-
14
- id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
15
- chapter_id = Column(String(255), nullable=False, index=True)
16
- content_hash = Column(String(64), nullable=False, index=True)
17
- source_language = Column(String(10), nullable=False, default="english")
18
- target_language = Column(String(10), nullable=False, index=True)
19
- original_content = Column(Text, nullable=False)
20
- translated_content = Column(Text, nullable=False)
21
- user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
22
- created_at = Column(DateTime(timezone=True), server_default=func.now())
23
- updated_at = Column(DateTime(timezone=True), onupdate=func.now())
24
-
25
- # Composite unique constraint: one translation per (chapter_id, content_hash, target_language)
26
- # This ensures we don't duplicate translations when content is the same
27
- __table_args__ = (
28
- UniqueConstraint('chapter_id', 'content_hash', 'target_language', name='uq_chapter_hash_language'),
29
- # Composite index for fast cache lookups
30
- Index('idx_translations_lookup', 'chapter_id', 'content_hash', 'target_language'),
31
- )
32
-
33
- def __repr__(self):
34
- return f"<Translation(chapter_id={self.chapter_id}, target_language={self.target_language}, cached=True)>"
 
1
+ """
2
+ Translation model for storing Urdu translations with caching
3
+ """
4
+ from sqlalchemy import Column, String, Text, DateTime, ForeignKey, UniqueConstraint, Index
5
+ from sqlalchemy.dialects.postgresql import UUID
6
+ from sqlalchemy.sql import func
7
+ import uuid
8
+ from database.db import Base
9
+
10
+
11
+ class Translation(Base):
12
+ __tablename__ = "translations"
13
+
14
+ id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
15
+ chapter_id = Column(String(255), nullable=False, index=True)
16
+ content_hash = Column(String(64), nullable=False, index=True)
17
+ source_language = Column(String(10), nullable=False, default="english")
18
+ target_language = Column(String(10), nullable=False, index=True)
19
+ original_content = Column(Text, nullable=False)
20
+ translated_content = Column(Text, nullable=False)
21
+ user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
22
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
23
+ updated_at = Column(DateTime(timezone=True), onupdate=func.now())
24
+
25
+ # Composite unique constraint: one translation per (chapter_id, content_hash, target_language)
26
+ # This ensures we don't duplicate translations when content is the same
27
+ __table_args__ = (
28
+ UniqueConstraint('chapter_id', 'content_hash', 'target_language', name='uq_chapter_hash_language'),
29
+ # Composite index for fast cache lookups
30
+ Index('idx_translations_lookup', 'chapter_id', 'content_hash', 'target_language'),
31
+ )
32
+
33
+ def __repr__(self):
34
+ return f"<Translation(chapter_id={self.chapter_id}, target_language={self.target_language}, cached=True)>"
models/translation_feedback.py CHANGED
@@ -1,34 +1,34 @@
1
- """
2
- TranslationFeedback Model
3
-
4
- Purpose: Store user feedback on translation quality for improvement
5
- """
6
-
7
- from sqlalchemy import Column, String, Text, DateTime, ForeignKey
8
- from sqlalchemy.dialects.postgresql import UUID
9
- from sqlalchemy.sql import func
10
- import uuid
11
- from database.db import Base
12
-
13
-
14
- class TranslationFeedback(Base):
15
- __tablename__ = "translation_feedback"
16
-
17
- id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
18
- translation_id = Column(
19
- UUID(as_uuid=True),
20
- ForeignKey("translations.id", ondelete="CASCADE"),
21
- nullable=False,
22
- index=True
23
- )
24
- user_id = Column(
25
- UUID(as_uuid=True),
26
- ForeignKey("users.id", ondelete="CASCADE"),
27
- nullable=False,
28
- index=True
29
- )
30
- issue_description = Column(Text, nullable=False)
31
- created_at = Column(DateTime(timezone=True), server_default=func.now())
32
-
33
- def __repr__(self):
34
- return f"<TranslationFeedback(translation_id={self.translation_id}, user_id={self.user_id}, issue='{self.issue_description[:50]}...')>"
 
1
+ """
2
+ TranslationFeedback Model
3
+
4
+ Purpose: Store user feedback on translation quality for improvement
5
+ """
6
+
7
+ from sqlalchemy import Column, String, Text, DateTime, ForeignKey
8
+ from sqlalchemy.dialects.postgresql import UUID
9
+ from sqlalchemy.sql import func
10
+ import uuid
11
+ from database.db import Base
12
+
13
+
14
+ class TranslationFeedback(Base):
15
+ __tablename__ = "translation_feedback"
16
+
17
+ id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
18
+ translation_id = Column(
19
+ UUID(as_uuid=True),
20
+ ForeignKey("translations.id", ondelete="CASCADE"),
21
+ nullable=False,
22
+ index=True
23
+ )
24
+ user_id = Column(
25
+ UUID(as_uuid=True),
26
+ ForeignKey("users.id", ondelete="CASCADE"),
27
+ nullable=False,
28
+ index=True
29
+ )
30
+ issue_description = Column(Text, nullable=False)
31
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
32
+
33
+ def __repr__(self):
34
+ return f"<TranslationFeedback(translation_id={self.translation_id}, user_id={self.user_id}, issue='{self.issue_description[:50]}...')>"
requirements.txt CHANGED
@@ -1,18 +1,18 @@
1
- fastapi>=0.104.1
2
- uvicorn[standard]>=0.24.0
3
- sqlalchemy[asyncio]>=2.0.23
4
- asyncpg>=0.29.0
5
- qdrant-client>=1.7.1
6
- google-generativeai>=0.4.0
7
- python-multipart>=0.0.6
8
- python-jose[cryptography]>=3.3.0
9
- passlib[bcrypt]>=1.7.4
10
- better-exceptions>=0.3.3
11
- python-dotenv>=1.0.0
12
- pydantic>=2.5.0
13
- pydantic-settings>=2.1.0
14
- httpx>=0.25.2
15
- pytest>=7.4.3
16
- pytest-asyncio>=0.21.1
17
- alembic>=1.13.1
18
  openai>=1.10.0
 
1
+ fastapi>=0.104.1
2
+ uvicorn[standard]>=0.24.0
3
+ sqlalchemy[asyncio]>=2.0.23
4
+ asyncpg>=0.29.0
5
+ qdrant-client>=1.7.1
6
+ google-generativeai>=0.4.0
7
+ python-multipart>=0.0.6
8
+ python-jose[cryptography]>=3.3.0
9
+ passlib[bcrypt]>=1.7.4
10
+ better-exceptions>=0.3.3
11
+ python-dotenv>=1.0.0
12
+ pydantic>=2.5.0
13
+ pydantic-settings>=2.1.0
14
+ httpx>=0.25.2
15
+ pytest>=7.4.3
16
+ pytest-asyncio>=0.21.1
17
+ alembic>=1.13.1
18
  openai>=1.10.0
services/rate_limiter.py CHANGED
@@ -1,115 +1,115 @@
1
- """
2
- Rate limiter service for translation endpoints
3
- Implements in-memory sliding window algorithm (10 requests per user per hour)
4
- """
5
- import time
6
- from collections import defaultdict
7
- from typing import Dict, List
8
- import logging
9
-
10
- logger = logging.getLogger(__name__)
11
-
12
-
13
- class RateLimiter:
14
- """
15
- In-memory rate limiter using sliding window algorithm
16
-
17
- Limits: 10 translations per user per hour (3600 seconds)
18
-
19
- Note: This is a simple in-memory implementation suitable for single-instance deployments.
20
- For multi-instance/production deployments, consider using Redis for distributed rate limiting.
21
- """
22
-
23
- def __init__(self, max_requests: int = 10, window_seconds: int = 3600):
24
- """
25
- Initialize rate limiter
26
-
27
- Args:
28
- max_requests: Maximum number of requests allowed per window
29
- window_seconds: Time window in seconds (default: 3600 = 1 hour)
30
- """
31
- self.max_requests = max_requests
32
- self.window_seconds = window_seconds
33
- # Store: {user_id: [timestamp1, timestamp2, ...]}
34
- self.request_history: Dict[str, List[float]] = defaultdict(list)
35
- logger.info(f"RateLimiter initialized: {max_requests} requests per {window_seconds}s")
36
-
37
- def check_rate_limit(self, user_id: str) -> tuple[bool, int]:
38
- """
39
- Check if user has exceeded rate limit
40
-
41
- Args:
42
- user_id: User identifier (UUID string)
43
-
44
- Returns:
45
- tuple[bool, int]: (is_allowed, retry_after_seconds)
46
- - is_allowed: True if request is allowed, False if rate limit exceeded
47
- - retry_after_seconds: Seconds until rate limit resets (0 if allowed)
48
- """
49
- current_time = time.time()
50
- cutoff_time = current_time - self.window_seconds
51
-
52
- # Get user's request history
53
- user_requests = self.request_history[user_id]
54
-
55
- # Remove timestamps outside the sliding window
56
- user_requests = [ts for ts in user_requests if ts > cutoff_time]
57
- self.request_history[user_id] = user_requests
58
-
59
- # Check if user has exceeded limit
60
- if len(user_requests) >= self.max_requests:
61
- # Calculate retry_after: time until oldest request expires
62
- oldest_timestamp = user_requests[0]
63
- retry_after = int(oldest_timestamp + self.window_seconds - current_time)
64
-
65
- logger.warning(
66
- f"Rate limit exceeded for user {user_id}: "
67
- f"{len(user_requests)}/{self.max_requests} requests in last {self.window_seconds}s. "
68
- f"Retry after {retry_after}s"
69
- )
70
- return False, retry_after
71
-
72
- # Record this request
73
- user_requests.append(current_time)
74
- self.request_history[user_id] = user_requests
75
-
76
- logger.debug(
77
- f"Rate limit check passed for user {user_id}: "
78
- f"{len(user_requests)}/{self.max_requests} requests in window"
79
- )
80
- return True, 0
81
-
82
- def reset_user(self, user_id: str) -> None:
83
- """
84
- Reset rate limit for a specific user (admin function)
85
-
86
- Args:
87
- user_id: User identifier to reset
88
- """
89
- if user_id in self.request_history:
90
- del self.request_history[user_id]
91
- logger.info(f"Rate limit reset for user {user_id}")
92
-
93
- def get_remaining_requests(self, user_id: str) -> int:
94
- """
95
- Get number of remaining requests for user in current window
96
-
97
- Args:
98
- user_id: User identifier
99
-
100
- Returns:
101
- int: Number of requests remaining
102
- """
103
- current_time = time.time()
104
- cutoff_time = current_time - self.window_seconds
105
-
106
- user_requests = self.request_history.get(user_id, [])
107
- user_requests = [ts for ts in user_requests if ts > cutoff_time]
108
-
109
- remaining = max(0, self.max_requests - len(user_requests))
110
- return remaining
111
-
112
-
113
- # Global rate limiter instance (singleton)
114
- # 10 translations per user per hour
115
- translation_rate_limiter = RateLimiter(max_requests=10, window_seconds=3600)
 
1
+ """
2
+ Rate limiter service for translation endpoints
3
+ Implements in-memory sliding window algorithm (10 requests per user per hour)
4
+ """
5
+ import time
6
+ from collections import defaultdict
7
+ from typing import Dict, List
8
+ import logging
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class RateLimiter:
14
+ """
15
+ In-memory rate limiter using sliding window algorithm
16
+
17
+ Limits: 10 translations per user per hour (3600 seconds)
18
+
19
+ Note: This is a simple in-memory implementation suitable for single-instance deployments.
20
+ For multi-instance/production deployments, consider using Redis for distributed rate limiting.
21
+ """
22
+
23
+ def __init__(self, max_requests: int = 10, window_seconds: int = 3600):
24
+ """
25
+ Initialize rate limiter
26
+
27
+ Args:
28
+ max_requests: Maximum number of requests allowed per window
29
+ window_seconds: Time window in seconds (default: 3600 = 1 hour)
30
+ """
31
+ self.max_requests = max_requests
32
+ self.window_seconds = window_seconds
33
+ # Store: {user_id: [timestamp1, timestamp2, ...]}
34
+ self.request_history: Dict[str, List[float]] = defaultdict(list)
35
+ logger.info(f"RateLimiter initialized: {max_requests} requests per {window_seconds}s")
36
+
37
+ def check_rate_limit(self, user_id: str) -> tuple[bool, int]:
38
+ """
39
+ Check if user has exceeded rate limit
40
+
41
+ Args:
42
+ user_id: User identifier (UUID string)
43
+
44
+ Returns:
45
+ tuple[bool, int]: (is_allowed, retry_after_seconds)
46
+ - is_allowed: True if request is allowed, False if rate limit exceeded
47
+ - retry_after_seconds: Seconds until rate limit resets (0 if allowed)
48
+ """
49
+ current_time = time.time()
50
+ cutoff_time = current_time - self.window_seconds
51
+
52
+ # Get user's request history
53
+ user_requests = self.request_history[user_id]
54
+
55
+ # Remove timestamps outside the sliding window
56
+ user_requests = [ts for ts in user_requests if ts > cutoff_time]
57
+ self.request_history[user_id] = user_requests
58
+
59
+ # Check if user has exceeded limit
60
+ if len(user_requests) >= self.max_requests:
61
+ # Calculate retry_after: time until oldest request expires
62
+ oldest_timestamp = user_requests[0]
63
+ retry_after = int(oldest_timestamp + self.window_seconds - current_time)
64
+
65
+ logger.warning(
66
+ f"Rate limit exceeded for user {user_id}: "
67
+ f"{len(user_requests)}/{self.max_requests} requests in last {self.window_seconds}s. "
68
+ f"Retry after {retry_after}s"
69
+ )
70
+ return False, retry_after
71
+
72
+ # Record this request
73
+ user_requests.append(current_time)
74
+ self.request_history[user_id] = user_requests
75
+
76
+ logger.debug(
77
+ f"Rate limit check passed for user {user_id}: "
78
+ f"{len(user_requests)}/{self.max_requests} requests in window"
79
+ )
80
+ return True, 0
81
+
82
+ def reset_user(self, user_id: str) -> None:
83
+ """
84
+ Reset rate limit for a specific user (admin function)
85
+
86
+ Args:
87
+ user_id: User identifier to reset
88
+ """
89
+ if user_id in self.request_history:
90
+ del self.request_history[user_id]
91
+ logger.info(f"Rate limit reset for user {user_id}")
92
+
93
+ def get_remaining_requests(self, user_id: str) -> int:
94
+ """
95
+ Get number of remaining requests for user in current window
96
+
97
+ Args:
98
+ user_id: User identifier
99
+
100
+ Returns:
101
+ int: Number of requests remaining
102
+ """
103
+ current_time = time.time()
104
+ cutoff_time = current_time - self.window_seconds
105
+
106
+ user_requests = self.request_history.get(user_id, [])
107
+ user_requests = [ts for ts in user_requests if ts > cutoff_time]
108
+
109
+ remaining = max(0, self.max_requests - len(user_requests))
110
+ return remaining
111
+
112
+
113
+ # Global rate limiter instance (singleton)
114
+ # 10 translations per user per hour
115
+ translation_rate_limiter = RateLimiter(max_requests=10, window_seconds=3600)
services/translation_service.py CHANGED
@@ -1,146 +1,146 @@
1
- from openai import OpenAI
2
- import logging
3
- from typing import Dict
4
- import time
5
- import os
6
-
7
- logger = logging.getLogger(__name__)
8
-
9
- class TranslationService:
10
- def __init__(self, openrouter_api_key: str = None):
11
- """Initialize OpenRouter client for translation using OpenAI SDK compatibility"""
12
- api_key = openrouter_api_key or os.getenv("OPENROUTER_API_KEY")
13
- self.client = OpenAI(
14
- base_url="https://openrouter.ai/api/v1",
15
- api_key=api_key
16
- )
17
- self.model = "google/gemini-2.0-flash-exp:free"
18
- self.translation_cache: Dict[str, str] = {}
19
- self.cache_timestamps: Dict[str, float] = {}
20
-
21
- def translate_to_urdu(self, text: str, ttl: int = 3600) -> str:
22
- """Translate English text to Urdu with caching"""
23
- # Create cache key
24
- cache_key = f"en_to_ur_{hash(text)}"
25
-
26
- # Check if translation is in cache and not expired
27
- if cache_key in self.translation_cache:
28
- if time.time() - self.cache_timestamps.get(cache_key, 0) < ttl:
29
- logger.info("Returning cached translation")
30
- return self.translation_cache[cache_key]
31
-
32
- # Call OpenRouter API for translation using OpenAI SDK
33
- try:
34
- system_prompt = self._get_urdu_translation_system_prompt()
35
-
36
- response = self.client.chat.completions.create(
37
- model=self.model,
38
- messages=[
39
- {"role": "system", "content": system_prompt},
40
- {"role": "user", "content": text}
41
- ],
42
- temperature=0.3,
43
- max_tokens=8000
44
- )
45
-
46
- translated_text = response.choices[0].message.content.strip()
47
-
48
- # Cache the translation
49
- self.translation_cache[cache_key] = translated_text
50
- self.cache_timestamps[cache_key] = time.time()
51
-
52
- logger.info(f"Translated text to Urdu (length: {len(translated_text)} chars)")
53
- return translated_text
54
-
55
- except Exception as e:
56
- logger.error(f"Translation failed: {str(e)}")
57
- # Return a professional fallback response
58
- return f"Translation unavailable: {text[:100]}..."
59
-
60
- def _get_urdu_translation_system_prompt(self) -> str:
61
- """Get the system prompt for Urdu translation from spec contract"""
62
- return """You are a technical translator specializing in AI, robotics, and computer science education.
63
- Translate the following English text to Urdu following these rules strictly:
64
-
65
- TECHNICAL TERMS:
66
- - Keep in English: ROS2, Python, API, HTTP, JSON, ML, AI, function, class, variable, loop, array
67
- - Translate common words: robot → روبوٹ, computer → کمپیوٹر, network → نیٹ ورک
68
- - Transliterate ambiguous terms: Sensor → سینسر (Sensor), Actuator → ایکچویٹر (Actuator)
69
- - NEVER translate code identifiers (function names, variables, etc.)
70
-
71
- FORMATTING:
72
- - Preserve ALL markdown syntax (headings #, bold **, italic _, lists -, links [](url))
73
- - Keep code blocks entirely in English (including comments): ```language ... ```
74
- - Keep inline code in English: `variable_name`
75
- - Keep LaTeX math unchanged: $equation$
76
- - Translate link text but keep URLs: [ترجمہ شدہ متن](https://example.com)
77
- - Translate image alt text but keep src: ![روبوٹ کی تصویر](robot.png)
78
-
79
- TONE:
80
- - Use formal educational tone (not conversational)
81
- - Follow standard Urdu grammar rules
82
- - Use proper Urdu punctuation (،؟ instead of ,?)
83
- - Do not mix English and Urdu in same sentence except for technical terms listed above
84
-
85
- Translate now:"""
86
-
87
- def translate_to_english(self, urdu_text: str, ttl: int = 3600) -> str:
88
- """Translate Urdu text back to English with caching"""
89
- # Create cache key
90
- cache_key = f"ur_to_en_{hash(urdu_text)}"
91
-
92
- # Check if translation is in cache and not expired
93
- if cache_key in self.translation_cache:
94
- if time.time() - self.cache_timestamps.get(cache_key, 0) < ttl:
95
- logger.info("Returning cached translation")
96
- return self.translation_cache[cache_key]
97
-
98
- # Call OpenRouter API for translation using OpenAI SDK
99
- try:
100
- system_prompt = self._get_english_translation_system_prompt()
101
-
102
- response = self.client.chat.completions.create(
103
- model=self.model,
104
- messages=[
105
- {"role": "system", "content": system_prompt},
106
- {"role": "user", "content": urdu_text}
107
- ],
108
- temperature=0.3,
109
- max_tokens=8000
110
- )
111
-
112
- translated_text = response.choices[0].message.content.strip()
113
-
114
- # Cache the translation
115
- self.translation_cache[cache_key] = translated_text
116
- self.cache_timestamps[cache_key] = time.time()
117
-
118
- logger.info(f"Translated text to English (length: {len(translated_text)} chars)")
119
- return translated_text
120
-
121
- except Exception as e:
122
- logger.error(f"Translation failed: {str(e)}")
123
- # Return a professional fallback response
124
- return f"Translation unavailable: {urdu_text[:100]}..."
125
-
126
- def _get_english_translation_system_prompt(self) -> str:
127
- """Get the system prompt for English translation"""
128
- return """You are a technical translator specializing in AI, robotics, and computer science education.
129
- Translate the following Urdu text to English with precision and accuracy.
130
-
131
- TRANSLATION REQUIREMENTS:
132
- - Maintain technical accuracy for robotics/AI terminology
133
- - Preserve the original meaning and context
134
- - Apply appropriate formality level for educational content
135
- - Ensure readability and flow in English
136
- - Keep technical terms (ROS2, Python, API, etc.) in English
137
- - Preserve markdown formatting and code blocks
138
- - Do not add any commentary or explanations
139
-
140
- Translate now:"""
141
-
142
- def clear_cache(self):
143
- """Clear the translation cache"""
144
- self.translation_cache.clear()
145
- self.cache_timestamps.clear()
146
  logger.info("Translation cache cleared")
 
1
+ from openai import OpenAI
2
+ import logging
3
+ from typing import Dict
4
+ import time
5
+ import os
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class TranslationService:
10
+ def __init__(self, openrouter_api_key: str = None):
11
+ """Initialize OpenRouter client for translation using OpenAI SDK compatibility"""
12
+ api_key = openrouter_api_key or os.getenv("OPENROUTER_API_KEY")
13
+ self.client = OpenAI(
14
+ base_url="https://openrouter.ai/api/v1",
15
+ api_key=api_key
16
+ )
17
+ self.model = "google/gemini-2.0-flash-exp:free"
18
+ self.translation_cache: Dict[str, str] = {}
19
+ self.cache_timestamps: Dict[str, float] = {}
20
+
21
+ def translate_to_urdu(self, text: str, ttl: int = 3600) -> str:
22
+ """Translate English text to Urdu with caching"""
23
+ # Create cache key
24
+ cache_key = f"en_to_ur_{hash(text)}"
25
+
26
+ # Check if translation is in cache and not expired
27
+ if cache_key in self.translation_cache:
28
+ if time.time() - self.cache_timestamps.get(cache_key, 0) < ttl:
29
+ logger.info("Returning cached translation")
30
+ return self.translation_cache[cache_key]
31
+
32
+ # Call OpenRouter API for translation using OpenAI SDK
33
+ try:
34
+ system_prompt = self._get_urdu_translation_system_prompt()
35
+
36
+ response = self.client.chat.completions.create(
37
+ model=self.model,
38
+ messages=[
39
+ {"role": "system", "content": system_prompt},
40
+ {"role": "user", "content": text}
41
+ ],
42
+ temperature=0.3,
43
+ max_tokens=8000
44
+ )
45
+
46
+ translated_text = response.choices[0].message.content.strip()
47
+
48
+ # Cache the translation
49
+ self.translation_cache[cache_key] = translated_text
50
+ self.cache_timestamps[cache_key] = time.time()
51
+
52
+ logger.info(f"Translated text to Urdu (length: {len(translated_text)} chars)")
53
+ return translated_text
54
+
55
+ except Exception as e:
56
+ logger.error(f"Translation failed: {str(e)}")
57
+ # Return a professional fallback response
58
+ return f"Translation unavailable: {text[:100]}..."
59
+
60
+ def _get_urdu_translation_system_prompt(self) -> str:
61
+ """Get the system prompt for Urdu translation from spec contract"""
62
+ return """You are a technical translator specializing in AI, robotics, and computer science education.
63
+ Translate the following English text to Urdu following these rules strictly:
64
+
65
+ TECHNICAL TERMS:
66
+ - Keep in English: ROS2, Python, API, HTTP, JSON, ML, AI, function, class, variable, loop, array
67
+ - Translate common words: robot → روبوٹ, computer → کمپیوٹر, network → نیٹ ورک
68
+ - Transliterate ambiguous terms: Sensor → سینسر (Sensor), Actuator → ایکچویٹر (Actuator)
69
+ - NEVER translate code identifiers (function names, variables, etc.)
70
+
71
+ FORMATTING:
72
+ - Preserve ALL markdown syntax (headings #, bold **, italic _, lists -, links [](url))
73
+ - Keep code blocks entirely in English (including comments): ```language ... ```
74
+ - Keep inline code in English: `variable_name`
75
+ - Keep LaTeX math unchanged: $equation$
76
+ - Translate link text but keep URLs: [ترجمہ شدہ متن](https://example.com)
77
+ - Translate image alt text but keep src: ![روبوٹ کی تصویر](robot.png)
78
+
79
+ TONE:
80
+ - Use formal educational tone (not conversational)
81
+ - Follow standard Urdu grammar rules
82
+ - Use proper Urdu punctuation (،؟ instead of ,?)
83
+ - Do not mix English and Urdu in same sentence except for technical terms listed above
84
+
85
+ Translate now:"""
86
+
87
+ def translate_to_english(self, urdu_text: str, ttl: int = 3600) -> str:
88
+ """Translate Urdu text back to English with caching"""
89
+ # Create cache key
90
+ cache_key = f"ur_to_en_{hash(urdu_text)}"
91
+
92
+ # Check if translation is in cache and not expired
93
+ if cache_key in self.translation_cache:
94
+ if time.time() - self.cache_timestamps.get(cache_key, 0) < ttl:
95
+ logger.info("Returning cached translation")
96
+ return self.translation_cache[cache_key]
97
+
98
+ # Call OpenRouter API for translation using OpenAI SDK
99
+ try:
100
+ system_prompt = self._get_english_translation_system_prompt()
101
+
102
+ response = self.client.chat.completions.create(
103
+ model=self.model,
104
+ messages=[
105
+ {"role": "system", "content": system_prompt},
106
+ {"role": "user", "content": urdu_text}
107
+ ],
108
+ temperature=0.3,
109
+ max_tokens=8000
110
+ )
111
+
112
+ translated_text = response.choices[0].message.content.strip()
113
+
114
+ # Cache the translation
115
+ self.translation_cache[cache_key] = translated_text
116
+ self.cache_timestamps[cache_key] = time.time()
117
+
118
+ logger.info(f"Translated text to English (length: {len(translated_text)} chars)")
119
+ return translated_text
120
+
121
+ except Exception as e:
122
+ logger.error(f"Translation failed: {str(e)}")
123
+ # Return a professional fallback response
124
+ return f"Translation unavailable: {urdu_text[:100]}..."
125
+
126
+ def _get_english_translation_system_prompt(self) -> str:
127
+ """Get the system prompt for English translation"""
128
+ return """You are a technical translator specializing in AI, robotics, and computer science education.
129
+ Translate the following Urdu text to English with precision and accuracy.
130
+
131
+ TRANSLATION REQUIREMENTS:
132
+ - Maintain technical accuracy for robotics/AI terminology
133
+ - Preserve the original meaning and context
134
+ - Apply appropriate formality level for educational content
135
+ - Ensure readability and flow in English
136
+ - Keep technical terms (ROS2, Python, API, etc.) in English
137
+ - Preserve markdown formatting and code blocks
138
+ - Do not add any commentary or explanations
139
+
140
+ Translate now:"""
141
+
142
+ def clear_cache(self):
143
+ """Clear the translation cache"""
144
+ self.translation_cache.clear()
145
+ self.cache_timestamps.clear()
146
  logger.info("Translation cache cleared")
start.bat CHANGED
@@ -1,26 +1,26 @@
1
- @echo off
2
- REM Batch script to start the backend server
3
- REM Usage: start.bat
4
-
5
- echo Starting AI Textbook Backend Server...
6
-
7
- REM Activate virtual environment
8
- if exist "venv\Scripts\activate.bat" (
9
- echo Activating virtual environment...
10
- call venv\Scripts\activate.bat
11
- ) else (
12
- echo Virtual environment not found. Creating one...
13
- python -m venv venv
14
- call venv\Scripts\activate.bat
15
- )
16
-
17
- REM Install dependencies
18
- echo Installing dependencies...
19
- pip install -r requirements.txt
20
-
21
- REM Start the server
22
- echo.
23
- echo Server starting on http://localhost:8001
24
- echo Press Ctrl+C to stop the server
25
- echo.
26
- python -m uvicorn main:app --host 0.0.0.0 --port 8001 --reload
 
1
+ @echo off
2
+ REM Batch script to start the backend server
3
+ REM Usage: start.bat
4
+
5
+ echo Starting AI Textbook Backend Server...
6
+
7
+ REM Activate virtual environment
8
+ if exist "venv\Scripts\activate.bat" (
9
+ echo Activating virtual environment...
10
+ call venv\Scripts\activate.bat
11
+ ) else (
12
+ echo Virtual environment not found. Creating one...
13
+ python -m venv venv
14
+ call venv\Scripts\activate.bat
15
+ )
16
+
17
+ REM Install dependencies
18
+ echo Installing dependencies...
19
+ pip install -r requirements.txt
20
+
21
+ REM Start the server
22
+ echo.
23
+ echo Server starting on http://localhost:8001
24
+ echo Press Ctrl+C to stop the server
25
+ echo.
26
+ python -m uvicorn main:app --host 0.0.0.0 --port 8001 --reload
start.ps1 CHANGED
@@ -1,23 +1,23 @@
1
- # PowerShell script to start the backend server
2
- # Usage: .\start.ps1
3
-
4
- Write-Host "🚀 Starting AI Textbook Backend Server..." -ForegroundColor Cyan
5
-
6
- # Check if virtual environment exists
7
- if (Test-Path ".\venv\Scripts\Activate.ps1") {
8
- Write-Host "✓ Activating virtual environment..." -ForegroundColor Green
9
- & .\venv\Scripts\Activate.ps1
10
- } else {
11
- Write-Host "⚠ Virtual environment not found. Creating one..." -ForegroundColor Yellow
12
- python -m venv venv
13
- & .\venv\Scripts\Activate.ps1
14
- }
15
-
16
- # Install dependencies
17
- Write-Host "📦 Installing dependencies..." -ForegroundColor Cyan
18
- pip install -r requirements.txt
19
-
20
- # Start the server
21
- Write-Host "🌐 Starting server on http://localhost:8001" -ForegroundColor Green
22
- Write-Host "Press Ctrl+C to stop the server" -ForegroundColor Yellow
23
- python -m uvicorn main:app --host 0.0.0.0 --port 8001 --reload
 
1
+ # PowerShell script to start the backend server
2
+ # Usage: .\start.ps1
3
+
4
+ Write-Host "🚀 Starting AI Textbook Backend Server..." -ForegroundColor Cyan
5
+
6
+ # Check if virtual environment exists
7
+ if (Test-Path ".\venv\Scripts\Activate.ps1") {
8
+ Write-Host "✓ Activating virtual environment..." -ForegroundColor Green
9
+ & .\venv\Scripts\Activate.ps1
10
+ } else {
11
+ Write-Host "⚠ Virtual environment not found. Creating one..." -ForegroundColor Yellow
12
+ python -m venv venv
13
+ & .\venv\Scripts\Activate.ps1
14
+ }
15
+
16
+ # Install dependencies
17
+ Write-Host "📦 Installing dependencies..." -ForegroundColor Cyan
18
+ pip install -r requirements.txt
19
+
20
+ # Start the server
21
+ Write-Host "🌐 Starting server on http://localhost:8001" -ForegroundColor Green
22
+ Write-Host "Press Ctrl+C to stop the server" -ForegroundColor Yellow
23
+ python -m uvicorn main:app --host 0.0.0.0 --port 8001 --reload
test_formatted_message.py CHANGED
@@ -1,61 +1,61 @@
1
- """
2
- Test the formatted message parsing
3
- """
4
- import os
5
- import sys
6
- from dotenv import load_dotenv
7
-
8
- load_dotenv()
9
-
10
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
11
- from services.rag_service import RAGService
12
-
13
- # Initialize
14
- api_key = os.getenv("OPENAI_API_KEY")
15
- rag_service = RAGService(api_key, None, "project_documents")
16
-
17
- # Test Case 1: User sends formatted message (what frontend sends)
18
- print("=== Test 1: Formatted Message (Current Issue) ===")
19
- selected_text = "Interactive Learning Features"
20
- question = """Selected text:
21
- "Interactive Learning Features"
22
-
23
- Ask a question about this text..."""
24
-
25
- print(f"Selected Text: {selected_text}")
26
- print(f"Question (formatted): {question}")
27
-
28
- # This is what's happening now - question should be extracted properly
29
- # In the fixed version, frontend will extract it, but let's test backend fallback
30
- answer = rag_service.query_rag(selected_text, question)
31
- print(f"\n✅ Answer: {answer[:200]}...")
32
- print(f"Length: {len(answer)} chars\n")
33
-
34
- # Test Case 2: User sends just the selected text (no question)
35
- print("=== Test 2: No Question, Just Selected Text ===")
36
- selected_text2 = "Physical AI refers to artificial intelligence"
37
- question2 = "" # Empty question
38
-
39
- answer2 = rag_service.query_rag(selected_text2, question2)
40
- print(f"\n✅ Answer: {answer2[:200]}...")
41
- print(f"Length: {len(answer2)} chars\n")
42
-
43
- # Test Case 3: Normal question (should work)
44
- print("=== Test 3: Normal Question ===")
45
- selected_text3 = "Robots use sensors and actuators to interact with the environment"
46
- question3 = "What are sensors and actuators?"
47
-
48
- answer3 = rag_service.query_rag(selected_text3, question3)
49
- print(f"\n✅ Answer: {answer3[:200]}...")
50
- print(f"Length: {len(answer3)} chars\n")
51
-
52
- # Test Case 4: Question same as selected text
53
- print("=== Test 4: Question Same as Selected Text ===")
54
- selected_text4 = "Humanoid Robotics"
55
- question4 = "Humanoid Robotics" # Same as selected text
56
-
57
- answer4 = rag_service.query_rag(selected_text4, question4)
58
- print(f"\n✅ Answer: {answer4[:200]}...")
59
- print(f"Length: {len(answer4)} chars\n")
60
-
61
- print("\n=== All Tests Complete ===")
 
1
+ """
2
+ Test the formatted message parsing
3
+ """
4
+ import os
5
+ import sys
6
+ from dotenv import load_dotenv
7
+
8
+ load_dotenv()
9
+
10
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
11
+ from services.rag_service import RAGService
12
+
13
+ # Initialize
14
+ api_key = os.getenv("OPENAI_API_KEY")
15
+ rag_service = RAGService(api_key, None, "project_documents")
16
+
17
+ # Test Case 1: User sends formatted message (what frontend sends)
18
+ print("=== Test 1: Formatted Message (Current Issue) ===")
19
+ selected_text = "Interactive Learning Features"
20
+ question = """Selected text:
21
+ "Interactive Learning Features"
22
+
23
+ Ask a question about this text..."""
24
+
25
+ print(f"Selected Text: {selected_text}")
26
+ print(f"Question (formatted): {question}")
27
+
28
+ # This is what's happening now - question should be extracted properly
29
+ # In the fixed version, frontend will extract it, but let's test backend fallback
30
+ answer = rag_service.query_rag(selected_text, question)
31
+ print(f"\n✅ Answer: {answer[:200]}...")
32
+ print(f"Length: {len(answer)} chars\n")
33
+
34
+ # Test Case 2: User sends just the selected text (no question)
35
+ print("=== Test 2: No Question, Just Selected Text ===")
36
+ selected_text2 = "Physical AI refers to artificial intelligence"
37
+ question2 = "" # Empty question
38
+
39
+ answer2 = rag_service.query_rag(selected_text2, question2)
40
+ print(f"\n✅ Answer: {answer2[:200]}...")
41
+ print(f"Length: {len(answer2)} chars\n")
42
+
43
+ # Test Case 3: Normal question (should work)
44
+ print("=== Test 3: Normal Question ===")
45
+ selected_text3 = "Robots use sensors and actuators to interact with the environment"
46
+ question3 = "What are sensors and actuators?"
47
+
48
+ answer3 = rag_service.query_rag(selected_text3, question3)
49
+ print(f"\n✅ Answer: {answer3[:200]}...")
50
+ print(f"Length: {len(answer3)} chars\n")
51
+
52
+ # Test Case 4: Question same as selected text
53
+ print("=== Test 4: Question Same as Selected Text ===")
54
+ selected_text4 = "Humanoid Robotics"
55
+ question4 = "Humanoid Robotics" # Same as selected text
56
+
57
+ answer4 = rag_service.query_rag(selected_text4, question4)
58
+ print(f"\n✅ Answer: {answer4[:200]}...")
59
+ print(f"Length: {len(answer4)} chars\n")
60
+
61
+ print("\n=== All Tests Complete ===")
test_openrouter_direct.py CHANGED
@@ -1,79 +1,79 @@
1
- """
2
- Direct test of OpenRouter API to verify it's working
3
- """
4
- import os
5
- from dotenv import load_dotenv
6
- from openai import OpenAI
7
-
8
- # Load environment variables
9
- load_dotenv()
10
-
11
- api_key = os.getenv("OPENAI_API_KEY")
12
- print(f"API Key loaded: {api_key[:20]}..." if api_key else "No API key found!")
13
-
14
- # Initialize OpenAI client with OpenRouter
15
- client = OpenAI(
16
- api_key=api_key,
17
- base_url="https://openrouter.ai/api/v1"
18
- )
19
-
20
- # Test 1: Simple chat completion
21
- print("\n=== Test 1: Simple Chat ===")
22
- try:
23
- response = client.chat.completions.create(
24
- model="openai/gpt-3.5-turbo",
25
- messages=[
26
- {"role": "user", "content": "Say 'Hello, I am working!' in one sentence."}
27
- ],
28
- temperature=0
29
- )
30
- print(f"✅ Success: {response.choices[0].message.content}")
31
- except Exception as e:
32
- print(f"❌ Error: {str(e)}")
33
-
34
- # Test 2: RAG-style query with context
35
- print("\n=== Test 2: RAG with Context ===")
36
- context = """
37
- Physical AI refers to artificial intelligence systems that interact with the physical world.
38
- These systems use sensors to perceive their environment and actuators to take actions.
39
- Examples include robots, self-driving cars, and drones.
40
- """
41
-
42
- question = "What is Physical AI?"
43
-
44
- try:
45
- response = client.chat.completions.create(
46
- model="openai/gpt-3.5-turbo",
47
- messages=[
48
- {"role": "system", "content": "You are a helpful AI assistant. Answer based on the provided context."},
49
- {"role": "user", "content": f"Context: {context}\n\nQuestion: {question}"}
50
- ],
51
- temperature=0.3,
52
- max_tokens=500
53
- )
54
- print(f"✅ Success: {response.choices[0].message.content}")
55
- except Exception as e:
56
- print(f"❌ Error: {str(e)}")
57
-
58
- # Test 3: Check if response is empty or too short
59
- print("\n=== Test 3: Response Length Check ===")
60
- try:
61
- response = client.chat.completions.create(
62
- model="openai/gpt-3.5-turbo",
63
- messages=[
64
- {"role": "user", "content": "Explain robotics in 50 words."}
65
- ],
66
- temperature=0.3
67
- )
68
- answer = response.choices[0].message.content
69
- print(f"Response length: {len(answer)} characters")
70
- print(f"Response: {answer}")
71
-
72
- if len(answer) < 20:
73
- print("⚠️ WARNING: Response too short!")
74
- else:
75
- print("✅ Response length is good")
76
- except Exception as e:
77
- print(f"❌ Error: {str(e)}")
78
-
79
- print("\n=== All Tests Complete ===")
 
1
+ """
2
+ Direct test of OpenRouter API to verify it's working
3
+ """
4
+ import os
5
+ from dotenv import load_dotenv
6
+ from openai import OpenAI
7
+
8
+ # Load environment variables
9
+ load_dotenv()
10
+
11
+ api_key = os.getenv("OPENAI_API_KEY")
12
+ print(f"API Key loaded: {api_key[:20]}..." if api_key else "No API key found!")
13
+
14
+ # Initialize OpenAI client with OpenRouter
15
+ client = OpenAI(
16
+ api_key=api_key,
17
+ base_url="https://openrouter.ai/api/v1"
18
+ )
19
+
20
+ # Test 1: Simple chat completion
21
+ print("\n=== Test 1: Simple Chat ===")
22
+ try:
23
+ response = client.chat.completions.create(
24
+ model="openai/gpt-3.5-turbo",
25
+ messages=[
26
+ {"role": "user", "content": "Say 'Hello, I am working!' in one sentence."}
27
+ ],
28
+ temperature=0
29
+ )
30
+ print(f"✅ Success: {response.choices[0].message.content}")
31
+ except Exception as e:
32
+ print(f"❌ Error: {str(e)}")
33
+
34
+ # Test 2: RAG-style query with context
35
+ print("\n=== Test 2: RAG with Context ===")
36
+ context = """
37
+ Physical AI refers to artificial intelligence systems that interact with the physical world.
38
+ These systems use sensors to perceive their environment and actuators to take actions.
39
+ Examples include robots, self-driving cars, and drones.
40
+ """
41
+
42
+ question = "What is Physical AI?"
43
+
44
+ try:
45
+ response = client.chat.completions.create(
46
+ model="openai/gpt-3.5-turbo",
47
+ messages=[
48
+ {"role": "system", "content": "You are a helpful AI assistant. Answer based on the provided context."},
49
+ {"role": "user", "content": f"Context: {context}\n\nQuestion: {question}"}
50
+ ],
51
+ temperature=0.3,
52
+ max_tokens=500
53
+ )
54
+ print(f"✅ Success: {response.choices[0].message.content}")
55
+ except Exception as e:
56
+ print(f"❌ Error: {str(e)}")
57
+
58
+ # Test 3: Check if response is empty or too short
59
+ print("\n=== Test 3: Response Length Check ===")
60
+ try:
61
+ response = client.chat.completions.create(
62
+ model="openai/gpt-3.5-turbo",
63
+ messages=[
64
+ {"role": "user", "content": "Explain robotics in 50 words."}
65
+ ],
66
+ temperature=0.3
67
+ )
68
+ answer = response.choices[0].message.content
69
+ print(f"Response length: {len(answer)} characters")
70
+ print(f"Response: {answer}")
71
+
72
+ if len(answer) < 20:
73
+ print("⚠️ WARNING: Response too short!")
74
+ else:
75
+ print("✅ Response length is good")
76
+ except Exception as e:
77
+ print(f"❌ Error: {str(e)}")
78
+
79
+ print("\n=== All Tests Complete ===")
test_rag_service_debug.py CHANGED
@@ -1,73 +1,73 @@
1
- """
2
- Debug the RAG service to see what's happening
3
- """
4
- import os
5
- import sys
6
- import logging
7
- from dotenv import load_dotenv
8
- from qdrant_client import QdrantClient
9
-
10
- # Setup logging
11
- logging.basicConfig(level=logging.DEBUG)
12
-
13
- # Load environment
14
- load_dotenv()
15
-
16
- # Import RAG service
17
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
18
- from services.rag_service import RAGService
19
-
20
- # Initialize
21
- api_key = os.getenv("OPENAI_API_KEY")
22
- qdrant_url = os.getenv("QDRANT_URL")
23
- qdrant_api_key = os.getenv("QDRANT_API_KEY")
24
- collection_name = os.getenv("QDRANT_COLLECTION", "project_documents")
25
-
26
- print(f"API Key: {api_key[:20]}..." if api_key else "No API key")
27
- print(f"Qdrant URL: {qdrant_url}")
28
- print(f"Collection: {collection_name}")
29
-
30
- # Initialize Qdrant client
31
- qdrant_host = os.getenv("QDRANT_HOST")
32
- if qdrant_api_key and qdrant_host and "qdrant.io" in qdrant_host:
33
- print("\n✅ Using Qdrant Cloud")
34
- qdrant_client = QdrantClient(
35
- url=qdrant_host,
36
- api_key=qdrant_api_key,
37
- prefer_grpc=False
38
- )
39
- else:
40
- print("\n✅ Using Local Qdrant (without Qdrant for testing)")
41
- # Don't initialize Qdrant for now - test without it
42
- qdrant_client = None
43
- print("⚠️ Qdrant disabled for testing - will use selected text only")
44
-
45
- # Initialize RAG service
46
- rag_service = RAGService(api_key, qdrant_client, collection_name)
47
-
48
- # Test query
49
- selected_text = """
50
- Physical AI refers to artificial intelligence systems that interact with the physical world.
51
- These systems use sensors to perceive their environment and actuators to take actions.
52
- Examples include robots, self-driving cars, and drones.
53
- """
54
-
55
- question = "What is Physical AI?"
56
-
57
- print("\n=== Testing RAG Service ===")
58
- print(f"Selected Text Length: {len(selected_text)} chars")
59
- print(f"Question: {question}")
60
-
61
- try:
62
- answer = rag_service.query_rag(selected_text, question)
63
- print(f"\n✅ Answer received:")
64
- print(f"Length: {len(answer)} chars")
65
- print(f"Content: {answer}")
66
-
67
- if "Is sawal ka jawab provided data me mojood nahi hai" in answer:
68
- print("\n❌ ERROR: Getting fallback message!")
69
- print("This means the RAG logic is failing somewhere.")
70
- except Exception as e:
71
- print(f"\n❌ Error: {str(e)}")
72
- import traceback
73
- traceback.print_exc()
 
1
+ """
2
+ Debug the RAG service to see what's happening
3
+ """
4
+ import os
5
+ import sys
6
+ import logging
7
+ from dotenv import load_dotenv
8
+ from qdrant_client import QdrantClient
9
+
10
+ # Setup logging
11
+ logging.basicConfig(level=logging.DEBUG)
12
+
13
+ # Load environment
14
+ load_dotenv()
15
+
16
+ # Import RAG service
17
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
18
+ from services.rag_service import RAGService
19
+
20
+ # Initialize
21
+ api_key = os.getenv("OPENAI_API_KEY")
22
+ qdrant_url = os.getenv("QDRANT_URL")
23
+ qdrant_api_key = os.getenv("QDRANT_API_KEY")
24
+ collection_name = os.getenv("QDRANT_COLLECTION", "project_documents")
25
+
26
+ print(f"API Key: {api_key[:20]}..." if api_key else "No API key")
27
+ print(f"Qdrant URL: {qdrant_url}")
28
+ print(f"Collection: {collection_name}")
29
+
30
+ # Initialize Qdrant client
31
+ qdrant_host = os.getenv("QDRANT_HOST")
32
+ if qdrant_api_key and qdrant_host and "qdrant.io" in qdrant_host:
33
+ print("\n✅ Using Qdrant Cloud")
34
+ qdrant_client = QdrantClient(
35
+ url=qdrant_host,
36
+ api_key=qdrant_api_key,
37
+ prefer_grpc=False
38
+ )
39
+ else:
40
+ print("\n✅ Using Local Qdrant (without Qdrant for testing)")
41
+ # Don't initialize Qdrant for now - test without it
42
+ qdrant_client = None
43
+ print("⚠️ Qdrant disabled for testing - will use selected text only")
44
+
45
+ # Initialize RAG service
46
+ rag_service = RAGService(api_key, qdrant_client, collection_name)
47
+
48
+ # Test query
49
+ selected_text = """
50
+ Physical AI refers to artificial intelligence systems that interact with the physical world.
51
+ These systems use sensors to perceive their environment and actuators to take actions.
52
+ Examples include robots, self-driving cars, and drones.
53
+ """
54
+
55
+ question = "What is Physical AI?"
56
+
57
+ print("\n=== Testing RAG Service ===")
58
+ print(f"Selected Text Length: {len(selected_text)} chars")
59
+ print(f"Question: {question}")
60
+
61
+ try:
62
+ answer = rag_service.query_rag(selected_text, question)
63
+ print(f"\n✅ Answer received:")
64
+ print(f"Length: {len(answer)} chars")
65
+ print(f"Content: {answer}")
66
+
67
+ if "Is sawal ka jawab provided data me mojood nahi hai" in answer:
68
+ print("\n❌ ERROR: Getting fallback message!")
69
+ print("This means the RAG logic is failing somewhere.")
70
+ except Exception as e:
71
+ print(f"\n❌ Error: {str(e)}")
72
+ import traceback
73
+ traceback.print_exc()
tests/e2e/test_translation_flow.py CHANGED
@@ -1,295 +1,295 @@
1
- """
2
- End-to-End Test: Complete Translation Flow
3
-
4
- Tests the full user journey:
5
- 1. User signup
6
- 2. Login
7
- 3. Navigate to chapter
8
- 4. Translate to Urdu
9
- 5. Toggle back to English
10
- 6. Submit feedback
11
-
12
- Note: Requires Playwright installation:
13
- pip install playwright pytest-playwright
14
- playwright install
15
- """
16
-
17
- import pytest
18
- from playwright.sync_api import Page, expect
19
- import time
20
-
21
-
22
- # Test configuration
23
- BASE_URL = "http://localhost:3000"
24
- API_URL = "http://localhost:8001"
25
- TEST_USER_EMAIL = f"test_{int(time.time())}@example.com"
26
- TEST_USER_PASSWORD = "Test1234!"
27
-
28
-
29
- @pytest.fixture(scope="module")
30
- def browser_context(playwright):
31
- """Create browser context for all tests"""
32
- browser = playwright.chromium.launch(headless=True)
33
- context = browser.new_context(viewport={"width": 1920, "height": 1080})
34
- yield context
35
- context.close()
36
- browser.close()
37
-
38
-
39
- @pytest.fixture
40
- def page(browser_context):
41
- """Create new page for each test"""
42
- page = browser_context.new_page()
43
- yield page
44
- page.close()
45
-
46
-
47
- class TestTranslationE2E:
48
- """End-to-end translation flow tests"""
49
-
50
- def test_01_user_signup(self, page: Page):
51
- """Test 1: User can sign up with software/hardware background"""
52
- print(f"\n🧪 Test 1: Signup with email: {TEST_USER_EMAIL}")
53
-
54
- # Navigate to signup page
55
- page.goto(f"{BASE_URL}/signup")
56
- page.wait_for_load_state("networkidle")
57
-
58
- # Fill signup form
59
- page.fill('input[type="email"]', TEST_USER_EMAIL)
60
- page.fill('input[type="password"]', TEST_USER_PASSWORD)
61
- page.fill('textarea[placeholder*="software"]', "Python, JavaScript, ROS2")
62
- page.fill('textarea[placeholder*="hardware"]', "Arduino, Raspberry Pi")
63
-
64
- # Submit form
65
- page.click('button[type="submit"]')
66
- page.wait_for_timeout(2000)
67
-
68
- # Verify redirect to homepage or dashboard
69
- assert "signup" not in page.url.lower() or "login" in page.url.lower()
70
- print("✅ Signup successful")
71
-
72
- def test_02_user_login(self, page: Page):
73
- """Test 2: User can login with credentials"""
74
- print(f"\n🧪 Test 2: Login with email: {TEST_USER_EMAIL}")
75
-
76
- # Navigate to login page
77
- page.goto(f"{BASE_URL}/login")
78
- page.wait_for_load_state("networkidle")
79
-
80
- # Fill login form
81
- page.fill('input[type="email"]', TEST_USER_EMAIL)
82
- page.fill('input[type="password"]', TEST_USER_PASSWORD)
83
-
84
- # Submit form
85
- page.click('button[type="submit"]')
86
- page.wait_for_timeout(2000)
87
-
88
- # Verify authentication (should see logout or profile)
89
- assert "login" not in page.url.lower()
90
- print("✅ Login successful")
91
-
92
- def test_03_navigate_to_chapter(self, page: Page):
93
- """Test 3: User can navigate to a chapter with translation support"""
94
- print("\n🧪 Test 3: Navigate to ROS2 Fundamentals chapter")
95
-
96
- # Login first
97
- page.goto(f"{BASE_URL}/login")
98
- page.fill('input[type="email"]', TEST_USER_EMAIL)
99
- page.fill('input[type="password"]', TEST_USER_PASSWORD)
100
- page.click('button[type="submit"]')
101
- page.wait_for_timeout(2000)
102
-
103
- # Navigate to chapter with translation support
104
- page.goto(f"{BASE_URL}/docs/ros2-fundamentals")
105
- page.wait_for_load_state("networkidle")
106
-
107
- # Verify chapter loaded
108
- assert "ros2" in page.url.lower() or "fundamentals" in page.url.lower()
109
-
110
- # Verify translation button exists
111
- translate_button = page.locator('button:has-text("Translate to Urdu")')
112
- expect(translate_button).to_be_visible(timeout=5000)
113
- print("✅ Chapter loaded with translation button")
114
-
115
- def test_04_translate_to_urdu(self, page: Page):
116
- """Test 4: User can translate chapter to Urdu (first time - API call)"""
117
- print("\n🧪 Test 4: Translate chapter to Urdu")
118
-
119
- # Login and navigate to chapter
120
- page.goto(f"{BASE_URL}/login")
121
- page.fill('input[type="email"]', TEST_USER_EMAIL)
122
- page.fill('input[type="password"]', TEST_USER_PASSWORD)
123
- page.click('button[type="submit"]')
124
- page.wait_for_timeout(2000)
125
-
126
- page.goto(f"{BASE_URL}/docs/intro")
127
- page.wait_for_load_state("networkidle")
128
-
129
- # Click translate button
130
- translate_button = page.locator('button:has-text("Translate to Urdu")')
131
- translate_button.click()
132
-
133
- # Verify loading state appears
134
- loading_indicator = page.locator('text=Translating')
135
- expect(loading_indicator).to_be_visible(timeout=2000)
136
- print("⏳ Loading indicator visible")
137
-
138
- # Wait for translation to complete (8-10 seconds)
139
- page.wait_for_timeout(12000)
140
-
141
- # Verify button changed to "View in English"
142
- english_button = page.locator('button:has-text("View in English")')
143
- expect(english_button).to_be_visible(timeout=15000)
144
- print("✅ Translation completed successfully")
145
-
146
- # Verify Urdu content is displayed (check for RTL)
147
- # Note: Actual Urdu verification would need OCR or specific text matching
148
- page.wait_for_timeout(1000)
149
-
150
- def test_05_toggle_to_english(self, page: Page):
151
- """Test 5: User can toggle back to English instantly"""
152
- print("\n🧪 Test 5: Toggle back to English")
153
-
154
- # Login, navigate, and translate first
155
- page.goto(f"{BASE_URL}/login")
156
- page.fill('input[type="email"]', TEST_USER_EMAIL)
157
- page.fill('input[type="password"]', TEST_USER_PASSWORD)
158
- page.click('button[type="submit"]')
159
- page.wait_for_timeout(2000)
160
-
161
- page.goto(f"{BASE_URL}/docs/intro")
162
- page.wait_for_load_state("networkidle")
163
-
164
- # Translate to Urdu first
165
- translate_button = page.locator('button:has-text("Translate to Urdu")')
166
- if translate_button.is_visible():
167
- translate_button.click()
168
- page.wait_for_timeout(12000)
169
-
170
- # Click "View in English" button
171
- english_button = page.locator('button:has-text("View in English")')
172
- start_time = time.time()
173
- english_button.click()
174
-
175
- # Verify instant toggle (<100ms target, but allow 1s for UI)
176
- page.wait_for_timeout(500)
177
- urdu_button = page.locator('button:has-text("Translate to Urdu")')
178
- expect(urdu_button).to_be_visible(timeout=2000)
179
-
180
- elapsed = time.time() - start_time
181
- print(f"✅ Toggled to English in {elapsed:.2f}s (instant)")
182
- assert elapsed < 2.0, "Toggle should be instant (<2s)"
183
-
184
- def test_06_cached_translation(self, page: Page):
185
- """Test 6: Second translation loads from cache (<1s)"""
186
- print("\n🧪 Test 6: Cached translation loading")
187
-
188
- # Login and navigate
189
- page.goto(f"{BASE_URL}/login")
190
- page.fill('input[type="email"]', TEST_USER_EMAIL)
191
- page.fill('input[type="password"]', TEST_USER_PASSWORD)
192
- page.click('button[type="submit"]')
193
- page.wait_for_timeout(2000)
194
-
195
- page.goto(f"{BASE_URL}/docs/intro")
196
- page.wait_for_load_state("networkidle")
197
-
198
- # Ensure we're in English view
199
- english_button = page.locator('button:has-text("View in English")')
200
- if english_button.is_visible():
201
- english_button.click()
202
- page.wait_for_timeout(500)
203
-
204
- # Click translate again (should hit cache)
205
- translate_button = page.locator('button:has-text("Translate to Urdu")')
206
- start_time = time.time()
207
- translate_button.click()
208
-
209
- # Wait for translation to complete
210
- english_button = page.locator('button:has-text("View in English")')
211
- expect(english_button).to_be_visible(timeout=5000)
212
-
213
- elapsed = time.time() - start_time
214
- print(f"✅ Cache hit in {elapsed:.2f}s")
215
-
216
- # Verify cache indicator visible
217
- cache_indicator = page.locator('text=Loaded from cache')
218
- # Cache indicator might not always be visible, so don't assert
219
- if cache_indicator.is_visible(timeout=1000):
220
- print("⚡ Cache indicator displayed")
221
-
222
- def test_07_submit_feedback(self, page: Page):
223
- """Test 7: User can submit translation feedback"""
224
- print("\n🧪 Test 7: Submit translation feedback")
225
-
226
- # Login, navigate, and translate
227
- page.goto(f"{BASE_URL}/login")
228
- page.fill('input[type="email"]', TEST_USER_EMAIL)
229
- page.fill('input[type="password"]', TEST_USER_PASSWORD)
230
- page.click('button[type="submit"]')
231
- page.wait_for_timeout(2000)
232
-
233
- page.goto(f"{BASE_URL}/docs/intro")
234
- page.wait_for_load_state("networkidle")
235
-
236
- # Translate to Urdu first
237
- translate_button = page.locator('button:has-text("Translate to Urdu")')
238
- if translate_button.is_visible():
239
- translate_button.click()
240
- page.wait_for_timeout(12000)
241
-
242
- # Click "Report Issue" button
243
- feedback_button = page.locator('button:has-text("Report Issue")')
244
- if feedback_button.is_visible():
245
- feedback_button.click()
246
- page.wait_for_timeout(500)
247
-
248
- # Fill feedback form
249
- textarea = page.locator('textarea[placeholder*="issue"]')
250
- textarea.fill("Test feedback: The term 'API' should remain in English.")
251
-
252
- # Submit feedback
253
- submit_button = page.locator('button:has-text("Submit Feedback")')
254
- submit_button.click()
255
- page.wait_for_timeout(2000)
256
-
257
- # Verify success message
258
- success_message = page.locator('text=Thank you')
259
- if success_message.is_visible(timeout=3000):
260
- print("✅ Feedback submitted successfully")
261
- else:
262
- print("⚠️ Feedback button found but submission uncertain")
263
- else:
264
- print("⚠️ Feedback button not visible (may need Urdu translation first)")
265
-
266
- def test_08_unauthenticated_user_prompt(self, page: Page):
267
- """Test 8: Unauthenticated users see login prompt"""
268
- print("\n🧪 Test 8: Unauthenticated user experience")
269
-
270
- # Navigate to chapter without login
271
- page.goto(f"{BASE_URL}/docs/intro")
272
- page.wait_for_load_state("networkidle")
273
-
274
- # Check if translation button is disabled or shows auth prompt
275
- translate_button = page.locator('button:has-text("Translate to Urdu")')
276
-
277
- if translate_button.is_visible():
278
- # Click button (should not translate)
279
- translate_button.click()
280
- page.wait_for_timeout(1000)
281
-
282
- # Should see authentication prompt or redirect to signup
283
- auth_prompt = page.locator('text=Login required, text=Sign up, text=login')
284
- if auth_prompt.is_visible(timeout=2000):
285
- print("✅ Authentication prompt shown to guest users")
286
- elif "signup" in page.url.lower() or "login" in page.url.lower():
287
- print("✅ Redirected to signup/login page")
288
- else:
289
- print("⚠️ Authentication handling unclear")
290
-
291
-
292
- # Run tests with: pytest backend/tests/e2e/test_translation_flow.py -v -s
293
- # Note: Requires backend and frontend servers running:
294
- # Terminal 1: cd backend && python3 main.py
295
- # Terminal 2: cd frontend && npm start
 
1
+ """
2
+ End-to-End Test: Complete Translation Flow
3
+
4
+ Tests the full user journey:
5
+ 1. User signup
6
+ 2. Login
7
+ 3. Navigate to chapter
8
+ 4. Translate to Urdu
9
+ 5. Toggle back to English
10
+ 6. Submit feedback
11
+
12
+ Note: Requires Playwright installation:
13
+ pip install playwright pytest-playwright
14
+ playwright install
15
+ """
16
+
17
+ import pytest
18
+ from playwright.sync_api import Page, expect
19
+ import time
20
+
21
+
22
+ # Test configuration
23
+ BASE_URL = "http://localhost:3000"
24
+ API_URL = "http://localhost:8001"
25
+ TEST_USER_EMAIL = f"test_{int(time.time())}@example.com"
26
+ TEST_USER_PASSWORD = "Test1234!"
27
+
28
+
29
+ @pytest.fixture(scope="module")
30
+ def browser_context(playwright):
31
+ """Create browser context for all tests"""
32
+ browser = playwright.chromium.launch(headless=True)
33
+ context = browser.new_context(viewport={"width": 1920, "height": 1080})
34
+ yield context
35
+ context.close()
36
+ browser.close()
37
+
38
+
39
+ @pytest.fixture
40
+ def page(browser_context):
41
+ """Create new page for each test"""
42
+ page = browser_context.new_page()
43
+ yield page
44
+ page.close()
45
+
46
+
47
+ class TestTranslationE2E:
48
+ """End-to-end translation flow tests"""
49
+
50
+ def test_01_user_signup(self, page: Page):
51
+ """Test 1: User can sign up with software/hardware background"""
52
+ print(f"\n🧪 Test 1: Signup with email: {TEST_USER_EMAIL}")
53
+
54
+ # Navigate to signup page
55
+ page.goto(f"{BASE_URL}/signup")
56
+ page.wait_for_load_state("networkidle")
57
+
58
+ # Fill signup form
59
+ page.fill('input[type="email"]', TEST_USER_EMAIL)
60
+ page.fill('input[type="password"]', TEST_USER_PASSWORD)
61
+ page.fill('textarea[placeholder*="software"]', "Python, JavaScript, ROS2")
62
+ page.fill('textarea[placeholder*="hardware"]', "Arduino, Raspberry Pi")
63
+
64
+ # Submit form
65
+ page.click('button[type="submit"]')
66
+ page.wait_for_timeout(2000)
67
+
68
+ # Verify redirect to homepage or dashboard
69
+ assert "signup" not in page.url.lower() or "login" in page.url.lower()
70
+ print("✅ Signup successful")
71
+
72
+ def test_02_user_login(self, page: Page):
73
+ """Test 2: User can login with credentials"""
74
+ print(f"\n🧪 Test 2: Login with email: {TEST_USER_EMAIL}")
75
+
76
+ # Navigate to login page
77
+ page.goto(f"{BASE_URL}/login")
78
+ page.wait_for_load_state("networkidle")
79
+
80
+ # Fill login form
81
+ page.fill('input[type="email"]', TEST_USER_EMAIL)
82
+ page.fill('input[type="password"]', TEST_USER_PASSWORD)
83
+
84
+ # Submit form
85
+ page.click('button[type="submit"]')
86
+ page.wait_for_timeout(2000)
87
+
88
+ # Verify authentication (should see logout or profile)
89
+ assert "login" not in page.url.lower()
90
+ print("✅ Login successful")
91
+
92
+ def test_03_navigate_to_chapter(self, page: Page):
93
+ """Test 3: User can navigate to a chapter with translation support"""
94
+ print("\n🧪 Test 3: Navigate to ROS2 Fundamentals chapter")
95
+
96
+ # Login first
97
+ page.goto(f"{BASE_URL}/login")
98
+ page.fill('input[type="email"]', TEST_USER_EMAIL)
99
+ page.fill('input[type="password"]', TEST_USER_PASSWORD)
100
+ page.click('button[type="submit"]')
101
+ page.wait_for_timeout(2000)
102
+
103
+ # Navigate to chapter with translation support
104
+ page.goto(f"{BASE_URL}/docs/ros2-fundamentals")
105
+ page.wait_for_load_state("networkidle")
106
+
107
+ # Verify chapter loaded
108
+ assert "ros2" in page.url.lower() or "fundamentals" in page.url.lower()
109
+
110
+ # Verify translation button exists
111
+ translate_button = page.locator('button:has-text("Translate to Urdu")')
112
+ expect(translate_button).to_be_visible(timeout=5000)
113
+ print("✅ Chapter loaded with translation button")
114
+
115
+ def test_04_translate_to_urdu(self, page: Page):
116
+ """Test 4: User can translate chapter to Urdu (first time - API call)"""
117
+ print("\n🧪 Test 4: Translate chapter to Urdu")
118
+
119
+ # Login and navigate to chapter
120
+ page.goto(f"{BASE_URL}/login")
121
+ page.fill('input[type="email"]', TEST_USER_EMAIL)
122
+ page.fill('input[type="password"]', TEST_USER_PASSWORD)
123
+ page.click('button[type="submit"]')
124
+ page.wait_for_timeout(2000)
125
+
126
+ page.goto(f"{BASE_URL}/docs/intro")
127
+ page.wait_for_load_state("networkidle")
128
+
129
+ # Click translate button
130
+ translate_button = page.locator('button:has-text("Translate to Urdu")')
131
+ translate_button.click()
132
+
133
+ # Verify loading state appears
134
+ loading_indicator = page.locator('text=Translating')
135
+ expect(loading_indicator).to_be_visible(timeout=2000)
136
+ print("⏳ Loading indicator visible")
137
+
138
+ # Wait for translation to complete (8-10 seconds)
139
+ page.wait_for_timeout(12000)
140
+
141
+ # Verify button changed to "View in English"
142
+ english_button = page.locator('button:has-text("View in English")')
143
+ expect(english_button).to_be_visible(timeout=15000)
144
+ print("✅ Translation completed successfully")
145
+
146
+ # Verify Urdu content is displayed (check for RTL)
147
+ # Note: Actual Urdu verification would need OCR or specific text matching
148
+ page.wait_for_timeout(1000)
149
+
150
+ def test_05_toggle_to_english(self, page: Page):
151
+ """Test 5: User can toggle back to English instantly"""
152
+ print("\n🧪 Test 5: Toggle back to English")
153
+
154
+ # Login, navigate, and translate first
155
+ page.goto(f"{BASE_URL}/login")
156
+ page.fill('input[type="email"]', TEST_USER_EMAIL)
157
+ page.fill('input[type="password"]', TEST_USER_PASSWORD)
158
+ page.click('button[type="submit"]')
159
+ page.wait_for_timeout(2000)
160
+
161
+ page.goto(f"{BASE_URL}/docs/intro")
162
+ page.wait_for_load_state("networkidle")
163
+
164
+ # Translate to Urdu first
165
+ translate_button = page.locator('button:has-text("Translate to Urdu")')
166
+ if translate_button.is_visible():
167
+ translate_button.click()
168
+ page.wait_for_timeout(12000)
169
+
170
+ # Click "View in English" button
171
+ english_button = page.locator('button:has-text("View in English")')
172
+ start_time = time.time()
173
+ english_button.click()
174
+
175
+ # Verify instant toggle (<100ms target, but allow 1s for UI)
176
+ page.wait_for_timeout(500)
177
+ urdu_button = page.locator('button:has-text("Translate to Urdu")')
178
+ expect(urdu_button).to_be_visible(timeout=2000)
179
+
180
+ elapsed = time.time() - start_time
181
+ print(f"✅ Toggled to English in {elapsed:.2f}s (instant)")
182
+ assert elapsed < 2.0, "Toggle should be instant (<2s)"
183
+
184
+ def test_06_cached_translation(self, page: Page):
185
+ """Test 6: Second translation loads from cache (<1s)"""
186
+ print("\n🧪 Test 6: Cached translation loading")
187
+
188
+ # Login and navigate
189
+ page.goto(f"{BASE_URL}/login")
190
+ page.fill('input[type="email"]', TEST_USER_EMAIL)
191
+ page.fill('input[type="password"]', TEST_USER_PASSWORD)
192
+ page.click('button[type="submit"]')
193
+ page.wait_for_timeout(2000)
194
+
195
+ page.goto(f"{BASE_URL}/docs/intro")
196
+ page.wait_for_load_state("networkidle")
197
+
198
+ # Ensure we're in English view
199
+ english_button = page.locator('button:has-text("View in English")')
200
+ if english_button.is_visible():
201
+ english_button.click()
202
+ page.wait_for_timeout(500)
203
+
204
+ # Click translate again (should hit cache)
205
+ translate_button = page.locator('button:has-text("Translate to Urdu")')
206
+ start_time = time.time()
207
+ translate_button.click()
208
+
209
+ # Wait for translation to complete
210
+ english_button = page.locator('button:has-text("View in English")')
211
+ expect(english_button).to_be_visible(timeout=5000)
212
+
213
+ elapsed = time.time() - start_time
214
+ print(f"✅ Cache hit in {elapsed:.2f}s")
215
+
216
+ # Verify cache indicator visible
217
+ cache_indicator = page.locator('text=Loaded from cache')
218
+ # Cache indicator might not always be visible, so don't assert
219
+ if cache_indicator.is_visible(timeout=1000):
220
+ print("⚡ Cache indicator displayed")
221
+
222
+ def test_07_submit_feedback(self, page: Page):
223
+ """Test 7: User can submit translation feedback"""
224
+ print("\n🧪 Test 7: Submit translation feedback")
225
+
226
+ # Login, navigate, and translate
227
+ page.goto(f"{BASE_URL}/login")
228
+ page.fill('input[type="email"]', TEST_USER_EMAIL)
229
+ page.fill('input[type="password"]', TEST_USER_PASSWORD)
230
+ page.click('button[type="submit"]')
231
+ page.wait_for_timeout(2000)
232
+
233
+ page.goto(f"{BASE_URL}/docs/intro")
234
+ page.wait_for_load_state("networkidle")
235
+
236
+ # Translate to Urdu first
237
+ translate_button = page.locator('button:has-text("Translate to Urdu")')
238
+ if translate_button.is_visible():
239
+ translate_button.click()
240
+ page.wait_for_timeout(12000)
241
+
242
+ # Click "Report Issue" button
243
+ feedback_button = page.locator('button:has-text("Report Issue")')
244
+ if feedback_button.is_visible():
245
+ feedback_button.click()
246
+ page.wait_for_timeout(500)
247
+
248
+ # Fill feedback form
249
+ textarea = page.locator('textarea[placeholder*="issue"]')
250
+ textarea.fill("Test feedback: The term 'API' should remain in English.")
251
+
252
+ # Submit feedback
253
+ submit_button = page.locator('button:has-text("Submit Feedback")')
254
+ submit_button.click()
255
+ page.wait_for_timeout(2000)
256
+
257
+ # Verify success message
258
+ success_message = page.locator('text=Thank you')
259
+ if success_message.is_visible(timeout=3000):
260
+ print("✅ Feedback submitted successfully")
261
+ else:
262
+ print("⚠️ Feedback button found but submission uncertain")
263
+ else:
264
+ print("⚠️ Feedback button not visible (may need Urdu translation first)")
265
+
266
+ def test_08_unauthenticated_user_prompt(self, page: Page):
267
+ """Test 8: Unauthenticated users see login prompt"""
268
+ print("\n🧪 Test 8: Unauthenticated user experience")
269
+
270
+ # Navigate to chapter without login
271
+ page.goto(f"{BASE_URL}/docs/intro")
272
+ page.wait_for_load_state("networkidle")
273
+
274
+ # Check if translation button is disabled or shows auth prompt
275
+ translate_button = page.locator('button:has-text("Translate to Urdu")')
276
+
277
+ if translate_button.is_visible():
278
+ # Click button (should not translate)
279
+ translate_button.click()
280
+ page.wait_for_timeout(1000)
281
+
282
+ # Should see authentication prompt or redirect to signup
283
+ auth_prompt = page.locator('text=Login required, text=Sign up, text=login')
284
+ if auth_prompt.is_visible(timeout=2000):
285
+ print("✅ Authentication prompt shown to guest users")
286
+ elif "signup" in page.url.lower() or "login" in page.url.lower():
287
+ print("✅ Redirected to signup/login page")
288
+ else:
289
+ print("⚠️ Authentication handling unclear")
290
+
291
+
292
+ # Run tests with: pytest backend/tests/e2e/test_translation_flow.py -v -s
293
+ # Note: Requires backend and frontend servers running:
294
+ # Terminal 1: cd backend && python3 main.py
295
+ # Terminal 2: cd frontend && npm start
tests/integration/test_feedback_endpoint.py CHANGED
@@ -1,126 +1,126 @@
1
- """
2
- Integration tests for Translation Feedback API endpoint
3
-
4
- Tests:
5
- - test_submit_feedback_authenticated: POST /api/translate/feedback with JWT
6
- - test_submit_feedback_unauthenticated: POST without JWT returns 401
7
- """
8
-
9
- import pytest
10
- from fastapi.testclient import TestClient
11
- from main import app
12
- from auth.jwt_utils import create_jwt_token
13
- import uuid
14
-
15
-
16
- client = TestClient(app)
17
-
18
-
19
- def test_submit_feedback_authenticated():
20
- """Test submitting feedback with valid JWT token"""
21
- # Arrange
22
- user_id = str(uuid.uuid4())
23
- user_email = "test@example.com"
24
- token = create_jwt_token(user_id, user_email)
25
-
26
- translation_id = str(uuid.uuid4())
27
- feedback_data = {
28
- "translation_id": translation_id,
29
- "issue_description": "Technical term 'ROS2' was incorrectly translated to Urdu"
30
- }
31
-
32
- # Act
33
- response = client.post(
34
- "/api/translate/feedback",
35
- json=feedback_data,
36
- headers={"Authorization": f"Bearer {token}"}
37
- )
38
-
39
- # Assert
40
- assert response.status_code in [201, 200, 503], f"Expected 201/200/503, got {response.status_code}: {response.text}"
41
-
42
- if response.status_code == 201:
43
- data = response.json()
44
- assert "feedback_id" in data or "id" in data
45
- print(f"✅ Feedback submitted successfully: {data}")
46
- elif response.status_code == 503:
47
- print("⚠️ Database unavailable (mock mode) - endpoint exists but DB not connected")
48
- else:
49
- print(f"✅ Feedback endpoint exists: {response.status_code}")
50
-
51
-
52
- def test_submit_feedback_unauthenticated():
53
- """Test submitting feedback without JWT token returns 401"""
54
- # Arrange
55
- feedback_data = {
56
- "translation_id": str(uuid.uuid4()),
57
- "issue_description": "Translation issue"
58
- }
59
-
60
- # Act
61
- response = client.post(
62
- "/api/translate/feedback",
63
- json=feedback_data
64
- # No Authorization header
65
- )
66
-
67
- # Assert
68
- assert response.status_code in [401, 404], f"Expected 401 or 404 (endpoint not implemented yet), got {response.status_code}"
69
-
70
- if response.status_code == 401:
71
- print("✅ Unauthenticated request properly rejected with 401")
72
- else:
73
- print("⚠️ Feedback endpoint not yet implemented (404)")
74
-
75
-
76
- def test_submit_feedback_missing_fields():
77
- """Test submitting feedback with missing required fields"""
78
- # Arrange
79
- user_id = str(uuid.uuid4())
80
- user_email = "test@example.com"
81
- token = create_jwt_token(user_id, user_email)
82
-
83
- incomplete_data = {
84
- "translation_id": str(uuid.uuid4())
85
- # Missing issue_description
86
- }
87
-
88
- # Act
89
- response = client.post(
90
- "/api/translate/feedback",
91
- json=incomplete_data,
92
- headers={"Authorization": f"Bearer {token}"}
93
- )
94
-
95
- # Assert
96
- assert response.status_code in [422, 400, 404], f"Expected 422/400/404, got {response.status_code}"
97
-
98
- if response.status_code in [422, 400]:
99
- print("✅ Validation error properly handled")
100
- else:
101
- print("⚠️ Endpoint not yet implemented")
102
-
103
-
104
- def test_submit_feedback_invalid_translation_id():
105
- """Test submitting feedback with non-existent translation_id"""
106
- # Arrange
107
- user_id = str(uuid.uuid4())
108
- user_email = "test@example.com"
109
- token = create_jwt_token(user_id, user_email)
110
-
111
- feedback_data = {
112
- "translation_id": str(uuid.uuid4()), # Non-existent translation
113
- "issue_description": "Test issue"
114
- }
115
-
116
- # Act
117
- response = client.post(
118
- "/api/translate/feedback",
119
- json=feedback_data,
120
- headers={"Authorization": f"Bearer {token}"}
121
- )
122
-
123
- # Assert
124
- # Should either accept (201) or reject with 404 if FK constraint enforced
125
- assert response.status_code in [201, 404, 400, 503], f"Got {response.status_code}"
126
- print(f"✅ Invalid translation_id handled: {response.status_code}")
 
1
+ """
2
+ Integration tests for Translation Feedback API endpoint
3
+
4
+ Tests:
5
+ - test_submit_feedback_authenticated: POST /api/translate/feedback with JWT
6
+ - test_submit_feedback_unauthenticated: POST without JWT returns 401
7
+ """
8
+
9
+ import pytest
10
+ from fastapi.testclient import TestClient
11
+ from main import app
12
+ from auth.jwt_utils import create_jwt_token
13
+ import uuid
14
+
15
+
16
+ client = TestClient(app)
17
+
18
+
19
+ def test_submit_feedback_authenticated():
20
+ """Test submitting feedback with valid JWT token"""
21
+ # Arrange
22
+ user_id = str(uuid.uuid4())
23
+ user_email = "test@example.com"
24
+ token = create_jwt_token(user_id, user_email)
25
+
26
+ translation_id = str(uuid.uuid4())
27
+ feedback_data = {
28
+ "translation_id": translation_id,
29
+ "issue_description": "Technical term 'ROS2' was incorrectly translated to Urdu"
30
+ }
31
+
32
+ # Act
33
+ response = client.post(
34
+ "/api/translate/feedback",
35
+ json=feedback_data,
36
+ headers={"Authorization": f"Bearer {token}"}
37
+ )
38
+
39
+ # Assert
40
+ assert response.status_code in [201, 200, 503], f"Expected 201/200/503, got {response.status_code}: {response.text}"
41
+
42
+ if response.status_code == 201:
43
+ data = response.json()
44
+ assert "feedback_id" in data or "id" in data
45
+ print(f"✅ Feedback submitted successfully: {data}")
46
+ elif response.status_code == 503:
47
+ print("⚠️ Database unavailable (mock mode) - endpoint exists but DB not connected")
48
+ else:
49
+ print(f"✅ Feedback endpoint exists: {response.status_code}")
50
+
51
+
52
+ def test_submit_feedback_unauthenticated():
53
+ """Test submitting feedback without JWT token returns 401"""
54
+ # Arrange
55
+ feedback_data = {
56
+ "translation_id": str(uuid.uuid4()),
57
+ "issue_description": "Translation issue"
58
+ }
59
+
60
+ # Act
61
+ response = client.post(
62
+ "/api/translate/feedback",
63
+ json=feedback_data
64
+ # No Authorization header
65
+ )
66
+
67
+ # Assert
68
+ assert response.status_code in [401, 404], f"Expected 401 or 404 (endpoint not implemented yet), got {response.status_code}"
69
+
70
+ if response.status_code == 401:
71
+ print("✅ Unauthenticated request properly rejected with 401")
72
+ else:
73
+ print("⚠️ Feedback endpoint not yet implemented (404)")
74
+
75
+
76
+ def test_submit_feedback_missing_fields():
77
+ """Test submitting feedback with missing required fields"""
78
+ # Arrange
79
+ user_id = str(uuid.uuid4())
80
+ user_email = "test@example.com"
81
+ token = create_jwt_token(user_id, user_email)
82
+
83
+ incomplete_data = {
84
+ "translation_id": str(uuid.uuid4())
85
+ # Missing issue_description
86
+ }
87
+
88
+ # Act
89
+ response = client.post(
90
+ "/api/translate/feedback",
91
+ json=incomplete_data,
92
+ headers={"Authorization": f"Bearer {token}"}
93
+ )
94
+
95
+ # Assert
96
+ assert response.status_code in [422, 400, 404], f"Expected 422/400/404, got {response.status_code}"
97
+
98
+ if response.status_code in [422, 400]:
99
+ print("✅ Validation error properly handled")
100
+ else:
101
+ print("⚠️ Endpoint not yet implemented")
102
+
103
+
104
+ def test_submit_feedback_invalid_translation_id():
105
+ """Test submitting feedback with non-existent translation_id"""
106
+ # Arrange
107
+ user_id = str(uuid.uuid4())
108
+ user_email = "test@example.com"
109
+ token = create_jwt_token(user_id, user_email)
110
+
111
+ feedback_data = {
112
+ "translation_id": str(uuid.uuid4()), # Non-existent translation
113
+ "issue_description": "Test issue"
114
+ }
115
+
116
+ # Act
117
+ response = client.post(
118
+ "/api/translate/feedback",
119
+ json=feedback_data,
120
+ headers={"Authorization": f"Bearer {token}"}
121
+ )
122
+
123
+ # Assert
124
+ # Should either accept (201) or reject with 404 if FK constraint enforced
125
+ assert response.status_code in [201, 404, 400, 503], f"Got {response.status_code}"
126
+ print(f"✅ Invalid translation_id handled: {response.status_code}")
tests/unit/test_feedback_model.py CHANGED
@@ -1,81 +1,81 @@
1
- """
2
- Unit tests for TranslationFeedback model
3
-
4
- Tests:
5
- - test_create_translation_feedback: Verify feedback creation
6
- - test_feedback_foreign_key_constraint: Verify FK relationship to translations
7
- """
8
-
9
- import pytest
10
- from models.translation_feedback import TranslationFeedback
11
- from models.translation import Translation
12
- from database.models import User
13
- from database.db import get_db_session
14
- import uuid
15
- from datetime import datetime
16
-
17
-
18
- @pytest.mark.asyncio
19
- async def test_create_translation_feedback():
20
- """Test creating a translation feedback record"""
21
- # Arrange
22
- feedback_id = uuid.uuid4()
23
- translation_id = uuid.uuid4()
24
- user_id = uuid.uuid4()
25
- issue_description = "Technical term 'API' was incorrectly translated to Urdu"
26
-
27
- # Act
28
- feedback = TranslationFeedback(
29
- id=feedback_id,
30
- translation_id=translation_id,
31
- user_id=user_id,
32
- issue_description=issue_description,
33
- created_at=datetime.utcnow()
34
- )
35
-
36
- # Assert
37
- assert feedback.id == feedback_id
38
- assert feedback.translation_id == translation_id
39
- assert feedback.user_id == user_id
40
- assert feedback.issue_description == issue_description
41
- assert feedback.created_at is not None
42
- print(f"✅ TranslationFeedback model created: {feedback}")
43
-
44
-
45
- @pytest.mark.asyncio
46
- async def test_feedback_foreign_key_constraint():
47
- """Test that feedback links to translation record via FK"""
48
- # Arrange
49
- translation_id = uuid.uuid4()
50
- user_id = uuid.uuid4()
51
-
52
- # Act
53
- feedback = TranslationFeedback(
54
- id=uuid.uuid4(),
55
- translation_id=translation_id, # FK to translations table
56
- user_id=user_id, # FK to users table
57
- issue_description="Translation quality issue",
58
- created_at=datetime.utcnow()
59
- )
60
-
61
- # Assert
62
- assert feedback.translation_id == translation_id
63
- assert hasattr(feedback, 'translation_id'), "Feedback must have translation_id FK"
64
- assert hasattr(feedback, 'user_id'), "Feedback must have user_id FK"
65
- print(f"✅ Foreign key constraint verified: translation_id={translation_id}")
66
-
67
-
68
- def test_feedback_model_repr():
69
- """Test __repr__ method for debugging"""
70
- feedback = TranslationFeedback(
71
- id=uuid.uuid4(),
72
- translation_id=uuid.uuid4(),
73
- user_id=uuid.uuid4(),
74
- issue_description="Test issue",
75
- created_at=datetime.utcnow()
76
- )
77
-
78
- repr_str = repr(feedback)
79
- assert "TranslationFeedback" in repr_str
80
- assert "issue_description" in repr_str or "translation_id" in repr_str
81
- print(f"✅ Model repr: {repr_str}")
 
1
+ """
2
+ Unit tests for TranslationFeedback model
3
+
4
+ Tests:
5
+ - test_create_translation_feedback: Verify feedback creation
6
+ - test_feedback_foreign_key_constraint: Verify FK relationship to translations
7
+ """
8
+
9
+ import pytest
10
+ from models.translation_feedback import TranslationFeedback
11
+ from models.translation import Translation
12
+ from database.models import User
13
+ from database.db import get_db_session
14
+ import uuid
15
+ from datetime import datetime
16
+
17
+
18
+ @pytest.mark.asyncio
19
+ async def test_create_translation_feedback():
20
+ """Test creating a translation feedback record"""
21
+ # Arrange
22
+ feedback_id = uuid.uuid4()
23
+ translation_id = uuid.uuid4()
24
+ user_id = uuid.uuid4()
25
+ issue_description = "Technical term 'API' was incorrectly translated to Urdu"
26
+
27
+ # Act
28
+ feedback = TranslationFeedback(
29
+ id=feedback_id,
30
+ translation_id=translation_id,
31
+ user_id=user_id,
32
+ issue_description=issue_description,
33
+ created_at=datetime.utcnow()
34
+ )
35
+
36
+ # Assert
37
+ assert feedback.id == feedback_id
38
+ assert feedback.translation_id == translation_id
39
+ assert feedback.user_id == user_id
40
+ assert feedback.issue_description == issue_description
41
+ assert feedback.created_at is not None
42
+ print(f"✅ TranslationFeedback model created: {feedback}")
43
+
44
+
45
+ @pytest.mark.asyncio
46
+ async def test_feedback_foreign_key_constraint():
47
+ """Test that feedback links to translation record via FK"""
48
+ # Arrange
49
+ translation_id = uuid.uuid4()
50
+ user_id = uuid.uuid4()
51
+
52
+ # Act
53
+ feedback = TranslationFeedback(
54
+ id=uuid.uuid4(),
55
+ translation_id=translation_id, # FK to translations table
56
+ user_id=user_id, # FK to users table
57
+ issue_description="Translation quality issue",
58
+ created_at=datetime.utcnow()
59
+ )
60
+
61
+ # Assert
62
+ assert feedback.translation_id == translation_id
63
+ assert hasattr(feedback, 'translation_id'), "Feedback must have translation_id FK"
64
+ assert hasattr(feedback, 'user_id'), "Feedback must have user_id FK"
65
+ print(f"✅ Foreign key constraint verified: translation_id={translation_id}")
66
+
67
+
68
+ def test_feedback_model_repr():
69
+ """Test __repr__ method for debugging"""
70
+ feedback = TranslationFeedback(
71
+ id=uuid.uuid4(),
72
+ translation_id=uuid.uuid4(),
73
+ user_id=uuid.uuid4(),
74
+ issue_description="Test issue",
75
+ created_at=datetime.utcnow()
76
+ )
77
+
78
+ repr_str = repr(feedback)
79
+ assert "TranslationFeedback" in repr_str
80
+ assert "issue_description" in repr_str or "translation_id" in repr_str
81
+ print(f"✅ Model repr: {repr_str}")