2024-05-12 05:08:53 +00:00
|
|
|
import { Avatar, IconButton, ImageList, ImageListItem, ListItem, ListItemAvatar, ListItemText, Typography } from '@mui/material';
|
|
|
|
import React, { Fragment, MutableRefObject } from 'react';
|
2024-05-05 18:54:39 +00:00
|
|
|
|
2024-05-12 20:47:18 +00:00
|
|
|
import { Maybe, doesExist } from '@apextoaster/js-utils';
|
2024-05-14 01:07:03 +00:00
|
|
|
import { Camera, Settings } from '@mui/icons-material';
|
2024-05-12 20:47:18 +00:00
|
|
|
import { useStore } from 'zustand';
|
2024-05-05 18:54:39 +00:00
|
|
|
import { formatters } from './format.js';
|
2024-05-14 01:07:03 +00:00
|
|
|
import { Actor } from './models.js';
|
2024-05-12 20:47:18 +00:00
|
|
|
import { StoreState, store } from './store.js';
|
2024-05-12 05:08:53 +00:00
|
|
|
|
|
|
|
export function openImage(image: string) {
|
|
|
|
const byteCharacters = atob(image);
|
|
|
|
const byteNumbers = new Array(byteCharacters.length);
|
|
|
|
for (let i = 0; i < byteCharacters.length; i++) {
|
|
|
|
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
|
|
}
|
|
|
|
const byteArray = new Uint8Array(byteNumbers);
|
|
|
|
const file = new Blob([byteArray], { type: 'image/jpeg;base64' });
|
|
|
|
const fileURL = URL.createObjectURL(file);
|
|
|
|
window.open(fileURL, '_blank');
|
|
|
|
}
|
2024-05-05 18:54:39 +00:00
|
|
|
|
|
|
|
export interface EventItemProps {
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
event: any;
|
2024-05-06 01:17:00 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
focusRef?: MutableRefObject<any>;
|
2024-05-12 05:08:53 +00:00
|
|
|
|
2024-05-12 20:47:18 +00:00
|
|
|
renderEntity: (type: string, entity: string) => void;
|
|
|
|
renderEvent: (event: string) => void;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function characterSelector(state: StoreState) {
|
|
|
|
return {
|
|
|
|
character: state.character,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export function sameCharacter(a: Maybe<Actor>, b: Maybe<Actor>): boolean {
|
|
|
|
if (doesExist(a) && doesExist(b)) {
|
|
|
|
return a.name === b.name;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
2024-05-05 18:54:39 +00:00
|
|
|
}
|
|
|
|
|
2024-05-10 04:45:10 +00:00
|
|
|
export function ActionEventItem(props: EventItemProps) {
|
2024-05-12 05:08:53 +00:00
|
|
|
const { event, renderEvent } = props;
|
|
|
|
const { id, actor, room, type } = event;
|
2024-05-05 18:54:39 +00:00
|
|
|
const content = formatters[type](event);
|
|
|
|
|
2024-05-12 20:47:18 +00:00
|
|
|
const state = useStore(store, characterSelector);
|
|
|
|
const { character } = state;
|
|
|
|
|
|
|
|
const playerAction = sameCharacter(actor, character);
|
|
|
|
const typographyProps = {
|
|
|
|
color: playerAction ? 'success.text' : 'primary.text',
|
|
|
|
};
|
|
|
|
|
2024-05-12 05:08:53 +00:00
|
|
|
return <ListItem
|
|
|
|
alignItems="flex-start"
|
|
|
|
ref={props.focusRef}
|
|
|
|
secondaryAction={
|
|
|
|
<IconButton edge="end" aria-label="render" onClick={() => renderEvent(id)}>
|
|
|
|
<Camera />
|
|
|
|
</IconButton>
|
|
|
|
}
|
|
|
|
>
|
2024-05-05 18:54:39 +00:00
|
|
|
<ListItemAvatar>
|
2024-05-14 01:07:03 +00:00
|
|
|
<Avatar>{room.name.substring(0, 1)}</Avatar>
|
2024-05-05 18:54:39 +00:00
|
|
|
</ListItemAvatar>
|
|
|
|
<ListItemText
|
2024-05-10 04:45:10 +00:00
|
|
|
primary={room.name}
|
2024-05-12 20:47:18 +00:00
|
|
|
primaryTypographyProps={typographyProps}
|
|
|
|
secondaryTypographyProps={typographyProps}
|
2024-05-05 18:54:39 +00:00
|
|
|
secondary={
|
|
|
|
<React.Fragment>
|
|
|
|
<Typography
|
|
|
|
sx={{ display: 'block' }}
|
|
|
|
component="span"
|
|
|
|
variant="body2"
|
|
|
|
color="text.primary"
|
|
|
|
>
|
2024-05-10 04:45:10 +00:00
|
|
|
{actor.name}
|
2024-05-05 18:54:39 +00:00
|
|
|
</Typography>
|
|
|
|
{content}
|
|
|
|
</React.Fragment>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
</ListItem>;
|
|
|
|
}
|
|
|
|
|
2024-05-10 04:45:10 +00:00
|
|
|
export function SnapshotEventItem(props: EventItemProps) {
|
2024-05-05 18:54:39 +00:00
|
|
|
const { event } = props;
|
|
|
|
const { step, world } = event;
|
2024-05-12 05:08:53 +00:00
|
|
|
const { name, theme } = world;
|
2024-05-05 18:54:39 +00:00
|
|
|
|
2024-05-06 01:17:00 +00:00
|
|
|
return <ListItem alignItems="flex-start" ref={props.focusRef}>
|
2024-05-05 18:54:39 +00:00
|
|
|
<ListItemAvatar>
|
2024-05-14 01:07:03 +00:00
|
|
|
<Avatar>{step}</Avatar>
|
2024-05-05 18:54:39 +00:00
|
|
|
</ListItemAvatar>
|
|
|
|
<ListItemText
|
2024-05-12 05:08:53 +00:00
|
|
|
primary={name}
|
2024-05-05 18:54:39 +00:00
|
|
|
secondary={
|
2024-05-12 05:08:53 +00:00
|
|
|
<Fragment>
|
|
|
|
<Typography
|
|
|
|
sx={{ display: 'block' }}
|
|
|
|
component="span"
|
|
|
|
variant="body2"
|
|
|
|
color="text.primary"
|
|
|
|
>
|
|
|
|
Step: {step}
|
|
|
|
</Typography>
|
|
|
|
World Theme: {theme}
|
|
|
|
</Fragment>
|
2024-05-05 18:54:39 +00:00
|
|
|
}
|
|
|
|
/>
|
|
|
|
</ListItem>;
|
|
|
|
}
|
|
|
|
|
2024-05-10 04:45:10 +00:00
|
|
|
export function ReplyEventItem(props: EventItemProps) {
|
2024-05-05 18:54:39 +00:00
|
|
|
const { event } = props;
|
2024-05-10 04:45:10 +00:00
|
|
|
const { text } = event;
|
2024-05-05 18:54:39 +00:00
|
|
|
|
2024-05-06 01:17:00 +00:00
|
|
|
return <ListItem alignItems="flex-start" ref={props.focusRef}>
|
2024-05-05 18:54:39 +00:00
|
|
|
<ListItemAvatar>
|
2024-05-14 01:07:03 +00:00
|
|
|
<Avatar alt="System">
|
|
|
|
<Settings />
|
|
|
|
</Avatar>
|
2024-05-05 18:54:39 +00:00
|
|
|
</ListItemAvatar>
|
|
|
|
<ListItemText
|
|
|
|
primary="System"
|
|
|
|
secondary={
|
|
|
|
<Typography
|
|
|
|
sx={{ display: 'block' }}
|
|
|
|
component="span"
|
|
|
|
variant="body2"
|
|
|
|
color="text.primary"
|
|
|
|
>
|
2024-05-10 04:45:10 +00:00
|
|
|
{text}
|
2024-05-05 18:54:39 +00:00
|
|
|
</Typography>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
</ListItem>;
|
|
|
|
}
|
|
|
|
|
2024-05-10 04:45:10 +00:00
|
|
|
export function PlayerEventItem(props: EventItemProps) {
|
2024-05-05 18:54:39 +00:00
|
|
|
const { event } = props;
|
2024-05-10 04:45:10 +00:00
|
|
|
const { character, status, client } = event;
|
2024-05-06 01:17:00 +00:00
|
|
|
|
|
|
|
let primary = '';
|
|
|
|
let secondary = '';
|
2024-05-10 04:45:10 +00:00
|
|
|
if (status === 'join') {
|
2024-05-11 22:38:07 +00:00
|
|
|
primary = 'Player Joined';
|
2024-05-10 04:45:10 +00:00
|
|
|
secondary = `${client} is now playing as ${character}`;
|
2024-05-06 01:17:00 +00:00
|
|
|
}
|
2024-05-10 04:45:10 +00:00
|
|
|
if (status === 'leave') {
|
2024-05-06 01:17:00 +00:00
|
|
|
primary = 'Player Left';
|
2024-05-10 04:45:10 +00:00
|
|
|
secondary = `${client} has left the game. ${character} is now controlled by an LLM`;
|
2024-05-06 01:17:00 +00:00
|
|
|
}
|
2024-05-05 18:54:39 +00:00
|
|
|
|
2024-05-12 05:08:53 +00:00
|
|
|
return <ListItem
|
|
|
|
alignItems="flex-start"
|
|
|
|
ref={props.focusRef}
|
|
|
|
>
|
2024-05-05 18:54:39 +00:00
|
|
|
<ListItemAvatar>
|
2024-05-14 01:07:03 +00:00
|
|
|
<Avatar>{character.substring(0, 1)}</Avatar>
|
2024-05-05 18:54:39 +00:00
|
|
|
</ListItemAvatar>
|
|
|
|
<ListItemText
|
2024-05-06 01:17:00 +00:00
|
|
|
primary={primary}
|
2024-05-05 18:54:39 +00:00
|
|
|
secondary={
|
|
|
|
<Typography
|
|
|
|
sx={{ display: 'block' }}
|
|
|
|
component="span"
|
|
|
|
variant="body2"
|
|
|
|
color="text.primary"
|
|
|
|
>
|
2024-05-06 01:17:00 +00:00
|
|
|
{secondary}
|
2024-05-05 18:54:39 +00:00
|
|
|
</Typography>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
</ListItem>;
|
|
|
|
}
|
|
|
|
|
2024-05-12 05:08:53 +00:00
|
|
|
export function RenderEventItem(props: EventItemProps) {
|
|
|
|
const { event } = props;
|
2024-05-19 20:51:58 +00:00
|
|
|
const { images, prompt, title = 'Render' } = event;
|
2024-05-12 05:08:53 +00:00
|
|
|
|
|
|
|
return <ListItem alignItems="flex-start" ref={props.focusRef}>
|
2024-05-16 04:12:06 +00:00
|
|
|
<ListItemAvatar>
|
|
|
|
<Avatar alt="Render">
|
|
|
|
<Camera />
|
|
|
|
</Avatar>
|
|
|
|
</ListItemAvatar>
|
2024-05-19 20:51:58 +00:00
|
|
|
<ListItemText
|
|
|
|
primary={title}
|
|
|
|
secondary={
|
|
|
|
<Typography
|
|
|
|
sx={{ display: 'block' }}
|
|
|
|
component="span"
|
|
|
|
variant="body2"
|
|
|
|
color="text.primary"
|
|
|
|
>{prompt}</Typography>
|
|
|
|
}
|
|
|
|
/>
|
2024-05-12 20:47:18 +00:00
|
|
|
<ImageList cols={3} rowHeight={256}>
|
|
|
|
{Object.entries(images).map(([name, image]) => <ImageListItem key={name}>
|
|
|
|
<a href='#' onClick={() => openImage(image as string)}>
|
|
|
|
<img src={`data:image/jpeg;base64,${image}`} alt="Render" style={{ maxHeight: 256, maxWidth: 256 }} />
|
|
|
|
</a>
|
|
|
|
</ImageListItem>)}
|
|
|
|
</ImageList>
|
|
|
|
</ListItem>;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function PromptEventItem(props: EventItemProps) {
|
|
|
|
const { event } = props;
|
|
|
|
const { character, prompt } = event;
|
|
|
|
|
|
|
|
const state = useStore(store, characterSelector);
|
|
|
|
const { character: playerCharacter } = state;
|
|
|
|
|
|
|
|
const playerPrompt = sameCharacter(playerCharacter, character);
|
|
|
|
const typographyProps = {
|
|
|
|
color: playerPrompt ? 'success.text' : 'primary.text',
|
|
|
|
};
|
|
|
|
|
|
|
|
return <ListItem alignItems="flex-start" ref={props.focusRef}>
|
|
|
|
<ListItemAvatar>
|
2024-05-14 01:07:03 +00:00
|
|
|
<Avatar>{character.substring(0, 1)}</Avatar>
|
2024-05-12 20:47:18 +00:00
|
|
|
</ListItemAvatar>
|
|
|
|
<ListItemText
|
|
|
|
primary="Prompt"
|
|
|
|
primaryTypographyProps={typographyProps}
|
|
|
|
secondaryTypographyProps={typographyProps}
|
|
|
|
secondary={
|
|
|
|
<Typography
|
|
|
|
sx={{ display: 'block' }}
|
|
|
|
component="span"
|
|
|
|
variant="body2"
|
|
|
|
color="text.primary"
|
|
|
|
>
|
|
|
|
Prompt for {character}: {prompt}
|
|
|
|
</Typography>
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
</ListItem>;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function GenerateEventItem(props: EventItemProps) {
|
|
|
|
const { event, renderEntity } = props;
|
|
|
|
const { entity, name } = event;
|
|
|
|
|
2024-05-19 04:30:40 +00:00
|
|
|
let renderButton;
|
|
|
|
if (doesExist(entity)) {
|
|
|
|
renderButton = <IconButton edge="end" aria-label="render" onClick={() => renderEntity(entity.type, entity.name)}>
|
|
|
|
<Camera />
|
|
|
|
</IconButton>;
|
|
|
|
}
|
|
|
|
|
2024-05-12 20:47:18 +00:00
|
|
|
return <ListItem
|
|
|
|
alignItems="flex-start"
|
|
|
|
ref={props.focusRef}
|
2024-05-19 04:30:40 +00:00
|
|
|
secondaryAction={renderButton}
|
2024-05-12 20:47:18 +00:00
|
|
|
>
|
|
|
|
<ListItemAvatar>
|
2024-05-14 01:07:03 +00:00
|
|
|
<Avatar>{name.substring(0, 1)}</Avatar>
|
2024-05-12 20:47:18 +00:00
|
|
|
</ListItemAvatar>
|
2024-05-12 05:08:53 +00:00
|
|
|
<ListItemText
|
2024-05-12 20:47:18 +00:00
|
|
|
primary="Generate"
|
|
|
|
secondary={
|
|
|
|
<Typography
|
|
|
|
sx={{ display: 'block' }}
|
|
|
|
component="span"
|
|
|
|
variant="body2"
|
|
|
|
color="text.primary"
|
|
|
|
>
|
|
|
|
{name}
|
|
|
|
</Typography>
|
|
|
|
}
|
2024-05-12 05:08:53 +00:00
|
|
|
/>
|
|
|
|
</ListItem>;
|
|
|
|
}
|
|
|
|
|
2024-05-05 18:54:39 +00:00
|
|
|
export function EventItem(props: EventItemProps) {
|
|
|
|
const { event } = props;
|
|
|
|
const { type } = event;
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
case 'action':
|
|
|
|
case 'result':
|
2024-05-12 05:08:53 +00:00
|
|
|
return <ActionEventItem {...props} />;
|
2024-05-10 04:45:10 +00:00
|
|
|
case 'reply':
|
2024-05-12 05:08:53 +00:00
|
|
|
case 'status': // TODO: should have a different component
|
|
|
|
return <ReplyEventItem {...props} />;
|
2024-05-05 18:54:39 +00:00
|
|
|
case 'player':
|
2024-05-12 05:08:53 +00:00
|
|
|
return <PlayerEventItem {...props} />;
|
|
|
|
case 'render':
|
|
|
|
return <RenderEventItem {...props} />;
|
2024-05-10 04:45:10 +00:00
|
|
|
case 'snapshot':
|
2024-05-12 05:08:53 +00:00
|
|
|
return <SnapshotEventItem {...props} />;
|
2024-05-12 20:47:18 +00:00
|
|
|
case 'prompt':
|
|
|
|
return <PromptEventItem {...props} />;
|
|
|
|
case 'generate':
|
|
|
|
return <GenerateEventItem {...props} />;
|
2024-05-05 18:54:39 +00:00
|
|
|
default:
|
2024-05-06 01:17:00 +00:00
|
|
|
return <ListItem ref={props.focusRef}>
|
2024-05-05 18:54:39 +00:00
|
|
|
<ListItemText primary={`Unknown event type: ${type}`} />
|
|
|
|
</ListItem>;
|
|
|
|
}
|
|
|
|
}
|