diff --git a/packages/jupyter-ai/src/components/chat-input.tsx b/packages/jupyter-ai/src/components/chat-input.tsx deleted file mode 100644 index 1e19f7774..000000000 --- a/packages/jupyter-ai/src/components/chat-input.tsx +++ /dev/null @@ -1,403 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; - -import { - Autocomplete, - Box, - SxProps, - TextField, - Theme, - InputAdornment, - Typography -} from '@mui/material'; -import { - Download, - FindInPage, - Help, - MoreHoriz, - MenuBook, - School, - HideSource, - AutoFixNormal -} from '@mui/icons-material'; -import { ISignal } from '@lumino/signaling'; - -import { AiService } from '../handler'; -import { SendButton, SendButtonProps } from './chat-input/send-button'; -import { useActiveCellContext } from '../contexts/active-cell-context'; -import { ChatHandler } from '../chat_handler'; - -type ChatInputProps = { - chatHandler: ChatHandler; - focusInputSignal: ISignal; - sendWithShiftEnter: boolean; - sx?: SxProps; - /** - * Name of the persona, set by the selected chat model. This defaults to - * `'Jupyternaut'`, but can differ for custom providers. - */ - personaName: string; - /** - * Whether the backend is streaming a reply to any message sent by the current - * user. - */ - streamingReplyHere: boolean; -}; - -/** - * List of icons per slash command, shown in the autocomplete popup. - * - * This list of icons should eventually be made configurable. However, it is - * unclear whether custom icons should be defined within a Lumino plugin (in the - * frontend) or served from a static server route (in the backend). - */ -const DEFAULT_COMMAND_ICONS: Record = { - '/ask': , - '/clear': , - '/export': , - '/fix': , - '/generate': , - '/help': , - '/learn': , - '@file': , - unknown: -}; - -/** - * Renders an option shown in the slash command autocomplete. - */ -function renderAutocompleteOption( - optionProps: React.HTMLAttributes, - option: AiService.AutocompleteOption -): JSX.Element { - const icon = - option.id in DEFAULT_COMMAND_ICONS - ? DEFAULT_COMMAND_ICONS[option.id] - : DEFAULT_COMMAND_ICONS.unknown; - - return ( -
  • - {icon} - - - {option.label} - - - {' — ' + option.description} - - -
  • - ); -} - -export function ChatInput(props: ChatInputProps): JSX.Element { - const [input, setInput] = useState(''); - const [autocompleteOptions, setAutocompleteOptions] = useState< - AiService.AutocompleteOption[] - >([]); - const [autocompleteCommandOptions, setAutocompleteCommandOptions] = useState< - AiService.AutocompleteOption[] - >([]); - const [autocompleteArgOptions, setAutocompleteArgOptions] = useState< - AiService.AutocompleteOption[] - >([]); - const [currSlashCommand, setCurrSlashCommand] = useState(null); - const activeCell = useActiveCellContext(); - - /** - * Effect: fetch the list of available slash commands from the backend on - * initial mount to populate the slash command autocomplete. - */ - useEffect(() => { - async function getAutocompleteCommandOptions() { - const response = await AiService.listAutocompleteOptions(); - setAutocompleteCommandOptions(response.options); - } - getAutocompleteCommandOptions(); - }, []); - - useEffect(() => { - async function getAutocompleteArgOptions() { - let options: AiService.AutocompleteOption[] = []; - const lastWord = getLastWord(input); - if (lastWord.includes(':')) { - const id = lastWord.split(':', 1)[0]; - // get option that matches the command - const option = autocompleteCommandOptions.find( - option => option.id === id - ); - if (option) { - const response = await AiService.listAutocompleteArgOptions(lastWord); - options = response.options; - } - } - setAutocompleteArgOptions(options); - } - getAutocompleteArgOptions(); - }, [autocompleteCommandOptions, input]); - - // Combine the fixed options with the argument options - useEffect(() => { - if (autocompleteArgOptions.length > 0) { - setAutocompleteOptions(autocompleteArgOptions); - } else { - setAutocompleteOptions(autocompleteCommandOptions); - } - }, [autocompleteCommandOptions, autocompleteArgOptions]); - - // whether any option is highlighted in the autocomplete - const [highlighted, setHighlighted] = useState(false); - - // controls whether the autocomplete is open - const [open, setOpen] = useState(false); - - // store reference to the input element to enable focusing it easily - const inputRef = useRef(); - - /** - * Effect: connect the signal emitted on input focus request. - */ - useEffect(() => { - const focusInputElement = () => { - if (inputRef.current) { - inputRef.current.focus(); - } - }; - props.focusInputSignal.connect(focusInputElement); - return () => { - props.focusInputSignal.disconnect(focusInputElement); - }; - }, []); - - /** - * Effect: Open the autocomplete when the user types a slash into an empty - * chat input. Close the autocomplete when the user clears the chat input. - */ - useEffect(() => { - if (filterAutocompleteOptions(autocompleteOptions, input).length > 0) { - setOpen(true); - return; - } - - if (input === '') { - setOpen(false); - return; - } - }, [input]); - - /** - * Effect: Set current slash command - */ - useEffect(() => { - const matchedSlashCommand = input.match(/^\s*\/\w+/); - setCurrSlashCommand(matchedSlashCommand && matchedSlashCommand[0]); - }, [input]); - - /** - * Effect: ensure that the `highlighted` is never `true` when `open` is - * `false`. - * - * For context: https://github.com/jupyterlab/jupyter-ai/issues/849 - */ - useEffect(() => { - if (!open && highlighted) { - setHighlighted(false); - } - }, [open, highlighted]); - - function onSend(selection?: AiService.Selection) { - const prompt = input; - setInput(''); - - // if the current slash command is `/fix`, we always include a code cell - // with error output in the selection. - if (currSlashCommand === '/fix') { - const cellWithError = activeCell.manager.getContent(true); - if (!cellWithError) { - return; - } - - props.chatHandler.sendMessage({ - prompt, - selection: { ...cellWithError, type: 'cell-with-error' } - }); - return; - } - - // otherwise, send a ChatRequest with the prompt and selection - props.chatHandler.sendMessage({ prompt, selection }); - } - - const inputExists = !!input.trim(); - function handleKeyDown(event: React.KeyboardEvent) { - if (event.key !== 'Enter') { - return; - } - - // do not send the message if the user was just trying to select a suggested - // slash command from the Autocomplete component. - if (highlighted) { - return; - } - - if (!inputExists) { - event.stopPropagation(); - event.preventDefault(); - return; - } - - if ( - event.key === 'Enter' && - ((props.sendWithShiftEnter && event.shiftKey) || - (!props.sendWithShiftEnter && !event.shiftKey)) - ) { - onSend(); - event.stopPropagation(); - event.preventDefault(); - } - } - - // Set the helper text based on whether Shift+Enter is used for sending. - const helperText = props.sendWithShiftEnter ? ( - - Press Shift+Enter to send message - - ) : ( - - Press Shift+Enter to add a new line - - ); - - const sendButtonProps: SendButtonProps = { - onSend, - onStop: () => { - props.chatHandler.sendMessage({ - type: 'stop' - }); - }, - streamingReplyHere: props.streamingReplyHere, - sendWithShiftEnter: props.sendWithShiftEnter, - inputExists, - activeCellHasError: activeCell.hasError, - currSlashCommand - }; - - function filterAutocompleteOptions( - options: AiService.AutocompleteOption[], - inputValue: string - ): AiService.AutocompleteOption[] { - const lastWord = getLastWord(inputValue); - if (lastWord === '') { - return []; - } - const isStart = lastWord === inputValue; - return options.filter( - option => - option.label.startsWith(lastWord) && (!option.only_start || isStart) - ); - } - - return ( - - { - return filterAutocompleteOptions(options, inputValue); - }} - onChange={(_, option) => { - const value = typeof option === 'string' ? option : option.label; - let matchLength = 0; - for (let i = 1; i <= value.length; i++) { - if (input.endsWith(value.slice(0, i))) { - matchLength = i; - } - } - setInput(input + value.slice(matchLength)); - }} - onInputChange={(_, newValue: string) => { - setInput(newValue); - }} - onHighlightChange={ - /** - * On highlight change: set `highlighted` to whether an option is - * highlighted by the user. - */ - (_, highlightedOption) => { - setHighlighted(!!highlightedOption); - } - } - onClose={(_, reason) => { - if (reason !== 'selectOption' || input.endsWith(' ')) { - setOpen(false); - } - }} - // set this to an empty string to prevent the last selected slash - // command from being shown in blue - value="" - open={open} - options={autocompleteOptions} - // hide default extra right padding in the text field - disableClearable - // ensure the autocomplete popup always renders on top - componentsProps={{ - popper: { - placement: 'top' - }, - paper: { - sx: { - border: '1px solid lightgray' - } - } - }} - renderOption={renderAutocompleteOption} - ListboxProps={{ - sx: { - '& .MuiAutocomplete-option': { - padding: 2 - } - } - }} - renderInput={params => ( - - - - ) - }} - FormHelperTextProps={{ - sx: { marginLeft: 'auto', marginRight: 0 } - }} - helperText={input.length > 2 ? helperText : ' '} - /> - )} - /> - - ); -} - -function getLastWord(input: string): string { - return input.split(/(? unknown; - onStop: () => unknown; - sendWithShiftEnter: boolean; - currSlashCommand: string | null; - inputExists: boolean; - activeCellHasError: boolean; - /** - * Whether the backend is streaming a reply to any message sent by the current - * user. - */ - streamingReplyHere: boolean; -}; - -export function SendButton(props: SendButtonProps): JSX.Element { - const [menuAnchorEl, setMenuAnchorEl] = useState(null); - const [menuOpen, setMenuOpen] = useState(false); - const [textSelection] = useSelectionContext(); - const activeCell = useActiveCellContext(); - - const openMenu = useCallback((el: HTMLElement | null) => { - setMenuAnchorEl(el); - setMenuOpen(true); - }, []); - - const closeMenu = useCallback(() => { - setMenuOpen(false); - }, []); - - let action: 'send' | 'stop' | 'fix' = props.inputExists - ? 'send' - : props.streamingReplyHere - ? 'stop' - : 'send'; - if (props.currSlashCommand === '/fix') { - action = 'fix'; - } - - let disabled = false; - if (action === 'send' && !props.inputExists) { - disabled = true; - } - if (action === 'fix' && !props.activeCellHasError) { - disabled = true; - } - - const includeSelectionDisabled = !(activeCell.exists || textSelection); - - const includeSelectionTooltip = - action === 'fix' - ? FIX_TOOLTIP - : textSelection - ? `${textSelection.text.split('\n').length} lines selected` - : activeCell.exists - ? 'Code from 1 active cell' - : 'No selection or active cell'; - - const defaultTooltip = props.sendWithShiftEnter - ? 'Send message (SHIFT+ENTER)' - : 'Send message (ENTER)'; - - const tooltip = - action === 'fix' && !props.activeCellHasError - ? FIX_TOOLTIP - : action === 'stop' - ? 'Stop streaming' - : !props.inputExists - ? 'Message must not be empty' - : defaultTooltip; - - function sendWithSelection() { - // if the current slash command is `/fix`, `props.onSend()` should always - // include the code cell with error output, so the `selection` argument does - // not need to be defined. - if (action === 'fix') { - props.onSend(); - closeMenu(); - return; - } - - // otherwise, parse the text selection or active cell, with the text - // selection taking precedence. - if (textSelection?.text) { - props.onSend({ - type: 'text', - source: textSelection.text - }); - closeMenu(); - return; - } - - if (activeCell.exists) { - props.onSend({ - type: 'cell', - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - source: activeCell.manager.getContent(false)!.source - }); - closeMenu(); - return; - } - } - - return ( - - (action === 'stop' ? props.onStop() : props.onSend())} - disabled={disabled} - tooltip={tooltip} - buttonProps={{ - size: 'small', - title: defaultTooltip, - variant: 'contained' - }} - sx={{ - minWidth: 'unset', - borderRadius: '2px 0px 0px 2px' - }} - > - {action === 'stop' ? : } - - { - openMenu(e.currentTarget); - }} - disabled={disabled} - tooltip="" - buttonProps={{ - variant: 'contained', - onKeyDown: e => { - if (e.key !== 'Enter' && e.key !== ' ') { - return; - } - openMenu(e.currentTarget); - // stopping propagation of this event prevents the prompt from being - // sent when the dropdown button is selected and clicked via 'Enter'. - e.stopPropagation(); - } - }} - sx={{ - minWidth: 'unset', - padding: '4px 0px', - borderRadius: '0px 2px 2px 0px', - borderLeft: '1px solid white' - }} - > - - - - { - sendWithSelection(); - // prevent sending second message with no selection - e.stopPropagation(); - }} - disabled={includeSelectionDisabled} - > - - - Send message with selection - - {includeSelectionTooltip} - - - - - - ); -} diff --git a/packages/jupyter-ai/src/components/chat-messages.tsx b/packages/jupyter-ai/src/components/chat-messages.tsx deleted file mode 100644 index 5c4286f8f..000000000 --- a/packages/jupyter-ai/src/components/chat-messages.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import React, { useState, useEffect } from 'react'; - -import { Avatar, Box, Typography } from '@mui/material'; -import type { SxProps, Theme } from '@mui/material'; -import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; -import { ServerConnection } from '@jupyterlab/services'; -// TODO: delete jupyternaut from frontend package - -import { AiService } from '../handler'; -import { RendermimeMarkdown } from './rendermime-markdown'; -import { useCollaboratorsContext } from '../contexts/collaborators-context'; -import { ChatMessageMenu } from './chat-messages/chat-message-menu'; -import { ChatMessageDelete } from './chat-messages/chat-message-delete'; -import { ChatHandler } from '../chat_handler'; -import { IJaiMessageFooter } from '../tokens'; - -type ChatMessagesProps = { - rmRegistry: IRenderMimeRegistry; - messages: AiService.ChatMessage[]; - chatHandler: ChatHandler; - messageFooter: IJaiMessageFooter | null; -}; - -type ChatMessageHeaderProps = { - message: AiService.ChatMessage; - chatHandler: ChatHandler; - timestamp: string; - sx?: SxProps; -}; - -function sortMessages( - messages: AiService.ChatMessage[] -): AiService.ChatMessage[] { - const timestampsById: Record = {}; - for (const message of messages) { - timestampsById[message.id] = message.time; - } - - return [...messages].sort((a, b) => { - /** - * Use the *origin timestamp* as the primary sort key. This ensures that - * each agent reply is grouped with the human message that triggered it. - * - * - If the message is from an agent, the origin timestamp is the timestamp - * of the message it is replying to. - * - * - Otherwise, the origin timestamp is the *message timestamp*, i.e. - * `message.time` itself. - */ - - const aOriginTimestamp = - 'reply_to' in a && a.reply_to in timestampsById - ? timestampsById[a.reply_to] - : a.time; - const bOriginTimestamp = - 'reply_to' in b && b.reply_to in timestampsById - ? timestampsById[b.reply_to] - : b.time; - - /** - * Use the message timestamp as a secondary sort key. This ensures that each - * agent reply is shown after the human message that triggered it. - */ - const aMessageTimestamp = a.time; - const bMessageTimestamp = b.time; - - return ( - aOriginTimestamp - bOriginTimestamp || - aMessageTimestamp - bMessageTimestamp - ); - }); -} - -export function ChatMessageHeader(props: ChatMessageHeaderProps): JSX.Element { - const collaborators = useCollaboratorsContext(); - - const sharedStyles: SxProps = { - height: '24px', - width: '24px' - }; - - let avatar: JSX.Element; - if (props.message.type === 'human') { - const bgcolor = collaborators?.[props.message.client.username]?.color; - avatar = ( - - - {props.message.client.initials} - - - ); - } else { - const baseUrl = ServerConnection.makeSettings().baseUrl; - const avatar_url = baseUrl + props.message.persona.avatar_route; - avatar = ( - - - - ); - } - - const name = - props.message.type === 'human' - ? props.message.client.display_name - : props.message.persona.name; - - const shouldShowMenu = - props.message.type === 'agent' || - (props.message.type === 'agent-stream' && props.message.complete); - const shouldShowDelete = props.message.type === 'human'; - - return ( - :not(:last-child)': { - marginRight: 3 - }, - ...props.sx - }} - > - {avatar} - - - {name} - - - - {props.timestamp} - - {shouldShowMenu && ( - - )} - {shouldShowDelete && ( - - )} - - - - ); -} - -export function ChatMessages(props: ChatMessagesProps): JSX.Element { - const [timestamps, setTimestamps] = useState>({}); - const [sortedMessages, setSortedMessages] = useState( - [] - ); - - /** - * Effect: update cached timestamp strings upon receiving a new message. - */ - useEffect(() => { - const newTimestamps: Record = { ...timestamps }; - let timestampAdded = false; - - for (const message of props.messages) { - if (!(message.id in newTimestamps)) { - // Use the browser's default locale - newTimestamps[message.id] = new Date(message.time * 1000) // Convert message time to milliseconds - .toLocaleTimeString([], { - hour: 'numeric', // Avoid leading zero for hours; we don't want "03:15 PM" - minute: '2-digit' - }); - - timestampAdded = true; - } - } - if (timestampAdded) { - setTimestamps(newTimestamps); - } - }, [props.messages]); - - useEffect(() => { - setSortedMessages(sortMessages(props.messages)); - }, [props.messages]); - - return ( - :not(:last-child)': { - borderBottom: '1px solid var(--jp-border-color2)' - } - }} - > - {sortedMessages.map(message => { - return ( - - - - {props.messageFooter && ( - - )} - - ); - })} - - ); -} diff --git a/packages/jupyter-ai/src/components/chat-messages/chat-message-delete.tsx b/packages/jupyter-ai/src/components/chat-messages/chat-message-delete.tsx deleted file mode 100644 index d6fc691bd..000000000 --- a/packages/jupyter-ai/src/components/chat-messages/chat-message-delete.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import { SxProps } from '@mui/material'; -import { Close } from '@mui/icons-material'; - -import { AiService } from '../../handler'; -import { ChatHandler } from '../../chat_handler'; -import { TooltippedIconButton } from '../mui-extras/tooltipped-icon-button'; - -type DeleteButtonProps = { - message: AiService.ChatMessage; - chatHandler: ChatHandler; - sx?: SxProps; -}; - -export function ChatMessageDelete(props: DeleteButtonProps): JSX.Element { - const request: AiService.ClearRequest = { - type: 'clear', - target: props.message.id - }; - return ( - props.chatHandler.sendMessage(request)} - sx={props.sx} - tooltip="Delete this exchange" - > - - - ); -} - -export default ChatMessageDelete; diff --git a/packages/jupyter-ai/src/components/chat.tsx b/packages/jupyter-ai/src/components/chat.tsx deleted file mode 100644 index d55b1a5ce..000000000 --- a/packages/jupyter-ai/src/components/chat.tsx +++ /dev/null @@ -1,310 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Box } from '@mui/system'; -import { Button, IconButton, Stack } from '@mui/material'; -import SettingsIcon from '@mui/icons-material/Settings'; -import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import AddIcon from '@mui/icons-material/Add'; -import type { Awareness } from 'y-protocols/awareness'; -import type { IThemeManager } from '@jupyterlab/apputils'; -import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; -import type { User } from '@jupyterlab/services'; -import { ISignal } from '@lumino/signaling'; - -import { JlThemeProvider } from './jl-theme-provider'; -import { ChatMessages } from './chat-messages'; -import { PendingMessages } from './pending-messages'; -import { ChatInput } from './chat-input'; -import { ChatSettings } from './chat-settings'; -import { AiService } from '../handler'; -import { SelectionContextProvider } from '../contexts/selection-context'; -import { SelectionWatcher } from '../selection-watcher'; -import { ChatHandler } from '../chat_handler'; -import { CollaboratorsContextProvider } from '../contexts/collaborators-context'; -import { - IJaiCompletionProvider, - IJaiMessageFooter, - IJaiTelemetryHandler -} from '../tokens'; -import { - ActiveCellContextProvider, - ActiveCellManager -} from '../contexts/active-cell-context'; -import { UserContextProvider, useUserContext } from '../contexts/user-context'; -import { ScrollContainer } from './scroll-container'; -import { TooltippedIconButton } from './mui-extras/tooltipped-icon-button'; -import { TelemetryContextProvider } from '../contexts/telemetry-context'; - -type ChatBodyProps = { - chatHandler: ChatHandler; - openSettingsView: () => void; - showWelcomeMessage: boolean; - setShowWelcomeMessage: (show: boolean) => void; - rmRegistry: IRenderMimeRegistry; - focusInputSignal: ISignal; - messageFooter: IJaiMessageFooter | null; -}; - -/** - * Determines the name of the current persona based on the message history. - * Defaults to `'Jupyternaut'` if history is insufficient. - */ -function getPersonaName(messages: AiService.ChatMessage[]): string { - for (let i = messages.length - 1; i >= 0; i--) { - const message = messages[i]; - if (message.type === 'agent' || message.type === 'agent-stream') { - return message.persona.name; - } - } - - return 'Jupyternaut'; -} - -function ChatBody({ - chatHandler, - focusInputSignal, - openSettingsView, - showWelcomeMessage, - setShowWelcomeMessage, - rmRegistry: renderMimeRegistry, - messageFooter -}: ChatBodyProps): JSX.Element { - const [messages, setMessages] = useState([ - ...chatHandler.history.messages - ]); - const [pendingMessages, setPendingMessages] = useState< - AiService.PendingMessage[] - >([...chatHandler.history.pending_messages]); - const [personaName, setPersonaName] = useState( - getPersonaName(messages) - ); - const [sendWithShiftEnter, setSendWithShiftEnter] = useState(true); - const user = useUserContext(); - - /** - * Effect: fetch config on initial render - */ - useEffect(() => { - async function fetchConfig() { - try { - const config = await AiService.getConfig(); - setSendWithShiftEnter(config.send_with_shift_enter ?? false); - if (!config.model_provider_id) { - setShowWelcomeMessage(true); - } - } catch (e) { - console.error(e); - } - } - - fetchConfig(); - }, [chatHandler]); - - /** - * Effect: listen to chat messages - */ - useEffect(() => { - function onHistoryChange(_: unknown, history: AiService.ChatHistory) { - setMessages([...history.messages]); - setPendingMessages([...history.pending_messages]); - setPersonaName(getPersonaName(history.messages)); - } - - chatHandler.historyChanged.connect(onHistoryChange); - - return function cleanup() { - chatHandler.historyChanged.disconnect(onHistoryChange); - }; - }, [chatHandler]); - - if (showWelcomeMessage) { - return ( - - -

    - Welcome to Jupyter AI! To get started, please select a language - model to chat with from the settings panel. You may also need to - provide API credentials, so have those handy. -

    - -
    -
    - ); - } - - // set of IDs of messages sent by the current user. - const myHumanMessageIds = new Set( - messages - .filter( - m => m.type === 'human' && m.client.username === user?.identity.username - ) - .map(m => m.id) - ); - - // whether the backend is currently streaming a reply to any message sent by - // the current user. - const streamingReplyHere = messages.some( - m => - m.type === 'agent-stream' && - myHumanMessageIds.has(m.reply_to) && - !m.complete - ); - - return ( - <> - - - - - - - ); -} - -export type ChatProps = { - selectionWatcher: SelectionWatcher; - chatHandler: ChatHandler; - globalAwareness: Awareness | null; - themeManager: IThemeManager | null; - rmRegistry: IRenderMimeRegistry; - chatView?: ChatView; - completionProvider: IJaiCompletionProvider | null; - openInlineCompleterSettings: () => void; - activeCellManager: ActiveCellManager; - focusInputSignal: ISignal; - messageFooter: IJaiMessageFooter | null; - telemetryHandler: IJaiTelemetryHandler | null; - userManager: User.IManager; -}; - -enum ChatView { - Chat, - Settings -} - -export function Chat(props: ChatProps): JSX.Element { - const [view, setView] = useState(props.chatView || ChatView.Chat); - const [showWelcomeMessage, setShowWelcomeMessage] = useState(false); - - const openSettingsView = () => { - setShowWelcomeMessage(false); - setView(ChatView.Settings); - }; - - return ( - - - - - - - =4.3.0. - // See: https://jupyterlab.readthedocs.io/en/latest/extension/extension_migration.html#css-styling - className="jp-ThemedContainer" - // root box should not include padding as it offsets the vertical - // scrollbar to the left - sx={{ - width: '100%', - height: '100%', - boxSizing: 'border-box', - background: 'var(--jp-layout-color0)', - display: 'flex', - flexDirection: 'column' - }} - > - {/* top bar */} - - {view !== ChatView.Chat ? ( - setView(ChatView.Chat)}> - - - ) : ( - - )} - {view === ChatView.Chat ? ( - - {!showWelcomeMessage && ( - - props.chatHandler.sendMessage({ type: 'clear' }) - } - tooltip="New chat" - > - - - )} - openSettingsView()}> - - - - ) : ( - - )} - - {/* body */} - {view === ChatView.Chat && ( - - )} - {view === ChatView.Settings && ( - - )} - - - - - - - - ); -} diff --git a/packages/jupyter-ai/src/components/pending-messages.tsx b/packages/jupyter-ai/src/components/pending-messages.tsx deleted file mode 100644 index c258c295e..000000000 --- a/packages/jupyter-ai/src/components/pending-messages.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import React, { useState, useEffect } from 'react'; - -import { Box, Typography } from '@mui/material'; -import { AiService } from '../handler'; -import { ChatMessageHeader } from './chat-messages'; -import { ChatHandler } from '../chat_handler'; - -type PendingMessagesProps = { - messages: AiService.PendingMessage[]; - chatHandler: ChatHandler; -}; - -type PendingMessageElementProps = { - text: string; - ellipsis: boolean; -}; - -function PendingMessageElement(props: PendingMessageElementProps): JSX.Element { - const [dots, setDots] = useState(''); - - useEffect(() => { - const interval = setInterval(() => { - setDots(dots => (dots.length < 3 ? dots + '.' : '')); - }, 500); - - return () => clearInterval(interval); - }, []); - - let text = props.text; - if (props.ellipsis) { - text = props.text + dots; - } - - return ( - - {text.split('\n').map((line, index) => ( - {line} - ))} - - ); -} - -export function PendingMessages( - props: PendingMessagesProps -): JSX.Element | null { - const [timestamp, setTimestamp] = useState(''); - const [agentMessage, setAgentMessage] = - useState(null); - - useEffect(() => { - if (props.messages.length === 0) { - setAgentMessage(null); - setTimestamp(''); - return; - } - const lastMessage = props.messages[props.messages.length - 1]; - setAgentMessage({ - type: 'agent', - id: lastMessage.id, - time: lastMessage.time, - body: '', - reply_to: '', - persona: lastMessage.persona, - metadata: {} - }); - - // timestamp format copied from ChatMessage - const newTimestamp = new Date(lastMessage.time * 1000).toLocaleTimeString( - [], - { - hour: 'numeric', - minute: '2-digit' - } - ); - setTimestamp(newTimestamp); - }, [props.messages]); - - if (!agentMessage) { - return null; - } - - return ( - - - :not(:last-child)': { - marginBottom: '2em' - } - }} - > - {props.messages.map(message => ( - - ))} - - - ); -} diff --git a/packages/jupyter-ai/src/components/scroll-container.tsx b/packages/jupyter-ai/src/components/scroll-container.tsx deleted file mode 100644 index d17d8d2a6..000000000 --- a/packages/jupyter-ai/src/components/scroll-container.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React, { useEffect, useMemo } from 'react'; -import { Box, SxProps, Theme } from '@mui/material'; - -type ScrollContainerProps = { - children: React.ReactNode; - sx?: SxProps; -}; - -/** - * Component that handles intelligent scrolling. - * - * - If viewport is at the bottom of the overflow container, appending new - * children keeps the viewport on the bottom of the overflow container. - * - * - If viewport is in the middle of the overflow container, appending new - * children leaves the viewport unaffected. - * - * Currently only works for Chrome and Firefox due to reliance on - * `overflow-anchor`. - * - * **References** - * - https://css-tricks.com/books/greatest-css-tricks/pin-scrolling-to-bottom/ - */ -export function ScrollContainer(props: ScrollContainerProps): JSX.Element { - const id = useMemo( - () => 'jupyter-ai-scroll-container-' + Date.now().toString(), - [] - ); - - /** - * Effect: Scroll the container to the bottom as soon as it is visible. - */ - useEffect(() => { - const el = document.querySelector(`#${id}`); - if (!el) { - return; - } - - const observer = new IntersectionObserver( - entries => { - entries.forEach(entry => { - if (entry.isIntersecting) { - el.scroll({ top: 999999999 }); - } - }); - }, - { threshold: 1.0 } - ); - - observer.observe(el); - return () => observer.disconnect(); - }, []); - - return ( - - {props.children} - - - ); -} diff --git a/packages/jupyter-ai/src/contexts/active-cell-context.tsx b/packages/jupyter-ai/src/contexts/active-cell-context.tsx index 72e93a8ca..d7791d93e 100644 --- a/packages/jupyter-ai/src/contexts/active-cell-context.tsx +++ b/packages/jupyter-ai/src/contexts/active-cell-context.tsx @@ -274,6 +274,9 @@ type ActiveCellContextProps = { children: React.ReactNode; }; +/** + * NOTE: unused in v3-dev branch. + */ export function ActiveCellContextProvider( props: ActiveCellContextProps ): JSX.Element { diff --git a/packages/jupyter-ai/src/contexts/collaborators-context.tsx b/packages/jupyter-ai/src/contexts/collaborators-context.tsx deleted file mode 100644 index 72fee7068..000000000 --- a/packages/jupyter-ai/src/contexts/collaborators-context.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React, { useContext, useEffect, useState } from 'react'; -import type { Awareness } from 'y-protocols/awareness'; - -import { AiService } from '../handler'; - -const CollaboratorsContext = React.createContext< - Record ->({}); - -/** - * Returns a dictionary mapping each collaborator's username to their associated - * Collaborator object. - */ -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export function useCollaboratorsContext() { - return useContext(CollaboratorsContext); -} - -type GlobalAwarenessStates = Map< - number, - { current: string; user: AiService.Collaborator } ->; - -type CollaboratorsContextProviderProps = { - globalAwareness: Awareness | null; - children: JSX.Element; -}; - -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export function CollaboratorsContextProvider({ - globalAwareness, - children -}: CollaboratorsContextProviderProps) { - const [collaborators, setCollaborators] = useState< - Record - >({}); - - /** - * Effect: listen to changes in global awareness and update collaborators - * dictionary. - */ - useEffect(() => { - function handleChange() { - const states = (globalAwareness?.getStates() ?? - new Map()) as GlobalAwarenessStates; - - const collaboratorsDict: Record = {}; - states.forEach(state => { - collaboratorsDict[state.user.username] = state.user; - }); - - setCollaborators(collaboratorsDict); - } - - globalAwareness?.on('change', handleChange); - return () => { - globalAwareness?.off('change', handleChange); - }; - }, [globalAwareness]); - - if (!globalAwareness) { - return children; - } - - return ( - - {children} - - ); -} diff --git a/packages/jupyter-ai/src/contexts/index.ts b/packages/jupyter-ai/src/contexts/index.ts index 0cc0c017f..8457bee31 100644 --- a/packages/jupyter-ai/src/contexts/index.ts +++ b/packages/jupyter-ai/src/contexts/index.ts @@ -1,4 +1,3 @@ export * from './active-cell-context'; -export * from './collaborators-context'; export * from './selection-context'; export * from './telemetry-context'; diff --git a/packages/jupyter-ai/src/contexts/selection-context.tsx b/packages/jupyter-ai/src/contexts/selection-context.tsx index e36d0de38..42faad6b8 100644 --- a/packages/jupyter-ai/src/contexts/selection-context.tsx +++ b/packages/jupyter-ai/src/contexts/selection-context.tsx @@ -20,6 +20,9 @@ type SelectionContextProviderProps = { children: React.ReactNode; }; +/** + * NOTE: unused in v3-dev branch. + */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export function SelectionContextProvider({ selectionWatcher, diff --git a/packages/jupyter-ai/src/contexts/user-context.tsx b/packages/jupyter-ai/src/contexts/user-context.tsx deleted file mode 100644 index ff9fe8e3d..000000000 --- a/packages/jupyter-ai/src/contexts/user-context.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React, { useContext, useEffect, useState } from 'react'; -import type { User } from '@jupyterlab/services'; -import { PartialJSONObject } from '@lumino/coreutils'; - -const UserContext = React.createContext(null); - -export function useUserContext(): User.IUser | null { - return useContext(UserContext); -} - -type UserContextProviderProps = { - userManager: User.IManager; - children: React.ReactNode; -}; - -export function UserContextProvider({ - userManager, - children -}: UserContextProviderProps): JSX.Element { - const [user, setUser] = useState(null); - - useEffect(() => { - userManager.ready.then(() => { - setUser({ - identity: userManager.identity!, - permissions: userManager.permissions as PartialJSONObject - }); - }); - userManager.userChanged.connect((sender, newUser) => { - setUser(newUser); - }); - }, []); - - return {children}; -} diff --git a/packages/jupyter-ai/src/index.ts b/packages/jupyter-ai/src/index.ts index c6d965ce0..6f0f5c03f 100644 --- a/packages/jupyter-ai/src/index.ts +++ b/packages/jupyter-ai/src/index.ts @@ -1,9 +1,7 @@ import { IAutocompletionRegistry } from '@jupyter/chat'; -import { IGlobalAwareness } from '@jupyter/collaboration'; import { JupyterFrontEnd, - JupyterFrontEndPlugin, - ILayoutRestorer + JupyterFrontEndPlugin } from '@jupyterlab/application'; import { IWidgetTracker, @@ -14,33 +12,18 @@ import { } from '@jupyterlab/apputils'; import { IDocumentWidget } from '@jupyterlab/docregistry'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; -import { Signal } from '@lumino/signaling'; -import type { Awareness } from 'y-protocols/awareness'; import { ChatHandler } from './chat_handler'; import { completionPlugin } from './completions'; -import { ActiveCellManager } from './contexts/active-cell-context'; -import { SelectionWatcher } from './selection-watcher'; -import { menuPlugin } from './plugins/menu-plugin'; import { autocompletion } from './slash-autocompletion'; import { statusItemPlugin } from './status'; -import { - IJaiCompletionProvider, - IJaiCore, - IJaiMessageFooter, - IJaiTelemetryHandler -} from './tokens'; +import { IJaiCompletionProvider, IJaiCore } from './tokens'; import { buildErrorWidget } from './widgets/chat-error'; -import { buildChatSidebar } from './widgets/chat-sidebar'; import { buildAiSettings } from './widgets/settings-widget'; export type DocumentTracker = IWidgetTracker; export namespace CommandIDs { - /** - * Command to focus the input. - */ - export const focusChatInput = 'jupyter-ai:focus-chat-input'; /** * Command to open the AI settings. */ @@ -54,37 +37,15 @@ const plugin: JupyterFrontEndPlugin = { id: '@jupyter-ai/core:plugin', autoStart: true, requires: [IRenderMimeRegistry], - optional: [ - ICommandPalette, - IGlobalAwareness, - ILayoutRestorer, - IThemeManager, - IJaiCompletionProvider, - IJaiMessageFooter, - IJaiTelemetryHandler - ], + optional: [ICommandPalette, IThemeManager, IJaiCompletionProvider], provides: IJaiCore, activate: async ( app: JupyterFrontEnd, rmRegistry: IRenderMimeRegistry, palette: ICommandPalette | null, - globalAwareness: Awareness | null, - restorer: ILayoutRestorer | null, themeManager: IThemeManager | null, - completionProvider: IJaiCompletionProvider | null, - messageFooter: IJaiMessageFooter | null, - telemetryHandler: IJaiTelemetryHandler | null + completionProvider: IJaiCompletionProvider | null ) => { - /** - * Initialize selection watcher singleton - */ - const selectionWatcher = new SelectionWatcher(app.shell); - - /** - * Initialize active cell manager singleton - */ - const activeCellManager = new ActiveCellManager(app.shell); - /** * Initialize chat handler, open WS connection */ @@ -96,8 +57,6 @@ const plugin: JupyterFrontEndPlugin = { }); }; - const focusInputSignal = new Signal({}); - // Create a AI settings widget. let aiSettings: MainAreaWidget; let settingsWidget: ReactWidget; @@ -136,49 +95,8 @@ const plugin: JupyterFrontEndPlugin = { }); } - let chatWidget: ReactWidget; - try { - chatWidget = buildChatSidebar( - selectionWatcher, - chatHandler, - globalAwareness, - themeManager, - rmRegistry, - completionProvider, - openInlineCompleterSettings, - activeCellManager, - focusInputSignal, - messageFooter, - telemetryHandler, - app.serviceManager.user - ); - } catch (e) { - chatWidget = buildErrorWidget(themeManager); - } - - /** - * Add Chat widget to right sidebar - */ - app.shell.add(chatWidget, 'left', { rank: 2000 }); - - if (restorer) { - restorer.add(chatWidget, 'jupyter-ai-chat'); - } - - // Define jupyter-ai commands - app.commands.addCommand(CommandIDs.focusChatInput, { - execute: () => { - app.shell.activateById(chatWidget.id); - focusInputSignal.emit(); - }, - label: 'Focus the jupyter-ai chat' - }); - return { - activeCellManager, - chatHandler, - chatWidget, - selectionWatcher + chatHandler }; } }; @@ -202,7 +120,6 @@ export default [ plugin, statusItemPlugin, completionPlugin, - menuPlugin, chat_autocompletion ]; diff --git a/packages/jupyter-ai/src/plugins/menu-plugin.ts b/packages/jupyter-ai/src/plugins/menu-plugin.ts index 8994a552d..48a6dead5 100644 --- a/packages/jupyter-ai/src/plugins/menu-plugin.ts +++ b/packages/jupyter-ai/src/plugins/menu-plugin.ts @@ -1,158 +1,158 @@ -import { - JupyterFrontEnd, - JupyterFrontEndPlugin -} from '@jupyterlab/application'; +// import { +// JupyterFrontEnd, +// JupyterFrontEndPlugin +// } from '@jupyterlab/application'; -import { IJaiCore } from '../tokens'; -import { AiService } from '../handler'; -import { Menu } from '@lumino/widgets'; -import { CommandRegistry } from '@lumino/commands'; +// import { IJaiCore } from '../tokens'; +// import { AiService } from '../handler'; +// import { Menu } from '@lumino/widgets'; +// import { CommandRegistry } from '@lumino/commands'; -export namespace CommandIDs { - export const explain = 'jupyter-ai:explain'; - export const fix = 'jupyter-ai:fix'; - export const optimize = 'jupyter-ai:optimize'; - export const refactor = 'jupyter-ai:refactor'; -} +// export namespace CommandIDs { +// export const explain = 'jupyter-ai:explain'; +// export const fix = 'jupyter-ai:fix'; +// export const optimize = 'jupyter-ai:optimize'; +// export const refactor = 'jupyter-ai:refactor'; +// } -/** - * Optional plugin that adds a "Generative AI" submenu to the context menu. - * These implement UI shortcuts that explain, fix, refactor, or optimize code in - * a notebook or file. - * - * **This plugin is experimental and may be removed in a future release.** - */ -export const menuPlugin: JupyterFrontEndPlugin = { - id: '@jupyter-ai/core:menu-plugin', - autoStart: true, - requires: [IJaiCore], - activate: (app: JupyterFrontEnd, jaiCore: IJaiCore) => { - const { activeCellManager, chatHandler, chatWidget, selectionWatcher } = - jaiCore; +// /** +// * Optional plugin that adds a "Generative AI" submenu to the context menu. +// * These implement UI shortcuts that explain, fix, refactor, or optimize code in +// * a notebook or file. +// * +// * **This plugin is experimental and may be removed in a future release.** +// */ +// export const menuPlugin: JupyterFrontEndPlugin = { +// id: '@jupyter-ai/core:menu-plugin', +// autoStart: true, +// requires: [IJaiCore], +// activate: (app: JupyterFrontEnd, jaiCore: IJaiCore) => { +// const { activeCellManager, chatHandler, chatWidget, selectionWatcher } = +// jaiCore; - function activateChatSidebar() { - app.shell.activateById(chatWidget.id); - } +// function activateChatSidebar() { +// app.shell.activateById(chatWidget.id); +// } - function getSelection(): AiService.Selection | null { - const textSelection = selectionWatcher.selection; - const activeCell = activeCellManager.getContent(false); - const selection: AiService.Selection | null = textSelection - ? { type: 'text', source: textSelection.text } - : activeCell - ? { type: 'cell', source: activeCell.source } - : null; +// function getSelection(): AiService.Selection | null { +// const textSelection = selectionWatcher.selection; +// const activeCell = activeCellManager.getContent(false); +// const selection: AiService.Selection | null = textSelection +// ? { type: 'text', source: textSelection.text } +// : activeCell +// ? { type: 'cell', source: activeCell.source } +// : null; - return selection; - } +// return selection; +// } - function buildLabelFactory(baseLabel: string): () => string { - return () => { - const textSelection = selectionWatcher.selection; - const activeCell = activeCellManager.getContent(false); +// function buildLabelFactory(baseLabel: string): () => string { +// return () => { +// const textSelection = selectionWatcher.selection; +// const activeCell = activeCellManager.getContent(false); - return textSelection - ? `${baseLabel} (${textSelection.numLines} lines selected)` - : activeCell - ? `${baseLabel} (1 active cell)` - : baseLabel; - }; - } +// return textSelection +// ? `${baseLabel} (${textSelection.numLines} lines selected)` +// : activeCell +// ? `${baseLabel} (1 active cell)` +// : baseLabel; +// }; +// } - // register commands - const menuCommands = new CommandRegistry(); - menuCommands.addCommand(CommandIDs.explain, { - execute: () => { - const selection = getSelection(); - if (!selection) { - return; - } +// // register commands +// const menuCommands = new CommandRegistry(); +// menuCommands.addCommand(CommandIDs.explain, { +// execute: () => { +// const selection = getSelection(); +// if (!selection) { +// return; +// } - activateChatSidebar(); - chatHandler.sendMessage({ - prompt: 'Explain the code below.', - selection - }); - }, - label: buildLabelFactory('Explain code'), - isEnabled: () => !!getSelection() - }); - menuCommands.addCommand(CommandIDs.fix, { - execute: () => { - const activeCellWithError = activeCellManager.getContent(true); - if (!activeCellWithError) { - return; - } +// activateChatSidebar(); +// chatHandler.sendMessage({ +// prompt: 'Explain the code below.', +// selection +// }); +// }, +// label: buildLabelFactory('Explain code'), +// isEnabled: () => !!getSelection() +// }); +// menuCommands.addCommand(CommandIDs.fix, { +// execute: () => { +// const activeCellWithError = activeCellManager.getContent(true); +// if (!activeCellWithError) { +// return; +// } - chatHandler.sendMessage({ - prompt: '/fix', - selection: { - type: 'cell-with-error', - error: activeCellWithError.error, - source: activeCellWithError.source - } - }); - }, - label: () => { - const activeCellWithError = activeCellManager.getContent(true); - return activeCellWithError - ? 'Fix code cell (1 error cell)' - : 'Fix code cell (no error cell)'; - }, - isEnabled: () => { - const activeCellWithError = activeCellManager.getContent(true); - return !!activeCellWithError; - } - }); - menuCommands.addCommand(CommandIDs.optimize, { - execute: () => { - const selection = getSelection(); - if (!selection) { - return; - } +// chatHandler.sendMessage({ +// prompt: '/fix', +// selection: { +// type: 'cell-with-error', +// error: activeCellWithError.error, +// source: activeCellWithError.source +// } +// }); +// }, +// label: () => { +// const activeCellWithError = activeCellManager.getContent(true); +// return activeCellWithError +// ? 'Fix code cell (1 error cell)' +// : 'Fix code cell (no error cell)'; +// }, +// isEnabled: () => { +// const activeCellWithError = activeCellManager.getContent(true); +// return !!activeCellWithError; +// } +// }); +// menuCommands.addCommand(CommandIDs.optimize, { +// execute: () => { +// const selection = getSelection(); +// if (!selection) { +// return; +// } - activateChatSidebar(); - chatHandler.sendMessage({ - prompt: 'Optimize the code below.', - selection - }); - }, - label: buildLabelFactory('Optimize code'), - isEnabled: () => !!getSelection() - }); - menuCommands.addCommand(CommandIDs.refactor, { - execute: () => { - const selection = getSelection(); - if (!selection) { - return; - } +// activateChatSidebar(); +// chatHandler.sendMessage({ +// prompt: 'Optimize the code below.', +// selection +// }); +// }, +// label: buildLabelFactory('Optimize code'), +// isEnabled: () => !!getSelection() +// }); +// menuCommands.addCommand(CommandIDs.refactor, { +// execute: () => { +// const selection = getSelection(); +// if (!selection) { +// return; +// } - activateChatSidebar(); - chatHandler.sendMessage({ - prompt: 'Refactor the code below.', - selection - }); - }, - label: buildLabelFactory('Refactor code'), - isEnabled: () => !!getSelection() - }); +// activateChatSidebar(); +// chatHandler.sendMessage({ +// prompt: 'Refactor the code below.', +// selection +// }); +// }, +// label: buildLabelFactory('Refactor code'), +// isEnabled: () => !!getSelection() +// }); - // add commands as a context menu item containing a "Generative AI" submenu - const submenu = new Menu({ - commands: menuCommands - }); - submenu.id = 'jupyter-ai:submenu'; - submenu.title.label = 'Generative AI'; - submenu.addItem({ command: CommandIDs.explain }); - submenu.addItem({ command: CommandIDs.fix }); - submenu.addItem({ command: CommandIDs.optimize }); - submenu.addItem({ command: CommandIDs.refactor }); +// // add commands as a context menu item containing a "Generative AI" submenu +// const submenu = new Menu({ +// commands: menuCommands +// }); +// submenu.id = 'jupyter-ai:submenu'; +// submenu.title.label = 'Generative AI'; +// submenu.addItem({ command: CommandIDs.explain }); +// submenu.addItem({ command: CommandIDs.fix }); +// submenu.addItem({ command: CommandIDs.optimize }); +// submenu.addItem({ command: CommandIDs.refactor }); - app.contextMenu.addItem({ - type: 'submenu', - selector: '.jp-Editor', - rank: 1, - submenu - }); - } -}; +// app.contextMenu.addItem({ +// type: 'submenu', +// selector: '.jp-Editor', +// rank: 1, +// submenu +// }); +// } +// }; diff --git a/packages/jupyter-ai/src/tokens.ts b/packages/jupyter-ai/src/tokens.ts index 1b1c2eb11..4ad409198 100644 --- a/packages/jupyter-ai/src/tokens.ts +++ b/packages/jupyter-ai/src/tokens.ts @@ -1,12 +1,10 @@ import React from 'react'; import { Token } from '@lumino/coreutils'; import { ISignal } from '@lumino/signaling'; -import type { IRankedMenu, ReactWidget } from '@jupyterlab/ui-components'; +import type { IRankedMenu } from '@jupyterlab/ui-components'; import { AiService } from './handler'; import { ChatHandler } from './chat_handler'; -import { ActiveCellManager } from './contexts/active-cell-context'; -import { SelectionWatcher } from './selection-watcher'; export interface IJaiStatusItem { addItem(item: IRankedMenu.IItemOptions): void; @@ -52,10 +50,7 @@ export const IJaiMessageFooter = new Token( ); export interface IJaiCore { - chatWidget: ReactWidget; chatHandler: ChatHandler; - activeCellManager: ActiveCellManager; - selectionWatcher: SelectionWatcher; } /** diff --git a/packages/jupyter-ai/src/widgets/chat-sidebar.tsx b/packages/jupyter-ai/src/widgets/chat-sidebar.tsx deleted file mode 100644 index 732eedd3c..000000000 --- a/packages/jupyter-ai/src/widgets/chat-sidebar.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import { ISignal } from '@lumino/signaling'; -import { ReactWidget } from '@jupyterlab/apputils'; -import type { IThemeManager } from '@jupyterlab/apputils'; -import type { User } from '@jupyterlab/services'; -import type { Awareness } from 'y-protocols/awareness'; - -import { Chat } from '../components/chat'; -import { chatIcon } from '../icons'; -import { SelectionWatcher } from '../selection-watcher'; -import { ChatHandler } from '../chat_handler'; -import { - IJaiCompletionProvider, - IJaiMessageFooter, - IJaiTelemetryHandler -} from '../tokens'; -import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; -import type { ActiveCellManager } from '../contexts/active-cell-context'; - -export function buildChatSidebar( - selectionWatcher: SelectionWatcher, - chatHandler: ChatHandler, - globalAwareness: Awareness | null, - themeManager: IThemeManager | null, - rmRegistry: IRenderMimeRegistry, - completionProvider: IJaiCompletionProvider | null, - openInlineCompleterSettings: () => void, - activeCellManager: ActiveCellManager, - focusInputSignal: ISignal, - messageFooter: IJaiMessageFooter | null, - telemetryHandler: IJaiTelemetryHandler | null, - userManager: User.IManager -): ReactWidget { - const ChatWidget = ReactWidget.create( - - ); - ChatWidget.id = 'jupyter-ai::chat'; - ChatWidget.title.icon = chatIcon; - ChatWidget.title.caption = 'Jupyter AI Chat'; // TODO: i18n - return ChatWidget; -}