feat(web): add profile management
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -7,7 +7,7 @@ type Controller = 'human' | 'ai' | 'delegated'
|
||||
interface CampaignPayload {
|
||||
campaign: Record<string, any>
|
||||
world: Record<string, any>
|
||||
members: Array<Record<string, any> & { profile?: { display_name?: string } | null }>
|
||||
members: Array<Record<string, any> & { profile?: { id?: string; display_name?: string; avatar_url?: string | null } | null }>
|
||||
characters: Array<Record<string, any>>
|
||||
round: Record<string, any> | null
|
||||
rounds: Array<Record<string, any>>
|
||||
@@ -311,7 +311,7 @@ onBeforeUnmount(() => {
|
||||
<aside class="control-panel">
|
||||
<header class="panel-title"><span>ROUND CONTROL</span><i :class="{live:!refreshing}" /></header>
|
||||
<section class="readiness"><p><b>{{ readyCount }} / {{ activeHumanMembers.length }}</b> HUMANS READY</p><div><i :style="{width:`${activeHumanMembers.length ? readyCount/activeHumanMembers.length*100 : 0}%`} " /></div></section>
|
||||
<section class="members"><small>PLAYERS</small><article v-for="member in payload.members" :key="member.id"><i :class="{ready:payload.intents.some(intent=>intent.member_id===member.id && intent.ready)}" /><span><b>{{ member.profile?.display_name || 'Adventurer' }}</b><em>{{ member.role }} · {{ payload.intents.some(intent=>intent.member_id===member.id && intent.ready) ? 'ready' : 'waiting' }}</em><button v-if="isOwner" class="takeover-toggle" :class="{enabled:member.ai_takeover_allowed}" :disabled="mutating" @click="toggleTakeover(member)">AI TAKEOVER {{ member.ai_takeover_allowed ? 'ON' : 'OFF' }}</button></span></article></section>
|
||||
<section class="members"><small>PLAYERS</small><article v-for="member in payload.members" :key="member.id"><i :class="{ready:payload.intents.some(intent=>intent.member_id===member.id && intent.ready)}" /><NuxtLink class="member-profile" :to="`/profile/${member.user_id}?campaign=${campaignId}`"><span class="member-avatar"><b>{{ initials(member.profile?.display_name || 'Adventurer') }}</b><img v-if="member.profile?.avatar_url" :src="member.profile.avatar_url" alt=""></span><span><b>{{ member.profile?.display_name || 'Adventurer' }}</b><em>{{ member.role }} · {{ payload.intents.some(intent=>intent.member_id===member.id && intent.ready) ? 'ready' : 'waiting' }}</em></span></NuxtLink><button v-if="isOwner" class="takeover-toggle" :class="{enabled:member.ai_takeover_allowed}" :disabled="mutating" @click="toggleTakeover(member)">AI TAKEOVER {{ member.ai_takeover_allowed ? 'ON' : 'OFF' }}</button></article></section>
|
||||
<section v-if="isOwner" class="invite-box"><small>INVITE PLAYERS</small><p>Private link · up to 8 joins · expires in 72 hours.</p><button v-if="!inviteLink" class="ghost-button" :disabled="mutating" @click="createInvite">CREATE INVITE LINK</button><template v-else><input :value="inviteLink" readonly aria-label="Campaign invite link"><button class="acid-button" @click="copyInvite">{{ inviteCopied ? 'COPIED' : 'COPY LINK' }}</button></template></section>
|
||||
<section class="ai-order"><small>AI TURN ORDER</small><p v-if="!payload.characters.some(character=>character.controller!=='human')">No AI heroes in this party.</p><div v-for="(character,index) in payload.characters.filter(character=>character.controller!=='human')" :key="character.id"><b>{{ String(index+1).padStart(2,'0') }}</b><span>{{ character.name }}<small>{{ character.controller === 'delegated' ? 'Temporary stand-in' : 'Acts after all humans' }}</small></span></div></section>
|
||||
<button v-if="isOwner && currentRound?.status==='open'" class="force-button" :disabled="mutating" @click="forceRound">CONTINUE WITHOUT WAITING <span>↗</span></button>
|
||||
@@ -334,4 +334,12 @@ onBeforeUnmount(() => {
|
||||
.takeover-toggle{margin-top:7px;padding:4px 6px;border:1px solid var(--line);background:transparent;color:var(--muted);font:500 6px var(--mono);text-align:left}
|
||||
.takeover-toggle.enabled{border-color:var(--acid-dim);color:var(--acid)}
|
||||
.ai-draft{min-height:34px;border:1px solid var(--acid-dim);background:rgba(207,255,70,.04);color:var(--acid);font:600 7px var(--mono);letter-spacing:.1em}.persona-editor{border:1px solid var(--line);padding:9px}.persona-editor summary{cursor:pointer;color:var(--muted);font:600 7px var(--mono);letter-spacing:.1em}.persona-editor input{margin-top:7px}
|
||||
.members article{display:grid;grid-template-columns:6px minmax(0,1fr);align-items:center}
|
||||
.member-profile{display:grid;grid-template-columns:30px minmax(0,1fr);gap:9px;align-items:center;color:var(--ink);text-decoration:none}
|
||||
.member-profile>span:last-child{display:flex;min-width:0;flex-direction:column}
|
||||
.member-avatar{position:relative;overflow:hidden;display:grid!important;place-items:center;width:30px;height:30px;border:1px solid var(--line);background:#171714;color:var(--acid)}
|
||||
.member-avatar>b{font:600 7px var(--mono)!important}
|
||||
.member-avatar img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
|
||||
.members .member-profile b{overflow-wrap:anywhere}
|
||||
.members .takeover-toggle{grid-column:2;margin-left:39px}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user