Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 37 additions & 6 deletions web/pages/Character/ImpersonateModal.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { Component } from 'solid-js'
import { Component, createMemo, Show } from 'solid-js'
import Modal from '/web/shared/Modal'
import { characterStore, userStore } from '/web/store'
import CharacterSelectList from '/web/shared/CharacterSelectList'
import { AppSchema } from '/common/types'
import Button from '/web/shared/Button'
import { rootModalStore } from '/web/store/root-modal'
import PageHeader from '/web/shared/PageHeader'
import { CharacterAvatar } from '/web/shared/AvatarIcon'
import { Star, X } from 'lucide-solid'

const ImpersonateModal: Component<{ show: boolean; close: () => void }> = (props) => {
const chars = characterStore((s) => s.characters)
const charStore = characterStore()
const chars = createMemo(() => charStore.characters)
const impersonating = createMemo(() => charStore.impersonating)
const user = userStore()

const onSelect = (char?: AppSchema.Character) => {
Expand All @@ -26,11 +30,38 @@ const ImpersonateModal: Component<{ show: boolean; close: () => void }> = (props
Instead of updating your profile to speak as somebody else, you can <b>impersonate</b> a
character.
</span>
<div class="flex w-full justify-center">
<Button onClick={() => onSelect()}>Use My Profile</Button>
</div>
<Show when={impersonating()}>
<div class="flex w-full justify-center gap-2 p-2">
<div class="bg-700 flex w-full cursor-pointer flex-row items-center justify-between gap-4 rounded-xl px-2 py-1">
<div class="ellipsis flex items-center gap-4">
<CharacterAvatar
char={impersonating()!}
format={{ size: 'sm', corners: 'circle' }}
/>

<div class="ellipsis flex w-full flex-col">
<div class="ellipsis font-bold">{impersonating()!.name}</div>
<div class="ellipsis">{impersonating()!.description}</div>
</div>
</div>
<div>
<div class="hidden flex-row items-center justify-center gap-2 sm:flex">
<Show when={impersonating()?.favorite}>
<Star class="icon-button fill-[var(--text-900)] text-[var(--text-900)]" />
</Show>
</div>
</div>
</div>

<Button ariaLabel="Stop impersonating" onClick={() => onSelect()}>
<X />
</Button>
</div>
</Show>
<CharacterSelectList
items={chars.list.filter((ch) => ch.userId === user.user?._id)}
items={chars().list.filter(
(ch) => impersonating()?._id !== ch._id && ch.userId === user.user?._id
)}
onSelect={onSelect}
/>
</div>
Expand Down
12 changes: 1 addition & 11 deletions web/pages/Chat/ChatFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { Component, createMemo, createSignal, For, Show } from 'solid-js'
import { VenetianMask } from 'lucide-solid'
import Button from '../../shared/Button'
import { CharacterPill } from '../../shared/CharacterPill'
import { characterStore, chatStore, settingStore, userStore } from '../../store'
import { characterStore, chatStore, userStore } from '../../store'
import { msgStore } from '../../store'
import InputBar from './components/InputBar'
import { ContextState } from '/web/store/context'
Expand Down Expand Up @@ -61,14 +59,6 @@ export const ChatFooter: Component<{
msgs.waiting ? 'opacity-70 saturate-0' : ''
}`}
>
<Button
size="md"
schema="bordered"
onClick={() => settingStore.toggleImpersonate(true)}
classList={{ 'impersonate-btn': true }}
>
<VenetianMask size={16} />
</Button>
<For each={props.pills}>
{(bot) => (
<CharacterPill
Expand Down
10 changes: 9 additions & 1 deletion web/pages/Chat/components/InputBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ const InputBar: Component<{
return `Send a message...`
})

const impersonationTitle = createMemo(() => {
if (chars.impersonating) return `Impersonating ${chars.impersonating.name}`
return `Open impersonation menu`
})

const [saveDraft, disposeSaveDraftDebounce] = createDebounce((text: string) => {
draft.update(text)
}, 50)
Expand Down Expand Up @@ -233,17 +238,20 @@ const InputBar: Component<{
</div>
</Show>

<div class="flex items-center sm:hidden">
<div class="flex items-center">
<a
href="#"
role="button"
aria-label="Open impersonation menu"
title={impersonationTitle()}
class="icon-button"
classList={{ 'text-[var(--text-700)]': !!chars.impersonating }}
onClick={() => settingStore.toggleImpersonate(true)}
>
<AvatarIcon
avatarUrl={chars.impersonating?.avatar || user.profile?.avatar}
format={{ corners: 'circle', size: 'sm' }}
active={!!chars.impersonating}
class="mr-2"
/>
</a>
Expand Down
5 changes: 3 additions & 2 deletions web/shared/AvatarIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Props = {
Icon?: (props: LucideProps) => JSX.Element
openable?: boolean
noBorder?: boolean
active?: boolean
}

type Format = {
Expand Down Expand Up @@ -138,7 +139,7 @@ const AvatarIcon: Component<Props> = (props) => {
[`avatar-${props.format.size}`]: true,
'avatar-circle': props.format.corners === 'circle',
'border-2': !props.noBorder,
'border-[var(--bg-800)]': !props.noBorder,
[`border-[${props.active ? 'var(--hl-500)' : 'var(--bg-800)'}]`]: !props.noBorder,
'border-0': props.noBorder,
visible: props.anonymize,
}}
Expand Down Expand Up @@ -175,7 +176,7 @@ const AvatarIcon: Component<Props> = (props) => {
}`}
classList={{
'border-2': !props.noBorder,
'border-[var(--bg-800)]': !props.noBorder,
[`border-[${props.active ? 'var(--hl-500)' : 'var(--bg-800)'}]`]: !props.noBorder,
'border-0': props.noBorder,
}}
aria-hidden="true"
Expand Down