feat: add ListOfficialProfiles endpoint + profiles grid in admin UI
This commit is contained in:
parent
2dae622dea
commit
3d371e965e
|
|
@ -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<Config[]>([]);
|
||||
const [profiles, setProfiles] = useState<OfficialProfile[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [setupProfile, setSetupProfile] = useState<OfficialProfile | null>(null);
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
const [actionLoading, setActionLoading] = useState<Record<string, boolean>>({});
|
||||
const [actionResult, setActionResult] = useState<Record<string, { ok: boolean; message: string }>>({});
|
||||
|
||||
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() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{showForm && <CreateAccountForm onDone={() => { setShowForm(false); fetchConfigs(); }} />}
|
||||
{showForm && <CreateAccountForm onDone={() => { setShowForm(false); fetchAll(); }} initialProfile={setupProfile} />}
|
||||
|
||||
{/* Official Profiles Overview */}
|
||||
{!loading && profiles.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<h2 className="text-sm font-semibold text-gray-600 uppercase tracking-wide mb-3">Official Profiles ({profiles.length})</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{profiles.map((p) => (
|
||||
<div key={p.profile_id} className="bg-white rounded-lg border border-warm-300 p-3 flex items-center gap-3">
|
||||
<div className="w-9 h-9 bg-brand-100 rounded-full flex items-center justify-center flex-shrink-0 text-brand-600 font-bold text-sm">
|
||||
{p.avatar_url ? <img src={p.avatar_url} className="w-9 h-9 rounded-full object-cover" /> : p.handle[0]?.toUpperCase()}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-medium text-gray-900 text-sm truncate">@{p.handle}</span>
|
||||
{p.is_verified && <CheckCircle className="w-3.5 h-3.5 text-brand-500 flex-shrink-0" />}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 truncate">{p.display_name}</p>
|
||||
</div>
|
||||
{p.has_config ? (
|
||||
<span className="text-[10px] px-2 py-0.5 rounded-full bg-green-100 text-green-700 flex-shrink-0">Configured</span>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => { setSetupProfile(p); setShowForm(true); }}
|
||||
className="text-xs px-2 py-1 bg-brand-50 text-brand-600 rounded hover:bg-brand-100 transition-colors flex-shrink-0"
|
||||
>
|
||||
Setup AI
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-gray-500">Loading...</div>
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -395,6 +395,10 @@ class ApiClient {
|
|||
}
|
||||
|
||||
// Official Accounts
|
||||
async listOfficialProfiles() {
|
||||
return this.request<any>('/api/v1/admin/official-accounts/profiles');
|
||||
}
|
||||
|
||||
async listOfficialAccounts() {
|
||||
return this.request<any>('/api/v1/admin/official-accounts');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue