separate model for aichat sidebar

This commit is contained in:
Red Adaya
2024-05-02 22:16:38 +08:00
parent 4f06cd8095
commit cc61b16cec
13 changed files with 290 additions and 18 deletions
+1 -1
View File
@@ -207,7 +207,7 @@ class Bookmark extends React.Component<BookmarkProps, {}> {
<div className="bookmark-id-div">{bm.bookmarkid.substr(0, 8)}</div>
<div className="bookmark-content">
<If condition={hasDesc}>
<Markdown text={markdown} extraClassName="bottom-margin" />
<Markdown inputModel={GlobalModel.inputModel} text={markdown} extraClassName="bottom-margin" />
</If>
<CmdStrCode
cmdstr={bm.cmdstr}
+14 -11
View File
@@ -31,7 +31,7 @@ function CodeRenderer(props: any): any {
@mobxReact.observer
class CodeBlockMarkdown extends React.Component<
{ children: React.ReactNode; codeSelectSelectedIndex?: number; uuid: string },
{ inputModel: any; children: React.ReactNode; codeSelectSelectedIndex?: number; uuid: string },
{}
> {
blockIndex: number;
@@ -40,13 +40,12 @@ class CodeBlockMarkdown extends React.Component<
constructor(props) {
super(props);
this.blockRef = React.createRef();
this.blockIndex = GlobalModel.inputModel.addCodeBlockToCodeSelect(this.blockRef, this.props.uuid);
this.blockIndex = props.inputModel.addCodeBlockToCodeSelect(this.blockRef, this.props.uuid);
}
render() {
let clickHandler: (e: React.MouseEvent<HTMLElement>, blockIndex: number) => void;
let inputModel = GlobalModel.inputModel;
clickHandler = (e: React.MouseEvent<HTMLElement>, blockIndex: number) => {
const inputModel = this.props.inputModel;
const clickHandler = (e: React.MouseEvent<HTMLElement>, blockIndex: number) => {
inputModel.setCodeSelectSelectedCodeBlock(blockIndex);
};
let selected = this.blockIndex == this.props.codeSelectSelectedIndex;
@@ -64,7 +63,7 @@ class CodeBlockMarkdown extends React.Component<
@mobxReact.observer
class Markdown extends React.Component<
{ text: string; style?: any; extraClassName?: string; codeSelect?: boolean },
{ inputModel: any; text: string; style?: any; extraClassName?: string; codeSelect?: boolean },
{}
> {
curUuid: string;
@@ -75,10 +74,14 @@ class Markdown extends React.Component<
}
@boundMethod
CodeBlockRenderer(props: any, codeSelect: boolean, codeSelectIndex: number, curUuid: string): any {
codeBlockRenderer(props: any, codeSelect: boolean, codeSelectIndex: number, curUuid: string): any {
if (codeSelect) {
return (
<CodeBlockMarkdown codeSelectSelectedIndex={codeSelectIndex} uuid={curUuid}>
<CodeBlockMarkdown
inputModel={this.props.inputModel}
codeSelectSelectedIndex={codeSelectIndex}
uuid={curUuid}
>
{props.children}
</CodeBlockMarkdown>
);
@@ -95,9 +98,9 @@ class Markdown extends React.Component<
}
render() {
let text = this.props.text;
let { text, inputModel } = this.props;
let codeSelect = this.props.codeSelect;
let curCodeSelectIndex = GlobalModel.inputModel.getCodeSelectSelectedIndex();
let curCodeSelectIndex = inputModel.getCodeSelectSelectedIndex();
let markdownComponents = {
a: LinkRenderer,
h1: (props) => HeaderRenderer(props, 1),
@@ -107,7 +110,7 @@ class Markdown extends React.Component<
h5: (props) => HeaderRenderer(props, 5),
h6: (props) => HeaderRenderer(props, 6),
code: (props) => CodeRenderer(props),
pre: (props) => this.CodeBlockRenderer(props, codeSelect, curCodeSelectIndex, this.curUuid),
pre: (props) => this.codeBlockRenderer(props, codeSelect, curCodeSelectIndex, this.curUuid),
};
return (
<div className={cn("markdown content", this.props.extraClassName)} style={this.props.style}>
+5 -1
View File
@@ -42,7 +42,11 @@ class AlertModal extends React.Component<{}, {}> {
<Modal.Header onClose={this.closeModal} title={title} keybindings={true} />
<div className="wave-modal-body">
<If condition={message?.markdown}>
<Markdown text={message?.message ?? ""} extraClassName="bottom-margin" />
<Markdown
inputModel={GlobalModel.inputModel}
text={message?.message ?? ""}
extraClassName="bottom-margin"
/>
</If>
<If condition={!message?.markdown}>{message?.message}</If>
<If condition={message?.confirmflag}>
+5 -1
View File
@@ -64,7 +64,11 @@ export const UserInputModal = (userInputRequest: UserInputRequest) => {
<div className="wave-modal-dialog">
<div className="userinput-query">
<If condition={userInputRequest.markdown}>
<Markdown text={userInputRequest.querytext} extraClassName="bottom-margin" />
<Markdown
inputModel={GlobalModel.inputModel}
text={userInputRequest.querytext}
extraClassName="bottom-margin"
/>
</If>
<If condition={!userInputRequest.markdown}>{userInputRequest.querytext}</If>
</div>
+1 -1
View File
@@ -78,7 +78,7 @@ class PluginsView extends React.Component<{}, {}> {
{plugin.readme && (
<div className="plugin-readme">
<div className="plugin-label">{"Readme"}</div>
<Markdown text={plugin.readme} />
<Markdown inputModel={GlobalModel.inputModel} text={plugin.readme} />
</div>
)}
</div>
+6 -1
View File
@@ -115,7 +115,11 @@ class ChatContent extends React.Component<{}, {}> {
<div className="chat-msg-header">
<i className="fa-sharp fa-solid fa-sparkles"></i>
</div>
<Markdown text={chatItem.assistantresponse.message} codeSelect />
<Markdown
inputModel={GlobalModel.aichatModel}
text={chatItem.assistantresponse.message}
codeSelect
/>
</>
);
}
@@ -179,6 +183,7 @@ class AIChat extends React.Component<{}, {}> {
submitChatMessage(messageStr: string) {
const curLine = GlobalModel.inputModel.curLine;
console.log("enter key pressed", messageStr, "curLine: ", curLine);
const prtn = GlobalModel.submitChatInfoCommand(messageStr, curLine, false);
prtn.then((rtn) => {
if (!rtn.success) {
+5 -1
View File
@@ -224,7 +224,11 @@ class AIChat extends React.Component<{}, {}> {
<i className="fa-sharp fa-solid fa-sparkles"></i>
<div className="chat-username">AI Assistant</div>
</div>
<Markdown text={chatItem.assistantresponse.message} codeSelect />
<Markdown
inputModel={GlobalModel.inputModel}
text={chatItem.assistantresponse.message}
codeSelect
/>
</span>
);
}
+234
View File
@@ -0,0 +1,234 @@
// Copyright 2023, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import type React from "react";
import * as mobx from "mobx";
import type { Model } from "./model";
import { GlobalCommandRunner } from "./global";
class AIChatModel {
globalModel: Model;
activeAuxView: OV<InputAuxViewType> = mobx.observable.box(null);
auxViewFocus: OV<boolean> = mobx.observable.box(false);
cmdInputHeight: OV<number> = mobx.observable.box(0);
aiChatTextAreaRef: React.RefObject<HTMLTextAreaElement>;
aiChatWindowRef: React.RefObject<HTMLDivElement>;
codeSelectBlockRefArray: Array<React.RefObject<HTMLElement>>;
codeSelectSelectedIndex: OV<number> = mobx.observable.box(-1);
codeSelectUuid: string;
AICmdInfoChatItems: mobx.IObservableArray<OpenAICmdInfoChatMessageType> = mobx.observable.array([], {
name: "aicmdinfo-chat",
});
readonly codeSelectTop: number = -2;
readonly codeSelectBottom: number = -1;
infoMsg: OV<InfoType> = mobx.observable.box(null);
infoTimeoutId: any = null;
inputExpanded: OV<boolean> = mobx.observable.box(false, {
name: "inputExpanded",
});
// focus
inputFocused: OV<boolean> = mobx.observable.box(false);
lineFocused: OV<boolean> = mobx.observable.box(false);
physicalInputFocused: OV<boolean> = mobx.observable.box(false);
forceInputFocus: boolean = false;
lastCurLine: string = "";
constructor(globalModel: Model) {
this.globalModel = globalModel;
mobx.makeObservable(this);
mobx.action(() => {
this.codeSelectSelectedIndex.set(-1);
this.codeSelectBlockRefArray = [];
})();
this.codeSelectUuid = "";
}
// Focuses the main input or the auxiliary view, depending on the active auxiliary view
@mobx.action
giveFocus(): void {
// focus aichat sidebar input
}
@mobx.action
setPhysicalInputFocused(isFocused: boolean): void {
this.physicalInputFocused.set(isFocused);
if (isFocused) {
const screen = this.globalModel.getActiveScreen();
if (screen != null) {
if (screen.focusType.get() != "input") {
GlobalCommandRunner.screenSetFocus("input");
}
}
}
}
hasFocus(): boolean {
const mainInputElem = document.getElementById("main-cmd-input");
if (document.activeElement == mainInputElem) {
return true;
}
const historyInputElem = document.querySelector(".cmd-input input.history-input");
if (document.activeElement == historyInputElem) {
return true;
}
let aiChatInputElem = document.querySelector(".cmd-input chat-cmd-input");
if (document.activeElement == aiChatInputElem) {
return true;
}
return false;
}
@mobx.action
setOpenAICmdInfoChat(chat: OpenAICmdInfoChatMessageType[]): void {
this.AICmdInfoChatItems.replace(chat);
this.codeSelectBlockRefArray = [];
}
closeAuxView(): void {
// close and give focus back to main input
}
shouldRenderAuxViewKeybindings(view: InputAuxViewType): boolean {
// when aichat sidebar is mounted, it will render the keybindings
return true;
}
setCmdInfoChatRefs(
textAreaRef: React.RefObject<HTMLTextAreaElement>,
chatWindowRef: React.RefObject<HTMLDivElement>
) {
this.aiChatTextAreaRef = textAreaRef;
this.aiChatWindowRef = chatWindowRef;
}
setAIChatFocus() {
if (this.aiChatTextAreaRef?.current != null) {
this.aiChatTextAreaRef.current.focus();
}
}
addCodeBlockToCodeSelect(blockRef: React.RefObject<HTMLElement>, uuid: string): number {
let rtn = -1;
if (uuid != this.codeSelectUuid) {
this.codeSelectUuid = uuid;
this.codeSelectBlockRefArray = [];
}
rtn = this.codeSelectBlockRefArray.length;
this.codeSelectBlockRefArray.push(blockRef);
return rtn;
}
@mobx.action
setCodeSelectSelectedCodeBlock(blockIndex: number) {
if (blockIndex >= 0 && blockIndex < this.codeSelectBlockRefArray.length) {
this.codeSelectSelectedIndex.set(blockIndex);
const currentRef = this.codeSelectBlockRefArray[blockIndex].current;
if (currentRef != null && this.aiChatWindowRef?.current != null) {
const chatWindowTop = this.aiChatWindowRef.current.scrollTop;
const chatWindowBottom = chatWindowTop + this.aiChatWindowRef.current.clientHeight - 100;
const elemTop = currentRef.offsetTop;
let elemBottom = elemTop - currentRef.offsetHeight;
const elementIsInView = elemBottom < chatWindowBottom && elemTop > chatWindowTop;
if (!elementIsInView) {
this.aiChatWindowRef.current.scrollTop = elemBottom - this.aiChatWindowRef.current.clientHeight / 3;
}
}
}
this.codeSelectBlockRefArray = [];
}
@mobx.action
codeSelectSelectNextNewestCodeBlock() {
// oldest code block = index 0 in array
// this decrements codeSelectSelected index
if (this.codeSelectSelectedIndex.get() == this.codeSelectTop) {
this.codeSelectSelectedIndex.set(this.codeSelectBottom);
} else if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) {
return;
}
const incBlockIndex = this.codeSelectSelectedIndex.get() + 1;
if (this.codeSelectSelectedIndex.get() == this.codeSelectBlockRefArray.length - 1) {
this.codeSelectDeselectAll();
if (this.aiChatWindowRef?.current != null) {
this.aiChatWindowRef.current.scrollTop = this.aiChatWindowRef.current.scrollHeight;
}
}
if (incBlockIndex >= 0 && incBlockIndex < this.codeSelectBlockRefArray.length) {
this.setCodeSelectSelectedCodeBlock(incBlockIndex);
}
}
@mobx.action
codeSelectSelectNextOldestCodeBlock() {
if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) {
if (this.codeSelectBlockRefArray.length > 0) {
this.codeSelectSelectedIndex.set(this.codeSelectBlockRefArray.length);
} else {
return;
}
} else if (this.codeSelectSelectedIndex.get() == this.codeSelectTop) {
return;
}
const decBlockIndex = this.codeSelectSelectedIndex.get() - 1;
if (decBlockIndex < 0) {
this.codeSelectDeselectAll(this.codeSelectTop);
if (this.aiChatWindowRef?.current != null) {
this.aiChatWindowRef.current.scrollTop = 0;
}
}
if (decBlockIndex >= 0 && decBlockIndex < this.codeSelectBlockRefArray.length) {
this.setCodeSelectSelectedCodeBlock(decBlockIndex);
}
}
getCodeSelectSelectedIndex() {
return this.codeSelectSelectedIndex.get();
}
getCodeSelectRefArrayLength() {
return this.codeSelectBlockRefArray.length;
}
codeBlockIsSelected(blockIndex: number): boolean {
return blockIndex == this.codeSelectSelectedIndex.get();
}
codeSelectDeselectAll(direction: number = this.codeSelectBottom) {
if (this.codeSelectSelectedIndex.get() == direction) {
return;
}
mobx.action(() => {
this.codeSelectSelectedIndex.set(direction);
this.codeSelectBlockRefArray = [];
})();
}
@mobx.action
openAIAssistantChat(): void {
// open aichat sidebar
}
clearAIAssistantChat(): void {
const prtn = this.globalModel.submitChatInfoCommand("", "", true);
prtn.then((rtn) => {
if (!rtn.success) {
console.log("submit chat command error: " + rtn.error);
}
}).catch((error) => {
console.log("submit chat command error: ", error);
});
}
_clearInfoTimeout(): void {
if (this.infoTimeoutId != null) {
clearTimeout(this.infoTimeoutId);
this.infoTimeoutId = null;
}
}
}
export { AIChatModel };
+1
View File
@@ -6,6 +6,7 @@ export { ClientSettingsViewModel } from "./clientsettingsview";
export { Cmd } from "./cmd";
export { ConnectionsViewModel } from "./connectionsview";
export { InputModel } from "./input";
export { AIChatModel } from "./aichat";
export { MainSidebarModel } from "./mainsidebar";
export { RightSidebarModel } from "./rightsidebar";
export { ModalsModel } from "./modals";
+6
View File
@@ -759,22 +759,28 @@ class InputModel {
@mobx.computed
get curLine(): string {
console.log("triggered get curLine");
const hidx = this.historyIndex.get();
if (hidx < this.modHistory.length && this.modHistory[hidx] != null) {
console.log("this.modHistory[hidx]", this.modHistory[hidx]);
return this.modHistory[hidx];
}
const hitems = this.filteredHistoryItems;
if (hidx == 0 || hitems == null || hidx > hitems.length) {
console.log("returning empty string 1");
return "";
}
const hitem = hitems[hidx - 1];
if (hitem == null) {
console.log("returning empty string 2");
return "";
}
console.log("returning hitem.cmdstr", hitem.cmdstr);
return hitem.cmdstr;
}
set curLine(val: string) {
console.log("triggered set curLine");
this.lastCurLine = this.curLine;
const hidx = this.historyIndex.get();
mobx.action(() => {
+4
View File
@@ -21,6 +21,7 @@ import { KeybindManager, adaptFromReactOrNativeKeyEvent, setKeyUtilPlatform } fr
import { Session } from "./session";
import { ScreenLines } from "./screenlines";
import { InputModel } from "./input";
import { AIChatModel } from "./aichat";
import { PluginsModel } from "./plugins";
import { BookmarksModel } from "./bookmarks";
import { HistoryViewModel } from "./historyview";
@@ -113,6 +114,7 @@ class Model {
keybindManager: KeybindManager;
inputModel: InputModel;
aichatModel: AIChatModel;
pluginsModel: PluginsModel;
bookmarksModel: BookmarksModel;
historyViewModel: HistoryViewModel;
@@ -162,6 +164,7 @@ class Model {
this.initSystemKeybindings();
this.initAppKeybindings();
this.inputModel = new InputModel(this);
this.aichatModel = new AIChatModel(this);
this.pluginsModel = new PluginsModel(this);
this.bookmarksModel = new BookmarksModel(this);
this.historyViewModel = new HistoryViewModel(this);
@@ -1341,6 +1344,7 @@ class Model {
interactive: boolean,
runUpdate: boolean = true
): Promise<CommandRtnType> {
console.log("cmdPk", cmdPk);
if (this.debugCmds > 0) {
console.log("[cmd]", cmdPacketString(cmdPk));
if (this.debugCmds > 1) {
+2
View File
@@ -6,6 +6,7 @@ import * as mobx from "mobx";
import * as mobxReact from "mobx-react";
import { sprintf } from "sprintf-js";
import { Markdown } from "@/elements";
import { GlobalModel } from "@/models/global";
import "./markdown.less";
@@ -78,6 +79,7 @@ class SimpleMarkdownRenderer extends React.Component<
}}
>
<Markdown
inputModel={GlobalModel.inputModel}
text={this.markdownText.get()}
style={{ maxHeight: opts.maxSize.height, maxWidth: DefaultMaxMarkdownWidth }}
/>
+6 -1
View File
@@ -8,6 +8,7 @@ import { debounce } from "throttle-debounce";
import { boundMethod } from "autobind-decorator";
import { PacketDataBuffer } from "../core/ptydata";
import { Markdown } from "@/elements";
import { GlobalModel } from "@/models/global";
import "./openai.less";
@@ -207,7 +208,11 @@ class OpenAIRenderer extends React.Component<{ model: OpenAIRendererModel }> {
paddingRight: 5,
}}
>
<Markdown text={message} style={{ maxHeight: opts.maxSize.height }} />
<Markdown
inputModel={GlobalModel.aichatModel}
text={message}
style={{ maxHeight: opts.maxSize.height }}
/>
</div>
</div>
</div>