From 7044dbb7e04c5eb480ea396ec06607605c6f18f4 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Wed, 18 Dec 2024 16:50:12 +0100 Subject: [PATCH 1/9] First pass to remove the front end chat --- .../jupyter-ai/src/components/chat-input.tsx | 403 ------------------ .../src/components/chat-input/send-button.tsx | 204 --------- .../src/components/chat-messages.tsx | 242 ----------- .../chat-messages/chat-message-delete.tsx | 31 -- packages/jupyter-ai/src/components/chat.tsx | 310 -------------- .../src/components/pending-messages.tsx | 117 ----- .../src/components/scroll-container.tsx | 69 --- .../src/contexts/active-cell-context.tsx | 3 + .../src/contexts/collaborators-context.tsx | 70 --- packages/jupyter-ai/src/contexts/index.ts | 1 - .../src/contexts/selection-context.tsx | 3 + .../jupyter-ai/src/contexts/user-context.tsx | 35 -- packages/jupyter-ai/src/index.ts | 93 +--- .../jupyter-ai/src/plugins/menu-plugin.ts | 286 ++++++------- packages/jupyter-ai/src/tokens.ts | 7 +- .../jupyter-ai/src/widgets/chat-sidebar.tsx | 54 --- 16 files changed, 155 insertions(+), 1773 deletions(-) delete mode 100644 packages/jupyter-ai/src/components/chat-input.tsx delete mode 100644 packages/jupyter-ai/src/components/chat-input/send-button.tsx delete mode 100644 packages/jupyter-ai/src/components/chat-messages.tsx delete mode 100644 packages/jupyter-ai/src/components/chat-messages/chat-message-delete.tsx delete mode 100644 packages/jupyter-ai/src/components/chat.tsx delete mode 100644 packages/jupyter-ai/src/components/pending-messages.tsx delete mode 100644 packages/jupyter-ai/src/components/scroll-container.tsx delete mode 100644 packages/jupyter-ai/src/contexts/collaborators-context.tsx delete mode 100644 packages/jupyter-ai/src/contexts/user-context.tsx delete mode 100644 packages/jupyter-ai/src/widgets/chat-sidebar.tsx 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; -} From a0477811a68494a79930b766ac4c128b1a895a19 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Thu, 19 Dec 2024 12:14:57 +0100 Subject: [PATCH 2/9] Remove code-toolbar by using a simplified markdown renderer in settings --- .../src/components/chat-settings.tsx | 4 +- .../components/code-blocks/code-toolbar.tsx | 197 ------------------ .../src/components/rendermime-markdown.tsx | 141 ------------- .../settings/rendermime-markdown.tsx | 79 +++++++ 4 files changed, 80 insertions(+), 341 deletions(-) delete mode 100644 packages/jupyter-ai/src/components/code-blocks/code-toolbar.tsx delete mode 100644 packages/jupyter-ai/src/components/rendermime-markdown.tsx create mode 100644 packages/jupyter-ai/src/components/settings/rendermime-markdown.tsx diff --git a/packages/jupyter-ai/src/components/chat-settings.tsx b/packages/jupyter-ai/src/components/chat-settings.tsx index 5922bcff1..8d936c46a 100644 --- a/packages/jupyter-ai/src/components/chat-settings.tsx +++ b/packages/jupyter-ai/src/components/chat-settings.tsx @@ -26,7 +26,7 @@ import { ExistingApiKeys } from './settings/existing-api-keys'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { minifyUpdate } from './settings/minify'; import { useStackingAlert } from './mui-extras/stacking-alert'; -import { RendermimeMarkdown } from './rendermime-markdown'; +import { RendermimeMarkdown } from './settings/rendermime-markdown'; import { IJaiCompletionProvider } from '../tokens'; import { getProviderId, getModelLocalId } from '../utils'; @@ -375,7 +375,6 @@ export function ChatSettings(props: ChatSettingsProps): JSX.Element { )} {lmGlobalId && ( @@ -491,7 +490,6 @@ export function ChatSettings(props: ChatSettingsProps): JSX.Element { )} {clmGlobalId && ( diff --git a/packages/jupyter-ai/src/components/code-blocks/code-toolbar.tsx b/packages/jupyter-ai/src/components/code-blocks/code-toolbar.tsx deleted file mode 100644 index 315e5d4d6..000000000 --- a/packages/jupyter-ai/src/components/code-blocks/code-toolbar.tsx +++ /dev/null @@ -1,197 +0,0 @@ -import React from 'react'; -import { Box } from '@mui/material'; -import { - addAboveIcon, - addBelowIcon, - copyIcon -} from '@jupyterlab/ui-components'; -import { replaceCellIcon } from '../../icons'; - -import { - ActiveCellManager, - useActiveCellContext -} from '../../contexts/active-cell-context'; -import { TooltippedIconButton } from '../mui-extras/tooltipped-icon-button'; -import { useReplace } from '../../hooks/use-replace'; -import { useCopy } from '../../hooks/use-copy'; -import { AiService } from '../../handler'; -import { useTelemetry } from '../../contexts/telemetry-context'; -import { TelemetryEvent } from '../../tokens'; - -export type CodeToolbarProps = { - /** - * The content of the Markdown code block this component is attached to. - */ - code: string; - /** - * Parent message which contains the code referenced by `content`. - */ - parentMessage?: AiService.ChatMessage; -}; - -export function CodeToolbar(props: CodeToolbarProps): JSX.Element { - const activeCell = useActiveCellContext(); - const sharedToolbarButtonProps: ToolbarButtonProps = { - code: props.code, - activeCellManager: activeCell.manager, - activeCellExists: activeCell.exists, - parentMessage: props.parentMessage - }; - - return ( - - - - - - - ); -} - -type ToolbarButtonProps = { - code: string; - activeCellExists: boolean; - activeCellManager: ActiveCellManager; - parentMessage?: AiService.ChatMessage; - // TODO: parentMessage should always be defined, but this can be undefined - // when the code toolbar appears in Markdown help messages in the Settings - // UI. The Settings UI should use a different component to render Markdown, - // and should never render code toolbars within it. -}; - -function buildTelemetryEvent( - type: string, - props: ToolbarButtonProps -): TelemetryEvent { - const charCount = props.code.length; - // number of lines = number of newlines + 1 - const lineCount = (props.code.match(/\n/g) ?? []).length + 1; - - return { - type, - message: { - id: props.parentMessage?.id ?? '', - type: props.parentMessage?.type ?? 'human', - time: props.parentMessage?.time ?? 0, - metadata: - props.parentMessage && 'metadata' in props.parentMessage - ? props.parentMessage.metadata - : {} - }, - code: { - charCount, - lineCount - } - }; -} - -function InsertAboveButton(props: ToolbarButtonProps) { - const telemetryHandler = useTelemetry(); - const tooltip = props.activeCellExists - ? 'Insert above active cell' - : 'Insert above active cell (no active cell)'; - - return ( - { - props.activeCellManager.insertAbove(props.code); - - try { - telemetryHandler.onEvent(buildTelemetryEvent('insert-above', props)); - } catch (e) { - console.error(e); - return; - } - }} - disabled={!props.activeCellExists} - > - - - ); -} - -function InsertBelowButton(props: ToolbarButtonProps) { - const telemetryHandler = useTelemetry(); - const tooltip = props.activeCellExists - ? 'Insert below active cell' - : 'Insert below active cell (no active cell)'; - - return ( - { - props.activeCellManager.insertBelow(props.code); - - try { - telemetryHandler.onEvent(buildTelemetryEvent('insert-below', props)); - } catch (e) { - console.error(e); - return; - } - }} - > - - - ); -} - -function ReplaceButton(props: ToolbarButtonProps) { - const telemetryHandler = useTelemetry(); - const { replace, replaceDisabled, replaceLabel } = useReplace(); - - return ( - { - replace(props.code); - - try { - telemetryHandler.onEvent(buildTelemetryEvent('replace', props)); - } catch (e) { - console.error(e); - return; - } - }} - > - - - ); -} - -export function CopyButton(props: ToolbarButtonProps): JSX.Element { - const telemetryHandler = useTelemetry(); - const { copy, copyLabel } = useCopy(); - - return ( - { - copy(props.code); - - try { - telemetryHandler.onEvent(buildTelemetryEvent('copy', props)); - } catch (e) { - console.error(e); - return; - } - }} - aria-label="Copy to clipboard" - > - - - ); -} diff --git a/packages/jupyter-ai/src/components/rendermime-markdown.tsx b/packages/jupyter-ai/src/components/rendermime-markdown.tsx deleted file mode 100644 index 9a0278517..000000000 --- a/packages/jupyter-ai/src/components/rendermime-markdown.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { createPortal } from 'react-dom'; - -import { CodeToolbar, CodeToolbarProps } from './code-blocks/code-toolbar'; -import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; -import { AiService } from '../handler'; - -const MD_MIME_TYPE = 'text/markdown'; -const RENDERMIME_MD_CLASS = 'jp-ai-rendermime-markdown'; - -type RendermimeMarkdownProps = { - markdownStr: string; - rmRegistry: IRenderMimeRegistry; - /** - * Reference to the parent message object in the Jupyter AI chat. - */ - parentMessage?: AiService.ChatMessage; - /** - * Whether the message is complete. This is generally `true` except in the - * case where `markdownStr` contains the incomplete contents of a - * `AgentStreamMessage`, in which case this should be set to `false`. - */ - complete: boolean; -}; - -/** - * Escapes backslashes in LaTeX delimiters such that they appear in the DOM - * after the initial MarkDown render. For example, this function takes '\(` and - * returns `\\(`. - * - * Required for proper rendering of MarkDown + LaTeX markup in the chat by - * `ILatexTypesetter`. - */ -function escapeLatexDelimiters(text: string) { - return text - .replace(/\\\(/g, '\\\\(') - .replace(/\\\)/g, '\\\\)') - .replace(/\\\[/g, '\\\\[') - .replace(/\\\]/g, '\\\\]'); -} - -function RendermimeMarkdownBase(props: RendermimeMarkdownProps): JSX.Element { - // create a single renderer object at component mount - const [renderer] = useState(() => { - return props.rmRegistry.createRenderer(MD_MIME_TYPE); - }); - - // ref that tracks the content container to store the rendermime node in - const renderingContainer = useRef(null); - // ref that tracks whether the rendermime node has already been inserted - const renderingInserted = useRef(false); - - // each element is a two-tuple with the structure [codeToolbarRoot, codeToolbarProps]. - const [codeToolbarDefns, setCodeToolbarDefns] = useState< - Array<[HTMLDivElement, CodeToolbarProps]> - >([]); - - /** - * Effect: use Rendermime to render `props.markdownStr` into an HTML element, - * and insert it into `renderingContainer` if not yet inserted. When the - * message is completed, add code toolbars. - */ - useEffect(() => { - const renderContent = async () => { - // initialize mime model - const mdStr = escapeLatexDelimiters(props.markdownStr); - const model = props.rmRegistry.createModel({ - data: { [MD_MIME_TYPE]: mdStr } - }); - - // step 1: render markdown - await renderer.renderModel(model); - if (!renderer.node) { - throw new Error( - 'Rendermime was unable to render Markdown content within a chat message. Please report this upstream to Jupyter AI on GitHub.' - ); - } - - // step 2: render LaTeX via MathJax - props.rmRegistry.latexTypesetter?.typeset(renderer.node); - - // insert the rendering into renderingContainer if not yet inserted - if (renderingContainer.current !== null && !renderingInserted.current) { - renderingContainer.current.appendChild(renderer.node); - renderingInserted.current = true; - } - - // if complete, render code toolbars - if (!props.complete) { - return; - } - const newCodeToolbarDefns: [HTMLDivElement, CodeToolbarProps][] = []; - - // Attach CodeToolbar root element to each
     block
    -      const preBlocks = renderer.node.querySelectorAll('pre');
    -      preBlocks.forEach(preBlock => {
    -        const codeToolbarRoot = document.createElement('div');
    -        preBlock.parentNode?.insertBefore(
    -          codeToolbarRoot,
    -          preBlock.nextSibling
    -        );
    -        newCodeToolbarDefns.push([
    -          codeToolbarRoot,
    -          {
    -            code: preBlock.textContent || '',
    -            parentMessage: props.parentMessage
    -          }
    -        ]);
    -      });
    -
    -      setCodeToolbarDefns(newCodeToolbarDefns);
    -    };
    -
    -    renderContent();
    -  }, [
    -    props.markdownStr,
    -    props.complete,
    -    props.rmRegistry,
    -    props.parentMessage
    -  ]);
    -
    -  return (
    -    
    -
    - { - // Render a `CodeToolbar` element underneath each code block. - // We use ReactDOM.createPortal() so each `CodeToolbar` element is able - // to use the context in the main React tree. - codeToolbarDefns.map(codeToolbarDefn => { - const [codeToolbarRoot, codeToolbarProps] = codeToolbarDefn; - return createPortal( - , - codeToolbarRoot - ); - }) - } -
    - ); -} - -export const RendermimeMarkdown = React.memo(RendermimeMarkdownBase); diff --git a/packages/jupyter-ai/src/components/settings/rendermime-markdown.tsx b/packages/jupyter-ai/src/components/settings/rendermime-markdown.tsx new file mode 100644 index 000000000..9d600cac4 --- /dev/null +++ b/packages/jupyter-ai/src/components/settings/rendermime-markdown.tsx @@ -0,0 +1,79 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; + +const MD_MIME_TYPE = 'text/markdown'; +const RENDERMIME_MD_CLASS = 'jp-ai-rendermime-markdown'; + +type RendermimeMarkdownProps = { + markdownStr: string; + rmRegistry: IRenderMimeRegistry; +}; + +/** + * Escapes backslashes in LaTeX delimiters such that they appear in the DOM + * after the initial MarkDown render. For example, this function takes '\(` and + * returns `\\(`. + * + * Required for proper rendering of MarkDown + LaTeX markup in the chat by + * `ILatexTypesetter`. + */ +function escapeLatexDelimiters(text: string) { + return text + .replace(/\\\(/g, '\\\\(') + .replace(/\\\)/g, '\\\\)') + .replace(/\\\[/g, '\\\\[') + .replace(/\\\]/g, '\\\\]'); +} + +export function RendermimeMarkdown( + props: RendermimeMarkdownProps +): JSX.Element { + // create a single renderer object at component mount + const [renderer] = useState(() => { + return props.rmRegistry.createRenderer(MD_MIME_TYPE); + }); + + // ref that tracks the content container to store the rendermime node in + const renderingContainer = useRef(null); + // ref that tracks whether the rendermime node has already been inserted + const renderingInserted = useRef(false); + + /** + * Effect: use Rendermime to render `props.markdownStr` into an HTML element, + * and insert it into `renderingContainer` if not yet inserted. + */ + useEffect(() => { + const renderContent = async () => { + // initialize mime model + const mdStr = escapeLatexDelimiters(props.markdownStr); + const model = props.rmRegistry.createModel({ + data: { [MD_MIME_TYPE]: mdStr } + }); + + // step 1: render markdown + await renderer.renderModel(model); + if (!renderer.node) { + throw new Error( + 'Rendermime was unable to render Markdown content. Please report this upstream to Jupyter AI on GitHub.' + ); + } + + // step 2: render LaTeX via MathJax + props.rmRegistry.latexTypesetter?.typeset(renderer.node); + + // insert the rendering into renderingContainer if not yet inserted + if (renderingContainer.current !== null && !renderingInserted.current) { + renderingContainer.current.appendChild(renderer.node); + renderingInserted.current = true; + } + }; + + renderContent(); + }, [props.markdownStr]); + + return ( +
    +
    +
    + ); +} From ba472dc7451a9e9924801110b5e39c0855f459b8 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Thu, 19 Dec 2024 12:27:35 +0100 Subject: [PATCH 3/9] Remove chat-message-menu (should be ported in jupyter-chat) --- packages/jupyter-ai/src/completions/plugin.ts | 2 +- .../chat-messages/chat-message-menu.tsx | 94 ----- .../src/contexts/active-cell-context.tsx | 332 ------------------ packages/jupyter-ai/src/contexts/index.ts | 2 - .../src/contexts/selection-context.tsx | 54 --- packages/jupyter-ai/src/hooks/use-replace.ts | 56 --- packages/jupyter-ai/src/selection-watcher.ts | 181 ---------- packages/jupyter-ai/src/utils.ts | 43 +-- 8 files changed, 20 insertions(+), 744 deletions(-) delete mode 100644 packages/jupyter-ai/src/components/chat-messages/chat-message-menu.tsx delete mode 100644 packages/jupyter-ai/src/contexts/active-cell-context.tsx delete mode 100644 packages/jupyter-ai/src/contexts/selection-context.tsx delete mode 100644 packages/jupyter-ai/src/hooks/use-replace.ts delete mode 100644 packages/jupyter-ai/src/selection-watcher.ts diff --git a/packages/jupyter-ai/src/completions/plugin.ts b/packages/jupyter-ai/src/completions/plugin.ts index 4487b2752..bcccd5984 100644 --- a/packages/jupyter-ai/src/completions/plugin.ts +++ b/packages/jupyter-ai/src/completions/plugin.ts @@ -8,7 +8,7 @@ import { IEditorLanguageRegistry, IEditorLanguage } from '@jupyterlab/codemirror'; -import { getEditor } from '../selection-watcher'; +import { getEditor } from '../utils'; import { IJaiStatusItem, IJaiCompletionProvider } from '../tokens'; import { displayName, JaiInlineProvider } from './provider'; import { CompletionWebsocketHandler } from './handler'; diff --git a/packages/jupyter-ai/src/components/chat-messages/chat-message-menu.tsx b/packages/jupyter-ai/src/components/chat-messages/chat-message-menu.tsx deleted file mode 100644 index a10c061ee..000000000 --- a/packages/jupyter-ai/src/components/chat-messages/chat-message-menu.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React, { useRef, useState } from 'react'; - -import { IconButton, Menu, MenuItem, SxProps } from '@mui/material'; -import { MoreVert } from '@mui/icons-material'; -import { - addAboveIcon, - addBelowIcon, - copyIcon -} from '@jupyterlab/ui-components'; - -import { AiService } from '../../handler'; -import { CopyStatus, useCopy } from '../../hooks/use-copy'; -import { useReplace } from '../../hooks/use-replace'; -import { useActiveCellContext } from '../../contexts/active-cell-context'; -import { replaceCellIcon } from '../../icons'; - -type ChatMessageMenuProps = { - message: AiService.ChatMessage; - - /** - * Styles applied to the menu icon button. - */ - sx?: SxProps; -}; - -export function ChatMessageMenu(props: ChatMessageMenuProps): JSX.Element { - const menuButtonRef = useRef(null); - const { copy, copyLabel } = useCopy({ - labelOverrides: { [CopyStatus.None]: 'Copy response' } - }); - const { replace, replaceLabel } = useReplace(); - const activeCell = useActiveCellContext(); - - const [menuOpen, setMenuOpen] = useState(false); - - const [anchorEl, setAnchorEl] = React.useState(null); - const openMenu = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - setMenuOpen(true); - }; - - const insertAboveLabel = activeCell.exists - ? 'Insert response above active cell' - : 'Insert response above active cell (no active cell)'; - - const insertBelowLabel = activeCell.exists - ? 'Insert response below active cell' - : 'Insert response below active cell (no active cell)'; - - const menuItemSx: SxProps = { - display: 'flex', - alignItems: 'center', - gap: '8px', - lineHeight: 0 - }; - - return ( - <> - - - - setMenuOpen(false)} - anchorEl={anchorEl} - > - copy(props.message.body)} sx={menuItemSx}> - - {copyLabel} - - replace(props.message.body)} sx={menuItemSx}> - - {replaceLabel} - - activeCell.manager.insertAbove(props.message.body)} - disabled={!activeCell.exists} - sx={menuItemSx} - > - - {insertAboveLabel} - - activeCell.manager.insertBelow(props.message.body)} - disabled={!activeCell.exists} - sx={menuItemSx} - > - - {insertBelowLabel} - - - - ); -} diff --git a/packages/jupyter-ai/src/contexts/active-cell-context.tsx b/packages/jupyter-ai/src/contexts/active-cell-context.tsx deleted file mode 100644 index d7791d93e..000000000 --- a/packages/jupyter-ai/src/contexts/active-cell-context.tsx +++ /dev/null @@ -1,332 +0,0 @@ -import React, { useState, useContext, useEffect } from 'react'; - -import { JupyterFrontEnd } from '@jupyterlab/application'; -import { DocumentWidget } from '@jupyterlab/docregistry'; -import { Notebook, NotebookActions } from '@jupyterlab/notebook'; -import { Cell } from '@jupyterlab/cells'; -import { IError as CellError } from '@jupyterlab/nbformat'; - -import { Widget } from '@lumino/widgets'; -import { Signal } from '@lumino/signaling'; - -function getNotebook(widget: Widget | null): Notebook | null { - if (!(widget instanceof DocumentWidget)) { - return null; - } - - const { content } = widget; - if (!(content instanceof Notebook)) { - return null; - } - - return content; -} - -function getActiveCell(widget: Widget | null): Cell | null { - const notebook = getNotebook(widget); - if (!notebook) { - return null; - } - - return notebook.activeCell; -} - -type CellContent = { - type: string; - source: string; -}; - -type CellWithErrorContent = { - type: 'code'; - source: string; - error: { - name: string; - value: string; - traceback: string[]; - }; -}; - -/** - * A manager that maintains a reference to the current active notebook cell in - * the main panel (if any), and provides methods for inserting or appending - * content to the active cell. - * - * The current active cell should be obtained by listening to the - * `activeCellChanged` signal. - */ -export class ActiveCellManager { - constructor(shell: JupyterFrontEnd.IShell) { - this._shell = shell; - this._shell.currentChanged?.connect((sender, args) => { - this._mainAreaWidget = args.newValue; - }); - - setInterval(() => { - this._pollActiveCell(); - }, 200); - } - - get activeCellChanged(): Signal { - return this._activeCellChanged; - } - - get activeCellErrorChanged(): Signal { - return this._activeCellErrorChanged; - } - - /** - * Returns an `ActiveCellContent` object that describes the current active - * cell. If no active cell exists, this method returns `null`. - * - * When called with `withError = true`, this method returns `null` if the - * active cell does not have an error output. Otherwise it returns an - * `ActiveCellContentWithError` object that describes both the active cell and - * the error output. - */ - getContent(withError: false): CellContent | null; - getContent(withError: true): CellWithErrorContent | null; - getContent(withError = false): CellContent | CellWithErrorContent | null { - const sharedModel = this._activeCell?.model.sharedModel; - if (!sharedModel) { - return null; - } - - // case where withError = false - if (!withError) { - return { - type: sharedModel.cell_type, - source: sharedModel.getSource() - }; - } - - // case where withError = true - const error = this._activeCellError; - if (error) { - return { - type: 'code', - source: sharedModel.getSource(), - error: { - name: error.ename, - value: error.evalue, - traceback: error.traceback - } - }; - } - - return null; - } - - /** - * Inserts `content` in a new cell above the active cell. - */ - insertAbove(content: string): void { - const notebook = getNotebook(this._mainAreaWidget); - if (!notebook) { - return; - } - - // create a new cell above the active cell and mark new cell as active - NotebookActions.insertAbove(notebook); - // emit activeCellChanged event to consumers - this._pollActiveCell(); - // replace content of this new active cell - this.replace(content); - } - - /** - * Inserts `content` in a new cell below the active cell. - */ - insertBelow(content: string): void { - const notebook = getNotebook(this._mainAreaWidget); - if (!notebook) { - return; - } - - // create a new cell below the active cell and mark new cell as active - NotebookActions.insertBelow(notebook); - // emit activeCellChanged event to consumers - this._pollActiveCell(); - // replace content of this new active cell - this.replace(content); - } - - /** - * Replaces the contents of the active cell. - */ - async replace(content: string): Promise { - // get reference to active cell directly from Notebook API. this avoids the - // possibility of acting on an out-of-date reference. - const activeCell = getNotebook(this._mainAreaWidget)?.activeCell; - if (!activeCell) { - return; - } - - // wait for editor to be ready - await activeCell.ready; - - // replace the content of the active cell - /** - * NOTE: calling this method sometimes emits an error to the browser console: - * - * ``` - * Error: Calls to EditorView.update are not allowed while an update is in progress - * ``` - * - * However, there seems to be no impact on the behavior/stability of the - * JupyterLab application after this error is logged. Furthermore, this is - * the official API for setting the content of a cell in JupyterLab 4, - * meaning that this is likely unavoidable. - */ - activeCell.editor?.model.sharedModel.setSource(content); - } - - protected _pollActiveCell(): void { - const prevActiveCell = this._activeCell; - const currActiveCell = getActiveCell(this._mainAreaWidget); - - // emit activeCellChanged when active cell changes - if (prevActiveCell !== currActiveCell) { - this._activeCell = currActiveCell; - this._activeCellChanged.emit(currActiveCell); - } - - const currSharedModel = currActiveCell?.model.sharedModel; - const prevExecutionCount = this._activeCellExecutionCount; - const currExecutionCount: number | null = - currSharedModel && 'execution_count' in currSharedModel - ? currSharedModel?.execution_count - : null; - this._activeCellExecutionCount = currExecutionCount; - - // emit activeCellErrorChanged when active cell changes or when the - // execution count changes - if ( - prevActiveCell !== currActiveCell || - prevExecutionCount !== currExecutionCount - ) { - const prevActiveCellError = this._activeCellError; - let currActiveCellError: CellError | null = null; - if (currSharedModel && 'outputs' in currSharedModel) { - currActiveCellError = - currSharedModel.outputs.find( - (output): output is CellError => output.output_type === 'error' - ) || null; - } - - // for some reason, the `CellError` object is not referentially stable, - // meaning that this condition always evaluates to `true` and the - // `activeCellErrorChanged` signal is emitted every 200ms, even when the - // error output is unchanged. this is why we have to rely on - // `execution_count` to track changes to the error output. - if (prevActiveCellError !== currActiveCellError) { - this._activeCellError = currActiveCellError; - this._activeCellErrorChanged.emit(this._activeCellError); - } - } - } - - protected _shell: JupyterFrontEnd.IShell; - protected _mainAreaWidget: Widget | null = null; - - /** - * The active cell. - */ - protected _activeCell: Cell | null = null; - /** - * The execution count of the active cell. This is the number shown on the - * left in square brackets after running a cell. Changes to this indicate that - * the error output may have changed. - */ - protected _activeCellExecutionCount: number | null = null; - /** - * The `CellError` output within the active cell, if any. - */ - protected _activeCellError: CellError | null = null; - - protected _activeCellChanged = new Signal(this); - protected _activeCellErrorChanged = new Signal(this); -} - -type ActiveCellContextReturn = { - exists: boolean; - hasError: boolean; - manager: ActiveCellManager; -}; - -type ActiveCellContextValue = { - exists: boolean; - hasError: boolean; - manager: ActiveCellManager | null; -}; - -const defaultActiveCellContext: ActiveCellContextValue = { - exists: false, - hasError: false, - manager: null -}; - -const ActiveCellContext = React.createContext( - defaultActiveCellContext -); - -type ActiveCellContextProps = { - activeCellManager: ActiveCellManager; - children: React.ReactNode; -}; - -/** - * NOTE: unused in v3-dev branch. - */ -export function ActiveCellContextProvider( - props: ActiveCellContextProps -): JSX.Element { - const [exists, setExists] = useState(false); - const [hasError, setHasError] = useState(false); - - useEffect(() => { - const manager = props.activeCellManager; - - manager.activeCellChanged.connect((_, newActiveCell) => { - setExists(!!newActiveCell); - }); - manager.activeCellErrorChanged.connect((_, newActiveCellError) => { - setHasError(!!newActiveCellError); - }); - }, [props.activeCellManager]); - - return ( - - {props.children} - - ); -} - -/** - * Usage: `const activeCell = useActiveCellContext()` - * - * Returns an object `activeCell` with the following properties: - * - `activeCell.exists`: whether an active cell exists - * - `activeCell.hasError`: whether an active cell exists with an error output - * - `activeCell.manager`: the `ActiveCellManager` singleton - */ -export function useActiveCellContext(): ActiveCellContextReturn { - const { exists, hasError, manager } = useContext(ActiveCellContext); - - if (!manager) { - throw new Error( - 'useActiveCellContext() cannot be called outside ActiveCellContextProvider.' - ); - } - - return { - exists, - hasError, - manager - }; -} diff --git a/packages/jupyter-ai/src/contexts/index.ts b/packages/jupyter-ai/src/contexts/index.ts index 8457bee31..479d11b15 100644 --- a/packages/jupyter-ai/src/contexts/index.ts +++ b/packages/jupyter-ai/src/contexts/index.ts @@ -1,3 +1 @@ -export * from './active-cell-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 deleted file mode 100644 index 42faad6b8..000000000 --- a/packages/jupyter-ai/src/contexts/selection-context.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { useCallback, useContext, useEffect, useState } from 'react'; -import { Selection, SelectionWatcher } from '../selection-watcher'; - -const SelectionContext = React.createContext< - [Selection | null, (value: Selection) => unknown] ->([ - null, - () => { - /* noop */ - } -]); - -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export function useSelectionContext() { - return useContext(SelectionContext); -} - -type SelectionContextProviderProps = { - selectionWatcher: SelectionWatcher; - children: React.ReactNode; -}; - -/** - * NOTE: unused in v3-dev branch. - */ -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -export function SelectionContextProvider({ - selectionWatcher, - children -}: SelectionContextProviderProps) { - const [selection, setSelection] = useState(null); - - /** - * Effect: subscribe to SelectionWatcher - */ - useEffect(() => { - selectionWatcher.selectionChanged.connect((sender, newSelection) => { - setSelection(newSelection); - }); - }, []); - - const replaceSelection = useCallback( - (value: Selection) => { - selectionWatcher.replaceSelection(value); - }, - [selectionWatcher] - ); - - return ( - - {children} - - ); -} diff --git a/packages/jupyter-ai/src/hooks/use-replace.ts b/packages/jupyter-ai/src/hooks/use-replace.ts deleted file mode 100644 index f4486b588..000000000 --- a/packages/jupyter-ai/src/hooks/use-replace.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { useActiveCellContext } from '../contexts/active-cell-context'; -import { useSelectionContext } from '../contexts/selection-context'; - -export type UseReplaceReturn = { - /** - * If a user has a range of text selected, this function replaces the - * selection range with `value`. Otherwise, if the user has an active notebook - * cell, this function replaces the contents of the active cell with `value`. - * - * Otherwise (if a user does not have a text selection or active cell), this - * function does nothing. - */ - replace: (value: string) => unknown; - /** - * Whether the replace button should be disabled, i.e. the user does not have - * a text selection or active cell. - */ - replaceDisabled: boolean; - /** - * Label that should be shown by the replace button using this hook. - */ - replaceLabel: string; -}; - -/** - * Hook that provides a function to either replace a text selection or an active - * cell. Manages related UI state. Should be used by any button that intends to - * replace some user selection. - */ -export function useReplace(): UseReplaceReturn { - const [textSelection, replaceTextSelection] = useSelectionContext(); - const activeCell = useActiveCellContext(); - - const replace = (value: string) => { - if (textSelection) { - replaceTextSelection({ ...textSelection, text: value }); - } else if (activeCell.exists) { - activeCell.manager.replace(value); - } - }; - - const replaceDisabled = !(textSelection || activeCell.exists); - - const numLines = textSelection?.text.split('\n').length || 0; - const replaceLabel = textSelection - ? `Replace selection (${numLines} ${numLines === 1 ? 'line' : 'lines'})` - : activeCell.exists - ? 'Replace selection (1 active cell)' - : 'Replace selection (no selection or active cell)'; - - return { - replace, - replaceDisabled, - replaceLabel - }; -} diff --git a/packages/jupyter-ai/src/selection-watcher.ts b/packages/jupyter-ai/src/selection-watcher.ts deleted file mode 100644 index 9cbb67f31..000000000 --- a/packages/jupyter-ai/src/selection-watcher.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { JupyterFrontEnd } from '@jupyterlab/application'; -import { DocumentWidget } from '@jupyterlab/docregistry'; -import { CodeEditor } from '@jupyterlab/codeeditor'; -import { CodeMirrorEditor } from '@jupyterlab/codemirror'; -import { FileEditor } from '@jupyterlab/fileeditor'; -import { Notebook } from '@jupyterlab/notebook'; - -import { find } from '@lumino/algorithm'; -import { Widget } from '@lumino/widgets'; -import { Signal } from '@lumino/signaling'; - -import { getCellIndex } from './utils'; - -/** - * Gets the editor instance used by a document widget. Returns `null` if unable. - */ -export function getEditor( - widget: Widget | null -): CodeMirrorEditor | null | undefined { - if (!(widget instanceof DocumentWidget)) { - return null; - } - - let editor: CodeEditor.IEditor | null | undefined; - const { content } = widget; - - if (content instanceof FileEditor) { - editor = content.editor; - } else if (content instanceof Notebook) { - editor = content.activeCell?.editor; - } - - if (!(editor instanceof CodeMirrorEditor)) { - return undefined; - } - - return editor; -} - -/** - * Gets a Selection object from a document widget. Returns `null` if unable. - */ -function getTextSelection(widget: Widget | null): Selection | null { - const editor = getEditor(widget); - // widget type check is redundant but hints the type to TypeScript - if (!editor || !(widget instanceof DocumentWidget)) { - return null; - } - - let cellId: string | undefined = undefined; - if (widget.content instanceof Notebook) { - cellId = widget.content.activeCell?.model.id; - } - - const selectionObj = editor.getSelection(); - let { start, end } = selectionObj; - const startOffset = editor.getOffsetAt(start); - const endOffset = editor.getOffsetAt(end); - const text = editor.model.sharedModel - .getSource() - .substring(startOffset, endOffset); - - // Do not return a Selection object if no text is selected - if (!text) { - return null; - } - - // ensure start <= end - // required for editor.model.sharedModel.updateSource() - if (startOffset > endOffset) { - [start, end] = [end, start]; - } - - return { - ...selectionObj, - start, - end, - text, - numLines: text.split('\n').length, - widgetId: widget.id, - ...(cellId && { - cellId - }) - }; -} - -export type Selection = CodeEditor.ITextSelection & { - /** - * The text within the selection as a string. - */ - text: string; - /** - * Number of lines contained by the text selection. - */ - numLines: number; - /** - * The ID of the document widget in which the selection was made. - */ - widgetId: string; - /** - * The ID of the cell in which the selection was made, if the original widget - * was a notebook. - */ - cellId?: string; -}; - -export class SelectionWatcher { - constructor(shell: JupyterFrontEnd.IShell) { - this._shell = shell; - this._shell.currentChanged?.connect((sender, args) => { - this._mainAreaWidget = args.newValue; - }); - - setInterval(this._poll.bind(this), 200); - } - - get selection(): Selection | null { - return this._selection; - } - - get selectionChanged(): Signal { - return this._selectionChanged; - } - - replaceSelection(selection: Selection): void { - // unfortunately shell.currentWidget doesn't update synchronously after - // shell.activateById(), which is why we have to get a reference to the - // widget manually. - const widget = find( - this._shell.widgets(), - widget => widget.id === selection.widgetId - ); - if (!(widget instanceof DocumentWidget)) { - return; - } - - // activate the widget if not already active - this._shell.activateById(selection.widgetId); - - // activate notebook cell if specified - if (widget.content instanceof Notebook && selection.cellId) { - const cellIndex = getCellIndex(widget.content, selection.cellId); - if (cellIndex !== -1) { - widget.content.activeCellIndex = cellIndex; - } - } - - // get editor instance - const editor = getEditor(widget); - if (!editor) { - return; - } - - editor.model.sharedModel.updateSource( - editor.getOffsetAt(selection.start), - editor.getOffsetAt(selection.end), - selection.text - ); - const newPosition = editor.getPositionAt( - editor.getOffsetAt(selection.start) + selection.text.length - ); - editor.setSelection({ start: newPosition, end: newPosition }); - } - - protected _poll(): void { - const prevSelection = this._selection; - const currSelection = getTextSelection(this._mainAreaWidget); - - if (prevSelection?.text === currSelection?.text) { - return; - } - - this._selection = currSelection; - this._selectionChanged.emit(currSelection); - } - - protected _shell: JupyterFrontEnd.IShell; - protected _mainAreaWidget: Widget | null = null; - protected _selection: Selection | null = null; - protected _selectionChanged = new Signal(this); -} diff --git a/packages/jupyter-ai/src/utils.ts b/packages/jupyter-ai/src/utils.ts index e21b47c6b..8790a90d6 100644 --- a/packages/jupyter-ai/src/utils.ts +++ b/packages/jupyter-ai/src/utils.ts @@ -1,39 +1,34 @@ /** * Contains various utility functions shared throughout the project. */ -import { Notebook } from '@jupyterlab/notebook'; -import { FileEditor } from '@jupyterlab/fileeditor'; import { CodeEditor } from '@jupyterlab/codeeditor'; +import { CodeMirrorEditor } from '@jupyterlab/codemirror'; +import { DocumentWidget } from '@jupyterlab/docregistry'; +import { FileEditor } from '@jupyterlab/fileeditor'; +import { Notebook } from '@jupyterlab/notebook'; import { Widget } from '@lumino/widgets'; /** - * Get text selection from an editor widget (DocumentWidget#content). + * Gets the editor instance used by a document widget. Returns `null` if unable. */ -export function getTextSelection(widget: Widget): string { - const editor = getEditor(widget); - if (!editor) { - return ''; +export function getEditor( + widget: Widget | null +): CodeMirrorEditor | null | undefined { + if (!(widget instanceof DocumentWidget)) { + return null; } - const selectionObj = editor.getSelection(); - const start = editor.getOffsetAt(selectionObj.start); - const end = editor.getOffsetAt(selectionObj.end); - const text = editor.model.sharedModel.getSource().substring(start, end); + let editor: CodeEditor.IEditor | null | undefined; + const { content } = widget; - return text; -} + if (content instanceof FileEditor) { + editor = content.editor; + } else if (content instanceof Notebook) { + editor = content.activeCell?.editor; + } -/** - * Get editor instance from an editor widget (i.e. `DocumentWidget#content`). - */ -export function getEditor( - widget: Widget -): CodeEditor.IEditor | null | undefined { - let editor: CodeEditor.IEditor | null | undefined; - if (widget instanceof FileEditor) { - editor = widget.editor; - } else if (widget instanceof Notebook) { - editor = widget.activeCell?.editor; + if (!(editor instanceof CodeMirrorEditor)) { + return undefined; } return editor; From 6a02df5ede96f3bae0d725760540676dbae179d4 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Thu, 19 Dec 2024 13:18:38 +0100 Subject: [PATCH 4/9] Remove chat handler --- packages/jupyter-ai/src/chat_handler.ts | 270 ------------------------ packages/jupyter-ai/src/index.ts | 16 +- packages/jupyter-ai/src/tokens.ts | 15 -- 3 files changed, 2 insertions(+), 299 deletions(-) delete mode 100644 packages/jupyter-ai/src/chat_handler.ts diff --git a/packages/jupyter-ai/src/chat_handler.ts b/packages/jupyter-ai/src/chat_handler.ts deleted file mode 100644 index e1b1e332c..000000000 --- a/packages/jupyter-ai/src/chat_handler.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { IDisposable } from '@lumino/disposable'; -import { ServerConnection } from '@jupyterlab/services'; -import { URLExt } from '@jupyterlab/coreutils'; -import { Signal } from '@lumino/signaling'; - -import { AiService } from './handler'; - -const CHAT_SERVICE_URL = 'api/ai/chats'; - -export class ChatHandler implements IDisposable { - /** - * The server settings used to make API requests. - */ - readonly serverSettings: ServerConnection.ISettings; - - /** - * ID of the connection. Requires `await initialize()`. - */ - id = ''; - - /** - * Create a new chat handler. - */ - constructor(options: AiService.IOptions = {}) { - this.serverSettings = - options.serverSettings ?? ServerConnection.makeSettings(); - } - - /** - * Initializes the WebSocket connection to the Chat backend. Promise is - * resolved when server acknowledges connection and sends the client ID. This - * must be awaited before calling any other method. - */ - public async initialize(): Promise { - await this._initialize(); - } - - /** - * Sends a message across the WebSocket. Promise resolves to the message ID - * when the server sends the same message back, acknowledging receipt. - */ - public sendMessage(message: AiService.Request): Promise { - return new Promise(resolve => { - this._socket?.send(JSON.stringify(message)); - this._sendResolverQueue.push(resolve); - }); - } - - /** - * Returns a Promise that resolves to the agent's reply, given the message ID - * of the human message. Should only be called once per message. - */ - public replyFor(messageId: string): Promise { - return new Promise(resolve => { - this._replyForResolverDict[messageId] = resolve; - }); - } - - public addListener(handler: (message: AiService.Message) => void): void { - this._listeners.push(handler); - } - - public removeListener(handler: (message: AiService.Message) => void): void { - const index = this._listeners.indexOf(handler); - if (index > -1) { - this._listeners.splice(index, 1); - } - } - - /** - * Whether the chat handler is disposed. - */ - get isDisposed(): boolean { - return this._isDisposed; - } - - /** - * Dispose the chat handler. - */ - dispose(): void { - if (this.isDisposed) { - return; - } - this._isDisposed = true; - this._listeners = []; - - // Clean up socket. - const socket = this._socket; - if (socket) { - this._socket = null; - socket.onopen = () => undefined; - socket.onerror = () => undefined; - socket.onmessage = () => undefined; - socket.onclose = () => undefined; - socket.close(); - } - } - - get history(): AiService.ChatHistory { - return { - messages: this._messages, - pending_messages: this._pendingMessages - }; - } - - get historyChanged(): Signal { - return this._historyChanged; - } - - private _onMessage(newMessage: AiService.Message): void { - // resolve promise from `sendMessage()` - if (newMessage.type === 'human' && newMessage.client.id === this.id) { - this._sendResolverQueue.shift()?.(newMessage.id); - } - - // resolve promise from `replyFor()` if it exists - if ( - newMessage.type === 'agent' && - newMessage.reply_to in this._replyForResolverDict - ) { - this._replyForResolverDict[newMessage.reply_to](newMessage); - delete this._replyForResolverDict[newMessage.reply_to]; - } - - // call listeners in serial - this._listeners.forEach(listener => listener(newMessage)); - - // append message to chat history. this block should always set `_messages` - // or `_pendingMessages` to a new array instance rather than modifying - // in-place so consumer React components re-render. - switch (newMessage.type) { - case 'connection': - break; - case 'clear': - if (newMessage.targets) { - const targets = newMessage.targets; - this._messages = this._messages.filter( - msg => - !targets.includes(msg.id) && - !('reply_to' in msg && targets.includes(msg.reply_to)) - ); - this._pendingMessages = this._pendingMessages.filter( - msg => !targets.includes(msg.reply_to) - ); - } else { - this._messages = []; - this._pendingMessages = []; - } - break; - case 'pending': - this._pendingMessages = [...this._pendingMessages, newMessage]; - break; - case 'close-pending': - this._pendingMessages = this._pendingMessages.filter( - p => p.id !== newMessage.id - ); - break; - case 'agent-stream-chunk': { - const target = newMessage.id; - const streamMessage = this._messages.find( - (m): m is AiService.AgentStreamMessage => - m.type === 'agent-stream' && m.id === target - ); - if (!streamMessage) { - console.error( - `Received stream chunk with ID ${target}, but no agent-stream message with that ID exists. ` + - 'Ignoring this stream chunk.' - ); - break; - } - - streamMessage.body += newMessage.content; - streamMessage.metadata = newMessage.metadata; - if (newMessage.stream_complete) { - streamMessage.complete = true; - } - this._messages = [...this._messages]; - break; - } - default: - // human or agent chat message - this._messages = [...this._messages, newMessage]; - break; - } - - // finally, trigger `historyChanged` signal - this._historyChanged.emit({ - messages: this._messages, - pending_messages: this._pendingMessages - }); - } - - /** - * Queue of Promise resolvers pushed onto by `send()` - */ - private _sendResolverQueue: ((value: string) => void)[] = []; - - /** - * Dictionary mapping message IDs to Promise resolvers, inserted into by - * `replyFor()`. - */ - private _replyForResolverDict: Record< - string, - (value: AiService.AgentChatMessage) => void - > = {}; - - private _onClose(e: CloseEvent, reject: any) { - reject(new Error('Chat UI websocket disconnected')); - console.error('Chat UI websocket disconnected'); - // only attempt re-connect if there was an abnormal closure - // WebSocket status codes defined in RFC 6455: https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1 - if (e.code === 1006) { - const delaySeconds = 1; - console.info(`Will try to reconnect in ${delaySeconds} s.`); - setTimeout(async () => await this._initialize(), delaySeconds * 1000); - } - } - - private _initialize(): Promise { - return new Promise((resolve, reject) => { - if (this.isDisposed) { - return; - } - console.log('Creating a new websocket connection for chat...'); - const { token, WebSocket, wsUrl } = this.serverSettings; - const url = - URLExt.join(wsUrl, CHAT_SERVICE_URL) + - (token ? `?token=${encodeURIComponent(token)}` : ''); - - const socket = (this._socket = new WebSocket(url)); - socket.onclose = e => this._onClose(e, reject); - socket.onerror = e => reject(e); - socket.onmessage = msg => - msg.data && this._onMessage(JSON.parse(msg.data)); - - const listenForConnection = (message: AiService.Message) => { - if (message.type !== 'connection') { - return; - } - this.id = message.client_id; - - // initialize chat history from `ConnectionMessage` - this._messages = message.history.messages; - this._pendingMessages = message.history.pending_messages; - - resolve(); - this.removeListener(listenForConnection); - }; - - this.addListener(listenForConnection); - }); - } - - private _isDisposed = false; - private _socket: WebSocket | null = null; - private _listeners: ((msg: any) => void)[] = []; - - /** - * The list of chat messages - */ - private _messages: AiService.ChatMessage[] = []; - private _pendingMessages: AiService.PendingMessage[] = []; - - /** - * Signal for when the chat history is changed. Components rendering the chat - * history should subscribe to this signal and update their state when this - * signal is triggered. - */ - private _historyChanged = new Signal(this); -} diff --git a/packages/jupyter-ai/src/index.ts b/packages/jupyter-ai/src/index.ts index 6f0f5c03f..14528b51b 100644 --- a/packages/jupyter-ai/src/index.ts +++ b/packages/jupyter-ai/src/index.ts @@ -13,11 +13,10 @@ import { import { IDocumentWidget } from '@jupyterlab/docregistry'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; -import { ChatHandler } from './chat_handler'; import { completionPlugin } from './completions'; import { autocompletion } from './slash-autocompletion'; import { statusItemPlugin } from './status'; -import { IJaiCompletionProvider, IJaiCore } from './tokens'; +import { IJaiCompletionProvider } from './tokens'; import { buildErrorWidget } from './widgets/chat-error'; import { buildAiSettings } from './widgets/settings-widget'; @@ -33,12 +32,11 @@ export namespace CommandIDs { /** * Initialization data for the jupyter_ai extension. */ -const plugin: JupyterFrontEndPlugin = { +const plugin: JupyterFrontEndPlugin = { id: '@jupyter-ai/core:plugin', autoStart: true, requires: [IRenderMimeRegistry], optional: [ICommandPalette, IThemeManager, IJaiCompletionProvider], - provides: IJaiCore, activate: async ( app: JupyterFrontEnd, rmRegistry: IRenderMimeRegistry, @@ -46,11 +44,6 @@ const plugin: JupyterFrontEndPlugin = { themeManager: IThemeManager | null, completionProvider: IJaiCompletionProvider | null ) => { - /** - * Initialize chat handler, open WS connection - */ - const chatHandler = new ChatHandler(); - const openInlineCompleterSettings = () => { app.commands.execute('settingeditor:open', { query: 'Inline Completer' @@ -61,7 +54,6 @@ const plugin: JupyterFrontEndPlugin = { let aiSettings: MainAreaWidget; let settingsWidget: ReactWidget; try { - await chatHandler.initialize(); settingsWidget = buildAiSettings( rmRegistry, completionProvider, @@ -94,10 +86,6 @@ const plugin: JupyterFrontEndPlugin = { command: CommandIDs.openAiSettings }); } - - return { - chatHandler - }; } }; diff --git a/packages/jupyter-ai/src/tokens.ts b/packages/jupyter-ai/src/tokens.ts index 4ad409198..d7ec4a146 100644 --- a/packages/jupyter-ai/src/tokens.ts +++ b/packages/jupyter-ai/src/tokens.ts @@ -4,7 +4,6 @@ import { ISignal } from '@lumino/signaling'; import type { IRankedMenu } from '@jupyterlab/ui-components'; import { AiService } from './handler'; -import { ChatHandler } from './chat_handler'; export interface IJaiStatusItem { addItem(item: IRankedMenu.IItemOptions): void; @@ -49,20 +48,6 @@ export const IJaiMessageFooter = new Token( 'Optional component that is used to render a footer on each Jupyter AI chat message, when provided.' ); -export interface IJaiCore { - chatHandler: ChatHandler; -} - -/** - * The Jupyter AI core provider token. Frontend plugins that want to extend the - * Jupyter AI frontend by adding features which send messages or observe the - * current text selection & active cell should require this plugin. - */ -export const IJaiCore = new Token( - 'jupyter_ai:core', - 'The core implementation of the frontend.' -); - /** * An object that describes an interaction event from the user. * From b4e17d6098427bd0f79be04437823b13d9bc4a57 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Thu, 19 Dec 2024 13:22:33 +0100 Subject: [PATCH 5/9] Follow up 'Remove chat-message-menu (should be ported in jupyter-chat)' commit --- packages/jupyter-ai/src/hooks/use-copy.ts | 89 ----------------------- 1 file changed, 89 deletions(-) delete mode 100644 packages/jupyter-ai/src/hooks/use-copy.ts diff --git a/packages/jupyter-ai/src/hooks/use-copy.ts b/packages/jupyter-ai/src/hooks/use-copy.ts deleted file mode 100644 index 62f748718..000000000 --- a/packages/jupyter-ai/src/hooks/use-copy.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { useState, useRef, useCallback } from 'react'; - -export enum CopyStatus { - None, - Copying, - Copied -} - -export type UseCopyProps = { - /** - * List of labels by copy status. Used to override the default labels provided - * by this hook. - */ - labelOverrides?: Partial>; -}; - -export type UseCopyReturn = { - /** - * The status of the copy operation. This is set to CopyStatus.None when no - * copy operation was performed, set to CopyStatus.Copying while the copy - * operation is executing, and set to CopyStatus.Copied for 1000ms after the - * copy operation completes. - * - */ - copyStatus: CopyStatus; - /** - * Label that should be shown by the copy button based on the copy status. - * This can be selectively overridden via the `labelOverrides` prop passed to - * the `useCopy()` hook. - */ - copyLabel: string; - /** - * Function that takes a string and copies it to the clipboard. - */ - copy: (value: string) => unknown; -}; - -const DEFAULT_LABELS_BY_COPY_STATUS: Record = { - [CopyStatus.None]: 'Copy to clipboard', - [CopyStatus.Copying]: 'Copying…', - [CopyStatus.Copied]: 'Copied!' -}; - -/** - * Hook that provides a function to copy a string to a clipboard and manages - * related UI state. Should be used by any button that intends to copy text. - */ -export function useCopy(props?: UseCopyProps): UseCopyReturn { - const [copyStatus, setCopyStatus] = useState(CopyStatus.None); - const timeoutId = useRef(null); - - const copy = useCallback( - async (value: string) => { - // ignore if we are already copying - if (copyStatus === CopyStatus.Copying) { - return; - } - - try { - await navigator.clipboard.writeText(value); - } catch (err) { - console.error('Failed to copy text: ', err); - setCopyStatus(CopyStatus.None); - return; - } - - setCopyStatus(CopyStatus.Copied); - if (timeoutId.current) { - clearTimeout(timeoutId.current); - } - timeoutId.current = setTimeout( - () => setCopyStatus(CopyStatus.None), - 1000 - ); - }, - [copyStatus] - ); - - const copyLabel = { - ...DEFAULT_LABELS_BY_COPY_STATUS, - ...props?.labelOverrides - }[copyStatus]; - - return { - copyStatus, - copyLabel, - copy - }; -} From bf5c283cb9817bb7dd1f8705dce1b3192a9a16d5 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Thu, 19 Dec 2024 13:47:22 +0100 Subject: [PATCH 6/9] Clean package.json --- packages/jupyter-ai/package.json | 2 - yarn.lock | 323 ++----------------------------- 2 files changed, 15 insertions(+), 310 deletions(-) diff --git a/packages/jupyter-ai/package.json b/packages/jupyter-ai/package.json index d0c899662..e105f1752 100644 --- a/packages/jupyter-ai/package.json +++ b/packages/jupyter-ai/package.json @@ -62,10 +62,8 @@ "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", "@jupyter/chat": "^0.6.0", - "@jupyter/collaboration": "^1", "@jupyterlab/application": "^4.2.0", "@jupyterlab/apputils": "^4.2.0", - "@jupyterlab/cells": "^4.2.0", "@jupyterlab/codeeditor": "^4.2.0", "@jupyterlab/codemirror": "^4.2.0", "@jupyterlab/completer": "^4.2.0", diff --git a/yarn.lock b/yarn.lock index dea68e659..8d21ce969 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1615,14 +1615,14 @@ __metadata: languageName: node linkType: hard -"@codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.2.0, @codemirror/state@npm:^6.4.0, @codemirror/state@npm:^6.4.1": +"@codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.4.0, @codemirror/state@npm:^6.4.1": version: 6.4.1 resolution: "@codemirror/state@npm:6.4.1" checksum: b81b55574091349eed4d32fc0eadb0c9688f1f7c98b681318f59138ee0f527cb4c4a97831b70547c0640f02f3127647838ae6730782de4a3dd2cc58836125d01 languageName: node linkType: hard -"@codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.26.0, @codemirror/view@npm:^6.27.0, @codemirror/view@npm:^6.7.0": +"@codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.26.0, @codemirror/view@npm:^6.27.0": version: 6.28.0 resolution: "@codemirror/view@npm:6.28.0" dependencies: @@ -2221,11 +2221,9 @@ __metadata: "@emotion/react": ^11.10.5 "@emotion/styled": ^11.10.5 "@jupyter/chat": ^0.6.0 - "@jupyter/collaboration": ^1 "@jupyterlab/application": ^4.2.0 "@jupyterlab/apputils": ^4.2.0 "@jupyterlab/builder": ^4.2.0 - "@jupyterlab/cells": ^4.2.0 "@jupyterlab/codeeditor": ^4.2.0 "@jupyterlab/codemirror": ^4.2.0 "@jupyterlab/completer": ^4.2.0 @@ -2311,44 +2309,6 @@ __metadata: languageName: node linkType: hard -"@jupyter/collaboration@npm:^1": - version: 1.2.1 - resolution: "@jupyter/collaboration@npm:1.2.1" - dependencies: - "@codemirror/state": ^6.2.0 - "@codemirror/view": ^6.7.0 - "@jupyter/docprovider": ^1.2.1 - "@jupyterlab/apputils": ^4.0.0 - "@jupyterlab/coreutils": ^6.0.0 - "@jupyterlab/services": ^7.0.0 - "@jupyterlab/ui-components": ^4.0.0 - "@lumino/coreutils": ^2.1.0 - "@lumino/virtualdom": ^2.0.0 - "@lumino/widgets": ^2.1.0 - react: ^18.2.0 - y-protocols: ^1.0.5 - yjs: ^13.5.40 - checksum: f38885112c337415df963782653866a058b68d1509faed9ca092c3d729cd27cdecb3e36365e49b78135d626a4d76c7c5479601a997f76142d61e7edf41015fc3 - languageName: node - linkType: hard - -"@jupyter/docprovider@npm:^1.2.1": - version: 1.2.1 - resolution: "@jupyter/docprovider@npm:1.2.1" - dependencies: - "@jupyter/ydoc": ^1.0.2 - "@jupyterlab/coreutils": ^6.0.0 - "@jupyterlab/services": ^7.0.0 - "@lumino/coreutils": ^2.1.0 - "@lumino/disposable": ^2.1.0 - "@lumino/signaling": ^2.1.0 - y-protocols: ^1.0.5 - y-websocket: ^1.3.15 - yjs: ^13.5.40 - checksum: 5a8ae37ec44f39754e30f0d7d697a0147bc15b2e9fe37aa98770971dfad77724cf83177220841e4a1ad21a2f5b021fc21bac95499e6476b281ec9491fd3a89b1 - languageName: node - linkType: hard - "@jupyter/react-components@npm:^0.15.2, @jupyter/react-components@npm:^0.15.3": version: 0.15.3 resolution: "@jupyter/react-components@npm:0.15.3" @@ -2372,20 +2332,6 @@ __metadata: languageName: node linkType: hard -"@jupyter/ydoc@npm:^1.0.2": - version: 1.1.1 - resolution: "@jupyter/ydoc@npm:1.1.1" - dependencies: - "@jupyterlab/nbformat": ^3.0.0 || ^4.0.0-alpha.21 || ^4.0.0 - "@lumino/coreutils": ^1.11.0 || ^2.0.0 - "@lumino/disposable": ^1.10.0 || ^2.0.0 - "@lumino/signaling": ^1.10.0 || ^2.0.0 - y-protocols: ^1.0.5 - yjs: ^13.5.40 - checksum: a239b1dd57cfc9ba36c06ac5032a1b6388849ae01a1d0db0d45094f71fdadf4d473b4bf8becbef0cfcdc85cae505361fbec0822b02da5aa48e06b66f742dd7a0 - languageName: node - linkType: hard - "@jupyter/ydoc@npm:^2.0.1": version: 2.0.1 resolution: "@jupyter/ydoc@npm:2.0.1" @@ -2428,7 +2374,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/apputils@npm:^4.0.0, @jupyterlab/apputils@npm:^4.2.0, @jupyterlab/apputils@npm:^4.3.2": +"@jupyterlab/apputils@npm:^4.2.0, @jupyterlab/apputils@npm:^4.3.2": version: 4.3.2 resolution: "@jupyterlab/apputils@npm:4.3.2" dependencies: @@ -2541,7 +2487,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/cells@npm:^4.2.0, @jupyterlab/cells@npm:^4.2.2": +"@jupyterlab/cells@npm:^4.2.2": version: 4.2.2 resolution: "@jupyterlab/cells@npm:4.2.2" dependencies: @@ -2671,7 +2617,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/coreutils@npm:^6.0.0, @jupyterlab/coreutils@npm:^6.2.0, @jupyterlab/coreutils@npm:^6.2.2": +"@jupyterlab/coreutils@npm:^6.2.0, @jupyterlab/coreutils@npm:^6.2.2": version: 6.2.2 resolution: "@jupyterlab/coreutils@npm:6.2.2" dependencies: @@ -2990,7 +2936,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/services@npm:^7.0.0, @jupyterlab/services@npm:^7.2.0, @jupyterlab/services@npm:^7.2.2": +"@jupyterlab/services@npm:^7.2.0, @jupyterlab/services@npm:^7.2.2": version: 7.2.2 resolution: "@jupyterlab/services@npm:7.2.2" dependencies: @@ -3208,7 +3154,7 @@ __metadata: languageName: node linkType: hard -"@jupyterlab/ui-components@npm:^4.0.0, @jupyterlab/ui-components@npm:^4.2.0, @jupyterlab/ui-components@npm:^4.2.2": +"@jupyterlab/ui-components@npm:^4.2.0, @jupyterlab/ui-components@npm:^4.2.2": version: 4.2.2 resolution: "@jupyterlab/ui-components@npm:4.2.2" dependencies: @@ -3593,7 +3539,7 @@ __metadata: languageName: node linkType: hard -"@lumino/coreutils@npm:^1.11.0 || ^2.0.0, @lumino/coreutils@npm:^1.11.0 || ^2.1.2, @lumino/coreutils@npm:^2.1.0, @lumino/coreutils@npm:^2.1.2": +"@lumino/coreutils@npm:^1.11.0 || ^2.0.0, @lumino/coreutils@npm:^1.11.0 || ^2.1.2, @lumino/coreutils@npm:^2.1.2": version: 2.1.2 resolution: "@lumino/coreutils@npm:2.1.2" checksum: 7865317ac0676b448d108eb57ab5d8b2a17c101995c0f7a7106662d9fe6c859570104525f83ee3cda12ae2e326803372206d6f4c1f415a5b59e4158a7b81066f @@ -3609,7 +3555,7 @@ __metadata: languageName: node linkType: hard -"@lumino/disposable@npm:^1.10.0 || ^2.0.0, @lumino/disposable@npm:^2.1.0, @lumino/disposable@npm:^2.1.2": +"@lumino/disposable@npm:^1.10.0 || ^2.0.0, @lumino/disposable@npm:^2.1.2": version: 2.1.2 resolution: "@lumino/disposable@npm:2.1.2" dependencies: @@ -3693,7 +3639,7 @@ __metadata: languageName: node linkType: hard -"@lumino/signaling@npm:^1.10.0 || ^2.0.0, @lumino/signaling@npm:^2.1.0, @lumino/signaling@npm:^2.1.2": +"@lumino/signaling@npm:^1.10.0 || ^2.0.0, @lumino/signaling@npm:^2.1.2": version: 2.1.2 resolution: "@lumino/signaling@npm:2.1.2" dependencies: @@ -3713,7 +3659,7 @@ __metadata: languageName: node linkType: hard -"@lumino/virtualdom@npm:^2.0.0, @lumino/virtualdom@npm:^2.0.1": +"@lumino/virtualdom@npm:^2.0.1": version: 2.0.1 resolution: "@lumino/virtualdom@npm:2.0.1" dependencies: @@ -3731,7 +3677,7 @@ __metadata: languageName: node linkType: hard -"@lumino/widgets@npm:^1.37.2 || ^2.3.2, @lumino/widgets@npm:^2.1.0, @lumino/widgets@npm:^2.3.2": +"@lumino/widgets@npm:^1.37.2 || ^2.3.2, @lumino/widgets@npm:^2.3.2": version: 2.3.2 resolution: "@lumino/widgets@npm:2.3.2" dependencies: @@ -5249,19 +5195,6 @@ __metadata: languageName: node linkType: hard -"abstract-leveldown@npm:^6.2.1, abstract-leveldown@npm:~6.2.1, abstract-leveldown@npm:~6.2.3": - version: 6.2.3 - resolution: "abstract-leveldown@npm:6.2.3" - dependencies: - buffer: ^5.5.0 - immediate: ^3.2.3 - level-concat-iterator: ~2.0.0 - level-supports: ~1.0.0 - xtend: ~4.0.0 - checksum: 00202b2eb7955dd7bc04f3e44d225e60160cedb8f96fe6ae0e6dca9c356d57071f001ece8ae1d53f48095c4c036d92b3440f2bc7666730610ddea030f9fbde4a - languageName: node - linkType: hard - "acorn-globals@npm:^7.0.0": version: 7.0.1 resolution: "acorn-globals@npm:7.0.1" @@ -5597,13 +5530,6 @@ __metadata: languageName: node linkType: hard -"async-limiter@npm:~1.0.0": - version: 1.0.1 - resolution: "async-limiter@npm:1.0.1" - checksum: 2b849695b465d93ad44c116220dee29a5aeb63adac16c1088983c339b0de57d76e82533e8e364a93a9f997f28bbfc6a92948cefc120652bd07f3b59f8d75cf2b - languageName: node - linkType: hard - "async@npm:^3.2.3": version: 3.2.5 resolution: "async@npm:3.2.5" @@ -5891,7 +5817,7 @@ __metadata: languageName: node linkType: hard -"buffer@npm:^5.5.0, buffer@npm:^5.6.0": +"buffer@npm:^5.5.0": version: 5.7.1 resolution: "buffer@npm:5.7.1" dependencies: @@ -6803,16 +6729,6 @@ __metadata: languageName: node linkType: hard -"deferred-leveldown@npm:~5.3.0": - version: 5.3.0 - resolution: "deferred-leveldown@npm:5.3.0" - dependencies: - abstract-leveldown: ~6.2.1 - inherits: ^2.0.3 - checksum: 5631e153528bb9de1aa60d59a5065d1a519374c5e4c1d486f2190dba4008dcf5c2ee8dd7f2f81396fc4d5a6bb6e7d0055e3dfe68afe00da02adaa3bf329addf7 - languageName: node - linkType: hard - "define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.2": version: 1.1.2 resolution: "define-data-property@npm:1.1.2" @@ -7073,18 +6989,6 @@ __metadata: languageName: node linkType: hard -"encoding-down@npm:^6.3.0": - version: 6.3.0 - resolution: "encoding-down@npm:6.3.0" - dependencies: - abstract-leveldown: ^6.2.1 - inherits: ^2.0.3 - level-codec: ^9.0.0 - level-errors: ^2.0.0 - checksum: 74043e6d9061a470614ff61d708c849259ab32932a428fd5ddfb0878719804f56a52f59b31cccd95fddc2e636c0fd22dc3e02481fb98d5bf1bdbbbc44ca09bdc - languageName: node - linkType: hard - "encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" @@ -7152,17 +7056,6 @@ __metadata: languageName: node linkType: hard -"errno@npm:~0.1.1": - version: 0.1.8 - resolution: "errno@npm:0.1.8" - dependencies: - prr: ~1.0.1 - bin: - errno: cli.js - checksum: 1271f7b9fbb3bcbec76ffde932485d1e3561856d21d847ec613a9722ee924cdd4e523a62dc71a44174d91e898fe21fdc8d5b50823f4b5e0ce8c35c8271e6ef4a - languageName: node - linkType: hard - "error-ex@npm:^1.3.1": version: 1.3.2 resolution: "error-ex@npm:1.3.2" @@ -8579,13 +8472,6 @@ __metadata: languageName: node linkType: hard -"immediate@npm:^3.2.3": - version: 3.3.0 - resolution: "immediate@npm:3.3.0" - checksum: 634b4305101e2452eba6c07d485bf3e415995e533c94b9c3ffbc37026fa1be34def6e4f2276b0dc2162a3f91628564a4bfb26280278b89d3ee54624e854d2f5f - languageName: node - linkType: hard - "import-fresh@npm:^3.0.0, import-fresh@npm:^3.2.1": version: 3.3.0 resolution: "import-fresh@npm:3.3.0" @@ -9978,109 +9864,6 @@ __metadata: languageName: node linkType: hard -"level-codec@npm:^9.0.0": - version: 9.0.2 - resolution: "level-codec@npm:9.0.2" - dependencies: - buffer: ^5.6.0 - checksum: 289003d51b8afcdd24c4d318606abf2bae81975e4b527d7349abfdbacc8fef26711f2f24e2d20da0e1dce0bb216a856c9433ccb9ca25fa78a96aed9f51e506ed - languageName: node - linkType: hard - -"level-concat-iterator@npm:~2.0.0": - version: 2.0.1 - resolution: "level-concat-iterator@npm:2.0.1" - checksum: 562583ef1292215f8e749c402510cb61c4d6fccf4541082b3d21dfa5ecde9fcccfe52bdcb5cfff9d2384e7ce5891f44df9439a6ddb39b0ffe31015600b4a828a - languageName: node - linkType: hard - -"level-errors@npm:^2.0.0, level-errors@npm:~2.0.0": - version: 2.0.1 - resolution: "level-errors@npm:2.0.1" - dependencies: - errno: ~0.1.1 - checksum: aca5d7670e2a40609db8d7743fce289bb5202c0bc13e4a78f81f36a6642e9abc0110f48087d3d3c2c04f023d70d4ee6f2db0e20c63d29b3fda323a67bfff6526 - languageName: node - linkType: hard - -"level-iterator-stream@npm:~4.0.0": - version: 4.0.2 - resolution: "level-iterator-stream@npm:4.0.2" - dependencies: - inherits: ^2.0.4 - readable-stream: ^3.4.0 - xtend: ^4.0.2 - checksum: 239e2c7e62bffb485ed696bcd3b98de7a2bc455d13be4fce175ae3544fe9cda81c2ed93d3e88b61380ae6d28cce02511862d77b86fb2ba5b5cf00471f3c1eccc - languageName: node - linkType: hard - -"level-js@npm:^5.0.0": - version: 5.0.2 - resolution: "level-js@npm:5.0.2" - dependencies: - abstract-leveldown: ~6.2.3 - buffer: ^5.5.0 - inherits: ^2.0.3 - ltgt: ^2.1.2 - checksum: 3c7f75979bb8c042e95a58245b8fe1230bb0f56a11ee418e08156e3eadda371efae6eb7b9bf10bf1e08e0b1b2a25d80c026858ca99ffd49109d6541e3d9d3b37 - languageName: node - linkType: hard - -"level-packager@npm:^5.1.0": - version: 5.1.1 - resolution: "level-packager@npm:5.1.1" - dependencies: - encoding-down: ^6.3.0 - levelup: ^4.3.2 - checksum: befe2aa54f2010a6ecf7ddce392c8dee225e1839205080a2704d75e560e28b01191b345494696196777b70d376e3eaae4c9e7c330cc70d3000839f5b18dd78f2 - languageName: node - linkType: hard - -"level-supports@npm:~1.0.0": - version: 1.0.1 - resolution: "level-supports@npm:1.0.1" - dependencies: - xtend: ^4.0.2 - checksum: 5d6bdb88cf00c3d9adcde970db06a548c72c5a94bf42c72f998b58341a105bfe2ea30d313ce1e84396b98cc9ddbc0a9bd94574955a86e929f73c986e10fc0df0 - languageName: node - linkType: hard - -"level@npm:^6.0.1": - version: 6.0.1 - resolution: "level@npm:6.0.1" - dependencies: - level-js: ^5.0.0 - level-packager: ^5.1.0 - leveldown: ^5.4.0 - checksum: bd4981f94162469a82a6c98d267d814d9d4a7beed4fc3d18fbe3b156f71cf4c6d35b424d14c46d401dbf0cd91425e842950a7cd17ddf7bf57acdab5af4c278da - languageName: node - linkType: hard - -"leveldown@npm:^5.4.0": - version: 5.6.0 - resolution: "leveldown@npm:5.6.0" - dependencies: - abstract-leveldown: ~6.2.1 - napi-macros: ~2.0.0 - node-gyp: latest - node-gyp-build: ~4.1.0 - checksum: 06d4683170d7fc661acd65457e531b42ad66480e9339d3154ba6d0de38ff0503d7d017c1c6eba12732b5488ecd2915c70c8dc3a7d67f4a836f3de34b8a993949 - languageName: node - linkType: hard - -"levelup@npm:^4.3.2": - version: 4.4.0 - resolution: "levelup@npm:4.4.0" - dependencies: - deferred-leveldown: ~5.3.0 - level-errors: ~2.0.0 - level-iterator-stream: ~4.0.0 - level-supports: ~1.0.0 - xtend: ~4.0.0 - checksum: 5a09e34c78cd7c23f9f6cb73563f1ebe8121ffc5f9f5f232242529d4fbdd40e8d1ffb337d2defa0b842334e0dbd4028fbfe7a072eebfe2c4d07174f0aa4aabca - languageName: node - linkType: hard - "leven@npm:^3.1.0": version: 3.1.0 resolution: "leven@npm:3.1.0" @@ -10098,7 +9881,7 @@ __metadata: languageName: node linkType: hard -"lib0@npm:^0.2.31, lib0@npm:^0.2.52, lib0@npm:^0.2.85, lib0@npm:^0.2.86": +"lib0@npm:^0.2.85, lib0@npm:^0.2.86": version: 0.2.88 resolution: "lib0@npm:0.2.88" dependencies: @@ -10351,13 +10134,6 @@ __metadata: languageName: node linkType: hard -"ltgt@npm:^2.1.2": - version: 2.2.1 - resolution: "ltgt@npm:2.2.1" - checksum: 7e3874296f7538bc8087b428ac4208008d7b76916354b34a08818ca7c83958c1df10ec427eeeaad895f6b81e41e24745b18d30f89abcc21d228b94f6961d50a2 - languageName: node - linkType: hard - "make-dir@npm:3.1.0": version: 3.1.0 resolution: "make-dir@npm:3.1.0" @@ -10879,13 +10655,6 @@ __metadata: languageName: node linkType: hard -"napi-macros@npm:~2.0.0": - version: 2.0.0 - resolution: "napi-macros@npm:2.0.0" - checksum: 30384819386977c1f82034757014163fa60ab3c5a538094f778d38788bebb52534966279956f796a92ea771c7f8ae072b975df65de910d051ffbdc927f62320c - languageName: node - linkType: hard - "natural-compare@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare@npm:1.4.0" @@ -10948,17 +10717,6 @@ __metadata: languageName: node linkType: hard -"node-gyp-build@npm:~4.1.0": - version: 4.1.1 - resolution: "node-gyp-build@npm:4.1.1" - bin: - node-gyp-build: ./bin.js - node-gyp-build-optional: ./optional.js - node-gyp-build-test: ./build-test.js - checksum: 959d42221cc44b92700003efae741652bc4e379e4cf375830ddde03ba43c89f99694bf0883078ed0d4e03ffe2f85decab0572e04068d3900b8538d165dbc17df - languageName: node - linkType: hard - "node-gyp@npm:^9.0.0": version: 9.4.1 resolution: "node-gyp@npm:9.4.1" @@ -12112,13 +11870,6 @@ __metadata: languageName: node linkType: hard -"prr@npm:~1.0.1": - version: 1.0.1 - resolution: "prr@npm:1.0.1" - checksum: 3bca2db0479fd38f8c4c9439139b0c42dcaadcc2fbb7bb8e0e6afaa1383457f1d19aea9e5f961d5b080f1cfc05bfa1fe9e45c97a1d3fd6d421950a73d3108381 - languageName: node - linkType: hard - "psl@npm:^1.1.33": version: 1.9.0 resolution: "psl@npm:1.9.0" @@ -14696,15 +14447,6 @@ __metadata: languageName: node linkType: hard -"ws@npm:^6.2.1": - version: 6.2.2 - resolution: "ws@npm:6.2.2" - dependencies: - async-limiter: ~1.0.0 - checksum: aec3154ec51477c094ac2cb5946a156e17561a581fa27005cbf22c53ac57f8d4e5f791dd4bbba6a488602cb28778c8ab7df06251d590507c3c550fd8ebeee949 - languageName: node - linkType: hard - "ws@npm:^8.11.0": version: 8.16.0 resolution: "ws@npm:8.16.0" @@ -14741,25 +14483,13 @@ __metadata: languageName: node linkType: hard -"xtend@npm:^4.0.1, xtend@npm:^4.0.2, xtend@npm:~4.0.0, xtend@npm:~4.0.1": +"xtend@npm:^4.0.1, xtend@npm:~4.0.1": version: 4.0.2 resolution: "xtend@npm:4.0.2" checksum: ac5dfa738b21f6e7f0dd6e65e1b3155036d68104e67e5d5d1bde74892e327d7e5636a076f625599dc394330a731861e87343ff184b0047fef1360a7ec0a5a36a languageName: node linkType: hard -"y-leveldb@npm:^0.1.0": - version: 0.1.2 - resolution: "y-leveldb@npm:0.1.2" - dependencies: - level: ^6.0.1 - lib0: ^0.2.31 - peerDependencies: - yjs: ^13.0.0 - checksum: 38e3293cfc5e754ba50af4c6bd03a96efde34c92809baf504b38cb4f45959187f896fe6971fa6a91823763e178807aaa14e190d1f7bea1b3a1e9b7265bb88b6d - languageName: node - linkType: hard - "y-protocols@npm:^1.0.5": version: 1.0.6 resolution: "y-protocols@npm:1.0.6" @@ -14771,29 +14501,6 @@ __metadata: languageName: node linkType: hard -"y-websocket@npm:^1.3.15": - version: 1.5.3 - resolution: "y-websocket@npm:1.5.3" - dependencies: - lib0: ^0.2.52 - lodash.debounce: ^4.0.8 - ws: ^6.2.1 - y-leveldb: ^0.1.0 - y-protocols: ^1.0.5 - peerDependencies: - yjs: ^13.5.6 - dependenciesMeta: - ws: - optional: true - y-leveldb: - optional: true - bin: - y-websocket: bin/server.js - y-websocket-server: bin/server.js - checksum: 4e658318a64feb131015cf4e284da23ad3f6b818a5a1c1e927404db3432c858d6ce4efe7e74f9a86ea70e003b7690aba517a34b8c5b6f3cd9eac86a94bf5c67f - languageName: node - linkType: hard - "y18n@npm:^5.0.5": version: 5.0.8 resolution: "y18n@npm:5.0.8" From 3251f3ac390898561285da65ff807216a470be28 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Thu, 19 Dec 2024 14:02:51 +0100 Subject: [PATCH 7/9] Remove UI tests --- .../ui-tests/tests/jupyter-ai.spec.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/packages/jupyter-ai/ui-tests/tests/jupyter-ai.spec.ts b/packages/jupyter-ai/ui-tests/tests/jupyter-ai.spec.ts index 3b6c53b70..d2064d9de 100644 --- a/packages/jupyter-ai/ui-tests/tests/jupyter-ai.spec.ts +++ b/packages/jupyter-ai/ui-tests/tests/jupyter-ai.spec.ts @@ -1,5 +1,4 @@ -import { expect, test } from '@jupyterlab/galata'; -import { AIHelper } from './helpers/AIHelper'; +import { test } from '@jupyterlab/galata'; enum FILENAMES { SIDEBAR = 'sidebar.png', @@ -13,17 +12,7 @@ enum FILENAMES { test.use({ autoGoto: false }); test.describe('Jupyter AI', () => { - let ai: AIHelper; - test.beforeEach(async ({ page }) => { - ai = new AIHelper(page); - await page.goto(); - }); - - test('shows sidebar chat icon', async () => { - await ai.assertSnapshot(FILENAMES.SIDEBAR, { locator: ai.sidebar }); - }); - - test('shows chat welcome message', async () => { - await ai.assertSnapshot(FILENAMES.CHAT_WELCOME_MESSAGE); + test('Should be tested', () => { + // no-op }); }); From 52b6cf6f15165553b7469c6760cea23ed5dbfa4c Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Thu, 19 Dec 2024 14:26:51 +0100 Subject: [PATCH 8/9] Remove the generative AI menu --- .../jupyter-ai/src/plugins/menu-plugin.ts | 158 ------------------ 1 file changed, 158 deletions(-) delete mode 100644 packages/jupyter-ai/src/plugins/menu-plugin.ts diff --git a/packages/jupyter-ai/src/plugins/menu-plugin.ts b/packages/jupyter-ai/src/plugins/menu-plugin.ts deleted file mode 100644 index 48a6dead5..000000000 --- a/packages/jupyter-ai/src/plugins/menu-plugin.ts +++ /dev/null @@ -1,158 +0,0 @@ -// import { -// JupyterFrontEnd, -// JupyterFrontEndPlugin -// } from '@jupyterlab/application'; - -// 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'; -// } - -// /** -// * 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 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; -// } - -// 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; -// }; -// } - -// // 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; -// } - -// 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: '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 }); - -// app.contextMenu.addItem({ -// type: 'submenu', -// selector: '.jp-Editor', -// rank: 1, -// submenu -// }); -// } -// }; From c9dca7606b49d7bc84bfc46b80cc0164cb8f06f8 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Fri, 20 Dec 2024 07:55:27 +0100 Subject: [PATCH 9/9] Remove unused components --- .../src/components/expandable-text-field.tsx | 80 ----------------- .../mui-extras/tooltipped-button.tsx | 87 ------------------- 2 files changed, 167 deletions(-) delete mode 100644 packages/jupyter-ai/src/components/expandable-text-field.tsx delete mode 100644 packages/jupyter-ai/src/components/mui-extras/tooltipped-button.tsx diff --git a/packages/jupyter-ai/src/components/expandable-text-field.tsx b/packages/jupyter-ai/src/components/expandable-text-field.tsx deleted file mode 100644 index 5e05511ef..000000000 --- a/packages/jupyter-ai/src/components/expandable-text-field.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; - -export interface IExpandableTextFieldProps { - label: string; - collapsedTextMaxLength?: number; - id?: string; - text?: string; - style?: React.CSSProperties; - InputProps?: { - startAdornment: JSX.Element; - }; - helperText?: string; - name?: string; -} - -export function ExpandableTextField( - props: IExpandableTextFieldProps -): JSX.Element { - const [expanded, setExpanded] = useState(false); - const [overflowing, setOverflowing] = useState(false); - const { label, style, helperText, InputProps } = props; - const textContainerRef = useRef(null); - - useEffect(() => { - setExpanded(false); - const el = textContainerRef.current; - if (el?.offsetWidth && el?.scrollWidth) { - setOverflowing(el.offsetWidth < el.scrollWidth); - } - }, [props.text]); - - return ( -
    - {label} -
    -
    - {InputProps?.startAdornment} - - {props.text ? props.text : !InputProps?.startAdornment && '\u2014'} - -
    - {overflowing && ( -
    setExpanded(!expanded)} - className="jp-ai-ExpandableTextField-value" - > - {expanded ? 'Show Less' : 'Show More'} -
    - )} - - {helperText} - -
    -
    - ); -} diff --git a/packages/jupyter-ai/src/components/mui-extras/tooltipped-button.tsx b/packages/jupyter-ai/src/components/mui-extras/tooltipped-button.tsx deleted file mode 100644 index 363a4e832..000000000 --- a/packages/jupyter-ai/src/components/mui-extras/tooltipped-button.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React from 'react'; -import { Button, ButtonProps, SxProps, TooltipProps } from '@mui/material'; - -import { ContrastingTooltip } from './contrasting-tooltip'; - -export type TooltippedButtonProps = { - onClick: React.MouseEventHandler; - tooltip: string; - children: JSX.Element; - disabled?: boolean; - placement?: TooltipProps['placement']; - /** - * The offset of the tooltip popup. - * - * The expected syntax is defined by the Popper library: - * https://popper.js.org/docs/v2/modifiers/offset/ - */ - offset?: [number, number]; - 'aria-label'?: string; - /** - * Props passed directly to the MUI `Button` component. - */ - buttonProps?: ButtonProps; - /** - * Styles applied to the MUI `Button` component. - */ - sx?: SxProps; -}; - -/** - * A component that renders an MUI `Button` with a high-contrast tooltip - * provided by `ContrastingTooltip`. This component differs from the MUI - * defaults in the following ways: - * - * - Shows the tooltip on hover even if disabled. - * - Renders the tooltip above the button by default. - * - Renders the tooltip closer to the button by default. - * - Lowers the opacity of the Button when disabled. - * - Renders the Button with `line-height: 0` to avoid showing extra - * vertical space in SVG icons. - * - * NOTE TO DEVS: Please keep this component's features synchronized with - * features available to `TooltippedIconButton`. - */ -export function TooltippedButton(props: TooltippedButtonProps): JSX.Element { - return ( - - {/* - By default, tooltips never appear when the Button is disabled. The - official way to support this feature in MUI is to wrap the child Button - element in a `span` element. - - See: https://mui.com/material-ui/react-tooltip/#disabled-elements - */} - - - - - ); -}