diff --git a/lib/actions.ts b/lib/actions.ts index e2085473..63b77e80 100644 --- a/lib/actions.ts +++ b/lib/actions.ts @@ -267,3 +267,26 @@ export async function getAgentUserFromDB( return byPrimary ?? null; } + +/** + * For a writer address, check if it's an ERC-8004 agent and return agent info + * plus the owner's Farcaster profile (if available). + * Returns null only if the address is NOT an agent. + */ +export async function getAgentOwnerProfile( + writerAddress: string, +): Promise<{ ownerProfile: FarcasterProfile | null; agentName: string; agentId: number } | null> { + "use server"; + const agentUser = await getAgentUserFromDB(writerAddress); + if (!agentUser?.agent_id) return null; + + const ownerProfile = agentUser.agent_owner + ? await getFarcasterProfile(agentUser.agent_owner) + : null; + + return { + ownerProfile, + agentName: agentUser.agent_name || `Agent #${agentUser.agent_id}`, + agentId: agentUser.agent_id, + }; +} diff --git a/package.json b/package.json index 4e921a7f..de308ca4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plotlink", - "version": "0.1.17", + "version": "0.1.18", "private": true, "workspaces": [ "packages/*" diff --git a/src/app/profile/[address]/page.tsx b/src/app/profile/[address]/page.tsx index ae6294ba..aa2b95d8 100644 --- a/src/app/profile/[address]/page.tsx +++ b/src/app/profile/[address]/page.tsx @@ -8,7 +8,7 @@ import { formatUnits, type Address } from "viem"; import Link from "next/link"; import { supabase, type Storyline, type Donation, type TradeHistory, type User } from "../../../../lib/supabase"; import { STORY_FACTORY, RESERVE_LABEL, EXPLORER_URL, MCV2_BOND, PLOT_TOKEN } from "../../../../lib/contracts/constants"; -import { getFullUserProfile } from "../../../../lib/actions"; +import { getFullUserProfile, getFarcasterProfile } from "../../../../lib/actions"; import { truncateAddress } from "../../../../lib/utils"; import { formatPrice, formatSupply } from "../../../../lib/format"; import { getTokenPrice, mcv2BondAbi, erc20Abi, type TokenPriceInfo, get24hPriceChange, getTokenTVL } from "../../../../lib/price"; @@ -237,6 +237,15 @@ function ProfileHeader({ onCooldown: boolean; cooldownRemaining: number; }) { + // Fetch owner's Farcaster profile for "Operated by" section + const ownerAddress = agentMeta?.owner; + const hasOwner = !!ownerAddress && ownerAddress.toLowerCase() !== address.toLowerCase(); + const { data: ownerFcProfile } = useQuery({ + queryKey: ["owner-fc-profile", ownerAddress], + queryFn: () => getFarcasterProfile(ownerAddress!), + enabled: hasOwner, + }); + const displayName = agentMeta?.name ?? fcProfile?.displayName ?? null; const hasFarcaster = dbUser?.fid != null && dbUser?.username != null; const hasX = dbUser?.twitter != null; @@ -430,12 +439,19 @@ function ProfileHeader({ )} - {agentMeta.owner && agentMeta.owner.toLowerCase() !== address.toLowerCase() && ( -
- Owner: - - {agentMeta.owner.slice(0, 6)}...{agentMeta.owner.slice(-4)} - + {hasOwner && ( +
+ Operated by +
+ {ownerFcProfile?.pfpUrl && ( + // eslint-disable-next-line @next/next/no-img-element + + )} + + {ownerFcProfile?.displayName || ownerFcProfile?.username || `${ownerAddress!.slice(0, 6)}...${ownerAddress!.slice(-4)}`} + + ERC-8004 Verified +
)}
diff --git a/src/app/story/[storylineId]/[plotIndex]/page.tsx b/src/app/story/[storylineId]/[plotIndex]/page.tsx index d8d902ac..041ca1a6 100644 --- a/src/app/story/[storylineId]/[plotIndex]/page.tsx +++ b/src/app/story/[storylineId]/[plotIndex]/page.tsx @@ -136,7 +136,7 @@ export default async function PlotDetailPage({ params }: { params: Params }) { by{" "} {truncateAddress(sl.writer_address)}}> - + {p.block_timestamp && ( diff --git a/src/app/story/[storylineId]/page.tsx b/src/app/story/[storylineId]/page.tsx index 9309db02..77d72d5f 100644 --- a/src/app/story/[storylineId]/page.tsx +++ b/src/app/story/[storylineId]/page.tsx @@ -364,7 +364,7 @@ function StoryHeader({
Writer {truncateAddress(storyline.writer_address)}}> - + {storyline.writer_type === 1 && }
diff --git a/src/components/StoryCard.tsx b/src/components/StoryCard.tsx index 6d13c9e7..e9ae5f99 100644 --- a/src/components/StoryCard.tsx +++ b/src/components/StoryCard.tsx @@ -116,7 +116,7 @@ export function StoryCard({ {/* Metadata below notebook: author → TVL → rating */}
- + {storyline.writer_type === 1 && } {storyline.token_address && ( diff --git a/src/components/WriterIdentity.tsx b/src/components/WriterIdentity.tsx index 29d9bb3a..783af2b9 100644 --- a/src/components/WriterIdentity.tsx +++ b/src/components/WriterIdentity.tsx @@ -1,21 +1,47 @@ import Link from "next/link"; -import { getFarcasterProfile } from "../../lib/actions"; +import { getFarcasterProfile, getAgentOwnerProfile } from "../../lib/actions"; import { truncateAddress } from "../../lib/utils"; /** - * Server component that displays a Farcaster identity (avatar + username) - * when available, falling back to a truncated Ethereum address. - * Links to the internal profile page at /profile/[address]. + * Server component that displays a writer identity. + * For agents with an owner who has a Farcaster profile, shows "{owner}'s AI Writer". + * For agents without owner FID, shows "AI Writer #{id}". + * Falls back to Farcaster profile or truncated address for non-agents. */ -export async function WriterIdentity({ address }: { address: string }) { +export async function WriterIdentity({ address, writerType }: { address: string; writerType?: number | null }) { + // For agents (or unknown), try owner lookup first + if (writerType === 1 || writerType === undefined || writerType === null) { + const ownerInfo = await getAgentOwnerProfile(address); + if (ownerInfo) { + // Agent with owner FID: "{owner}'s AI Writer" + if (ownerInfo.ownerProfile) { + return ( + + {ownerInfo.ownerProfile.pfpUrl && ( + // eslint-disable-next-line @next/next/no-img-element + + )} + {ownerInfo.ownerProfile.displayName || ownerInfo.ownerProfile.username}'s AI Writer + + ); + } + // Agent without owner FID: plain "AI Writer #{id}" + return ( + + AI Writer #{ownerInfo.agentId} + + ); + } + } + const profile = await getFarcasterProfile(address); if (!profile) { return ( - + {truncateAddress(address)} ); @@ -28,13 +54,7 @@ export async function WriterIdentity({ address }: { address: string }) { > {profile.pfpUrl && ( // eslint-disable-next-line @next/next/no-img-element - + )} @{profile.username} diff --git a/src/components/WriterIdentityClient.tsx b/src/components/WriterIdentityClient.tsx index fb683ad8..e9e42514 100644 --- a/src/components/WriterIdentityClient.tsx +++ b/src/components/WriterIdentityClient.tsx @@ -2,72 +2,124 @@ import { useEffect, useState } from "react"; import Link from "next/link"; -import { getFarcasterProfile } from "../../lib/actions"; +import { getFarcasterProfile, getAgentOwnerProfile } from "../../lib/actions"; import { truncateAddress } from "../../lib/utils"; import type { FarcasterProfile } from "../../lib/farcaster"; +interface OwnerInfo { + ownerProfile: FarcasterProfile | null; + agentName: string; + agentId: number; +} + /** - * Client component that resolves a Farcaster identity via server action. - * Shows a truncated address while loading, then replaces with avatar + username. - * Links to the internal profile page at /profile/[address]. + * Client component that resolves a writer identity via server action. + * For agents with an owner who has a Farcaster profile, shows "{owner}'s AI Writer". + * Falls back to Farcaster profile or truncated address. */ -export function WriterIdentityClient({ address, linkProfile = true }: { address: string; linkProfile?: boolean }) { +export function WriterIdentityClient({ + address, + linkProfile = true, + writerType, +}: { + address: string; + linkProfile?: boolean; + writerType?: number | null; +}) { const [profile, setProfile] = useState(null); + const [ownerInfo, setOwnerInfo] = useState(null); const [loaded, setLoaded] = useState(false); useEffect(() => { let cancelled = false; - getFarcasterProfile(address).then((p) => { + + async function resolve() { + // For agents (or unknown), try owner lookup first + if (writerType === 1 || writerType === undefined || writerType === null) { + const owner = await getAgentOwnerProfile(address); + if (!cancelled && owner) { + setOwnerInfo(owner); + setLoaded(true); + return; + } + } + // Fall back to writer's own Farcaster profile + const p = await getFarcasterProfile(address); if (!cancelled) { setProfile(p); setLoaded(true); } - }); - return () => { - cancelled = true; - }; - }, [address]); + } - const label = !loaded || !profile - ? truncateAddress(address) - : null; + resolve(); + return () => { cancelled = true; }; + }, [address, writerType]); - if (!loaded || !profile) { + if (!loaded) { + const label = truncateAddress(address); if (!linkProfile) return {label}; return ( - + {label} ); } - const inner = ( - - {profile.pfpUrl && ( - // eslint-disable-next-line @next/next/no-img-element - - )} - @{profile.username} - - ); + // Agent with owner Farcaster profile: "{owner}'s AI Writer" + if (ownerInfo && ownerInfo.ownerProfile) { + const inner = ( + + {ownerInfo.ownerProfile.pfpUrl && ( + // eslint-disable-next-line @next/next/no-img-element + + )} + {ownerInfo.ownerProfile.displayName || ownerInfo.ownerProfile.username}'s AI Writer + + ); + if (!linkProfile) return inner; + return ( + + {inner} + + ); + } + + // Agent without owner FID: plain "AI Writer #{id}" + if (ownerInfo) { + const label = `AI Writer #${ownerInfo.agentId}`; + if (!linkProfile) return {label}; + return ( + + {label} + + ); + } - if (!linkProfile) return inner; + // Regular writer with Farcaster profile + if (profile) { + const inner = ( + + {profile.pfpUrl && ( + // eslint-disable-next-line @next/next/no-img-element + + )} + @{profile.username} + + ); + if (!linkProfile) return inner; + return ( + + {inner} + + ); + } + // Fallback: truncated address + const label = truncateAddress(address); + if (!linkProfile) return {label}; return ( - - {inner} + + {label} ); }