aichat improvements (#667)

This commit is contained in:
Red J Adaya 2024-06-07 08:31:52 +08:00 committed by GitHub
parent 1234919229
commit d7173c970c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 315 additions and 234 deletions

View File

@ -313,6 +313,10 @@
"command": "aichat:clearHistory", "command": "aichat:clearHistory",
"keys": ["Ctrl:l"] "keys": ["Ctrl:l"]
}, },
{
"command": "aichat:setCmdInputValue",
"keys": ["Ctrl:Shift:e"]
},
{ {
"command": "terminal:copy", "command": "terminal:copy",
"keys": ["Ctrl:Shift:c"] "keys": ["Ctrl:Shift:c"]

View File

@ -31,12 +31,20 @@ class App extends React.Component<{}, {}> {
dcWait: OV<boolean> = mobx.observable.box(false, { name: "dcWait" }); dcWait: OV<boolean> = mobx.observable.box(false, { name: "dcWait" });
mainContentRef: React.RefObject<HTMLDivElement> = React.createRef(); mainContentRef: React.RefObject<HTMLDivElement> = React.createRef();
termThemesLoaded: OV<boolean> = mobx.observable.box(false, { name: "termThemesLoaded" }); termThemesLoaded: OV<boolean> = mobx.observable.box(false, { name: "termThemesLoaded" });
chatFocusTimeoutId: NodeJS.Timeout = null;
constructor(props: {}) { constructor(props: {}) {
super(props); super(props);
if (GlobalModel.isDev) document.body.classList.add("is-dev"); if (GlobalModel.isDev) document.body.classList.add("is-dev");
} }
componentWillUnmount() {
if (this.chatFocusTimeoutId) {
clearTimeout(this.chatFocusTimeoutId);
this.chatFocusTimeoutId = null;
}
}
@boundMethod @boundMethod
handleContextMenu(e: any) { handleContextMenu(e: any) {
let isInNonTermInput = false; let isInNonTermInput = false;
@ -68,16 +76,15 @@ class App extends React.Component<{}, {}> {
@boundMethod @boundMethod
openMainSidebar() { openMainSidebar() {
const mainSidebarModel = GlobalModel.mainSidebarModel; GlobalModel.mainSidebarModel.setCollapsed(false);
const width = mainSidebarModel.getWidth(true);
mainSidebarModel.saveState(width, false);
} }
@boundMethod @boundMethod
openRightSidebar() { openRightSidebar() {
const rightSidebarModel = GlobalModel.rightSidebarModel; GlobalModel.rightSidebarModel.setCollapsed(false);
const width = rightSidebarModel.getWidth(true); this.chatFocusTimeoutId = setTimeout(() => {
rightSidebarModel.saveState(width, false); GlobalModel.inputModel.setChatSidebarFocus();
}, 100);
} }
@boundMethod @boundMethod

View File

@ -128,7 +128,8 @@ class LineActions extends React.Component<{ screen: LineContainerType; line: Lin
screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, 300) * 2, screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, 300) * 2,
cmdShouldMarkError(cmd) cmdShouldMarkError(cmd)
); );
GlobalModel.sidebarchatModel.setFocus("input", true); GlobalModel.inputModel.setChatSidebarFocus();
GlobalModel.sidebarchatModel.resetSelectedCodeBlockIndex();
} }
} }

View File

@ -20,6 +20,11 @@
.filler { .filler {
flex: 1 1 auto; flex: 1 1 auto;
} }
> * {
cursor: default;
user-select: none;
}
} }
.chat-msg { .chat-msg {
@ -78,6 +83,7 @@
font-family: var(--termfontfamily); font-family: var(--termfontfamily);
font-weight: normal; font-weight: normal;
line-height: var(--termlineheight); line-height: var(--termlineheight);
height: 21px;
} }
} }
} }

View File

