Skip to content

Commit c387c01

Browse files
committed
feat(teams): add team icon, recommended status and favorites features
- Add icon and is_recommended fields to Team model (CRD spec) - Create user_team_favorites table for user-team favorite relationships - Add favorites API endpoints (add/remove/list favorites) - Add showcase API endpoints (recommended teams, favorite teams) - Create icon-picker component with Lucide icons - Create TeamCard and TeamShowcase components for chat page - Update TeamEdit with icon picker and recommended switch - Add i18n translations for new features - Add database migration for user_team_favorites table
1 parent 2131537 commit c387c01

File tree

17 files changed

+746
-11
lines changed

17 files changed

+746
-11
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# SPDX-FileCopyrightText: 2025 Weibo, Inc.
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
"""add_user_team_favorites_table
6+
7+
Revision ID: 2a3b4c5d6e7f
8+
Revises: 1a2b3c4d5e6f
9+
Create Date: 2025-07-16 10:00:00.000000
10+
11+
"""
12+
from alembic import op
13+
import sqlalchemy as sa
14+
15+
16+
# revision identifiers, used by Alembic.
17+
revision = '2a3b4c5d6e7f'
18+
down_revision = '1a2b3c4d5e6f'
19+
branch_labels = None
20+
depends_on = None
21+
22+
23+
def upgrade() -> None:
24+
# Create user_team_favorites table
25+
op.create_table(
26+
'user_team_favorites',
27+
sa.Column('id', sa.Integer(), nullable=False),
28+
sa.Column('user_id', sa.Integer(), nullable=False),
29+
sa.Column('team_id', sa.Integer(), nullable=False),
30+
sa.Column('created_at', sa.DateTime(), nullable=True),
31+
sa.PrimaryKeyConstraint('id'),
32+
mysql_charset='utf8mb4',
33+
mysql_collate='utf8mb4_unicode_ci'
34+
)
35+
op.create_index('ix_user_team_favorites_id', 'user_team_favorites', ['id'], unique=False)
36+
op.create_index('ix_user_team_favorites_user_id', 'user_team_favorites', ['user_id'], unique=False)
37+
op.create_index('ix_user_team_favorites_team_id', 'user_team_favorites', ['team_id'], unique=False)
38+
op.create_index('idx_user_team_favorite', 'user_team_favorites', ['user_id', 'team_id'], unique=True)
39+
40+
41+
def downgrade() -> None:
42+
op.drop_index('idx_user_team_favorite', table_name='user_team_favorites')
43+
op.drop_index('ix_user_team_favorites_team_id', table_name='user_team_favorites')
44+
op.drop_index('ix_user_team_favorites_user_id', table_name='user_team_favorites')
45+
op.drop_index('ix_user_team_favorites_id', table_name='user_team_favorites')
46+
op.drop_table('user_team_favorites')

backend/app/api/endpoints/adapter/teams.py

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from fastapi import APIRouter, Depends, Query, status
66
from sqlalchemy.orm import Session
7+
from typing import List, Dict, Any
78

89
from app.api.dependencies import get_db
910
from app.core import security
@@ -13,6 +14,7 @@
1314
from app.schemas.shared_team import TeamShareRequest, TeamShareResponse, JoinSharedTeamRequest, JoinSharedTeamResponse
1415
from app.services.adapters.team_kinds import team_kinds_service
1516
from app.services.shared_team import shared_team_service
17+
from app.services.team_favorite import team_favorite_service
1618

1719
router = APIRouter()
1820

