import cls from 'classnames';
import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { AINativeConfigService, useInjectable, useLatest } from '@opensumi/ide-core-browser';
import { Icon, Popover, PopoverPosition, getIcon } from '@opensumi/ide-core-browser/lib/components';
import { EnhanceIcon } from '@opensumi/ide-core-browser/lib/components/ai-native';
import { InteractiveInput } from '@opensumi/ide-core-browser/lib/components/ai-native/interactive-input/index';
import {
ChatAgentViewServiceToken,
ChatFeatureRegistryToken,
MessageType,
localize,
runWhenIdle,
} from '@opensumi/ide-core-common';
import { CommandService } from '@opensumi/ide-core-common/lib/command';
import { MonacoCommandRegistry } from '@opensumi/ide-editor/lib/browser/monaco-contrib/command/command.service';
import { IDialogService } from '@opensumi/ide-overlay';
import {
AT_SIGN_SYMBOL,
IChatAgentService,
IChatInternalService,
SLASH_SYMBOL,
TokenMCPServerProxyService,
} from '../../common';
import { ChatAgentViewService } from '../chat/chat-agent.view.service';
import { ChatSlashCommandItemModel } from '../chat/chat-model';
import { ChatProxyService } from '../chat/chat-proxy.service';
import { ChatFeatureRegistry } from '../chat/chat.feature.registry';
import { ChatInternalService } from '../chat/chat.internal.service';
import { MCPConfigCommands } from '../mcp/config/mcp-config.commands';
import { MCPServerProxyService } from '../mcp/mcp-server-proxy.service';
import { MCPToolsDialog } from '../mcp/mcp-tools-dialog.view';
import { IChatSlashCommandItem } from '../types';
import styles from './components.module.less';
const INSTRUCTION_BOTTOM = 8;
const EXPAND_CRITICAL_HEIGHT = 68;
interface IBlockProps extends IChatSlashCommandItem {
command?: string;
agentId?: string;
}
const Block = ({
icon,
name,
description,
agentId,
command,
selectedAgentId,
}: IBlockProps & { selectedAgentId?: string }) => {
const renderAgent = useMemo(() => {
if (!selectedAgentId && agentId && agentId !== ChatProxyService.AGENT_ID && command) {
return @{agentId};
}
return null;
}, []);
return (
{icon && }
{name && {name}}
{description && {description}}
{renderAgent}
);
};
const InstructionOptions = ({ onClick, bottom, trigger, agentId: selectedAgentId }) => {
const chatAgentService = useInjectable(IChatAgentService);
const chatAgentViewService = useInjectable(ChatAgentViewServiceToken);
const options = useMemo(() => {
if (trigger === AT_SIGN_SYMBOL) {
return chatAgentViewService.getRenderAgents().map(
(a) =>
new ChatSlashCommandItemModel(
{
icon: '',
name: `${AT_SIGN_SYMBOL}${a.id} `,
description: a.metadata.description,
},
'',
a.id,
),
);
} else {
return chatAgentService
.getCommands()
.map(
(c) =>
new ChatSlashCommandItemModel(
{
icon: '',
name: `${SLASH_SYMBOL} ${c.name} `,
description: c.description,
},
c.name,
c.agentId,
),
)
.filter((item) => !selectedAgentId || item.agentId === selectedAgentId);
}
}, [trigger, chatAgentService]);
const handleClick = useCallback(
(name: string | undefined, agentId?: string, command?: string) => {
if (onClick) {
onClick(name || '', agentId, command);
}
},
[onClick],
);
if (options.length === 0) {
return null;
}
return (
{options.map(({ icon, name, nameWithSlash, description, agentId, command }) => (
- handleClick(nameWithSlash, agentId, command)}>
))}
);
};
const ThemeWidget = ({ themeBlock }) => (
);
const AgentWidget = ({ agentId, command }) => (
{agentId !== ChatProxyService.AGENT_ID && (
@{agentId}
)}
{command && (
{SLASH_SYMBOL} {command}
)}
);
export interface IChatInputProps {
onSend: (value: string, images?: string[], agentId?: string, command?: string) => void;
onValueChange?: (value: string) => void;
onExpand?: (value: boolean) => void;
placeholder?: string;
enableOptions?: boolean;
disabled?: boolean;
sendBtnClassName?: string;
defaultHeight?: number;
value?: string;
autoFocus?: boolean;
theme?: string | null;
setTheme: (theme: string | null) => void;
agentId: string;
setAgentId: (id: string) => void;
defaultAgentId?: string;
command: string;
setCommand: (command: string) => void;
}
// 指令命令激活组件
export const ChatInput = React.forwardRef((props: IChatInputProps, ref) => {
const {
onSend,
onValueChange,
enableOptions = false,
disabled = false,
defaultHeight = 32,
autoFocus,
setTheme,
theme,
setAgentId,
agentId: propsAgentId,
defaultAgentId,
setCommand,
command,
sendBtnClassName,
} = props;
const agentId = propsAgentId || defaultAgentId;
const textareaRef = useRef(null);
const instructionRef = useRef(null);
const [value, setValue] = useState(props.value || '');
const [isShowOptions, setIsShowOptions] = useState(false);
const [inputHeight, setInputHeight] = useState(defaultHeight);
const [focus, setFocus] = useState(false);
const [showExpand, setShowExpand] = useState(false);
const [isExpand, setIsExpand] = useState(false);
const [placeholder, setPlaceHolder] = useState(localize('aiNative.chat.input.placeholder.default'));
const aiChatService = useInjectable(IChatInternalService);
const dialogService = useInjectable(IDialogService);
const aiNativeConfigService = useInjectable(AINativeConfigService);
const mcpServerProxyService = useInjectable(TokenMCPServerProxyService);
const monacoCommandRegistry = useInjectable(MonacoCommandRegistry);
const chatAgentService = useInjectable(IChatAgentService);
const chatFeatureRegistry = useInjectable(ChatFeatureRegistryToken);
const commandService = useInjectable(CommandService);
const currentAgentIdRef = useLatest(agentId);
const handleShowMCPConfig = React.useCallback(() => {
commandService.executeCommand(MCPConfigCommands.OPEN_MCP_CONFIG.id);
}, [commandService]);
const handleShowMCPTools = React.useCallback(async () => {
const tools = await mcpServerProxyService.getAllMCPTools();
dialogService.open({
message: ,
type: MessageType.Empty,
buttons: [localize('dialog.file.close')],
});
}, [mcpServerProxyService, dialogService]);
useImperativeHandle(ref, () => ({
setInputValue: (v: string) => {
setValue(v);
runWhenIdle(() => {
textareaRef.current?.focus();
}, 120);
},
}));
useEffect(() => {
if (props.value !== value) {
setValue(props.value || '');
}
}, [props.value]);
useEffect(() => {
textareaRef.current?.focus();
const defaultPlaceholder = localize('aiNative.chat.input.placeholder.default');
const findCommandHandler = chatFeatureRegistry.getSlashCommandHandler(command);
if (findCommandHandler && findCommandHandler.providerInputPlaceholder) {
const editor = monacoCommandRegistry.getActiveCodeEditor();
const placeholder = findCommandHandler.providerInputPlaceholder(value, editor);
setPlaceHolder(placeholder || defaultPlaceholder);
} else {
setPlaceHolder(defaultPlaceholder);
}
}, [chatFeatureRegistry, command]);
useEffect(() => {
acquireOptionsCheck(theme || '', agentId, command);
}, [theme, agentId, command]);
useEffect(() => {
if (textareaRef && autoFocus) {
textareaRef.current?.focus();
}
}, [textareaRef, autoFocus, props.value]);
useEffect(() => {
if (enableOptions) {
if (
(value === SLASH_SYMBOL || (value === AT_SIGN_SYMBOL && chatAgentService.getAgents().length > 0)) &&
!isExpand
) {
setIsShowOptions(true);
} else {
setIsShowOptions(false);
}
}
if (value.startsWith(SLASH_SYMBOL)) {
const { value: newValue, nameWithSlash } = chatFeatureRegistry.parseSlashCommand(value);
if (nameWithSlash) {
const commandModel = chatFeatureRegistry.getSlashCommandBySlashName(nameWithSlash);
setValue(newValue);
setTheme(nameWithSlash);
if (commandModel) {
setAgentId(commandModel.agentId!);
setCommand(commandModel.command!);
}
return;
}
}
if (chatAgentService.getAgents().length) {
const parsedInfo = chatAgentService.parseMessage(value, currentAgentIdRef.current);
if (parsedInfo.agentId || parsedInfo.command) {
setTheme('');
setValue(parsedInfo.message);
if (parsedInfo.agentId) {
setAgentId(parsedInfo.agentId);
}
if (parsedInfo.command) {
setCommand(parsedInfo.command);
}
}
}
}, [textareaRef, value, enableOptions, chatFeatureRegistry]);
useEffect(() => {
if (!value) {
setInputHeight(defaultHeight);
setShowExpand(false);
setIsExpand(false);
}
}, [value]);
const handleInputChange = useCallback((value: string) => {
setValue(value);
if (onValueChange) {
onValueChange(value);
}
}, []);
const handleStop = useCallback(() => {
aiChatService.cancelRequest();
}, []);
const handleSend = useCallback(async () => {
if (disabled) {
return;
}
const handleSendLogic = (newValue: string = value) => {
onSend(newValue, [], agentId, command);
setValue('');
setTheme('');
setAgentId('');
setCommand('');
};
if (command) {
const chatCommandHandler = chatFeatureRegistry.getSlashCommandHandler(command);
if (chatCommandHandler && chatCommandHandler.execute) {
const editor = monacoCommandRegistry.getActiveCodeEditor();
await chatCommandHandler.execute(value, (newValue: string) => handleSendLogic(newValue), editor);
return;
}
}
handleSendLogic();
}, [onSend, value, agentId, command, chatFeatureRegistry]);
const acquireOptionsCheck = useCallback(
(themeValue: string, agentId?: string, command?: string) => {
if (agentId) {
setIsShowOptions(false);
setTheme('');
setAgentId(agentId);
setCommand(command || '');
if (textareaRef?.current) {
const inputValue = textareaRef.current.value;
if (inputValue === AT_SIGN_SYMBOL || (command && inputValue === SLASH_SYMBOL)) {
setValue('');
}
runWhenIdle(() => textareaRef.current!.focus());
}
} else if (themeValue) {
setIsShowOptions(false);
setAgentId('');
setCommand('');
const findCommand = chatFeatureRegistry.getSlashCommandBySlashName(themeValue);
if (findCommand) {
setTheme(findCommand.nameWithSlash);
} else {
setTheme('');
}
if (textareaRef && textareaRef.current) {
const inputValue = textareaRef.current.value;
if (inputValue.length === 1 && inputValue.startsWith(SLASH_SYMBOL)) {
setValue('');
}
runWhenIdle(() => textareaRef.current!.focus());
}
}
},
[textareaRef, chatFeatureRegistry],
);
const optionsBottomPosition = useMemo(() => {
const customBottom = INSTRUCTION_BOTTOM + inputHeight;
if (isExpand) {
setIsShowOptions(false);
}
return customBottom;
}, [inputHeight]);
const handleKeyDown = (event) => {
if (event.key === 'Backspace') {
if (textareaRef.current?.selectionEnd === 0 && textareaRef.current?.selectionStart === 0) {
setTheme('');
if (agentId === ChatProxyService.AGENT_ID) {
setCommand('');
setAgentId('');
return;
}
if (agentId) {
if (command) {
setCommand('');
} else {
setAgentId('');
}
}
}
}
};
const handleHeightChange = useCallback((height: number) => {
setInputHeight(height);
if (height > EXPAND_CRITICAL_HEIGHT) {
setShowExpand(true);
} else {
setShowExpand(false);
}
}, []);
const handleBlur = useCallback(() => {
setFocus(false);
setIsShowOptions(false);
}, [textareaRef]);
const handleFocus = useCallback(() => {
setFocus(true);
}, [textareaRef]);
const handleExpandClick = useCallback(() => {
const expand = isExpand;
setIsExpand(!expand);
if (!expand) {
const ele = document.querySelector('#ai_chat_left_container');
const maxHeight = ele!.clientHeight - 68 - (theme ? 32 : 0) - 16;
setInputHeight(maxHeight);
} else {
setInputHeight(defaultHeight);
setShowExpand(false);
}
}, [isExpand]);
return (
{isShowOptions && (
)}
{theme &&
}
{agentId &&
}
{showExpand && (
)}
{aiNativeConfigService.capabilities.supportsMCP && (
)}
);
});