-
Notifications
You must be signed in to change notification settings - Fork 79
Feat: Brand Dashboard Revamp(UI & AI) #124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughAdds brand dashboard and AI query features end-to-end: new FastAPI routers (brand_dashboard, ai_query), models, schemas, SQL tables/seeds, and services (Groq-backed AI router). Frontend adds API clients, a dashboard hook, a redesigned Brand Dashboard page with AI search, minor component tweaks, a new font import, and integration README. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant UI as Brand Dashboard UI
participant Hook as useBrandDashboard
participant BAPI as brandApi (FE)
participant API as FastAPI /api/brand
participant DB as Supabase
UI->>Hook: mount / refreshData()
Hook->>BAPI: getDashboardOverview(brandId)
BAPI->>API: GET /api/brand/overview?brand_id=...
API->>DB: Fetch campaigns/profile/apps/payments/matches
DB-->>API: Aggregated records
API-->>BAPI: DashboardOverviewResponse
BAPI-->>Hook: data
Hook-->>UI: state updated (loading -> idle)
sequenceDiagram
autonumber
participant UI as Dashboard AI Search
participant Hook as useBrandDashboard
participant AIFE as aiApi (FE)
participant AIB as FastAPI /api/ai
participant SVC as AIRouter
participant LLM as Groq Chat Completions
UI->>Hook: queryAI("Find creators in fitness", brandId)
Hook->>AIFE: POST /query {query, brand_id}
AIFE->>AIB: POST /api/ai/query
AIB->>SVC: process_query(query, brand_id)
SVC->>LLM: system + user messages
LLM-->>SVC: JSON-like output
SVC-->>AIB: Enhanced AIQueryResponse
AIB-->>AIFE: AIQueryResponse
AIFE-->>Hook: AIQueryResponse
Hook-->>UI: render intent/explanation/route
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 12
🧹 Nitpick comments (1)
Backend/app/models/models.py (1)
40-54: Make brand → brand_profile truly one-to-one
BrandProfile.useruses a plainbackref, souser.brand_profilebecomes a list. The consuming routes and schemas treat it as a single profile, which will break at runtime (e.g., attribute access) and allows multiple profiles per user. Define the relationship explicitly withuselist=Falseon the user side and switch toback_populatesso both ends stay in sync.-from sqlalchemy.orm import relationship +from sqlalchemy.orm import relationship @@ brand_payments = relationship( "SponsorshipPayment", foreign_keys="[SponsorshipPayment.brand_id]", back_populates="brand", ) + brand_profile = relationship( + "BrandProfile", + back_populates="user", + uselist=False, + cascade="all, delete-orphan", + ) @@ - user = relationship("User", backref="brand_profile") + user = relationship("User", back_populates="brand_profile")Also applies to: 185-241
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
Frontend/public/aossielogo.pngis excluded by!**/*.png
📒 Files selected for processing (18)
Backend/.env-example(1 hunks)Backend/app/main.py(2 hunks)Backend/app/models/models.py(2 hunks)Backend/app/routes/ai_query.py(1 hunks)Backend/app/routes/brand_dashboard.py(1 hunks)Backend/app/schemas/schema.py(2 hunks)Backend/app/services/ai_router.py(1 hunks)Backend/requirements.txt(1 hunks)Backend/sql.txt(1 hunks)Frontend/README-INTEGRATION.md(1 hunks)Frontend/src/components/collaboration-hub/CreatorMatchGrid.tsx(1 hunks)Frontend/src/components/user-nav.tsx(2 hunks)Frontend/src/context/AuthContext.tsx(1 hunks)Frontend/src/hooks/useBrandDashboard.ts(1 hunks)Frontend/src/index.css(1 hunks)Frontend/src/pages/Brand/Dashboard.tsx(1 hunks)Frontend/src/services/aiApi.ts(1 hunks)Frontend/src/services/brandApi.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (8)
Frontend/src/components/user-nav.tsx (1)
Frontend/src/components/ui/dropdown-menu.tsx (1)
DropdownMenuItem(180-180)
Backend/app/models/models.py (2)
Backend/app/routes/post.py (1)
generate_uuid(31-32)Backend/app/models/chat.py (1)
generate_uuid(9-10)
Frontend/src/components/collaboration-hub/CreatorMatchGrid.tsx (1)
Frontend/src/components/collaboration-hub/CreatorMatchCard.tsx (1)
CreatorMatchCard(65-130)
Frontend/src/hooks/useBrandDashboard.ts (3)
Frontend/src/context/AuthContext.tsx (1)
useAuth(216-222)Frontend/src/services/brandApi.ts (14)
DashboardOverview(7-13)BrandProfile(15-24)Campaign(26-36)CreatorMatch(38-44)Application(46-56)Payment(58-68)brandApi(246-246)createCampaign(132-144)updateCampaign(146-151)deleteCampaign(153-157)updateApplicationStatus(202-212)searchCreators(164-178)getCampaignPerformance(185-187)getRevenueAnalytics(189-191)Frontend/src/services/aiApi.ts (2)
queryAI(54-64)aiApi(83-83)
Backend/app/services/ai_router.py (1)
Backend/app/routes/ai_query.py (1)
get_route_info(83-100)
Backend/app/routes/brand_dashboard.py (3)
Backend/app/models/models.py (5)
User(25-53)Sponsorship(76-92)CampaignMetrics(189-204)Contract(208-224)SponsorshipApplication(114-128)Backend/app/schemas/schema.py (19)
BrandProfileCreate(61-67)BrandProfileUpdate(69-74)BrandProfileResponse(76-87)CampaignMetricsCreate(91-97)CampaignMetricsResponse(99-110)ContractCreate(114-119)ContractUpdate(121-123)ContractResponse(125-135)CreatorMatchResponse(139-147)DashboardOverviewResponse(151-156)CampaignAnalyticsResponse(158-166)SponsorshipApplicationResponse(182-194)ApplicationUpdateRequest(196-198)ApplicationSummaryResponse(200-206)PaymentResponse(210-222)PaymentStatusUpdate(224-225)PaymentAnalyticsResponse(227-233)CampaignMetricsUpdate(237-242)SponsorshipCreate(21-27)Frontend/src/utils/supabase.tsx (1)
supabase(11-11)
Frontend/src/pages/Brand/Dashboard.tsx (2)
Frontend/src/hooks/useBrandDashboard.ts (1)
useBrandDashboard(6-288)Frontend/src/services/aiApi.ts (1)
queryAI(54-64)
Backend/app/routes/ai_query.py (1)
Backend/app/services/ai_router.py (3)
process_query(122-172)list_available_routes(202-204)get_route_info(198-200)
🪛 Ruff (0.13.3)
Backend/app/services/ai_router.py
22-22: Avoid specifying long messages outside the exception class
(TRY003)
122-122: PEP 484 prohibits implicit Optional
Convert to Optional[T]
(RUF013)
168-168: Consider moving this statement to an else block
(TRY300)
170-170: Do not catch blind exception: Exception
(BLE001)
171-171: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
172-172: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
Backend/app/routes/brand_dashboard.py
71-71: Consider moving this statement to an else block
(TRY300)
72-72: Do not catch blind exception: Exception
(BLE001)
73-73: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
74-74: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
79-79: Unused function argument: window_seconds
(ARG001)
115-115: Local variable profile is assigned to but never used
Remove assignment to unused variable profile
(F841)
157-157: Do not catch blind exception: Exception
(BLE001)
158-158: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
159-159: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
188-188: Abstract raise to an inner function
(TRY301)
190-190: Do not catch blind exception: Exception
(BLE001)
191-191: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
192-192: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
205-205: Abstract raise to an inner function
(TRY301)
209-209: Do not catch blind exception: Exception
(BLE001)
210-210: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
211-211: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
226-226: Abstract raise to an inner function
(TRY301)
230-230: Do not catch blind exception: Exception
(BLE001)
231-231: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
232-232: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
268-268: Abstract raise to an inner function
(TRY301)
272-272: Do not catch blind exception: Exception
(BLE001)
273-273: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
274-274: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
310-310: Abstract raise to an inner function
(TRY301)
314-314: Do not catch blind exception: Exception
(BLE001)
315-315: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
316-316: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
327-327: Abstract raise to an inner function
(TRY301)
334-334: Abstract raise to an inner function
(TRY301)
338-338: Do not catch blind exception: Exception
(BLE001)
339-339: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
340-340: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
351-351: Abstract raise to an inner function
(TRY301)
353-353: Local variable response is assigned to but never used
Remove assignment to unused variable response
(F841)
355-355: Consider moving this statement to an else block
(TRY300)
359-359: Do not catch blind exception: Exception
(BLE001)
360-360: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
361-361: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
390-390: Consider moving this statement to an else block
(TRY300)
392-392: Do not catch blind exception: Exception
(BLE001)
393-393: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
394-394: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
398-398: Unused function argument: brand_id
(ARG001)
399-399: Unused function argument: industry
(ARG001)
401-401: Unused function argument: location
(ARG001)
435-435: Consider moving this statement to an else block
(TRY300)
437-437: Do not catch blind exception: Exception
(BLE001)
438-438: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
439-439: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
442-442: Unused function argument: brand_id
(ARG001)
450-450: Abstract raise to an inner function
(TRY301)
465-470: Consider moving this statement to an else block
(TRY300)
474-474: Do not catch blind exception: Exception
(BLE001)
475-475: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
476-476: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
521-521: Consider moving this statement to an else block
(TRY300)
523-523: Do not catch blind exception: Exception
(BLE001)
524-524: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
525-525: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
554-554: Do not catch blind exception: Exception
(BLE001)
555-555: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
556-556: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
569-569: Consider moving this statement to an else block
(TRY300)
571-571: Do not catch blind exception: Exception
(BLE001)
572-572: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
573-573: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
597-597: Abstract raise to an inner function
(TRY301)
599-599: Do not catch blind exception: Exception
(BLE001)
600-600: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
601-601: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
616-616: Abstract raise to an inner function
(TRY301)
623-623: Abstract raise to an inner function
(TRY301)
627-627: Do not catch blind exception: Exception
(BLE001)
628-628: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
629-629: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
679-679: Consider moving this statement to an else block
(TRY300)
683-683: Do not catch blind exception: Exception
(BLE001)
684-684: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
685-685: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
700-700: Abstract raise to an inner function
(TRY301)
707-707: Abstract raise to an inner function
(TRY301)
722-722: Consider moving this statement to an else block
(TRY300)
726-726: Do not catch blind exception: Exception
(BLE001)
727-727: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
728-728: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
747-747: Abstract raise to an inner function
(TRY301)
752-752: Abstract raise to an inner function
(TRY301)
764-764: Abstract raise to an inner function
(TRY301)
768-768: Do not catch blind exception: Exception
(BLE001)
769-769: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
770-770: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
810-810: Do not catch blind exception: Exception
(BLE001)
811-811: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
812-812: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
851-851: Consider moving this statement to an else block
(TRY300)
855-855: Do not catch blind exception: Exception
(BLE001)
856-856: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
857-857: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
871-871: Abstract raise to an inner function
(TRY301)
889-889: Consider moving this statement to an else block
(TRY300)
893-893: Do not catch blind exception: Exception
(BLE001)
894-894: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
895-895: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
914-914: Abstract raise to an inner function
(TRY301)
922-922: Abstract raise to an inner function
(TRY301)
926-926: Do not catch blind exception: Exception
(BLE001)
927-927: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
928-928: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
966-966: Do not catch blind exception: Exception
(BLE001)
967-967: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
968-968: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
992-992: Abstract raise to an inner function
(TRY301)
1014-1014: Abstract raise to an inner function
(TRY301)
1018-1018: Do not catch blind exception: Exception
(BLE001)
1019-1019: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
1020-1020: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
1035-1035: Abstract raise to an inner function
(TRY301)
1043-1043: Consider moving this statement to an else block
(TRY300)
1047-1047: Do not catch blind exception: Exception
(BLE001)
1048-1048: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
1049-1049: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
1070-1070: Abstract raise to an inner function
(TRY301)
1079-1079: Abstract raise to an inner function
(TRY301)
1083-1083: Do not catch blind exception: Exception
(BLE001)
1084-1084: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
1085-1085: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
Backend/app/routes/ai_query.py
38-38: Abstract raise to an inner function
(TRY301)
59-59: Consider moving this statement to an else block
(TRY300)
63-63: Do not catch blind exception: Exception
(BLE001)
64-64: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
65-65: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
78-78: Do not catch blind exception: Exception
(BLE001)
79-79: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
80-80: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
90-90: Abstract raise to an inner function
(TRY301)
92-95: Consider moving this statement to an else block
(TRY300)
98-98: Do not catch blind exception: Exception
(BLE001)
99-99: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
100-100: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
111-115: Consider moving this statement to an else block
(TRY300)
116-116: Do not catch blind exception: Exception
(BLE001)
117-117: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
| routes = ai_router.list_available_routes() | ||
| return { | ||
| "available_routes": routes, | ||
| "total_routes": len(routes) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Return the route list in the shape the client expects
ai_router.list_available_routes() yields a dict, but the frontend’s getAvailableRoutes() expects available_routes: string[]. Emit the keys (and optionally pass details separately) so the contract matches.
- return {
- "available_routes": routes,
- "total_routes": len(routes)
- }
+ return {
+ "available_routes": list(routes.keys()),
+ "total_routes": len(routes),
+ "route_details": routes,
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| routes = ai_router.list_available_routes() | |
| return { | |
| "available_routes": routes, | |
| "total_routes": len(routes) | |
| } | |
| routes = ai_router.list_available_routes() | |
| return { | |
| "available_routes": list(routes.keys()), | |
| "total_routes": len(routes), | |
| "route_details": routes, | |
| } |
🤖 Prompt for AI Agents
In Backend/app/routes/ai_query.py around lines 73 to 77, the handler currently
returns the dict from ai_router.list_available_routes(), but the frontend
expects available_routes to be a list of strings; change the response to emit
available_routes as a list of the dict's keys (e.g., list(routes.keys())) and,
if you need to keep the full dict, return it under a separate field like
route_details so the API contract matches available_routes: string[].
| @router.post("/test") | ||
| async def test_ai_query(query: str = Query(..., description="Test query")): | ||
| """ | ||
| Test endpoint for AI query processing (for development) | ||
| """ | ||
| try: | ||
| # Process test query | ||
| result = await ai_router.process_query(query=query) | ||
|
|
||
| return { | ||
| "test_query": query, | ||
| "result": result, | ||
| "status": "success" | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let the test endpoint accept GET (or update the client)
The frontend already issues GET /api/ai/test?query=…; with the current @router.post decorator the call will 405. Either change this route to @router.get or adjust the client before merging—just make sure both sides use the same verb.
🧰 Tools
🪛 Ruff (0.13.3)
111-115: Consider moving this statement to an else block
(TRY300)
🤖 Prompt for AI Agents
In Backend/app/routes/ai_query.py around lines 102 to 115, the test endpoint is
currently decorated with @router.post which causes a 405 when the frontend sends
GET; change the route decorator to @router.get("/test") so it accepts query
string GET requests (no other code changes required since the query param is
already declared), or alternatively coordinate with the frontend to call
POST—prefer updating the decorator to @router.get to match existing client
behavior.
| # Security Helper Functions | ||
| def validate_brand_access(brand_id: str, current_user_id: str): | ||
| """Validate that the current user can access the brand data""" | ||
| if brand_id != current_user_id: | ||
| raise HTTPException(status_code=403, detail="Access denied: You can only access your own data") | ||
| return True | ||
|
|
||
| def require_brand_role(user_role: str): | ||
| """Ensure user has brand role""" | ||
| if user_role != "brand": | ||
| raise HTTPException(status_code=403, detail="Access denied: Brand role required") | ||
| return True | ||
|
|
||
| def validate_uuid_format(id_value: str, field_name: str = "ID"): | ||
| """Validate UUID format""" | ||
| if not id_value or len(id_value) != 36: | ||
| raise HTTPException(status_code=400, detail=f"Invalid {field_name} format") | ||
| return True | ||
|
|
||
| def safe_supabase_query(query_func, error_message: str = "Database operation failed"): | ||
| """Safely execute Supabase queries with proper error handling""" | ||
| try: | ||
| result = query_func() | ||
| return result.data if result.data else [] | ||
| except Exception as e: | ||
| logger.error(f"Supabase error in {error_message}: {e}") | ||
| raise HTTPException(status_code=500, detail=error_message) | ||
|
|
||
| # Simple in-memory rate limiting (for development) | ||
| request_counts = {} | ||
|
|
||
| def check_rate_limit(user_id: str, max_requests: int = 100, window_seconds: int = 60): | ||
| """Simple rate limiting check (in production, use Redis)""" | ||
| current_time = datetime.now(timezone.utc) | ||
| key = f"{user_id}:{current_time.minute}" | ||
|
|
||
| if key not in request_counts: | ||
| request_counts[key] = 0 | ||
|
|
||
| request_counts[key] += 1 | ||
|
|
||
| if request_counts[key] > max_requests: | ||
| raise HTTPException(status_code=429, detail="Rate limit exceeded") | ||
|
|
||
| return True | ||
|
|
||
| # ============================================================================ | ||
| # DASHBOARD OVERVIEW ROUTES | ||
| # ============================================================================ | ||
|
|
||
| @router.get("/dashboard/overview", response_model=DashboardOverviewResponse) | ||
| async def get_dashboard_overview(brand_id: str = Query(..., description="Brand user ID")): | ||
| """ | ||
| Get dashboard overview with key metrics for a brand | ||
| """ | ||
| # Validate brand_id format | ||
| validate_uuid_format(brand_id, "brand_id") | ||
|
|
||
| try: | ||
| # Get brand's campaigns | ||
| campaigns = safe_supabase_query( | ||
| lambda: supabase.table("sponsorships").select("*").eq("brand_id", brand_id).execute(), | ||
| "Failed to fetch campaigns" | ||
| ) | ||
|
|
||
| # Get brand's profile | ||
| profile_result = supabase.table("brand_profiles").select("*").eq("user_id", brand_id).execute() | ||
| profile = profile_result.data[0] if profile_result.data else None | ||
|
|
||
| # Get recent applications (only if campaigns exist) | ||
| applications = [] | ||
| if campaigns: | ||
| campaign_ids = [campaign["id"] for campaign in campaigns] | ||
| applications = safe_supabase_query( | ||
| lambda: supabase.table("sponsorship_applications").select("*").in_("sponsorship_id", campaign_ids).execute(), | ||
| "Failed to fetch applications" | ||
| ) | ||
|
|
||
| # Calculate metrics | ||
| total_campaigns = len(campaigns) | ||
| active_campaigns = len([c for c in campaigns if c.get("status") == "open"]) | ||
|
|
||
| # Calculate total revenue from completed payments | ||
| payments = safe_supabase_query( | ||
| lambda: supabase.table("sponsorship_payments").select("*").eq("brand_id", brand_id).eq("status", "completed").execute(), | ||
| "Failed to fetch payments" | ||
| ) | ||
| total_revenue = sum(float(payment.get("amount", 0)) for payment in payments) | ||
|
|
||
| # Get creator matches | ||
| matches = safe_supabase_query( | ||
| lambda: supabase.table("creator_matches").select("*").eq("brand_id", brand_id).execute(), | ||
| "Failed to fetch creator matches" | ||
| ) | ||
| total_creators_matched = len(matches) | ||
|
|
||
| # Recent activity (last 5 applications) | ||
| recent_activity = applications[:5] if applications else [] | ||
|
|
||
| return DashboardOverviewResponse( | ||
| total_campaigns=total_campaigns, | ||
| active_campaigns=active_campaigns, | ||
| total_revenue=total_revenue, | ||
| total_creators_matched=total_creators_matched, | ||
| recent_activity=recent_activity | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Blocker: brand data exposed via trust-on-query brand_id.
Every route here trusts a caller-supplied brand_id query/body value and never authenticates the requester. A malicious user can enumerate UUIDs and read/modify any brand’s campaigns, payments, contracts, etc. Defining validate_brand_access/require_brand_role doesn’t help because they are never invoked. We need to require an authenticated brand user (e.g., current_user = Depends(get_current_user)), derive brand_id from that session, and reject mismatches before touching Supabase.
Also applies to: 165-362
🧰 Tools
🪛 Ruff (0.13.3)
71-71: Consider moving this statement to an else block
(TRY300)
72-72: Do not catch blind exception: Exception
(BLE001)
73-73: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
74-74: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
79-79: Unused function argument: window_seconds
(ARG001)
115-115: Local variable profile is assigned to but never used
Remove assignment to unused variable profile
(F841)
🤖 Prompt for AI Agents
In Backend/app/routes/brand_dashboard.py around lines 48 to 153 (and similarly
for lines 165-362), the endpoint blindly trusts a caller-supplied brand_id query
parameter and never authenticates the requester; change the handlers to require
an authenticated user via Depends(get_current_user) and derive brand_id from
that session object instead of accepting it from the client, enforce
require_brand_role(current_user.role) and validate_brand_access(current_user.id,
current_user.id) (or simply compare derived brand_id to the session id), remove
or ignore the incoming brand_id parameter, and update all Supabase queries to
use the authenticated brand_id; apply the same pattern to the other routes
referenced (165-362) and ensure any helper calls (validate_uuid_format,
safe_supabase_query) still receive the derived ID and that unauthorized access
returns 403.
| response = self.client.chat.completions.create( | ||
| model="llama3-8b-8192", # Fast and cost-effective | ||
| messages=messages, | ||
| temperature=0.1, # Low temperature for consistent routing | ||
| max_tokens=500 | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid blocking the event loop with the Groq client
Groq.chat.completions.create() is synchronous; invoking it directly inside an async def will block the FastAPI event loop during every AI call. Run it in a thread (or switch to an async client) so concurrent requests stay responsive.
-from groq import Groq
+from groq import Groq
+import anyio
@@
- response = self.client.chat.completions.create(
- model="llama3-8b-8192", # Fast and cost-effective
- messages=messages,
- temperature=0.1, # Low temperature for consistent routing
- max_tokens=500
- )
+ response = await anyio.to_thread.run_sync(
+ lambda: self.client.chat.completions.create(
+ model="llama3-8b-8192", # Fast and cost-effective
+ messages=messages,
+ temperature=0.1, # Low temperature for consistent routing
+ max_tokens=500,
+ )
+ )📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| response = self.client.chat.completions.create( | |
| model="llama3-8b-8192", # Fast and cost-effective | |
| messages=messages, | |
| temperature=0.1, # Low temperature for consistent routing | |
| max_tokens=500 | |
| ) | |
| from groq import Groq | |
| import anyio | |
| response = await anyio.to_thread.run_sync( | |
| lambda: self.client.chat.completions.create( | |
| model="llama3-8b-8192", # Fast and cost-effective | |
| messages=messages, | |
| temperature=0.1, # Low temperature for consistent routing | |
| max_tokens=500, | |
| ) | |
| ) |
🤖 Prompt for AI Agents
In Backend/app/services/ai_router.py around lines 139 to 144, the synchronous
call to Groq.chat.completions.create() is being invoked directly inside an async
function which will block the FastAPI event loop; change it to run in a thread
(or switch to an async client). Specifically, import asyncio if not present and
replace the direct call with an awaited call that runs the blocking function in
a thread (e.g., await asyncio.to_thread(...) or await loop.run_in_executor(...))
passing the same model/messages/temperature/max_tokens arguments, and preserve
surrounding error handling and return value usage so the async handler remains
non-blocking and exceptions are propagated correctly.
| {currentCreators.map((creator, index) => ( | ||
| <CreatorMatchCard key={`${creator.name}-${index}`} {...creator} /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
Avoid using array index in React keys.
Using index as part of the key is an anti-pattern that can cause React reconciliation bugs, especially with pagination. When navigating between pages, the index resets (0-3 on each page), so creators with the same name on different pages will have identical keys during their render cycles. This can lead to:
- Wrong component instances being reused (state not resetting properly)
- Unnecessary re-renders and performance issues
- Subtle bugs if the list is filtered, reordered, or dynamically updated
The AI summary indicates the backend provides a CreatorMatch model with a primary key. If creator.id exists and is unique (which it should be), use it directly as the key instead.
Run the following script to verify whether id is available in the creator data:
If id is available, apply this diff:
- {currentCreators.map((creator, index) => (
- <CreatorMatchCard key={`${creator.name}-${index}`} {...creator} />
+ {currentCreators.map((creator) => (
+ <CreatorMatchCard key={creator.id} {...creator} />If id is truly unavailable, this indicates a data structure issue that should be addressed at the API level rather than worked around with composite keys.
🏁 Script executed:
#!/bin/bash
# Description: Check if CreatorMatchCardProps includes an id field and verify backend API response structure
# Check the props interface definition
echo "=== Checking CreatorMatchCardProps interface ==="
ast-grep --pattern $'export interface CreatorMatchCardProps {
$$$
}'
# Check if id is used elsewhere in creator-related components
echo -e "\n=== Checking for id usage in creator components ==="
rg -n --type=tsx --type=ts -C3 'creator\.id' Frontend/src/components/collaboration-hub/
# Check backend schema for CreatorMatch to confirm id field
echo -e "\n=== Checking backend CreatorMatch schema ==="
rg -n --type=py -C5 'class CreatorMatch'Length of output: 3313
Avoid index-based React keys; use a stable id instead. Refactor currentCreators and CreatorMatchCardProps to include the API’s id, then replace key={${creator.name}-${index}} with key={creator.id}.
🤖 Prompt for AI Agents
In Frontend/src/components/collaboration-hub/CreatorMatchGrid.tsx around lines
21-22, the list is using an index-based key (`${creator.name}-${index}`) which
is unstable; update the creator data model and prop types to include the API's
stable `id` field (add `id` to currentCreators items and CreatorMatchCardProps),
then replace the key with `key={creator.id}` on the CreatorMatchCard; ensure all
places that construct or map creators (API parsing, tests, and parent
components) populate `id` and adjust types/imports accordingly.
| // AI API Service | ||
| // Handles AI-related API calls to the backend | ||
|
|
||
| const AI_API_BASE_URL = 'http://localhost:8000/api/ai'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not pin the AI API base to localhost
The singleton will break everywhere except a developer’s laptop. Pull the base URL from runtime config (with a sane default) so staging/production work.
-const AI_API_BASE_URL = 'http://localhost:8000/api/ai';
+const AI_API_BASE_URL = `${import.meta.env.VITE_API_BASE_URL ?? window.location.origin}/api/ai`;🤖 Prompt for AI Agents
In Frontend/src/services/aiApi.ts around line 4 the AI API base is hardcoded to
'http://localhost:8000/api/ai' which will break non-local environments; change
the constant initialization to pull the base URL from runtime configuration
(e.g. process.env.AI_API_BASE_URL, import.meta.env.VITE_AI_API_BASE_URL, or a
window/global runtime config like window.__RUNTIME_CONFIG__.AI_API_BASE_URL) and
fall back to a sensible default (e.g. the current localhost URL) so
staging/production can override it at runtime.
| // Test AI query (for development) | ||
| async testQuery(query: string): Promise<any> { | ||
| return this.makeRequest<any>(`/test?query=${encodeURIComponent(query)}`); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Align the test endpoint verb with the backend
The backend route is declared as POST /api/ai/test, so this GET call will return 405. Either hit it with POST (body or query string) or change the backend to accept GET—just make sure both sides agree before merging.
🤖 Prompt for AI Agents
In Frontend/src/services/aiApi.ts around lines 76 to 79, the client calls GET
/test but the backend expects POST /api/ai/test; change testQuery to use
this.makeRequest with method 'POST' to the '/test' path and send the query in
the request body (e.g., { query }) with appropriate JSON content-type handling
used by makeRequest so the client and backend verb agree.
| // Brand Dashboard API Service | ||
| // Handles all API calls to the backend for brand dashboard functionality | ||
|
|
||
| const API_BASE_URL = 'http://localhost:8000/api/brand'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Stop hard-coding localhost for API base
Shipping with http://localhost will make every deployed build talk to the wrong host (or fail outright) outside local dev. Please drive this from configuration (env/proxy) with a safe fallback to the current origin.
-const API_BASE_URL = 'http://localhost:8000/api/brand';
+const API_BASE_URL = `${import.meta.env.VITE_API_BASE_URL ?? window.location.origin}/api/brand`;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const API_BASE_URL = 'http://localhost:8000/api/brand'; | |
| const API_BASE_URL = `${import.meta.env.VITE_API_BASE_URL ?? window.location.origin}/api/brand`; |
🤖 Prompt for AI Agents
In Frontend/src/services/brandApi.ts around line 4, the API base URL is
hard-coded to 'http://localhost:8000/api/brand'; replace this with a
configurable value driven by environment or runtime config (e.g. read from
process.env.REACT_APP_API_BASE_URL or a config module, falling back to
window.location.origin) and then append the '/api/brand' path as needed so
deployed builds use the correct host; ensure the fallback is safe (use
window.location.origin) and that the code handles trailing slashes consistently.
| const params = new URLSearchParams({ brand_id: brandId }); | ||
| if (filters?.industry) params.append('industry', filters.industry); | ||
| if (filters?.min_engagement) params.append('min_engagement', filters.min_engagement.toString()); | ||
| if (filters?.location) params.append('location', filters.location); | ||
|
|
||
| return this.makeRequest<any[]>(`/creators/search?${params.toString()}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Preserve zero values when forwarding min_engagement
A falsy check drops legitimate 0 thresholds. Check for undefined instead so we forward zero safely.
- if (filters?.min_engagement) params.append('min_engagement', filters.min_engagement.toString());
+ if (filters?.min_engagement !== undefined) {
+ params.append('min_engagement', filters.min_engagement.toString());
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const params = new URLSearchParams({ brand_id: brandId }); | |
| if (filters?.industry) params.append('industry', filters.industry); | |
| if (filters?.min_engagement) params.append('min_engagement', filters.min_engagement.toString()); | |
| if (filters?.location) params.append('location', filters.location); | |
| return this.makeRequest<any[]>(`/creators/search?${params.toString()}`); | |
| const params = new URLSearchParams({ brand_id: brandId }); | |
| if (filters?.industry) params.append('industry', filters.industry); | |
| if (filters?.min_engagement !== undefined) { | |
| params.append('min_engagement', filters.min_engagement.toString()); | |
| } | |
| if (filters?.location) params.append('location', filters.location); | |
| return this.makeRequest<any[]>(`/creators/search?${params.toString()}`); |
🤖 Prompt for AI Agents
In Frontend/src/services/brandApi.ts around lines 172 to 177, the falsy check
for filters?.min_engagement drops legitimate 0 values; change the condition to
explicitly check for undefined (e.g., filters?.min_engagement !== undefined)
before appending, and only call toString when the value is defined so zero is
forwarded correctly in the query params.
| async updateApplicationStatus( | ||
| applicationId: string, | ||
| status: string, | ||
| notes?: string, | ||
| brandId?: string | ||
| ): Promise<Application> { | ||
| return this.makeRequest<Application>(`/applications/${applicationId}?brand_id=${brandId}`, { | ||
| method: 'PUT', | ||
| body: JSON.stringify({ status, notes }), | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Require brandId when building the status update URL
Treating brandId as optional means we hit /applications/{id}?brand_id=undefined, which will 404 and pollute server logs. Make the argument mandatory (or explicitly guard/throw when it is missing).
- status: string,
- notes?: string,
- brandId?: string
+ status: string,
+ notes?: string,
+ brandId: string
): Promise<Application> {
return this.makeRequest<Application>(`/applications/${applicationId}?brand_id=${brandId}`, {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async updateApplicationStatus( | |
| applicationId: string, | |
| status: string, | |
| notes?: string, | |
| brandId?: string | |
| ): Promise<Application> { | |
| return this.makeRequest<Application>(`/applications/${applicationId}?brand_id=${brandId}`, { | |
| method: 'PUT', | |
| body: JSON.stringify({ status, notes }), | |
| }); | |
| async updateApplicationStatus( | |
| applicationId: string, | |
| status: string, | |
| notes?: string, | |
| brandId: string | |
| ): Promise<Application> { | |
| return this.makeRequest<Application>(`/applications/${applicationId}?brand_id=${brandId}`, { | |
| method: 'PUT', | |
| body: JSON.stringify({ status, notes }), | |
| }); | |
| } |
🤖 Prompt for AI Agents
In Frontend/src/services/brandApi.ts around lines 202 to 211, the
updateApplicationStatus function builds the URL with an optional brandId which
can produce /applications/{id}?brand_id=undefined; make brandId mandatory in the
function signature or validate and throw if missing before constructing the URL,
and when building the query param use a proper encoded value (e.g.
encodeURIComponent) to avoid injecting "undefined"; update any callers/types
accordingly so the function is never called without a valid brandId.
📝 Description
This pull request introduces significant updates to the brand-dashboard-api-part-1 branch, focusing on enhancing the user experience, backend functionality, and frontend-backend integration. The changes include a revamped UI, new database tables, FastAPI models, Pydantic schemas, API routes, and AI-powered search features. Additionally, several fixes and improvements have been made to ensure a seamless and secure user experience.
🔧 Changes Made
Frontend:
Revamped the user interface for a modern and intuitive design.
Added smart features to the search bar for improved usability.
Resolved CSS import order issues and TypeScript errors.
Improved AI response display with better formatting.
Backend:
Added new database tables to support additional features.
Implemented FastAPI models and Pydantic schemas for data validation.
Developed new API routes and endpoints for enhanced functionality.
Made minor security improvements to the API.
Separated the AI API service to address 404 errors for AI queries.
Integration:
Connected the frontend to the backend with API integration.
Integrated AI-powered search functionality.
Added documentation for frontend-backend integration.
Fixes:
Fixed missing SponsorshipCreate import.
Removed metric cards from the brand dashboard.
📷 Screenshots or Visual Changes (if applicable)
✅ Checklist
Summary by CodeRabbit
New Features
Documentation
Chores