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
7 changes: 6 additions & 1 deletion packages/jupyter-chat/src/components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
InputToolbarRegistry
} from './input';
import { JlThemeProvider } from './jl-theme-provider';
import { ChatMessages } from './messages';
import { ChatMessages, WriterComponent } from './messages';
import { AttachmentOpenerContext } from '../context';
import { IChatModel } from '../model';
import {
Expand All @@ -42,6 +42,7 @@ export function ChatBody(props: Chat.IChatBodyProps): JSX.Element {
inputToolbarRegistry={inputToolbarRegistry}
messageFooterRegistry={props.messageFooterRegistry}
welcomeMessage={props.welcomeMessage}
writerComponent={props.writerComponent}
/>
<ChatInput
sx={{
Expand Down Expand Up @@ -141,6 +142,10 @@ export namespace Chat {
* The welcome message.
*/
welcomeMessage?: string;
/**
* The typing notification widget.
*/
writerComponent?: WriterComponent;
}

/**
Expand Down
19 changes: 14 additions & 5 deletions packages/jupyter-chat/src/components/messages/messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ import { ChatMessageHeader } from './header';
import { ChatMessage } from './message';
import { Navigation } from './navigation';
import { WelcomeMessage } from './welcome';
import { WritingUsersList } from './writers';
import { WriterComponent, WritingUsersList } from './writers';
import { IInputToolbarRegistry } from '../input';
import { ScrollContainer } from '../scroll-container';
import { IChatCommandRegistry, IMessageFooterRegistry } from '../../registers';
import { IChatModel } from '../../model';
import { IChatMessage, IUser } from '../../types';
import { IChatMessage } from '../../types';

const MESSAGES_BOX_CLASS = 'jp-chat-messages-container';
const MESSAGE_CLASS = 'jp-chat-message';
Expand Down Expand Up @@ -53,6 +53,10 @@ export type BaseMessageProps = {
* The welcome message.
*/
welcomeMessage?: string;
/**
* The typing notification widget.
*/
writerComponent?: WriterComponent;
};

/**
Expand All @@ -62,7 +66,9 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
const { model } = props;
const [messages, setMessages] = useState<IChatMessage[]>(model.messages);
const refMsgBox = useRef<HTMLDivElement>(null);
const [currentWriters, setCurrentWriters] = useState<IUser[]>([]);
const [currentWriters, setCurrentWriters] = useState<IChatModel.IWriter[]>(
[]
);
const [allRendered, setAllRendered] = useState<boolean>(false);

// The list of message DOM and their rendered promises.
Expand Down Expand Up @@ -96,7 +102,7 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
}

function handleWritersChange(_: IChatModel, writers: IChatModel.IWriter[]) {
setCurrentWriters(writers.map(writer => writer.user));
setCurrentWriters([...writers]);
}

model.messagesUpdated.connect(handleChatEvents);
Expand Down Expand Up @@ -211,7 +217,10 @@ export function ChatMessages(props: BaseMessageProps): JSX.Element {
})}
</Box>
</ScrollContainer>
<WritingUsersList writers={currentWriters}></WritingUsersList>
<WritingUsersList
writers={currentWriters}
writerComponent={props.writerComponent}
></WritingUsersList>
<Navigation {...props} refMsgBox={refMsgBox} allRendered={allRendered} />
</>
);
Expand Down
144 changes: 140 additions & 4 deletions packages/jupyter-chat/src/components/messages/writers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ import { Box, Typography } from '@mui/material';
import React, { useMemo } from 'react';

import { Avatar } from '../avatar';
import { IChatModel } from '../../model';
import { ISignal, Signal } from '@lumino/signaling';
import { IUser } from '../../types';

const WRITERS_CLASS = 'jp-chat-writers';
const WRITERS_ROW_CLASS = 'jp-chat-writers-row';

const DEFAULT_TEXT = 'is writing';

/**
* The writers component props.
Expand All @@ -18,7 +23,11 @@ type writersProps = {
/**
* The list of users currently writing.
*/
writers: IUser[];
writers: IChatModel.IWriter[];
/**
* The component to render next to the writers.
*/
writerComponent?: WriterComponent;
};

/**
Expand All @@ -36,14 +45,53 @@ const TypingIndicator = (): JSX.Element => (
* The writers component, displaying the current writers.
*/
export function WritingUsersList(props: writersProps): JSX.Element | null {
const { writers, writerComponent } = props;

// Don't render if no writers
if (writers.length === 0) {
return null;
}

// Default rendering for users without custom typing indicator and if there is no
// component to add to the writing notification.
const defaultWriters = writerComponent?.component
? []
: writers.filter(writer => !writer.typingIndicator);
const defaultWritersComponent = defaultWritingUsers({
writers: defaultWriters.map(writer => writer.user)
});

// Custom rendering for users with custom typing indicator or if there is a component
// to add to the writing notification.
const customWriters = writerComponent?.component
? writers
: writers.filter(writer => writer.typingIndicator);
const customWritersComponent = customWritingUser({
writers: customWriters,
writerComponent: writerComponent?.component
});

return (
<Box className={WRITERS_CLASS}>
{defaultWritersComponent !== null && defaultWritersComponent}
{customWritersComponent !== null && customWritersComponent}
</Box>
);
}

/**
* The default rendering of writing users, all in a row.
* This renderer is used if there is no custom component and no custom typing indicator.
*/
function defaultWritingUsers(props: { writers: IUser[] }): JSX.Element | null {
const { writers } = props;

// Don't render if no writers
if (writers.length === 0) {
return null;
}

const writersText = writers.length > 1 ? ' are writing' : ' is writing';
const writersText = writers.length > 1 ? 'are writing' : DEFAULT_TEXT;

const writingUsers: JSX.Element[] = useMemo(
() =>
Expand All @@ -66,16 +114,104 @@ export function WritingUsersList(props: writersProps): JSX.Element | null {
);

return (
<Box className={`${WRITERS_CLASS}`}>
<Box className={`${WRITERS_ROW_CLASS}`}>
<Box className="jp-chat-writers-content">
{writingUsers}
<Box className="jp-chat-writing-status">
<Typography variant="body2" className="jp-chat-writing-text">
{writersText}
{` ${writersText}`}
</Typography>
<TypingIndicator />
</Box>
</Box>
</Box>
);
}

/**
* The custom rendering of writing users, one per row.
* This renderer is used if there is a custom component or a custom typing indicator.
*/
function customWritingUser(props: {
writers: IChatModel.IWriter[];
writerComponent?: React.FC<WriterComponentProps>;
}): JSX.Element | null {
const { writers } = props;

// Don't render if no writers
if (writers.length === 0) {
return null;
}

const writingUsers: JSX.Element[] = writers.map(writer => {
const username =
writer.user.display_name ??
writer.user.name ??
(writer.user.username || 'User undefined');

const writerText = writer.typingIndicator ?? DEFAULT_TEXT;
return (
<Box key={writer.user.username} className="jp-chat-writer-item">
<Avatar user={writer.user} small />
<Typography variant="body2" className="jp-chat-writer-name">
{username}
</Typography>
<Box className="jp-chat-writing-status">
<Typography variant="body2" className="jp-chat-writing-text">
{` ${writerText}`}
</Typography>
<TypingIndicator />
</Box>
{props.writerComponent && <props.writerComponent writer={writer} />}
</Box>
);
});

return (
<>
{writingUsers.map(writingUser => (
<Box className={`${WRITERS_ROW_CLASS}`}>{writingUser}</Box>
))}
</>
);
}

export type WriterComponentProps = {
/**
* The writer associated to this component.
*/
writer: IChatModel.IWriter;
};

/**
* The writer component class containing a react component to display with the user
* writing notification.
*/
export class WriterComponent {
/**
* The react component.
*/
get component(): React.FC<WriterComponentProps> | undefined {
return this._component;
}
set component(value: React.FC<WriterComponentProps> | undefined) {
this._component = value;
this._changed.emit(this._component);
}

/**
* Emitting when the component changed.
*/
get changed(): ISignal<
WriterComponent,
React.FC<WriterComponentProps> | undefined
> {
return this._changed;
}

private _component: React.FC<WriterComponentProps> | undefined;
private _changed = new Signal<
WriterComponent,
React.FC<WriterComponentProps> | undefined
>(this);
}
6 changes: 5 additions & 1 deletion packages/jupyter-chat/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -791,9 +791,13 @@ export namespace IChatModel {
*/
user: IUser;
/**
* The message ID (optional)
* The message ID (optional).
*/
messageID?: string;
/**
* The writer typing indicator (optional)
*/
typingIndicator?: string;
}
}

Expand Down
8 changes: 6 additions & 2 deletions packages/jupyter-chat/src/widgets/multichat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ import { ChatWidget } from './chat-widget';
import {
Chat,
IInputToolbarRegistry,
IInputToolbarRegistryFactory
IInputToolbarRegistryFactory,
WriterComponent
} from '../components';
import { chatIcon, readIcon } from '../icons';
import { IChatModel } from '../model';
Expand Down Expand Up @@ -65,6 +66,7 @@ export class MultiChatPanel extends SidePanel {
this._inputToolbarFactory = options.inputToolbarFactory;
this._messageFooterRegistry = options.messageFooterRegistry;
this._welcomeMessage = options.welcomeMessage;
this._writerComponent = options.writerComponent;

this._getChatNames = options.getChatNames;
this._createModel = options.createModel;
Expand Down Expand Up @@ -155,7 +157,8 @@ export class MultiChatPanel extends SidePanel {
attachmentOpenerRegistry: this._attachmentOpenerRegistry,
inputToolbarRegistry,
messageFooterRegistry: this._messageFooterRegistry,
welcomeMessage: this._welcomeMessage
welcomeMessage: this._welcomeMessage,
writerComponent: this._writerComponent
});

const section = new ChatSection({
Expand Down Expand Up @@ -273,6 +276,7 @@ export class MultiChatPanel extends SidePanel {
private _inputToolbarFactory?: IInputToolbarRegistryFactory;
private _messageFooterRegistry?: IMessageFooterRegistry;
private _welcomeMessage?: string;
private _writerComponent?: WriterComponent;
private _updateChatListDebouncer: Debouncer;

private _createModel?: (
Expand Down
9 changes: 7 additions & 2 deletions packages/jupyter-chat/style/chat.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,12 @@
margin-top: 0;
}

.jp-chat-writers {
.jp-chat-writer {
display: flex;
flex-direction: column;
}

.jp-chat-writers-row {
display: flex;
flex-wrap: wrap;
position: sticky;
Expand Down Expand Up @@ -160,7 +165,7 @@
}
}

.jp-chat-writers > div {
.jp-chat-writers-row > div {
display: flex;
align-items: center;
gap: 0.2em;
Expand Down
Loading
Loading