diff --git a/admin/src/app/official-accounts/page.tsx b/admin/src/app/official-accounts/page.tsx index 7887bec..de7f344 100644 --- a/admin/src/app/official-accounts/page.tsx +++ b/admin/src/app/official-accounts/page.tsx @@ -62,26 +62,47 @@ interface Config { avatar_url: string; } +interface OfficialProfile { + profile_id: string; + user_id: string; + handle: string; + display_name: string; + avatar_url: string; + bio: string; + is_verified: boolean; + has_config: boolean; + config_id?: string; +} + export default function OfficialAccountsPage() { const [configs, setConfigs] = useState([]); + const [profiles, setProfiles] = useState([]); const [loading, setLoading] = useState(true); const [showForm, setShowForm] = useState(false); + const [setupProfile, setSetupProfile] = useState(null); const [expandedId, setExpandedId] = useState(null); const [actionLoading, setActionLoading] = useState>({}); const [actionResult, setActionResult] = useState>({}); - const fetchConfigs = async () => { + const fetchAll = async () => { setLoading(true); try { - const data = await api.listOfficialAccounts(); - setConfigs(data.configs || []); + const [configData, profileData] = await Promise.all([ + api.listOfficialAccounts(), + api.listOfficialProfiles(), + ]); + setConfigs(configData.configs || []); + setProfiles(profileData.profiles || []); } catch { setConfigs([]); + setProfiles([]); } setLoading(false); }; - useEffect(() => { fetchConfigs(); }, []); + const fetchConfigs = fetchAll; + + useEffect(() => { fetchAll(); }, []); const setAction = (id: string, loading: boolean, result?: { ok: boolean; message: string }) => { setActionLoading((p) => ({ ...p, [id]: loading })); @@ -152,7 +173,40 @@ export default function OfficialAccountsPage() { - {showForm && { setShowForm(false); fetchConfigs(); }} />} + {showForm && { setShowForm(false); fetchAll(); }} initialProfile={setupProfile} />} + + {/* Official Profiles Overview */} + {!loading && profiles.length > 0 && ( +
+

Official Profiles ({profiles.length})

+
+ {profiles.map((p) => ( +
+
+ {p.avatar_url ? : p.handle[0]?.toUpperCase()} +
+
+
+ @{p.handle} + {p.is_verified && } +
+

{p.display_name}

+
+ {p.has_config ? ( + Configured + ) : ( + + )} +
+ ))} +
+
+ )} {loading ? (
Loading...
@@ -276,8 +330,8 @@ export default function OfficialAccountsPage() { } // ─── Create Account Form ────────────────────────────── -function CreateAccountForm({ onDone }: { onDone: () => void }) { - const [handle, setHandle] = useState(''); +function CreateAccountForm({ onDone, initialProfile }: { onDone: () => void; initialProfile?: OfficialProfile | null }) { + const [handle, setHandle] = useState(initialProfile?.handle || ''); const [accountType, setAccountType] = useState('general'); const [modelId, setModelId] = useState('google/gemini-2.0-flash-001'); const [systemPrompt, setSystemPrompt] = useState(DEFAULT_GENERAL_PROMPT); diff --git a/admin/src/lib/api.ts b/admin/src/lib/api.ts index da22f70..50c4d70 100644 --- a/admin/src/lib/api.ts +++ b/admin/src/lib/api.ts @@ -395,6 +395,10 @@ class ApiClient { } // Official Accounts + async listOfficialProfiles() { + return this.request('/api/v1/admin/official-accounts/profiles'); + } + async listOfficialAccounts() { return this.request('/api/v1/admin/official-accounts'); } diff --git a/go-backend/cmd/api/main.go b/go-backend/cmd/api/main.go index 6d992c5..08f2192 100644 --- a/go-backend/cmd/api/main.go +++ b/go-backend/cmd/api/main.go @@ -481,6 +481,7 @@ func main() { // Official Accounts Management admin.GET("/official-accounts", adminHandler.ListOfficialAccounts) + admin.GET("/official-accounts/profiles", adminHandler.ListOfficialProfiles) admin.GET("/official-accounts/:id", adminHandler.GetOfficialAccount) admin.POST("/official-accounts", adminHandler.UpsertOfficialAccount) admin.DELETE("/official-accounts/:id", adminHandler.DeleteOfficialAccount) diff --git a/go-backend/internal/handlers/admin_handler.go b/go-backend/internal/handlers/admin_handler.go index eeedc30..0718fc3 100644 --- a/go-backend/internal/handlers/admin_handler.go +++ b/go-backend/internal/handlers/admin_handler.go @@ -2833,6 +2833,18 @@ func (h *AdminHandler) ListOfficialAccounts(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"configs": configs}) } +func (h *AdminHandler) ListOfficialProfiles(c *gin.Context) { + profiles, err := h.officialAccountsService.ListOfficialProfiles(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if profiles == nil { + profiles = []services.OfficialProfile{} + } + c.JSON(http.StatusOK, gin.H{"profiles": profiles}) +} + func (h *AdminHandler) GetOfficialAccount(c *gin.Context) { id := c.Param("id") cfg, err := h.officialAccountsService.GetConfig(c.Request.Context(), id) diff --git a/go-backend/internal/services/official_accounts_service.go b/go-backend/internal/services/official_accounts_service.go index ae8c500..ed92eb3 100644 --- a/go-backend/internal/services/official_accounts_service.go +++ b/go-backend/internal/services/official_accounts_service.go @@ -608,6 +608,50 @@ func stripHTMLTags(s string) string { return strings.TrimSpace(result.String()) } +// OfficialProfile represents a profile with is_official = true +type OfficialProfile struct { + ProfileID string `json:"profile_id"` + UserID string `json:"user_id"` + Handle string `json:"handle"` + DisplayName string `json:"display_name"` + AvatarURL string `json:"avatar_url"` + Bio string `json:"bio"` + IsVerified bool `json:"is_verified"` + HasConfig bool `json:"has_config"` + ConfigID *string `json:"config_id,omitempty"` +} + +// ListOfficialProfiles returns all profiles where is_official = true, +// along with whether they have an official_account_configs entry +func (s *OfficialAccountsService) ListOfficialProfiles(ctx context.Context) ([]OfficialProfile, error) { + rows, err := s.pool.Query(ctx, ` + SELECT p.id, p.user_id, p.handle, p.display_name, COALESCE(p.avatar_url, ''), + COALESCE(p.bio, ''), COALESCE(p.is_verified, false), + c.id AS config_id + FROM public.profiles p + LEFT JOIN official_account_configs c ON c.profile_id = p.id + WHERE p.is_official = true + ORDER BY p.handle + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var profiles []OfficialProfile + for rows.Next() { + var p OfficialProfile + var configID *string + if err := rows.Scan(&p.ProfileID, &p.UserID, &p.Handle, &p.DisplayName, &p.AvatarURL, &p.Bio, &p.IsVerified, &configID); err != nil { + continue + } + p.ConfigID = configID + p.HasConfig = configID != nil + profiles = append(profiles, p) + } + return profiles, nil +} + // LookupProfileID finds a profile ID by handle func (s *OfficialAccountsService) LookupProfileID(ctx context.Context, handle string) (string, error) { var id string