@@ -124,4 +126,92 @@ def join_shared_team(
124126
db=db,
125127
share_token=request.share_token,
126128
user_id=current_user.id
127-
)
129+
)
130+
131+
132+
@router.post("/{team_id}/favorite")
133+
def add_team_to_favorites(
134+
team_id: int,
135+
current_user: User = Depends(security.get_current_user),
136+
db: Session = Depends(get_db)
137+
):
138+
"""Add a team to user's favorites"""
139+
return team_favorite_service.add_favorite(
140+
db=db,
141+
team_id=team_id,
142+
user_id=current_user.id
143+
)
144+
145+
146+
@router.delete("/{team_id}/favorite")
147+
def remove_team_from_favorites(
148+
team_id: int,
149+
current_user: User = Depends(security.get_current_user),
150+
db: Session = Depends(get_db)
151+
):
152+
"""Remove a team from user's favorites"""
153+
return team_favorite_service.remove_favorite(
154+
db=db,
155+
team_id=team_id,
156+
user_id=current_user.id
157+
)
158+
159+
160+
@router.get("/showcase/recommended", response_model=List[Dict[str, Any]])
161+
def get_recommended_teams(
162+
limit: int = Query(6, ge=1, le=20, description="Max teams to return"),
163+
db: Session = Depends(get_db),
164+
current_user: User = Depends(security.get_current_user)
165+
):
166+
"""Get recommended teams (is_recommended=true)"""
167+
from app.schemas.kind import Team
168+
169+
# Get all teams where isRecommended is true
170+
teams = db.query(Kind).filter(
171+
Kind.kind == "Team",
172+
Kind.is_active == True
173+
).all()
174+
175+
recommended_teams = []
176+
favorite_team_ids = team_favorite_service.get_user_favorite_team_ids(db=db, user_id=current_user.id)
177+
178+
for team in teams:
179+
team_crd = Team.model_validate(team.json)
180+
if team_crd.spec.isRecommended:
181+
team_dict = team_kinds_service._convert_to_team_dict(team, db, team.user_id)
182+
team_dict["is_favorited"] = team.id in favorite_team_ids
183+
recommended_teams.append(team_dict)
184+
if len(recommended_teams) >= limit:
185+
break
186+
187+
return recommended_teams
188+
189+
190+
@router.get("/showcase/favorites", response_model=List[Dict[str, Any]])
191+
def get_favorite_teams(
192+
limit: int = Query(6, ge=1, le=20, description="Max teams to return"),
193+
db: Session = Depends(get_db),
194+
current_user: User = Depends(security.get_current_user)
195+
):
196+
"""Get user's favorite teams"""
197+
from app.models.user_team_favorite import UserTeamFavorite
198+
199+
# Get user's favorite team IDs
200+
favorites = db.query(UserTeamFavorite).filter(
201+
UserTeamFavorite.user_id == current_user.id
202+
).order_by(UserTeamFavorite.created_at.desc()).limit(limit).all()
203+
204+
favorite_teams = []
205+
for favorite in favorites:
206+
team = db.query(Kind).filter(
207+
Kind.id == favorite.team_id,
208+
Kind.kind == "Team",
209+
Kind.is_active == True
210+
).first()
211+
212+
if team:
213+
team_dict = team_kinds_service._convert_to_team_dict(team, db, team.user_id)
214+
team_dict["is_favorited"] = True
215+
favorite_teams.append(team_dict)
216+
217+
return favorite_teams

backend/app/models/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
from app.models.subtask import Subtask
1515
from app.models.shared_team import SharedTeam
1616
from app.models.skill_binary import SkillBinary
17+
from app.models.user_team_favorite import UserTeamFavorite
1718

1819
__all__ = [
1920
"User",
2021
"Kind",
2122
"Subtask",
2223
"SharedTeam",
23-
"SkillBinary"
24+
"SkillBinary",
25+
"UserTeamFavorite"
2426
]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# SPDX-FileCopyrightText: 2025 Weibo, Inc.
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
from datetime import datetime
6+
from sqlalchemy import Column, Integer, DateTime, Index
7+
from app.db.base import Base
8+
9+
10+
class UserTeamFavorite(Base):
11+
"""User team favorite model for maintaining user-team favorite relationships"""
12+
__tablename__ = "user_team_favorites"
13+
14+
id = Column(Integer, primary_key=True, index=True)
15+
user_id = Column(Integer, nullable=False, index=True) # User who favorited the team
16+
team_id = Column(Integer, nullable=False, index=True) # Team that was favorited
17+
created_at = Column(DateTime, default=datetime.now)
18+
19+
__table_args__ = (
20+
Index('idx_user_team_favorite', 'user_id', 'team_id', unique=True),
21+
{'mysql_charset': 'utf8mb4', 'mysql_collate': 'utf8mb4_unicode_ci'},
22+
)

backend/app/schemas/kind.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ class TeamSpec(BaseModel):
177177
"""Team specification"""
178178
members: List[TeamMember]
179179
collaborationModel: str # pipeline、route、coordinate、collaborate
180+
icon: Optional[str] = None # Lucide icon name (e.g., "Users", "Bot", "Zap")
181+
isRecommended: bool = False # Whether this team is recommended
180182

181183

182184
class TeamStatus(Status):

backend/app/schemas/team.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ class TeamBase(BaseModel):
3434
bots: List[BotInfo]
3535
workflow: Optional[dict[str, Any]] = None
3636
is_active: bool = True
37+
icon: Optional[str] = None # Lucide icon name (e.g., "Users", "Bot", "Zap")
38+
is_recommended: bool = False # Whether this team is recommended
3739

3840
class TeamCreate(TeamBase):
3941
"""Team creation model"""
@@ -45,6 +47,8 @@ class TeamUpdate(BaseModel):
4547
bots: Optional[List[BotInfo]] = None
4648
workflow: Optional[dict[str, Any]] = None
4749
is_active: Optional[bool] = None
50+
icon: Optional[str] = None # Lucide icon name
51+
is_recommended: Optional[bool] = None # Whether this team is recommended
4852

4953
class TeamInDB(TeamBase):
5054
"""Database team model"""
@@ -55,6 +59,7 @@ class TeamInDB(TeamBase):
5559
user: Optional[dict[str, Any]] = None
5660
share_status: int = 0 # 0-private, 1-sharing, 2-shared from others
5761
agent_type: Optional[str] = None # agno, claude, dify, etc.
62+
is_favorited: bool = False # Whether current user has favorited this team
5863

5964
class Config:
6065
from_attributes = True
@@ -71,6 +76,9 @@ class TeamDetail(BaseModel):
7176
updated_at: datetime
7277
user: Optional[UserInDB] = None
7378
share_status: int = 0 # 0-private, 1-sharing, 2-shared from others
79+
icon: Optional[str] = None # Lucide icon name
80+
is_recommended: bool = False # Whether this team is recommended
81+
is_favorited: bool = False # Whether current user has favorited this team
7482

7583
class Config:
7684
from_attributes = True

backend/app/services/adapters/team_kinds.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ def create_with_user(
125125
"kind": "Team",
126126
"spec": {
127127
"members": members,
128-
"collaborationModel": collaboration_model
128+
"collaborationModel": collaboration_model,
129+
"icon": getattr(obj_in, 'icon', None),
130+
"isRecommended": getattr(obj_in, 'is_recommended', False)
129131
},
130132
"status": {
131133
"state": "Available"
@@ -462,6 +464,14 @@ def update_with_user(
462464

463465
team_crd.spec.collaborationModel = collaboration_model
464466

467+
# Handle icon update
468+
if "icon" in update_data:
469+
team_crd.spec.icon = update_data["icon"]
470+
471+
# Handle is_recommended update
472+
if "is_recommended" in update_data:
473+
team_crd.spec.isRecommended = update_data["is_recommended"]
474+
465475
# Save the updated team CRD
466476
team.json = team_crd.model_dump(mode='json')
467477
team.updated_at = datetime.now()
@@ -779,7 +789,7 @@ def _convert_to_team_dict(self, team: Kind, db: Session, user_id: int) -> Dict[s
779789

780790
# Convert collaboration model to workflow format
781791
workflow = {"mode": team_crd.spec.collaborationModel}
782-
792+
783793
return {
784794
"id": team.id,
785795
"user_id": team.user_id,
@@ -791,6 +801,8 @@ def _convert_to_team_dict(self, team: Kind, db: Session, user_id: int) -> Dict[s
791801
"created_at": team.created_at,
792802
"updated_at": team.updated_at,
793803
"agent_type": agent_type, # Add agent_type field
804+
"icon": team_crd.spec.icon, # Lucide icon name
805+
"is_recommended": team_crd.spec.isRecommended, # Whether this team is recommended
794806
}
795807

796808
def _get_bot_summary(self, bot: Kind, db: Session, user_id: int) -> Dict[str, Any]:
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# SPDX-FileCopyrightText: 2025 Weibo, Inc.
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
from typing import List, Dict, Any, Set
6+
from sqlalchemy.orm import Session
7+
from fastapi import HTTPException
8+
9+
from app.models.user_team_favorite import UserTeamFavorite
10+
from app.models.kind import Kind
11+
12+
13+
class TeamFavoriteService:
14+
"""Service for team favorite operations"""
15+
16+
def add_favorite(self, db: Session, *, team_id: int, user_id: int) -> Dict[str, Any]:
17+
"""Add a team to user's favorites"""
18+
# Check if team exists
19+
team = db.query(Kind).filter(
20+
Kind.id == team_id,
21+
Kind.kind == "Team",
22+
Kind.is_active == True
23+
).first()
24+
25+
if not team:
26+
raise HTTPException(
27+
status_code=404,
28+
detail="Team not found"
29+
)
30+
31+
# Check if already favorited
32+
existing = db.query(UserTeamFavorite).filter(
33+
UserTeamFavorite.user_id == user_id,
34+
UserTeamFavorite.team_id == team_id
35+
).first()
36+
37+
if existing:
38+
return {"message": "Team already in favorites", "is_favorited": True}
39+
40+
# Create favorite record
41+
favorite = UserTeamFavorite(
42+
user_id=user_id,
43+
team_id=team_id
44+
)
45+
db.add(favorite)
46+
db.commit()
47+
48+
return {"message": "Team added to favorites", "is_favorited": True}
49+
50+
def remove_favorite(self, db: Session, *, team_id: int, user_id: int) -> Dict[str, Any]:
51+
"""Remove a team from user's favorites"""
52+
favorite = db.query(UserTeamFavorite).filter(
53+
UserTeamFavorite.user_id == user_id,
54+
UserTeamFavorite.team_id == team_id
55+
).first()
56+
57+
if not favorite:
58+
return {"message": "Team not in favorites", "is_favorited": False}
59+
60+
db.delete(favorite)
61+
db.commit()
62+
63+
return {"message": "Team removed from favorites", "is_favorited": False}
64+
65+
def get_user_favorite_team_ids(self, db: Session, *, user_id: int) -> Set[int]:
66+
"""Get set of team IDs that user has favorited"""
67+
favorites = db.query(UserTeamFavorite.team_id).filter(
68+
UserTeamFavorite.user_id == user_id
69+
).all()
70+
return {f.team_id for f in favorites}
71+
72+
def is_team_favorited(self, db: Session, *, team_id: int, user_id: int) -> bool:
73+
"""Check if a team is in user's favorites"""
74+
favorite = db.query(UserTeamFavorite).filter(
75+
UserTeamFavorite.user_id == user_id,
76+
UserTeamFavorite.team_id == team_id
77+
).first()
78+
return favorite is not None
79+
80+
81+
team_favorite_service = TeamFavoriteService()

frontend/src/apis/team.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export interface CreateTeamRequest {
1212
bots?: TeamBot[];
1313
workflow?: Record<string, unknown>;
1414
is_active?: boolean;
15+
icon?: string; // Lucide icon name
16+
is_recommended?: boolean; // Whether this team is recommended
1517
}
1618

1719
export interface TeamListResponse {
@@ -82,4 +84,16 @@ export const teamApis = {
8284
async getTeamInputParameters(teamId: number): Promise<TeamInputParametersResponse> {
8385
return apiClient.get(`/teams/${teamId}/input-parameters`);
8486
},
87+
async addTeamToFavorites(teamId: number): Promise<{ message: string; is_favorited: boolean }> {
88+
return apiClient.post(`/teams/${teamId}/favorite`);
89+
},
90+
async removeTeamFromFavorites(teamId: number): Promise<{ message: string; is_favorited: boolean }> {
91+
return apiClient.delete(`/teams/${teamId}/favorite`);
92+
},
93+
async getRecommendedTeams(limit: number = 6): Promise<Team[]> {
94+
return apiClient.get(`/teams/showcase/recommended?limit=${limit}`);
95+
},
96+
async getFavoriteTeams(limit: number = 6): Promise<Team[]> {
97+
return apiClient.get(`/teams/showcase/favorites?limit=${limit}`);
98+
},
8599
};

0 commit comments

Comments
 (0)