Skip to content

Commit

Permalink
Add optional configurable message footer (jupyterlab#942)
Browse files Browse the repository at this point in the history
* add configurable message footer token

* add custom footer docs
  • Loading branch information
dlqqq authored and Marchlak committed Oct 28, 2024
1 parent ba76f96 commit 481fe93
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 9 deletions.
47 changes: 47 additions & 0 deletions docs/source/developers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,3 +391,50 @@ custom = "custom_package:CustomChatHandler"

Then, install your package so that Jupyter AI adds custom chat handlers
to the existing chat handlers.

## Custom message footer

You can provide a custom message footer that will be rendered under each message
in the UI. To do so, you need to write or install a labextension containing a
plugin that provides the `IJaiMessageFooter` token. This plugin should return a
`IJaiMessageFooter` object, which defines the custom footer to be rendered.

The `IJaiMessageFooter` object contains a single property `component`, which
should reference a React component that defines the custom message footer.
Jupyter AI will render this component under each chat message, passing the
component a `message` prop with the definition of each chat message as an
object. The `message` prop takes the type `AiService.ChatMessage`, where
`AiService` is imported from `@jupyter-ai/core/handler`.

Here is a reference plugin that shows some custom text under each agent message:

```tsx
import React from 'react';
import {
JupyterFrontEnd,
JupyterFrontEndPlugin
} from '@jupyterlab/application';
import { IJaiMessageFooter, IJaiMessageFooterProps } from '@jupyter-ai/core/tokens';

export const footerPlugin: JupyterFrontEndPlugin<IJaiMessageFooter> = {
id: '@your-org/your-package:custom-footer',
autoStart: true,
requires: [],
provides: IJaiMessageFooter,
activate: (app: JupyterFrontEnd): IJaiMessageFooter => {
return {
component: MessageFooter
};
}
};

function MessageFooter(props: IJaiMessageFooterProps) {
if (props.message.type !== 'agent' && props.message.type !== 'agent-stream') {
return null;
}

return (
<div>This is a test footer that renders under each agent message.</div>
);
}
```
5 changes: 5 additions & 0 deletions packages/jupyter-ai/src/components/chat-messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import { AiService } from '../handler';
import { RendermimeMarkdown } from './rendermime-markdown';
import { useCollaboratorsContext } from '../contexts/collaborators-context';
import { ChatMessageMenu } from './chat-messages/chat-message-menu';
import { IJaiMessageFooter } from '../tokens';

type ChatMessagesProps = {
rmRegistry: IRenderMimeRegistry;
messages: AiService.ChatMessage[];
messageFooter: IJaiMessageFooter | null;
};

type ChatMessageHeaderProps = {
Expand Down Expand Up @@ -215,6 +217,9 @@ export function ChatMessages(props: ChatMessagesProps): JSX.Element {
message.type === 'agent-stream' ? !!message.complete : true
}
/>
{props.messageFooter && (
<props.messageFooter.component message={message} />
)}
</Box>
);
})}
Expand Down
14 changes: 11 additions & 3 deletions packages/jupyter-ai/src/components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { SelectionContextProvider } from '../contexts/selection-context';
import { SelectionWatcher } from '../selection-watcher';
import { ChatHandler } from '../chat_handler';
import { CollaboratorsContextProvider } from '../contexts/collaborators-context';
import { IJaiCompletionProvider } from '../tokens';
import { IJaiCompletionProvider, IJaiMessageFooter } from '../tokens';
import {
ActiveCellContextProvider,
ActiveCellManager
Expand All @@ -30,6 +30,7 @@ type ChatBodyProps = {
setChatView: (view: ChatView) => void;
rmRegistry: IRenderMimeRegistry;
focusInputSignal: ISignal<unknown, void>;
messageFooter: IJaiMessageFooter | null;
};

/**
Expand All @@ -51,7 +52,8 @@ function ChatBody({
chatHandler,
focusInputSignal,
setChatView: chatViewHandler,
rmRegistry: renderMimeRegistry
rmRegistry: renderMimeRegistry,
messageFooter
}: ChatBodyProps): JSX.Element {
const [messages, setMessages] = useState<AiService.ChatMessage[]>([
...chatHandler.history.messages
Expand Down Expand Up @@ -139,7 +141,11 @@ function ChatBody({
return (
<>
<ScrollContainer sx={{ flexGrow: 1 }}>
<ChatMessages messages={messages} rmRegistry={renderMimeRegistry} />
<ChatMessages
messages={messages}
rmRegistry={renderMimeRegistry}
messageFooter={messageFooter}
/>
<PendingMessages messages={pendingMessages} />
</ScrollContainer>
<ChatInput
Expand Down Expand Up @@ -170,6 +176,7 @@ export type ChatProps = {
openInlineCompleterSettings: () => void;
activeCellManager: ActiveCellManager;
focusInputSignal: ISignal<unknown, void>;
messageFooter: IJaiMessageFooter | null;
};

enum ChatView {
Expand Down Expand Up @@ -223,6 +230,7 @@ export function Chat(props: ChatProps): JSX.Element {
setChatView={setView}
rmRegistry={props.rmRegistry}
focusInputSignal={props.focusInputSignal}
messageFooter={props.messageFooter}
/>
)}
{view === ChatView.Settings && (
Expand Down
11 changes: 7 additions & 4 deletions packages/jupyter-ai/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { ChatHandler } from './chat_handler';
import { buildErrorWidget } from './widgets/chat-error';
import { completionPlugin } from './completions';
import { statusItemPlugin } from './status';
import { IJaiCompletionProvider } from './tokens';
import { IJaiCompletionProvider, IJaiMessageFooter } from './tokens';
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import { ActiveCellManager } from './contexts/active-cell-context';
import { Signal } from '@lumino/signaling';
Expand All @@ -42,7 +42,8 @@ const plugin: JupyterFrontEndPlugin<void> = {
IGlobalAwareness,
ILayoutRestorer,
IThemeManager,
IJaiCompletionProvider
IJaiCompletionProvider,
IJaiMessageFooter
],
requires: [IRenderMimeRegistry],
activate: async (
Expand All @@ -51,7 +52,8 @@ const plugin: JupyterFrontEndPlugin<void> = {
globalAwareness: Awareness | null,
restorer: ILayoutRestorer | null,
themeManager: IThemeManager | null,
completionProvider: IJaiCompletionProvider | null
completionProvider: IJaiCompletionProvider | null,
messageFooter: IJaiMessageFooter | null
) => {
/**
* Initialize selection watcher singleton
Expand Down Expand Up @@ -88,7 +90,8 @@ const plugin: JupyterFrontEndPlugin<void> = {
completionProvider,
openInlineCompleterSettings,
activeCellManager,
focusInputSignal
focusInputSignal,
messageFooter
);
} catch (e) {
chatWidget = buildErrorWidget(themeManager);
Expand Down
20 changes: 20 additions & 0 deletions packages/jupyter-ai/src/tokens.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from 'react';
import { Token } from '@lumino/coreutils';
import { ISignal } from '@lumino/signaling';
import type { IRankedMenu } from '@jupyterlab/ui-components';
import { AiService } from './handler';

export interface IJaiStatusItem {
addItem(item: IRankedMenu.IItemOptions): void;
Expand All @@ -26,3 +28,21 @@ export const IJaiCompletionProvider = new Token<IJaiCompletionProvider>(
'jupyter_ai:IJaiCompletionProvider',
'The jupyter-ai inline completion provider API'
);

export type IJaiMessageFooterProps = {
message: AiService.ChatMessage;
};

export interface IJaiMessageFooter {
component: React.FC<IJaiMessageFooterProps>;
}

/**
* The message footer provider token. Another extension should provide this
* token to add a footer to each message.
*/

export const IJaiMessageFooter = new Token<IJaiMessageFooter>(
'jupyter_ai:IJaiMessageFooter',
'Optional component that is used to render a footer on each Jupyter AI chat message, when provided.'
);
6 changes: 4 additions & 2 deletions packages/jupyter-ai/src/widgets/chat-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Chat } from '../components/chat';
import { chatIcon } from '../icons';
import { SelectionWatcher } from '../selection-watcher';
import { ChatHandler } from '../chat_handler';
import { IJaiCompletionProvider } from '../tokens';
import { IJaiCompletionProvider, IJaiMessageFooter } from '../tokens';
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
import type { ActiveCellManager } from '../contexts/active-cell-context';

Expand All @@ -21,7 +21,8 @@ export function buildChatSidebar(
completionProvider: IJaiCompletionProvider | null,
openInlineCompleterSettings: () => void,
activeCellManager: ActiveCellManager,
focusInputSignal: ISignal<unknown, void>
focusInputSignal: ISignal<unknown, void>,
messageFooter: IJaiMessageFooter | null
): ReactWidget {
const ChatWidget = ReactWidget.create(
<Chat
Expand All @@ -34,6 +35,7 @@ export function buildChatSidebar(
openInlineCompleterSettings={openInlineCompleterSettings}
activeCellManager={activeCellManager}
focusInputSignal={focusInputSignal}
messageFooter={messageFooter}
/>
);
ChatWidget.id = 'jupyter-ai::chat';
Expand Down

0 comments on commit 481fe93

Please sign in to comment.