wip adding dashboard, profile and settings page

This commit is contained in:
SwiftyOS
2024-11-13 15:15:26 +01:00
parent 9aec1f51ed
commit 175f17b131
15 changed files with 366 additions and 220 deletions

View File

@@ -131,40 +131,6 @@ async def get_store_agent_details(
) from e
async def get_user_profile(
user_id: str,
) -> backend.server.v2.store.model.ProfileDetails:
logger.debug(f"Getting user profile for {user_id}")
try:
profile = await prisma.models.Profile.prisma().find_unique(
where={"userId": user_id} # type: ignore
)
if not profile:
logger.warning(f"Profile not found for user {user_id}")
raise backend.server.v2.store.exceptions.ProfileNotFoundError(
f"Profile not found for user {user_id}"
)
return backend.server.v2.store.model.ProfileDetails(
name=profile.name,
username=profile.username,
description=profile.description,
links=profile.links,
avatar_url=profile.avatarUrl,
)
except Exception as e:
logger.error(f"Error getting user profile: {str(e)}")
return backend.server.v2.store.model.ProfileDetails(
name="No Profile Data",
username="No Profile Data",
description="No Profile Data",
links=[],
avatar_url="",
)
async def get_store_creators(
featured: bool = False,
search_query: str | None = None,
@@ -448,7 +414,41 @@ async def create_store_submission(
) from e
async def update_profile(
async def get_user_profile(
user_id: str,
) -> backend.server.v2.store.model.ProfileDetails:
logger.debug(f"Getting user profile for {user_id}")
try:
profile = await prisma.models.Profile.prisma().find_unique(
where={"userId": user_id} # type: ignore
)
if not profile:
logger.warning(f"Profile not found for user {user_id}")
raise backend.server.v2.store.exceptions.ProfileNotFoundError(
f"Profile not found for user {user_id}"
)
return backend.server.v2.store.model.ProfileDetails(
name=profile.name,
username=profile.username,
description=profile.description,
links=profile.links,
avatar_url=profile.avatarUrl,
)
except Exception as e:
logger.error(f"Error getting user profile: {str(e)}")
return backend.server.v2.store.model.ProfileDetails(
name="No Profile Data",
username="No Profile Data",
description="No Profile Data",
links=[],
avatar_url="",
)
async def update_or_create_profile(
user_id: str, profile: backend.server.v2.store.model.CreatorDetails
) -> backend.server.v2.store.model.CreatorDetails:
"""
@@ -473,7 +473,7 @@ async def update_profile(
where={"userId": user_id}
)
# If no profile exists, continue with upsert
# If no profile exists, create a new one
if not existing_profile:
logger.debug(f"Creating new profile for user {user_id}")
# Create new profile since one doesn't exist

View File

@@ -219,7 +219,7 @@ async def test_update_profile(mocker):
)
# Call function
result = await db.update_profile("user-id", profile)
result = await db.update_or_create_profile("user-id", profile)
# Verify results
assert result.username == "creator"

View File

@@ -37,6 +37,40 @@ async def get_profile(
raise
@router.post(
"/profile",
tags=["store", "private"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
)
async def update_or_create_profile(
profile: backend.server.v2.store.model.CreatorDetails,
user_id: typing.Annotated[
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
],
) -> backend.server.v2.store.model.CreatorDetails:
"""
Update the store profile for the authenticated user.
Args:
profile (CreatorDetails): The updated profile details
user_id (str): ID of the authenticated user
Returns:
CreatorDetails: The updated profile
Raises:
HTTPException: If there is an error updating the profile
"""
try:
updated_profile = await backend.server.v2.store.db.update_or_create_profile(
user_id=user_id, profile=profile
)
return updated_profile
except Exception:
logger.exception("Exception occurred whilst updating profile")
raise
##############################################
############### Agent Endpoints ##############
##############################################
@@ -315,37 +349,3 @@ async def upload_submission_media(
except Exception:
logger.exception("Exception occurred whilst uploading submission media")
raise
@router.put(
"/profile",
tags=["store", "private"],
dependencies=[fastapi.Depends(autogpt_libs.auth.middleware.auth_middleware)],
)
async def update_profile(
profile: backend.server.v2.store.model.CreatorDetails,
user_id: typing.Annotated[
str, fastapi.Depends(autogpt_libs.auth.depends.get_user_id)
],
) -> backend.server.v2.store.model.CreatorDetails:
"""
Update the store profile for the authenticated user.
Args:
profile (CreatorDetails): The updated profile details
user_id (str): ID of the authenticated user
Returns:
CreatorDetails: The updated profile
Raises:
HTTPException: If there is an error updating the profile
"""
try:
updated_profile = await backend.server.v2.store.db.update_profile(
user_id=user_id, profile=profile
)
return updated_profile
except Exception:
logger.exception("Exception occurred whilst updating profile")
raise

View File

@@ -0,0 +1,81 @@
"use client";
import * as React from "react";
import { Navbar } from "@/components/agptui/Navbar";
import { Sidebar } from "@/components/agptui/Sidebar";
import { AgentTable } from "@/components/agptui/AgentTable";
import { Button } from "@/components/agptui/Button";
import { Separator } from "@/components/ui/separator";
import { IconType } from "@/components/ui/icons";
interface CreatorDashboardPageProps {
isLoggedIn: boolean;
userName: string;
userEmail: string;
navLinks: { name: string; href: string }[];
activeLink: string;
menuItemGroups: {
groupName?: string;
items: {
icon: IconType;
text: string;
href?: string;
onClick?: () => void;
}[];
}[];
sidebarLinkGroups: {
links: {
text: string;
href: string;
}[];
}[];
agents: {
agentName: string;
description: string;
imageSrc: string;
dateSubmitted: string;
status: string;
runs: number;
rating: number;
onEdit: () => void;
}[];
}
export default function Page({
sidebarLinkGroups,
agents,
}: CreatorDashboardPageProps) {
return (
<div className="flex">
<Sidebar linkGroups={sidebarLinkGroups} />
<main className="flex-1 px-6 py-8 md:px-10">
{/* Header Section */}
<div className="mb-8 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<h1 className="font-neue text-3xl font-medium leading-9 tracking-tight text-neutral-900">
Submit a New Agent
</h1>
<p className="mt-2 font-neue text-sm text-[#707070]">
Select from the list of agents you currently have, or upload from
your local machine.
</p>
</div>
<Button variant="default" size="lg">
Create New Agent
</Button>
</div>
<Separator className="mb-8" />
{/* Agents Section */}
<div>
<h2 className="mb-4 text-xl font-bold text-neutral-900">
Your Agents
</h2>
<AgentTable agents={agents} />
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,23 @@
import * as React from "react";
import { Sidebar } from "@/components/agptui/Sidebar";
export default function Layout({ children }: { children: React.ReactNode }) {
const sidebarLinkGroups = [
{
links: [
{ text: "Creator Dashboard", href: "/store/dashboard" },
{ text: "Agent dashboard", href: "/store/agent-dashboard" },
{ text: "Integrations", href: "/store/integrations" },
{ text: "Profile", href: "/store/profile" },
{ text: "Settings", href: "/store/settings" },
],
},
];
return (
<div className="flex min-h-screen flex-col lg:flex-row">
<Sidebar linkGroups={sidebarLinkGroups} />
{children}
</div>
);
}

View File

@@ -0,0 +1,104 @@
import * as React from "react";
import { Sidebar } from "@/components/agptui/Sidebar";
import { ProfileInfoForm } from "@/components/agptui/ProfileInfoForm";
import { IconType } from "@/components/ui/icons";
import { Navbar } from "@/components/agptui/Navbar";
interface ProfilePageProps {
userName?: string;
userEmail?: string;
credits?: number;
displayName?: string;
handle?: string;
bio?: string;
links?: Array<{ id: number; url: string }>;
categories?: Array<{ id: number; name: string }>;
menuItemGroups?: Array<{
items: Array<{
icon: IconType;
text: string;
href?: string;
onClick?: () => void;
}>;
}>;
isLoggedIn?: boolean;
avatarSrc?: string;
}
const ProfilePage = ({
userName = "",
userEmail = "",
credits = 0,
displayName = "",
handle = "",
bio = "",
links = [],
categories = [],
menuItemGroups = [],
isLoggedIn = true,
avatarSrc,
}: ProfilePageProps) => {
const sidebarLinkGroups = [
{
links: [
{ text: "Creator Dashboard", href: "/dashboard" },
{ text: "Agent dashboard", href: "/agent-dashboard" },
{ text: "Integrations", href: "/integrations" },
{ text: "Profile", href: "/profile" },
{ text: "Settings", href: "/settings" },
],
},
];
const updatedMenuItemGroups = [
{
items: [
{ icon: IconType.Edit, text: "Edit profile", href: "/profile/edit" },
],
},
{
items: [
{
icon: IconType.LayoutDashboard,
text: "Creator Dashboard",
href: "/dashboard",
},
{
icon: IconType.UploadCloud,
text: "Publish an agent",
href: "/publish",
},
],
},
{
items: [{ icon: IconType.Settings, text: "Settings", href: "/settings" }],
},
{
items: [
{
icon: IconType.LogOut,
text: "Log out",
onClick: () => console.log("Logged out"),
},
],
},
];
const navLinks = [
{ name: "Marketplace", href: "/marketplace" },
{ name: "Library", href: "/library" },
{ name: "Build", href: "/build" },
];
return (
<ProfileInfoForm
displayName={displayName}
handle={handle}
bio={bio}
links={links}
categories={categories}
/>
);
};
export default ProfilePage;

View File

@@ -0,0 +1,6 @@
import * as React from "react";
import { SettingsInputForm } from "@/components/agptui/SettingsInputForm";
export default function Page() {
return <SettingsInputForm />;
}

View File

@@ -70,7 +70,7 @@ export default async function RootLayout({
{
icon: IconType.Edit,
text: "Edit profile",
href: "/profile/edit",
href: "/store/profile",
},
],
},
@@ -79,12 +79,12 @@ export default async function RootLayout({
{
icon: IconType.LayoutDashboard,
text: "Creator Dashboard",
href: "/dashboard",
href: "/store/dashboard",
},
{
icon: IconType.UploadCloud,
text: "Publish an agent",
href: "/publish",
href: "/store/publish",
},
],
},
@@ -93,7 +93,7 @@ export default async function RootLayout({
{
icon: IconType.Settings,
text: "Settings",
href: "/settings",
href: "/store/settings",
},
],
},

View File

@@ -16,9 +16,9 @@ export function Providers({
return (
<NextThemesProvider {...props}>
<SupabaseProvider initialUser={initialUser}>
<CredentialsProvider>
<TooltipProvider>{children}</TooltipProvider>
</CredentialsProvider>
{/* <CredentialsProvider> */}
<TooltipProvider>{children}</TooltipProvider>
{/* </CredentialsProvider> */}
</SupabaseProvider>
</NextThemesProvider>
);

View File

@@ -31,7 +31,6 @@ interface NavbarProps {
}
async function getProfileData(user: User | null) {
console.log(user);
const api = new AutoGPTServerAPIServerSide();
const [profile, credits] = await Promise.all([
api.getStoreProfile(),
@@ -53,8 +52,6 @@ export const Navbar = async ({
let profile: ProfileDetails | null = null;
let credits: { credits: number } = { credits: 0 };
if (isLoggedIn) {
console.log("Fetching profile data");
console.log(user);
const { profile: t_profile, credits: t_credits } =
await getProfileData(user);
profile = t_profile;

View File

@@ -10,6 +10,8 @@ export interface ProfileInfoFormProps {
profileImage?: string;
links: { id: number; url: string }[];
categories: { id: number; name: string }[];
onCategoryClick: (category: string) => void;
selectedCategories: string[];
}
export const AVAILABLE_CATEGORIES = [
@@ -24,27 +26,16 @@ export const AVAILABLE_CATEGORIES = [
"Research",
] as const;
export const ProfileInfoForm: React.FC<ProfileInfoFormProps> = ({
export const ProfileInfoForm = ({
displayName,
handle,
bio,
profileImage,
links,
categories,
}) => {
const [selectedCategories, setSelectedCategories] = React.useState<string[]>(
categories.map((cat) => cat.name),
);
const handleCategoryClick = (category: string) => {
console.log(`${category} category button was pressed`);
if (selectedCategories.includes(category)) {
setSelectedCategories(selectedCategories.filter((c) => c !== category));
} else if (selectedCategories.length < 5) {
setSelectedCategories([...selectedCategories, category]);
}
};
onCategoryClick,
selectedCategories,
}: ProfileInfoFormProps) => {
return (
<main className="p-4 sm:p-8">
<h1 className="mb-6 font-['Poppins'] text-[28px] font-medium text-neutral-900 sm:mb-8 sm:text-[35px]">
@@ -82,7 +73,7 @@ export const ProfileInfoForm: React.FC<ProfileInfoFormProps> = ({
<div className="rounded-[55px] border border-slate-200 px-4 py-2.5">
<input
type="text"
value={displayName}
defaultValue={displayName}
placeholder="Enter your display name"
className="w-full border-none bg-transparent font-['Inter'] text-base font-normal text-[#666666] focus:outline-none"
/>
@@ -104,7 +95,7 @@ export const ProfileInfoForm: React.FC<ProfileInfoFormProps> = ({
<div className="rounded-[55px] border border-slate-200 px-4 py-2.5">
<input
type="text"
value={handle}
defaultValue={handle}
placeholder="@username"
className="w-full border-none bg-transparent font-['Inter'] text-base font-normal text-[#666666] focus:outline-none"
/>
@@ -125,7 +116,7 @@ export const ProfileInfoForm: React.FC<ProfileInfoFormProps> = ({
</label>
<div className="h-[220px] rounded-2xl border border-slate-200 py-2.5 pl-4 pr-4">
<textarea
value={bio}
defaultValue={bio}
placeholder="Tell us about yourself..."
className="h-full w-full resize-none border-none bg-transparent font-['Geist'] text-base font-normal text-[#666666] focus:outline-none"
/>
@@ -167,7 +158,7 @@ export const ProfileInfoForm: React.FC<ProfileInfoFormProps> = ({
<input
type="text"
placeholder="https://"
value={link?.url || ""}
defaultValue={link?.url || ""}
className="w-full border-none bg-transparent font-['Inter'] text-base font-normal text-[#666666] focus:outline-none"
/>
</div>
@@ -190,7 +181,7 @@ export const ProfileInfoForm: React.FC<ProfileInfoFormProps> = ({
<hr className="my-8 border-neutral-300" />
<section>
{/* <section>
<div className="relative min-h-[190px] w-full">
<div className="absolute left-0 top-0 h-[68px] w-full">
<div className="absolute left-0 top-[48px] w-full font-['Geist'] text-base font-medium leading-tight text-slate-950">
@@ -205,7 +196,7 @@ export const ProfileInfoForm: React.FC<ProfileInfoFormProps> = ({
<Button
key={index}
variant="outline"
onClick={() => handleCategoryClick(category)}
onClick={() => onCategoryClick(category)}
className={`rounded-[34px] border border-neutral-600 px-5 py-3 transition-colors ${
selectedCategories.includes(category)
? "bg-slate-900 text-white hover:bg-slate-800"
@@ -216,8 +207,8 @@ export const ProfileInfoForm: React.FC<ProfileInfoFormProps> = ({
</Button>
))}
</div>
</div>
</section>
</div> */}
{/* </section> */}
</main>
);
};

View File

@@ -36,82 +36,6 @@ interface ProfilePopoutMenuProps {
}[];
}
interface PopoutMenuItemProps {
icon: IconType;
text: React.ReactNode;
href?: string;
onClick?: () => void;
}
const PopoutMenuItem: React.FC<PopoutMenuItemProps> = ({
icon,
text,
href,
onClick,
}) => {
const getIcon = (iconType: IconType) => {
let iconClass = "w-6 h-6 relative";
const getIconWithAccessibility = (
Icon: React.ComponentType<any>,
label: string,
) => (
<Icon className={iconClass} role="img" aria-label={label}>
<title>{label}</title>
</Icon>
);
switch (iconType) {
case IconType.Marketplace:
return getIconWithAccessibility(IconMarketplace, "Marketplace");
case IconType.Library:
return getIconWithAccessibility(IconLibrary, "Library");
case IconType.Builder:
return getIconWithAccessibility(IconBuilder, "Builder");
case IconType.Edit:
return getIconWithAccessibility(IconEdit, "Edit");
case IconType.LayoutDashboard:
return getIconWithAccessibility(IconLayoutDashboard, "Dashboard");
case IconType.UploadCloud:
return getIconWithAccessibility(IconUploadCloud, "Upload");
case IconType.Settings:
return getIconWithAccessibility(IconSettings, "Settings");
case IconType.LogOut:
return getIconWithAccessibility(IconLogOut, "Log Out");
default:
return getIconWithAccessibility(IconRefresh, "Refresh");
}
};
if (onClick && href) {
console.warn("onClick and href are both defined");
}
const content = (
<div className="inline-flex w-full items-center justify-start gap-2.5 hover:rounded hover:bg-[#e0e0e0]">
{getIcon(icon)}
<div className="font-['Inter'] text-base font-normal leading-7 text-[#474747]">
{text}
</div>
</div>
);
if (onClick) {
return (
<div className="w-full" onClick={onClick}>
{content}
</div>
);
}
if (href) {
return (
<Link href={href} className="w-full">
{content}
</Link>
);
}
return content;
};
export const ProfilePopoutMenu: React.FC<ProfilePopoutMenuProps> = ({
userName,
userEmail,
@@ -120,6 +44,30 @@ export const ProfilePopoutMenu: React.FC<ProfilePopoutMenuProps> = ({
}) => {
const popupId = React.useId();
const getIcon = (icon: IconType) => {
const iconClass = "w-6 h-6";
switch (icon) {
case IconType.LayoutDashboard:
return <IconLayoutDashboard className={iconClass} />;
case IconType.UploadCloud:
return <IconUploadCloud className={iconClass} />;
case IconType.Edit:
return <IconEdit className={iconClass} />;
case IconType.Settings:
return <IconSettings className={iconClass} />;
case IconType.LogOut:
return <IconLogOut className={iconClass} />;
case IconType.Marketplace:
return <IconMarketplace className={iconClass} />;
case IconType.Library:
return <IconLibrary className={iconClass} />;
case IconType.Builder:
return <IconBuilder className={iconClass} />;
default:
return <IconRefresh className={iconClass} />;
}
};
return (
<Popover>
<PopoverTrigger asChild>
@@ -168,20 +116,33 @@ export const ProfilePopoutMenu: React.FC<ProfilePopoutMenuProps> = ({
key={groupIndex}
className="flex w-full flex-col items-start justify-start gap-5 rounded-[18px] bg-white p-3.5"
>
{group.items.map((item, itemIndex) => (
<div
key={itemIndex}
className="inline-flex w-full items-center justify-start gap-2.5"
onClick={item.onClick}
role="button"
tabIndex={0}
>
<div className="relative h-6 w-6">{getIcon(item.icon)}</div>
<div className="font-['Geist'] text-base font-medium leading-normal text-neutral-800">
{item.text}
{group.items.map((item, itemIndex) =>
item.href ? (
<Link
key={itemIndex}
href={item.href}
className="inline-flex w-full items-center justify-start gap-2.5"
>
<div className="relative h-6 w-6">{getIcon(item.icon)}</div>
<div className="font-['Geist'] text-base font-medium leading-normal text-neutral-800">
{item.text}
</div>
</Link>
) : (
<div
key={itemIndex}
className="inline-flex w-full items-center justify-start gap-2.5"
onClick={item.onClick}
role="button"
tabIndex={0}
>
<div className="relative h-6 w-6">{getIcon(item.icon)}</div>
<div className="font-['Geist'] text-base font-medium leading-normal text-neutral-800">
{item.text}
</div>
</div>
</div>
))}
),
)}
</div>
))}
</div>
@@ -189,22 +150,3 @@ export const ProfilePopoutMenu: React.FC<ProfilePopoutMenuProps> = ({
</Popover>
);
};
// Helper function to get the icon component
const getIcon = (icon: IconType) => {
const iconClass = "w-6 h-6";
switch (icon) {
case IconType.LayoutDashboard:
return <IconLayoutDashboard className={iconClass} />;
case IconType.UploadCloud:
return <IconUploadCloud className={iconClass} />;
case IconType.Edit:
return <IconEdit className={iconClass} />;
case IconType.Settings:
return <IconSettings className={iconClass} />;
case IconType.LogOut:
return <IconLogOut className={iconClass} />;
default:
return null;
}
};

View File

@@ -1,3 +1,5 @@
"use client";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";

View File

@@ -41,12 +41,12 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
<div className="h-full w-full rounded-2xl bg-zinc-200">
<div className="inline-flex h-[264px] flex-col items-start justify-start gap-6 p-3">
<Link
href="/dashboard"
href="/store/dashboard"
className="inline-flex items-center justify-center gap-2.5 self-stretch rounded-xl px-7 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white"
>
<IconDashboardLayout className="h-6 w-6" />
<div className="shrink grow basis-0 font-['Inter'] text-base font-medium leading-normal">
Agent dashboard
Creator dashboard
</div>
</Link>
<Link
@@ -59,7 +59,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
</div>
</Link>
<Link
href="/profile"
href="/store/profile"
className="inline-flex items-center justify-center gap-2.5 self-stretch rounded-xl px-7 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white"
>
<IconProfile className="h-6 w-6" />
@@ -68,7 +68,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
</div>
</Link>
<Link
href="/settings"
href="/store/settings"
className="inline-flex items-center justify-center gap-2.5 self-stretch rounded-xl px-7 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white"
>
<IconSliders className="h-6 w-6" />
@@ -85,7 +85,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
<div className="h-full w-full rounded-2xl bg-zinc-200">
<div className="inline-flex h-[264px] flex-col items-start justify-start gap-6 p-3">
<Link
href="/dashboard"
href="/store/dashboard"
className="inline-flex items-center justify-center gap-2.5 self-stretch rounded-xl px-7 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white"
>
<IconDashboardLayout className="h-6 w-6" />
@@ -103,7 +103,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
</div>
</Link>
<Link
href="/profile"
href="/store/profile"
className="inline-flex items-center justify-center gap-2.5 self-stretch rounded-xl px-7 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white"
>
<IconProfile className="h-6 w-6" />
@@ -112,7 +112,7 @@ export const Sidebar: React.FC<SidebarProps> = ({ linkGroups }) => {
</div>
</Link>
<Link
href="/settings"
href="/store/settings"
className="inline-flex items-center justify-center gap-2.5 self-stretch rounded-xl px-7 py-3 text-neutral-800 hover:bg-neutral-800 hover:text-white"
>
<IconSliders className="h-6 w-6" />

View File

@@ -336,7 +336,7 @@ export default class BaseAutoGPTServerAPI {
(await this.supabaseClient?.auth.getSession())?.data.session
?.access_token || "no-token-found";
console.log("Token for request type: ", method, path, token);
console.log("Request: ", method, path);
let url = this.baseUrl + path;
if (method === "GET" && payload) {