Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add optional configurable message footer #942

Merged
merged 2 commits into from
Aug 12, 2024
Merged
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
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
Loading