@ -16,7 +16,7 @@ import "./aichat.less";
const outline = "2px solid var(--markdown-outline-color)"; const outline = "2px solid var(--markdown-outline-color)";
class ChatKeyBindings extends React.Component<{ component: ChatSidebar; bindArrowUpDownKeys: boolean }, {}> { class ChatKeyBindings extends React.Component<{ component: ChatSidebar }, {}> {
componentDidMount(): void { componentDidMount(): void {
const { component } = this.props; const { component } = this.props;
const keybindManager = GlobalModel.keybindManager; const keybindManager = GlobalModel.keybindManager;
@ -34,26 +34,19 @@ class ChatKeyBindings extends React.Component<{ component: ChatSidebar; bindArro
inputModel.clearAIAssistantChat(); inputModel.clearAIAssistantChat();
return true; return true;
}); });
} keybindManager.registerKeybinding("pane", "aichat", "generic:selectAbove", (waveEvent) => {
return component.onArrowUpPressed();
componentDidUpdate(): void { });
const { component, bindArrowUpDownKeys } = this.props; keybindManager.registerKeybinding("pane", "aichat", "generic:selectBelow", (waveEvent) => {
const keybindManager = GlobalModel.keybindManager; return component.onArrowDownPressed();
if (bindArrowUpDownKeys) { });
keybindManager.registerKeybinding("pane", "aichat:arrowupdown", "generic:selectAbove", (waveEvent) => { keybindManager.registerKeybinding("pane", "aichat", "aichat:setCmdInputValue", (waveEvent) => {
return component.onArrowUpPressed(); return component.onSetCmdInputValue();
}); });
keybindManager.registerKeybinding("pane", "aichat:arrowupdown", "generic:selectBelow", (waveEvent) => {
return component.onArrowDownPressed();
});
} else {
GlobalModel.keybindManager.unregisterDomain("aichat:arrowupdown");
}
} }
componentWillUnmount(): void { componentWillUnmount(): void {
GlobalModel.keybindManager.unregisterDomain("aichat"); GlobalModel.keybindManager.unregisterDomain("aichat");
GlobalModel.keybindManager.unregisterDomain("aichat:arrowupdown");
} }
render() { render() {
@ -62,20 +55,16 @@ class ChatKeyBindings extends React.Component<{ component: ChatSidebar; bindArro
} }
@mobxReact.observer @mobxReact.observer
class ChatItem extends React.Component<{ chatItem: OpenAICmdInfoChatMessageType; itemCount: number }, {}> { class ChatItem extends React.Component<
handleExecuteCommand(cmd: string) { { chatItem: OpenAICmdInfoChatMessageType; itemCount: number; onSetCmdInputValue: (cmd: string) => void },
GlobalModel.sidebarchatModel.setCmdToExec(cmd); {}
GlobalModel.sidebarchatModel.resetFocus(); > {
GlobalModel.inputModel.curLine = cmd;
GlobalModel.inputModel.giveFocus();
}
renderError(err: string): any { renderError(err: string): any {
return <div className="chat-msg-error">{err}</div>; return <div className="chat-msg-error">{err}</div>;
} }
render() { render() {
const { chatItem, itemCount } = this.props; const { chatItem, itemCount, onSetCmdInputValue } = this.props;
const { isassistantresponse, assistantresponse } = chatItem; const { isassistantresponse, assistantresponse } = chatItem;
const curKey = "chatmsg-" + itemCount; const curKey = "chatmsg-" + itemCount;
const senderClassName = isassistantresponse ? "chat-msg-assistant" : "chat-msg-user"; const senderClassName = isassistantresponse ? "chat-msg-assistant" : "chat-msg-user";
@ -108,7 +97,7 @@ class ChatItem extends React.Component<{ chatItem: OpenAICmdInfoChatMessageType;
<div className="chat-msg-header"> <div className="chat-msg-header">
<i className="fa-sharp fa-solid fa-sparkles"></i> <i className="fa-sharp fa-solid fa-sparkles"></i>
</div> </div>
<Markdown2 text={assistantresponse.message} onClickExecute={this.handleExecuteCommand} /> <Markdown2 text={assistantresponse.message} onClickExecute={onSetCmdInputValue} />
</> </>
); );
} }
@ -130,7 +119,14 @@ class ChatItem extends React.Component<{ chatItem: OpenAICmdInfoChatMessageType;
} }
@mobxReact.observer @mobxReact.observer
class ChatWindow extends React.Component<{ chatWindowRef; onRendered }, {}> { class ChatWindow extends React.Component<
{
chatWindowRef: React.RefObject<HTMLDivElement>;
onRendered: (osInstance: OverlayScrollbars) => void;
onSetCmdInputValue: (cmd: string) => void;
},
{}
> {
itemCount: number = 0; itemCount: number = 0;
containerRef: React.RefObject<OverlayScrollbarsComponentRef> = React.createRef(); containerRef: React.RefObject<OverlayScrollbarsComponentRef> = React.createRef();
osInstance: OverlayScrollbars = null; osInstance: OverlayScrollbars = null;
@ -164,6 +160,7 @@ class ChatWindow extends React.Component<{ chatWindowRef; onRendered }, {}> {
} }
render() { render() {
const { onSetCmdInputValue } = this.props;
const chatMessageItems = GlobalModel.inputModel.AICmdInfoChatItems.slice(); const chatMessageItems = GlobalModel.inputModel.AICmdInfoChatItems.slice();
const chitem: OpenAICmdInfoChatMessageType = null; const chitem: OpenAICmdInfoChatMessageType = null;
let idx; let idx;
@ -177,7 +174,12 @@ class ChatWindow extends React.Component<{ chatWindowRef; onRendered }, {}> {
<div ref={this.props.chatWindowRef} className="chat-window"> <div ref={this.props.chatWindowRef} className="chat-window">
<div className="filler"></div> <div className="filler"></div>
<For each="chitem" index="idx" of={chatMessageItems}> <For each="chitem" index="idx" of={chatMessageItems}>
<ChatItem key={idx} chatItem={chitem} itemCount={idx + 1} /> <ChatItem
key={idx}
chatItem={chitem}
itemCount={idx + 1}
onSetCmdInputValue={onSetCmdInputValue}
/>
</For> </For>
</div> </div>
</OverlayScrollbarsComponent> </OverlayScrollbarsComponent>
@ -190,11 +192,11 @@ class ChatSidebar extends React.Component<{}, {}> {
sidebarRef: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>(); sidebarRef: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>();
textAreaRef: React.RefObject<HTMLTextAreaElement> = React.createRef<HTMLTextAreaElement>(); textAreaRef: React.RefObject<HTMLTextAreaElement> = React.createRef<HTMLTextAreaElement>();
chatWindowRef: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>(); chatWindowRef: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>();
bindArrowUpDownKeys: OV<boolean> = mobx.observable.box(false, { name: "bindArrowUpDownKeys" });
value: OV<string> = mobx.observable.box("", { deep: false, name: "value" }); value: OV<string> = mobx.observable.box("", { deep: false, name: "value" });
osInstance: OverlayScrollbars; osInstance: OverlayScrollbars;
termFontSize: number = 14; termFontSize: number = 14;
blockIndex: number; blockIndex: number;
disposeReaction: () => void;
constructor(props) { constructor(props) {
super(props); super(props);
@ -202,27 +204,32 @@ class ChatSidebar extends React.Component<{}, {}> {
} }
componentDidUpdate() { componentDidUpdate() {
if (GlobalModel.sidebarchatModel.focused == "input") {
this.textAreaRef.current.focus();
}
if (GlobalModel.sidebarchatModel.hasCmdAndOutput()) {
const newCmdAndOutput = GlobalModel.sidebarchatModel.getCmdAndOutput();
const newValue = this.formChatMessage(newCmdAndOutput);
if (newValue !== this.value.get()) {
this.value.set(newValue);
GlobalModel.sidebarchatModel.resetCmdAndOutput();
}
}
this.adjustTextAreaHeight(); this.adjustTextAreaHeight();
} }
componentDidMount() { componentDidMount() {
GlobalModel.sidebarchatModel.setFocus("input", true); this.disposeReaction = mobx.reaction(
this.textAreaRef.current.focus(); () => [
GlobalModel.sidebarchatModel.hasCmdAndOutput(),
GlobalModel.sidebarchatModel.getSelectedCodeBlockIndex(),
],
([hasCmdAndOutput, selectedCodeBlockIndex]) => {
if (hasCmdAndOutput) {
const newCmdAndOutput = GlobalModel.sidebarchatModel.getCmdAndOutput();
const newValue = this.formChatMessage(newCmdAndOutput);
this.value.set(newValue);
GlobalModel.sidebarchatModel.resetCmdAndOutput();
}
if (selectedCodeBlockIndex == null) {
this.updatePreTagOutline();
}
}
);
if (this.sidebarRef.current) { if (this.sidebarRef.current) {
this.sidebarRef.current.addEventListener("click", this.handleSidebarClick); this.sidebarRef.current.addEventListener("click", this.handleSidebarClick);
} }
document.addEventListener("mousedown", this.handleClickOutside); document.addEventListener("click", this.handleClickOutside);
this.requestChatUpdate(); this.requestChatUpdate();
} }
@ -230,13 +237,19 @@ class ChatSidebar extends React.Component<{}, {}> {
if (this.sidebarRef.current) { if (this.sidebarRef.current) {
this.sidebarRef.current.removeEventListener("click", this.handleSidebarClick); this.sidebarRef.current.removeEventListener("click", this.handleSidebarClick);
} }
document.removeEventListener("mousedown", this.handleClickOutside); document.removeEventListener("click", this.handleClickOutside);
GlobalModel.sidebarchatModel.resetFocus();
if (this.disposeReaction) {
this.disposeReaction();
}
} }
@mobx.action.bound @mobx.action.bound
handleClickOutside(event) { handleClickOutside(e: MouseEvent) {
if (this.sidebarRef.current && !this.sidebarRef.current.contains(event.target)) { const sidebar = this.sidebarRef.current;
this.onClickOutsideSidebar(); if (sidebar && !sidebar.contains(e.target as Node)) {
GlobalModel.sidebarchatModel.resetFocus();
GlobalModel.inputModel.giveFocus();
} }
} }
@ -281,20 +294,20 @@ class ChatSidebar extends React.Component<{}, {}> {
}).catch((_) => {}); }).catch((_) => {});
} }
onClickOutsideSidebar() { @mobx.action.bound
GlobalModel.sidebarchatModel.resetFocus(); onTextAreaFocus() {
GlobalModel.inputModel.setChatSidebarFocus();
return true;
} }
@mobx.action.bound @mobx.action.bound
onTextAreaFocused(e) { onTextAreaMouseDown(e) {
GlobalModel.sidebarchatModel.setFocus("input", true);
this.bindArrowUpDownKeys.set(false);
const pres = this.chatWindowRef.current?.querySelectorAll("pre");
this.blockIndex = pres.length - 1;
this.onTextAreaChange(e);
this.updatePreTagOutline(); this.updatePreTagOutline();
// Reset blockIndex to null
GlobalModel.sidebarchatModel.resetSelectedCodeBlockIndex();
} }
@mobx.action.bound
onEnterKeyPressed() { onEnterKeyPressed() {
const messageStr = this.value.get(); const messageStr = this.value.get();
this.submitChatMessage(messageStr); this.submitChatMessage(messageStr);
@ -302,6 +315,7 @@ class ChatSidebar extends React.Component<{}, {}> {
GlobalModel.sidebarchatModel.resetCmdAndOutput(); GlobalModel.sidebarchatModel.resetCmdAndOutput();
} }
@mobx.action.bound
onExpandInputPressed() { onExpandInputPressed() {
const currentRef = this.textAreaRef.current; const currentRef = this.textAreaRef.current;
if (currentRef == null) { if (currentRef == null) {
@ -317,7 +331,7 @@ class ChatSidebar extends React.Component<{}, {}> {
} }
pres.forEach((preElement, idx) => { pres.forEach((preElement, idx) => {
if (preElement === clickedPre) { if (preElement === clickedPre) {
this.blockIndex = idx; GlobalModel.sidebarchatModel.setSelectedCodeBlockIndex(idx);
preElement.style.outline = outline; preElement.style.outline = outline;
} else { } else {
preElement.style.outline = "none"; preElement.style.outline = "none";
@ -327,28 +341,28 @@ class ChatSidebar extends React.Component<{}, {}> {
@mobx.action.bound @mobx.action.bound
handleSidebarClick(event) { handleSidebarClick(event) {
let detection = 0;
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
if (
if (target.closest(".copy-button") || target.closest(".fa-square-terminal")) { target.closest(".copy-button") ||
target.closest(".fa-square-terminal") ||
target.closest(".chat-textarea")
) {
return; return;
} }
const chatWindow = target.closest(".chat-window");
if (chatWindow) {
detection++;
}
const pre = target.closest("pre"); const pre = target.closest("pre");
if (pre) { if (pre) {
detection++; const pres = this.chatWindowRef.current?.querySelectorAll("pre");
this.updatePreTagOutline(pre); if (pres) {
} pres.forEach((preElement, idx) => {
if (preElement === pre) {
if (detection > 0) { GlobalModel.sidebarchatModel.setSelectedCodeBlockIndex(idx);
this.bindArrowUpDownKeys.set(true); this.updatePreTagOutline(pre);
GlobalModel.sidebarchatModel.setFocus("block", true); }
});
}
} }
GlobalModel.inputModel.setChatSidebarFocus();
} }
updateScrollTop() { updateScrollTop() {
@ -356,7 +370,7 @@ class ChatSidebar extends React.Component<{}, {}> {
if (pres == null) { if (pres == null) {
return; return;
} }
const block = pres[this.blockIndex]; const block = pres[GlobalModel.sidebarchatModel.getSelectedCodeBlockIndex()];
if (block == null) { if (block == null) {
return; return;
} }
@ -387,42 +401,100 @@ class ChatSidebar extends React.Component<{}, {}> {
this.osInstance = osInstance; this.osInstance = osInstance;
} }
@mobx.action.bound
onArrowUpPressed() { onArrowUpPressed() {
const pres = this.chatWindowRef.current?.querySelectorAll("pre"); if (this.onTextAreaKeyDown("ArrowUp")) {
if (pres == null) { const pres = this.chatWindowRef.current?.querySelectorAll("pre");
return; let blockIndex = GlobalModel.sidebarchatModel.getSelectedCodeBlockIndex();
if (pres == null) {
return false;
}
if (blockIndex == null) {
GlobalModel.sidebarchatModel.setSelectedCodeBlockIndex(pres.length - 1);
} else if (blockIndex > 0) {
blockIndex--;
GlobalModel.sidebarchatModel.setSelectedCodeBlockIndex(blockIndex);
}
blockIndex = GlobalModel.sidebarchatModel.getSelectedCodeBlockIndex();
this.updatePreTagOutline(pres[blockIndex]);
this.updateScrollTop();
return true;
} }
if (this.blockIndex == null) { return false;
this.blockIndex = pres.length - 1;
} else if (this.blockIndex > 0) {
this.blockIndex--;
}
this.updatePreTagOutline(pres[this.blockIndex]);
this.updateScrollTop();
return true;
} }
@mobx.action.bound @mobx.action.bound
onArrowDownPressed() { onArrowDownPressed() {
const pres = this.chatWindowRef.current?.querySelectorAll("pre"); if (this.onTextAreaKeyDown("ArrowDown")) {
if (pres == null) { const pres = this.chatWindowRef.current?.querySelectorAll("pre");
return; let blockIndex = GlobalModel.sidebarchatModel.getSelectedCodeBlockIndex();
if (pres == null) {
return false;
}
if (blockIndex == null) {
return false;
}
if (blockIndex < pres.length - 1 && blockIndex >= 0) {
GlobalModel.sidebarchatModel.setSelectedCodeBlockIndex(blockIndex++);
this.updatePreTagOutline(pres[blockIndex]);
} else {
GlobalModel.sidebarchatModel.setFocus(true);
this.textAreaRef.current.focus();
this.updatePreTagOutline();
GlobalModel.sidebarchatModel.setSelectedCodeBlockIndex(null);
}
this.updateScrollTop();
return true;
} }
if (this.blockIndex == null) { return false;
return; }
@mobx.action.bound
onTextAreaKeyDown(key: "ArrowUp" | "ArrowDown") {
const textarea = this.textAreaRef.current;
const cursorPosition = textarea.selectionStart;
const textBeforeCursor = textarea.value.slice(0, cursorPosition);
const blockIndex = GlobalModel.sidebarchatModel.getSelectedCodeBlockIndex();
// Check if the cursor is at the first line for ArrowUp
if ((textBeforeCursor.indexOf("\n") == -1 && cursorPosition == 0 && key == "ArrowUp") || blockIndex != null) {
return true;
} }
if (this.blockIndex < pres.length && this.blockIndex >= 0) { GlobalModel.sidebarchatModel.setFocus(true);
this.blockIndex++; return false;
this.updatePreTagOutline(pres[this.blockIndex]); }
@mobx.action.bound
onSetCmdInputValue(cmd?: string) {
console.log("got here");
if (cmd) {
this.setCmdInputValue(cmd);
} else { } else {
this.bindArrowUpDownKeys.set(false); const pres = this.chatWindowRef.current?.querySelectorAll("pre");
this.textAreaRef.current.focus(); if (pres) {
this.updatePreTagOutline(); const selectedIdx = GlobalModel.sidebarchatModel.getSelectedCodeBlockIndex();
pres.forEach((preElement, idx) => {
if (selectedIdx === idx) {
const codeElement = preElement.querySelector("code");
if (codeElement) {
const command = codeElement.textContent.replace(/\n$/, "");
this.setCmdInputValue(command);
}
}
});
}
} }
this.updateScrollTop();
return true; return true;
} }
@mobx.action.bound
setCmdInputValue(cmd: string) {
GlobalModel.sidebarchatModel.setCmdToExec(cmd);
GlobalModel.sidebarchatModel.resetFocus();
GlobalModel.inputModel.curLine = cmd;
GlobalModel.inputModel.giveFocus();
}
@mobx.action.bound @mobx.action.bound
formChatMessage(cmdAndOutput) { formChatMessage(cmdAndOutput) {
const { cmd, output, usedRows, isError } = cmdAndOutput; const { cmd, output, usedRows, isError } = cmdAndOutput;
@ -430,7 +502,7 @@ class ChatSidebar extends React.Component<{}, {}> {
return ""; return "";
} }
// Escape backticks in the output // Escape backticks in the output
let escapedOutput = output ? output.replace(/```/g, "\\`\\`\\`") : ""; let escapedOutput = output ? output.replace(/`/g, "\\`") : "";
// Truncate the output if usedRows is over 100 // Truncate the output if usedRows is over 100
if (usedRows > 100) { if (usedRows > 100) {
const outputLines = escapedOutput.split("\n"); const outputLines = escapedOutput.split("\n");
@ -450,38 +522,22 @@ class ChatSidebar extends React.Component<{}, {}> {
return chatMessage; return chatMessage;
} }
@mobx.action.bound
handleKeyDown(e) {
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
if (this.bindArrowUpDownKeys.get()) {
GlobalModel.sidebarchatModel.setFocus("block", true);
this.textAreaRef.current.blur();
}
const textarea = this.textAreaRef.current;
const cursorPosition = textarea.selectionStart;
const textBeforeCursor = textarea.value.slice(0, cursorPosition);
// Check if the cursor is at the first line
if (textBeforeCursor.indexOf("\n") === -1 && cursorPosition === 0 && e.key === "ArrowUp") {
this.bindArrowUpDownKeys.set(true);
}
}
}
render() { render() {
const chatMessageItems = GlobalModel.inputModel.AICmdInfoChatItems.slice(); const chatMessageItems = GlobalModel.inputModel.AICmdInfoChatItems.slice();
const renderAIChatKeybindings = GlobalModel.sidebarchatModel.hasFocus; const renderAIChatKeybindings = GlobalModel.sidebarchatModel.hasFocus();
const textAreaValue = this.value.get(); const textAreaValue = this.value.get();
const bindArrowUpDownKeys = this.bindArrowUpDownKeys.get();
return ( return (
<div ref={this.sidebarRef} className="sidebarchat"> <div ref={this.sidebarRef} className="sidebarchat">
<If condition={renderAIChatKeybindings}> <If condition={renderAIChatKeybindings}>
<ChatKeyBindings component={this} bindArrowUpDownKeys={bindArrowUpDownKeys} /> <ChatKeyBindings component={this} />
</If> </If>
{chatMessageItems.length > 0 && ( {chatMessageItems.length > 0 && (
<ChatWindow chatWindowRef={this.chatWindowRef} onRendered={this.onChatWindowRendered} /> <ChatWindow
chatWindowRef={this.chatWindowRef}
onRendered={this.onChatWindowRendered}
onSetCmdInputValue={this.onSetCmdInputValue}
/>
)} )}
<div className="sidebarchat-input-wrapper"> <div className="sidebarchat-input-wrapper">
<textarea <textarea
@ -490,8 +546,8 @@ class ChatSidebar extends React.Component<{}, {}> {
autoComplete="off" autoComplete="off"
autoCorrect="off" autoCorrect="off"
className="sidebarchat-input chat-textarea" className="sidebarchat-input chat-textarea"
onKeyDown={this.handleKeyDown} onFocus={this.onTextAreaFocus}
onMouseDown={this.onTextAreaFocused} onMouseDown={this.onTextAreaMouseDown} // When the user clicks on the textarea
onChange={this.onTextAreaChange} onChange={this.onTextAreaChange}
style={{ fontSize: this.termFontSize }} style={{ fontSize: this.termFontSize }}
placeholder="Send a Message..." placeholder="Send a Message..."

View File

@ -53,16 +53,12 @@ class KeybindDevPane extends React.Component<{}, {}> {
} }
} }
class SidebarKeyBindings extends React.Component<{ component: RightSideBar; isOpen: boolean }, {}> { class SidebarKeyBindings extends React.Component<{ component: RightSideBar }, {}> {
componentDidMount(): void { componentDidMount(): void {
const { component } = this.props; const { component } = this.props;
const keybindManager = GlobalModel.keybindManager; const keybindManager = GlobalModel.keybindManager;
keybindManager.registerKeybinding("pane", "rightsidebar", "rightsidebar:toggle", (waveEvent) => { keybindManager.registerKeybinding("pane", "rightsidebar", "rightsidebar:toggle", (waveEvent) => {
if (this.props.isOpen) { return component.toggleCollapse();
return component.onClose();
} else {
return component.onOpen();
}
}); });
} }
@ -87,13 +83,21 @@ class RightSideBar extends React.Component<
{} {}
> { > {
mode: OV<string> = mobx.observable.box("aichat", { name: "RightSideBar-mode" }); mode: OV<string> = mobx.observable.box("aichat", { name: "RightSideBar-mode" });
timeoutId: NodeJS.Timeout = null;
constructor(props) { constructor(props) {
super(props); super(props);
mobx.makeObservable(this); mobx.makeObservable(this);
} }
@mobx.action componentWillUnmount() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
}
@mobx.action.bound
setMode(mode: string) { setMode(mode: string) {
if (mode == this.mode.get()) { if (mode == this.mode.get()) {
return; return;
@ -101,15 +105,19 @@ class RightSideBar extends React.Component<
this.mode.set(mode); this.mode.set(mode);
} }
@boundMethod @mobx.action.bound
onOpen() { toggleCollapse() {
GlobalModel.rightSidebarModel.setCollapsed(false); const isCollapsed = GlobalModel.rightSidebarModel.getCollapsed();
return true; GlobalModel.rightSidebarModel.setCollapsed(!isCollapsed);
} if (this.mode.get() == "aichat") {
if (isCollapsed) {
@boundMethod this.timeoutId = setTimeout(() => {
onClose() { GlobalModel.inputModel.setChatSidebarFocus();
GlobalModel.rightSidebarModel.setCollapsed(true); }, 100);
} else {
GlobalModel.inputModel.setChatSidebarFocus(false);
}
}
return true; return true;
} }
@ -124,9 +132,9 @@ class RightSideBar extends React.Component<
enableSnap={true} enableSnap={true}
parentRef={this.props.parentRef} parentRef={this.props.parentRef}
> >
{(toggleCollapse) => ( {() => (
<React.Fragment> <React.Fragment>
<SidebarKeyBindings component={this} isOpen={!isCollapsed} /> <SidebarKeyBindings component={this} />
<div className="header"> <div className="header">
<div className="rsb-modes"> <div className="rsb-modes">
<div <div
@ -148,7 +156,7 @@ class RightSideBar extends React.Component<
</div> </div>
</If> </If>
</div> </div>
<Button className="secondary ghost close" onClick={toggleCollapse}> <Button className="secondary ghost close" onClick={this.toggleCollapse}>
<i className="fa-sharp fa-solid fa-xmark-large" /> <i className="fa-sharp fa-solid fa-xmark-large" />
</Button> </Button>
</div> </div>

View File

@ -139,6 +139,7 @@ class InputModel {
const activeAuxView = this.getAuxViewFocus() ? this.getActiveAuxView() : null; const activeAuxView = this.getAuxViewFocus() ? this.getActiveAuxView() : null;
switch (activeAuxView) { switch (activeAuxView) {
case appconst.InputAuxView_History: case appconst.InputAuxView_History:
console.log("focus history");
const elem: HTMLElement = document.querySelector(".cmd-input input.history-input"); const elem: HTMLElement = document.querySelector(".cmd-input input.history-input");
if (elem) { if (elem) {
elem.focus(); elem.focus();
@ -148,8 +149,12 @@ class InputModel {
this.setAIChatFocus(); this.setAIChatFocus();
break; break;
case null: case null:
if (GlobalModel.sidebarchatModel.hasFocus) { if (GlobalModel.sidebarchatModel.hasFocus()) {
this.auxViewFocus.set(false); this.auxViewFocus.set(false);
const elem: HTMLElement = document.querySelector(".sidebarchat-input");
if (elem != null) {
elem.focus();
}
} else { } else {
const elem = document.getElementById("main-cmd-input"); const elem = document.getElementById("main-cmd-input");
if (elem) { if (elem) {
@ -159,6 +164,7 @@ class InputModel {
} }
break; break;
default: { default: {
console.log("focus auxview");
const elem: HTMLElement = document.querySelector(".cmd-input .auxview"); const elem: HTMLElement = document.querySelector(".cmd-input .auxview");
if (elem != null) { if (elem != null) {
elem.focus(); elem.focus();
@ -282,6 +288,12 @@ class InputModel {
} }
} }
@mobx.action
setChatSidebarFocus(focus = true): void {
GlobalModel.sidebarchatModel.setFocus(focus);
this.giveFocus();
}
@mobx.action @mobx.action
updateCmdLine(cmdLine: StrWithPos): void { updateCmdLine(cmdLine: StrWithPos): void {
this.curLine = cmdLine.str; this.curLine = cmdLine.str;
@ -468,7 +480,7 @@ class InputModel {
} }
// (view == null) means standard cmdinput keybindings // (view == null) means standard cmdinput keybindings
if (view == null) { if (view == null) {
return !this.getAuxViewFocus() && !GlobalModel.sidebarchatModel.hasFocus; return !this.getAuxViewFocus() && !GlobalModel.sidebarchatModel.hasFocus();
} else { } else {
return this.getAuxViewFocus() && view == this.getActiveAuxView(); return this.getAuxViewFocus() && view == this.getActiveAuxView();
} }

View File

@ -82,6 +82,11 @@ class MainSidebarModel {
return collapsed; return collapsed;
} }
setCollapsed(collapsed: boolean): void {
const width = this.getWidth(true);
this.saveState(width, collapsed);
}
saveState(width: number, collapsed: boolean): void { saveState(width: number, collapsed: boolean): void {
GlobalCommandRunner.clientSetMainSidebar(width, collapsed).finally(() => { GlobalCommandRunner.clientSetMainSidebar(width, collapsed).finally(() => {
mobx.action(() => { mobx.action(() => {

View File

@ -738,10 +738,12 @@ class Model {
setTimeout(() => { setTimeout(() => {
// allows for the session view to load // allows for the session view to load
this.inputModel.setAuxViewFocus(false); this.inputModel.setAuxViewFocus(false);
this.inputModel.setChatSidebarFocus(false);
}, 100); }, 100);
})(); })();
} else { } else {
this.inputModel.setAuxViewFocus(false); this.inputModel.setAuxViewFocus(false);
this.inputModel.setChatSidebarFocus(false);
} }
} }

View File

@ -1,104 +1,91 @@
import * as mobx from "mobx"; import * as mobx from "mobx";
import { Model } from "./model"; import { Model } from "./model";
type SidebarChatFocus = {
input: boolean;
block: boolean;
};
class SidebarChatModel { class SidebarChatModel {
globalModel: Model; globalModel: Model;
sidebarChatFocus: SidebarChatFocus; sidebarChatFocused: OV<boolean> = mobx.observable.box(false, { name: "SidebarChatModel-sidebarChatFocused" });
cmdAndOutput: CmdAndOutput; cmdAndOutput: OV<{ cmd: string; output: string; usedRows: number; isError: boolean }> = mobx.observable.box(
cmdFromChat: string; { cmd: "", output: "", usedRows: 0, isError: false },
{ name: "SidebarChatModel-cmdAndOutput" }
);
cmdFromChat: OV<string> = mobx.observable.box("", { name: "SidebarChatModel-cmdFromChat" });
selectedCodeBlockIndex: OV<number> = mobx.observable.box(null, { name: "SidebarChatModel-codeBlockIndex" });
constructor(globalModel: Model) { constructor(globalModel: Model) {
this.globalModel = globalModel; this.globalModel = globalModel;
mobx.makeObservable(this, { mobx.makeObservable(this);
sidebarChatFocus: mobx.observable, }
cmdAndOutput: mobx.observable,
setFocus: mobx.action, // block can be the chat-window in terms of focus
resetFocus: mobx.action, @mobx.action
setCmdAndOutput: mobx.action, setFocus(focus: boolean): void {
resetCmdAndOutput: mobx.action, this.resetFocus();
setCmdToExec: mobx.action, this.sidebarChatFocused.set(focus);
resetCmdToExec: mobx.action, }
hasFocus: mobx.computed,
focused: mobx.computed, hasFocus(): boolean {
cmdToExec: mobx.computed, return this.sidebarChatFocused.get();
}
@mobx.action
resetFocus(): void {
this.sidebarChatFocused.set(false);
}
@mobx.action
setCmdAndOutput(cmd: string, output: string, usedRows: number, isError: boolean): void {
console.log("cmd", cmd);
this.cmdAndOutput.set({
cmd: cmd,
output: output,
usedRows: usedRows,
isError: isError,
}); });
this.sidebarChatFocus = { }
input: false,
block: false, getCmdAndOutput(): { cmd: string; output: string; usedRows: number; isError: boolean } {
}; return this.cmdAndOutput.get();
this.cmdAndOutput = { }
@mobx.action
resetCmdAndOutput(): void {
this.cmdAndOutput.set({
cmd: "", cmd: "",
output: "", output: "",
usedRows: 0, usedRows: 0,
isError: false, isError: false,
}; });
this.cmdFromChat = "";
}
// block can be the chat-window in terms of focus
setFocus(section: "input" | "block", focus: boolean): void {
document.querySelector(".sidebarchat .sidebarchat-input");
this.resetFocus();
this.sidebarChatFocus[section] = focus;
}
get hasFocus(): boolean {
return this.sidebarChatFocus.input || this.sidebarChatFocus.block;
}
get focused(): "input" | "block" | null {
if (this.sidebarChatFocus.input) return "input";
if (this.sidebarChatFocus.block) return "block";
return null;
}
resetFocus(): void {
this.sidebarChatFocus.input = false;
this.sidebarChatFocus.block = false;
}
setCmdAndOutput(cmd: string, output: string, usedRows: number, isError: boolean): void {
this.cmdAndOutput.cmd = cmd;
this.cmdAndOutput.output = output;
this.cmdAndOutput.usedRows = usedRows;
this.cmdAndOutput.isError = isError;
}
getCmdAndOutput(): CmdAndOutput {
return {
cmd: this.cmdAndOutput.cmd,
output: this.cmdAndOutput.output,
usedRows: this.cmdAndOutput.usedRows,
isError: this.cmdAndOutput.isError,
};
}
resetCmdAndOutput(): void {
this.cmdAndOutput.cmd = "";
this.cmdAndOutput.output = "";
this.cmdAndOutput.usedRows = 0;
this.cmdAndOutput.isError = false;
} }
hasCmdAndOutput(): boolean { hasCmdAndOutput(): boolean {
return this.cmdAndOutput.cmd.length > 0 || this.cmdAndOutput.output.length > 0; const { cmd, output } = this.cmdAndOutput.get();
return cmd.length > 0 || output.length > 0;
} }
@mobx.action
setCmdToExec(cmd: string): void { setCmdToExec(cmd: string): void {
this.cmdFromChat = cmd; this.cmdFromChat.set(cmd);
} }
@mobx.action
resetCmdToExec(): void { resetCmdToExec(): void {
this.cmdFromChat = ""; this.cmdFromChat.set("");
} }
get cmdToExec(): string { getCmdToExec(): string {
return this.cmdFromChat; return this.cmdFromChat.get();
}
getSelectedCodeBlockIndex(): number {
return this.selectedCodeBlockIndex.get();
}
setSelectedCodeBlockIndex(index: number): void {
this.selectedCodeBlockIndex.set(index);
}
resetSelectedCodeBlockIndex(): void {
this.selectedCodeBlockIndex.set(null);
} }
} }

View File

@ -990,13 +990,6 @@ declare global {
click?: () => void; // not required if role is set click?: () => void; // not required if role is set
submenu?: ContextMenuItem[]; submenu?: ContextMenuItem[];
}; };
type CmdAndOutput = {
cmd: string;
output: string;
usedRows: number;
isError: boolean;
};
} }
export {}; export {};