mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-22 16:48:23 +01:00
get chatgpt working inline
This commit is contained in:
parent
162dd5a7a9
commit
6ffe0732e0
@ -326,6 +326,12 @@ body .xterm .xterm-viewport {
|
||||
color: #32afff;
|
||||
}
|
||||
|
||||
table {
|
||||
tr th {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
list-style-position: outside;
|
||||
|
89
src/fullrenderer.tsx
Normal file
89
src/fullrenderer.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import {sprintf} from "sprintf-js";
|
||||
import {boundMethod} from "autobind-decorator";
|
||||
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
|
||||
import type {RendererModelInitializeParams, TermOptsType, RendererContext, RendererOpts, SimpleBlobRendererComponent, RendererModelContainerApi, RendererPluginType, PtyDataType, RendererModel, RendererOptsUpdate, LineType, TermContextUnion, RendererContainerType} from "./types";
|
||||
import {PacketDataBuffer} from "./ptydata";
|
||||
import {debounce, throttle} from "throttle-debounce";
|
||||
|
||||
type OV<V> = mobx.IObservableValue<V>;
|
||||
type CV<V> = mobx.IComputedValue<V>;
|
||||
|
||||
@mobxReact.observer
|
||||
class FullRenderer extends React.Component<{rendererContainer : RendererContainerType, cmdId : string, plugin : RendererPluginType, onHeightChange : () => void, initParams : RendererModelInitializeParams}, {}> {
|
||||
model : RendererModel;
|
||||
wrapperDivRef : React.RefObject<any> = React.createRef();
|
||||
rszObs : ResizeObserver;
|
||||
updateHeight_debounced : (newHeight : number) => void;
|
||||
|
||||
constructor(props : any) {
|
||||
super(props);
|
||||
let {rendererContainer, cmdId, plugin, initParams} = this.props;
|
||||
this.model = plugin.modelCtor();
|
||||
this.model.initialize(initParams);
|
||||
rendererContainer.registerRenderer(cmdId, this.model);
|
||||
this.updateHeight_debounced = debounce(1000, this.updateHeight.bind(this));
|
||||
}
|
||||
|
||||
updateHeight(newHeight : number) : void {
|
||||
this.model.updateHeight(newHeight);
|
||||
}
|
||||
|
||||
handleResize(entries : ResizeObserverEntry[]) : void {
|
||||
if (this.props.onHeightChange) {
|
||||
this.props.onHeightChange();
|
||||
}
|
||||
if (this.wrapperDivRef.current != null) {
|
||||
let height = this.wrapperDivRef.current.offsetHeight;
|
||||
this.updateHeight_debounced(height);
|
||||
}
|
||||
}
|
||||
|
||||
checkRszObs() {
|
||||
if (this.rszObs != null) {
|
||||
return;
|
||||
}
|
||||
if (this.wrapperDivRef.current == null) {
|
||||
return;
|
||||
}
|
||||
this.rszObs = new ResizeObserver(this.handleResize.bind(this));
|
||||
this.rszObs.observe(this.wrapperDivRef.current);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.checkRszObs();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
let {rendererContainer, cmdId} = this.props;
|
||||
rendererContainer.unloadRenderer(cmdId);
|
||||
if (this.rszObs != null) {
|
||||
this.rszObs.disconnect();
|
||||
this.rszObs = null;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.checkRszObs();
|
||||
}
|
||||
|
||||
render() {
|
||||
let {plugin} = this.props;
|
||||
let Comp = plugin.fullComponent;
|
||||
if (Comp == null) {
|
||||
<div ref={this.wrapperDivRef}>
|
||||
(no component found in plugin)
|
||||
</div>
|
||||
}
|
||||
return (
|
||||
<div ref={this.wrapperDivRef}>
|
||||
<Comp model={this.model}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export {FullRenderer};
|
||||
|
@ -14,6 +14,7 @@ import {TermWrap} from "./term";
|
||||
import type {LineContainerModel} from "./model";
|
||||
import {renderCmdText} from "./elements";
|
||||
import {SimpleBlobRendererModel, SimpleBlobRenderer} from "./simplerenderer";
|
||||
import {FullRenderer} from "./fullrenderer";
|
||||
import {isBlank} from "./util";
|
||||
import {PluginModel} from "./plugins";
|
||||
import {PtyDataBuffer} from "./ptydata";
|
||||
@ -146,199 +147,6 @@ class SmallLineAvatar extends React.Component<{line : LineType, cmd : Cmd, onRig
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class LineOpenAI extends React.Component<{screen : LineContainerModel, line : LineType, width : number, staticRender : boolean, visible : OV<boolean>, onHeightChange : LineHeightChangeCallbackType, topBorder : boolean, renderMode : RenderModeType, overrideCollapsed : OV<boolean>, noSelect? : boolean, showHints? : boolean}, {}> {
|
||||
dataBuffer : PtyDataBuffer = new PtyDataBuffer();
|
||||
loading : OV<boolean> = mobx.observable.box(null, {name: "loading"});
|
||||
loadError : OV<string> = mobx.observable.box(null, {name: "loadError"});
|
||||
dataLines : OArr<string> = mobx.observable.array([], {name: "dataLines"});
|
||||
dataPos : number = 0;
|
||||
lineRef : React.RefObject<any> = React.createRef();
|
||||
|
||||
renderSimple() {
|
||||
let {screen, line, topBorder, width} = this.props;
|
||||
let cmd = screen.getCmd(line);
|
||||
let usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
|
||||
let height = 36 + usedRows;
|
||||
let formattedTime = lineutil.getLineDateTimeStr(line.ts);
|
||||
let mainDivCn = cn(
|
||||
"line",
|
||||
"line-openai",
|
||||
"line-simple",
|
||||
{"top-border": topBorder},
|
||||
);
|
||||
return (
|
||||
<div className={mainDivCn} ref={this.lineRef} data-lineid={line.lineid} data-linenum={line.linenum} data-screenid={line.screenid} style={{height: height}}>
|
||||
<SmallLineAvatar line={line} cmd={cmd}/>
|
||||
<div className="ts">{formattedTime}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.reload(0);
|
||||
}
|
||||
|
||||
updateLines() : void {
|
||||
|
||||
}
|
||||
|
||||
reload(delayMs : number) {
|
||||
let {line} = this.props;
|
||||
|
||||
mobx.action(() => {
|
||||
this.loading.set(true);
|
||||
this.dataLines.clear();
|
||||
})();
|
||||
let rtnp = getTermPtyData(lineutil.getRendererContext(line));
|
||||
if (rtnp == null) {
|
||||
console.log("no promise returned from ptyDataSource (simplerenderer)", this.context);
|
||||
return;
|
||||
}
|
||||
rtnp.then((ptydata) => {
|
||||
setTimeout(() => {
|
||||
this.dataPos = 0;
|
||||
this.dataBuffer.reset();
|
||||
this.dataBuffer.receiveData(ptydata.pos, ptydata.data, "reload");
|
||||
mobx.action(() => {
|
||||
this.loadError.set(null);
|
||||
})();
|
||||
}, delayMs);
|
||||
}).catch((e) => {
|
||||
console.log("error loading data", e);
|
||||
mobx.action(() => {
|
||||
this.loadError.set("error loading data: " + e);
|
||||
})();
|
||||
}).finally(() => {
|
||||
mobx.action(() => {
|
||||
this.loading.set(false);
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleClick() {
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
onAvatarRightClick(e : any) {
|
||||
this.handleLineSettings(e)
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleLineSettings(e : any) : void {
|
||||
let {line, noSelect} = this.props;
|
||||
if (noSelect) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (line != null) {
|
||||
mobx.action(() => {
|
||||
GlobalModel.lineSettingsModal.set(line.linenum);
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
renderMetaWrap(cmd : Cmd) {
|
||||
let {line} = this.props;
|
||||
let model = GlobalModel;
|
||||
let formattedTime = lineutil.getLineDateTimeStr(line.ts);
|
||||
let termOpts = cmd.getTermOpts();
|
||||
let remote = model.getRemote(cmd.remoteId);
|
||||
let renderer = line.renderer;
|
||||
return (
|
||||
<div key="meta" className="meta-wrap">
|
||||
<div key="meta1" className="meta meta-line1">
|
||||
<div className="ts">{formattedTime}</div>
|
||||
<div> </div>
|
||||
<div className="renderer"><i className="fa-sharp fa-solid fa-fill"/>openai </div>
|
||||
<div className="termopts">
|
||||
({termOpts.rows}x{termOpts.cols})
|
||||
</div>
|
||||
<div className="settings" onClick={this.handleLineSettings}>
|
||||
<i className="fa-sharp fa-solid fa-gear"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderPrompt(cmd : Cmd) {
|
||||
let cmdStr = cmd.getCmdStr().trim();
|
||||
if (cmdStr.startsWith("/openai")) {
|
||||
let spaceIdx = cmdStr.indexOf(" ");
|
||||
if (spaceIdx > 0) {
|
||||
cmdStr = cmdStr.substr(spaceIdx+1).trim();
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="openai-message">
|
||||
<span className="openai-role openai-role-user">[user]</span>
|
||||
<div className="openai-content">{cmdStr}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderOutput(cmd : Cmd) {
|
||||
let output = "...\nhello\nmore";
|
||||
return (
|
||||
<div className="openai-message">
|
||||
<div className="openai-role openai-role-assistant">[assistant]</div>
|
||||
<div className="openai-content">{output}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let {screen, line, width, staticRender, visible, topBorder, renderMode} = this.props;
|
||||
let model = GlobalModel;
|
||||
let lineid = line.lineid;
|
||||
let isVisible = visible.get();
|
||||
if (staticRender || !isVisible) {
|
||||
return this.renderSimple();
|
||||
}
|
||||
let formattedTime = lineutil.getLineDateTimeStr(line.ts);
|
||||
let cmd = screen.getCmd(line);
|
||||
if (cmd == null) {
|
||||
return (
|
||||
<div className="line line-invalid" ref={this.lineRef} data-lineid={line.lineid} data-linenum={line.linenum} data-screenid={line.screenid}>
|
||||
[cmd not found '{line.cmdid}']
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let status = cmd.getStatus();
|
||||
let lineNumStr = (line.linenumtemp ? "~" : "") + String(line.linenum);
|
||||
let isSelected = mobx.computed(() => (screen.getSelectedLine() == line.linenum), {name: "computed-isSelected"}).get();
|
||||
let isFocused = mobx.computed(() => {
|
||||
let screenFocusType = screen.getFocusType();
|
||||
return isSelected && (screenFocusType == "cmd");
|
||||
}, {name: "computed-isFocused"}).get();
|
||||
let isStatic = staticRender;
|
||||
let isRunning = cmd.isRunning()
|
||||
let mainDivCn = cn(
|
||||
"line",
|
||||
"line-openai",
|
||||
{"focus": isFocused},
|
||||
{"cmd-done": !isRunning},
|
||||
{"has-rtnstate": cmd.getRtnState()},
|
||||
{"top-border": topBorder},
|
||||
);
|
||||
return (
|
||||
<div className={mainDivCn} onClick={this.handleClick}
|
||||
data-lineid={line.lineid} data-linenum={line.linenum} data-screenid={line.screenid} data-cmdid={line.cmdid}>
|
||||
<div key="focus" className={cn("focus-indicator", {"selected": isSelected}, {"active": isSelected && isFocused})}/>
|
||||
<div key="header" className={cn("line-header")}>
|
||||
<SmallLineAvatar line={line} cmd={cmd} onRightClick={this.onAvatarRightClick}/>
|
||||
{this.renderMetaWrap(cmd)}
|
||||
</div>
|
||||
{this.renderPrompt(cmd)}
|
||||
{this.renderOutput(cmd)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class LineCmd extends React.Component<{screen : LineContainerModel, line : LineType, width : number, staticRender : boolean, visible : OV<boolean>, onHeightChange : LineHeightChangeCallbackType, topBorder : boolean, renderMode : RenderModeType, overrideCollapsed : OV<boolean>, noSelect? : boolean, showHints? : boolean}, {}> {
|
||||
lineRef : React.RefObject<any> = React.createRef();
|
||||
@ -562,14 +370,27 @@ class LineCmd extends React.Component<{screen : LineContainerModel, line : LineT
|
||||
console.log("resize button");
|
||||
}
|
||||
|
||||
getIsHidePrompt() : boolean {
|
||||
let {line} = this.props;
|
||||
let rendererPlugin : RendererPluginType = null;
|
||||
let isNoneRenderer = (line.renderer == "none");
|
||||
if (!isBlank(line.renderer) && line.renderer != "terminal" && !isNoneRenderer) {
|
||||
rendererPlugin = PluginModel.getRendererPluginByName(line.renderer);
|
||||
}
|
||||
let hidePrompt = (rendererPlugin != null && rendererPlugin.hidePrompt);
|
||||
return hidePrompt;
|
||||
}
|
||||
|
||||
getTerminalRendererHeight(cmd : Cmd) : number {
|
||||
let {screen, line, width, topBorder, renderMode} = this.props;
|
||||
// header is 36px tall, padding+border = 6px
|
||||
// header is 16px tall with hide-prompt, padding+border = 6px
|
||||
// zero-terminal is 0px
|
||||
// terminal-wrapper overhead is 11px (margin/padding)
|
||||
// inner-height, if zero-lines => 42
|
||||
// else: 53+(lines*lineheight)
|
||||
let height = 42; // height of zero height terminal
|
||||
let hidePrompt = this.getIsHidePrompt();
|
||||
let height = (hidePrompt ? 22 : 42); // height of zero height terminal
|
||||
let usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
|
||||
if (usedRows > 0) {
|
||||
height = 53 + termHeightFromRows(usedRows, GlobalModel.termFontSize.get());
|
||||
@ -654,33 +475,6 @@ class LineCmd extends React.Component<{screen : LineContainerModel, line : LineT
|
||||
);
|
||||
}
|
||||
|
||||
renderMetaWrap(cmd : Cmd) {
|
||||
let {line} = this.props;
|
||||
let model = GlobalModel;
|
||||
let formattedTime = lineutil.getLineDateTimeStr(line.ts);
|
||||
let termOpts = cmd.getTermOpts();
|
||||
let renderer = line.renderer;
|
||||
return (
|
||||
<div key="meta" className="meta-wrap">
|
||||
<div key="meta1" className="meta meta-line1">
|
||||
<SmallLineAvatar line={line} cmd={cmd}/>
|
||||
<div className="ts">{formattedTime}</div>
|
||||
<div> </div>
|
||||
<If condition={!isBlank(renderer) && renderer != "terminal"}>
|
||||
<div className="renderer"><i className="fa-sharp fa-solid fa-fill"/>{renderer} </div>
|
||||
</If>
|
||||
<div className="termopts">
|
||||
({termOpts.rows}x{termOpts.cols})
|
||||
</div>
|
||||
<div className="settings" onClick={this.handleLineSettings}>
|
||||
<i className="fa-sharp fa-solid fa-gear"/>
|
||||
</div>
|
||||
</div>
|
||||
{this.renderCmdText(cmd)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getRendererOpts(cmd : Cmd) : RendererOpts {
|
||||
let {screen} = this.props;
|
||||
return {
|
||||
@ -722,6 +516,7 @@ class LineCmd extends React.Component<{screen : LineContainerModel, line : LineT
|
||||
opts: this.getRendererOpts(cmd),
|
||||
ptyDataSource: getTermPtyData,
|
||||
api: api,
|
||||
rawCmd: cmd.getAsWebCmd(line.lineid),
|
||||
};
|
||||
}
|
||||
|
||||
@ -769,15 +564,18 @@ class LineCmd extends React.Component<{screen : LineContainerModel, line : LineT
|
||||
rendererPlugin = PluginModel.getRendererPluginByName(line.renderer);
|
||||
}
|
||||
let rendererType = lineutil.getRendererType(line);
|
||||
let hidePrompt = (rendererPlugin != null && rendererPlugin.hidePrompt);
|
||||
return (
|
||||
<div className={mainDivCn}
|
||||
ref={this.lineRef} onClick={this.handleClick}
|
||||
data-lineid={line.lineid} data-linenum={line.linenum} data-screenid={line.screenid} data-cmdid={line.cmdid}>
|
||||
<div key="focus" className={cn("focus-indicator", {"selected": isSelected}, {"active": isSelected && isFocused})}/>
|
||||
<div key="header" className={cn("line-header", {"is-expanded": isExpanded})}>
|
||||
<div key="header" className={cn("line-header", {"is-expanded": isExpanded}, {"hide-prompt": hidePrompt})}>
|
||||
<div key="meta" className="meta-wrap">
|
||||
{this.renderMeta1(cmd)}
|
||||
{this.renderCmdText(cmd)}
|
||||
<If condition={!hidePrompt}>
|
||||
{this.renderCmdText(cmd)}
|
||||
</If>
|
||||
</div>
|
||||
<div key="pin" title="Pin" className={cn("line-icon", {"active": line.pinned})} onClick={this.clickPin} style={{display: "none"}}>
|
||||
<i className="fa-sharp fa-solid fa-thumbtack"/>
|
||||
@ -789,9 +587,12 @@ class LineCmd extends React.Component<{screen : LineContainerModel, line : LineT
|
||||
<If condition={rendererPlugin == null && !isNoneRenderer}>
|
||||
<TerminalRenderer screen={screen} line={line} width={width} staticRender={staticRender} visible={visible} onHeightChange={this.handleHeightChange} collapsed={false}/>
|
||||
</If>
|
||||
<If condition={rendererPlugin != null}>
|
||||
<If condition={rendererPlugin != null && rendererPlugin.rendererType == "simple"}>
|
||||
<SimpleBlobRenderer rendererContainer={screen} cmdId={line.cmdid} plugin={rendererPlugin} onHeightChange={this.handleHeightChange} initParams={this.makeRendererModelInitializeParams()}/>
|
||||
</If>
|
||||
<If condition={rendererPlugin != null && rendererPlugin.rendererType == "full"}>
|
||||
<FullRenderer rendererContainer={screen} cmdId={line.cmdid} plugin={rendererPlugin} onHeightChange={this.handleHeightChange} initParams={this.makeRendererModelInitializeParams()}/>
|
||||
</If>
|
||||
<If condition={cmd.getRtnState()}>
|
||||
<div key="rtnstate" className="cmd-rtnstate" style={{visibility: ((cmd.getStatus() == "done") ? "visible" : "hidden")}}>
|
||||
<If condition={rsdiff == null || rsdiff == ""}>
|
||||
@ -825,12 +626,9 @@ class Line extends React.Component<{screen : LineContainerModel, line : LineType
|
||||
if (line.linetype == "text") {
|
||||
return <LineText {...this.props}/>;
|
||||
}
|
||||
if (line.linetype == "cmd") {
|
||||
if (line.linetype == "cmd" || line.linetype == "openai") {
|
||||
return <LineCmd {...this.props}/>;
|
||||
}
|
||||
if (line.linetype == "openai") {
|
||||
return <LineOpenAI {...this.props}/>;
|
||||
}
|
||||
return <div className="line line-invalid">[invalid line type '{line.linetype}']</div>;
|
||||
}
|
||||
}
|
||||
|
@ -21,46 +21,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.line.line-openai {
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
.line-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 18px;
|
||||
width: 100%;
|
||||
|
||||
.line-icon {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
padding: 3px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.openai-message {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
.openai-role {
|
||||
color: @term-bright-green;
|
||||
font-weight: bold;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.openai-role.openai-role-assistant {
|
||||
color: @term-bright-white;
|
||||
}
|
||||
|
||||
.openai-content {
|
||||
white-space: pre;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.line.line-cmd {
|
||||
flex-direction: column;
|
||||
scroll-margin-bottom: 20px;
|
||||
@ -81,6 +41,10 @@
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&.hide-prompt {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.line-icon {
|
||||
display: block;
|
||||
visibility: hidden;
|
||||
|
35
src/model.ts
35
src/model.ts
@ -5,7 +5,7 @@ import {debounce} from "throttle-debounce";
|
||||
import {handleJsonFetchResponse, base64ToArray, genMergeData, genMergeDataMap, genMergeSimpleData, boundInt, isModKeyPress} from "./util";
|
||||
import {TermWrap} from "./term";
|
||||
import {v4 as uuidv4} from "uuid";
|
||||
import type {SessionDataType, LineType, RemoteType, HistoryItem, RemoteInstanceType, RemotePtrType, CmdDataType, FeCmdPacketType, TermOptsType, RemoteStateType, ScreenDataType, ScreenOptsType, PtyDataUpdateType, ModelUpdateType, UpdateMessage, InfoType, CmdLineUpdateType, UIContextType, HistoryInfoType, HistoryQueryOpts, FeInputPacketType, TermWinSize, RemoteInputPacketType, ContextMenuOpts, RendererContext, RendererModel, PtyDataType, BookmarkType, ClientDataType, HistoryViewDataType, AlertMessageType, HistorySearchParams, FocusTypeStrs, ScreenLinesType, HistoryTypeStrs, RendererPluginType, WindowSize, ClientMigrationInfo, WebShareOpts, TermContextUnion, RemoteEditType, RemoteViewType, CommandRtnType} from "./types";
|
||||
import type {SessionDataType, LineType, RemoteType, HistoryItem, RemoteInstanceType, RemotePtrType, CmdDataType, FeCmdPacketType, TermOptsType, RemoteStateType, ScreenDataType, ScreenOptsType, PtyDataUpdateType, ModelUpdateType, UpdateMessage, InfoType, CmdLineUpdateType, UIContextType, HistoryInfoType, HistoryQueryOpts, FeInputPacketType, TermWinSize, RemoteInputPacketType, ContextMenuOpts, RendererContext, RendererModel, PtyDataType, BookmarkType, ClientDataType, HistoryViewDataType, AlertMessageType, HistorySearchParams, FocusTypeStrs, ScreenLinesType, HistoryTypeStrs, RendererPluginType, WindowSize, ClientMigrationInfo, WebShareOpts, TermContextUnion, RemoteEditType, RemoteViewType, CommandRtnType, WebCmd, WebRemote} from "./types";
|
||||
import {WSControl} from "./ws";
|
||||
import {measureText, getMonoFontSize, windowWidthToCols, windowHeightToRows, termWidthFromCols, termHeightFromRows} from "./textmeasure";
|
||||
import dayjs from "dayjs";
|
||||
@ -139,7 +139,6 @@ function ces(s : string) {
|
||||
class Cmd {
|
||||
screenId : string;
|
||||
remote : RemotePtrType;
|
||||
remoteId : string;
|
||||
cmdId : string;
|
||||
data : OV<CmdDataType>;
|
||||
|
||||
@ -160,6 +159,38 @@ class Cmd {
|
||||
})();
|
||||
}
|
||||
|
||||
getAsWebCmd(lineid : string) : WebCmd {
|
||||
let cmd = this.data.get();
|
||||
let remote = GlobalModel.getRemote(this.remote.remoteid);
|
||||
let webRemote : WebRemote = null;
|
||||
if (remote != null) {
|
||||
webRemote = {
|
||||
remoteid: cmd.remote.remoteid,
|
||||
alias: remote.remotealias,
|
||||
canonicalname: remote.remotecanonicalname,
|
||||
name: this.remote.name,
|
||||
homedir: remote.remotevars["home"],
|
||||
isroot: !!remote.remotevars["isroot"],
|
||||
}
|
||||
}
|
||||
let webCmd : WebCmd = {
|
||||
screenid: cmd.screenid,
|
||||
lineid: lineid,
|
||||
remote: webRemote,
|
||||
status: cmd.status,
|
||||
cmdstr: cmd.cmdstr,
|
||||
rawcmdstr: cmd.rawcmdstr,
|
||||
festate: cmd.festate,
|
||||
termopts: cmd.termopts,
|
||||
startpk: cmd.startpk,
|
||||
doneinfo: cmd.doneinfo,
|
||||
rtnstate: cmd.rtnstate,
|
||||
vts: 0,
|
||||
rtnstatestr: null,
|
||||
};
|
||||
return webCmd;
|
||||
}
|
||||
|
||||
getRtnState() : boolean {
|
||||
return this.data.get().rtnstate;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import {RendererPluginType} from "./types";
|
||||
import {SimpleImageRenderer} from "./view/image";
|
||||
import {SimpleMarkdownRenderer} from "./view/markdown";
|
||||
import {SimpleJsonRenderer} from "./view/json";
|
||||
import {OpenAIRenderer, OpenAIRendererModel} from "./view/openai";
|
||||
import {isBlank} from "./util";
|
||||
import {sprintf} from "sprintf-js";
|
||||
|
||||
@ -13,7 +14,7 @@ const ImagePlugin : RendererPluginType = {
|
||||
collapseType: "hide",
|
||||
globalCss: null,
|
||||
mimeTypes: ["image/*"],
|
||||
component: SimpleImageRenderer,
|
||||
simpleComponent: SimpleImageRenderer,
|
||||
};
|
||||
|
||||
const MarkdownPlugin : RendererPluginType = {
|
||||
@ -24,7 +25,7 @@ const MarkdownPlugin : RendererPluginType = {
|
||||
collapseType: "hide",
|
||||
globalCss: null,
|
||||
mimeTypes: ["text/markdown"],
|
||||
component: SimpleMarkdownRenderer,
|
||||
simpleComponent: SimpleMarkdownRenderer,
|
||||
};
|
||||
|
||||
const JsonPlugin : RendererPluginType = {
|
||||
@ -35,7 +36,20 @@ const JsonPlugin : RendererPluginType = {
|
||||
collapseType: "hide",
|
||||
globalCss: null,
|
||||
mimeTypes: ["application/json"],
|
||||
component: SimpleJsonRenderer,
|
||||
simpleComponent: SimpleJsonRenderer,
|
||||
};
|
||||
|
||||
const OpenAIPlugin : RendererPluginType = {
|
||||
name: "openai",
|
||||
rendererType: "full",
|
||||
heightType: "pixels",
|
||||
dataType: "model",
|
||||
collapseType: "remove",
|
||||
hidePrompt: true,
|
||||
globalCss: null,
|
||||
mimeTypes: ["application/json"],
|
||||
fullComponent: OpenAIRenderer,
|
||||
modelCtor: () => new OpenAIRendererModel(),
|
||||
};
|
||||
|
||||
class PluginModelClass {
|
||||
@ -72,6 +86,7 @@ if ((window as any).PluginModel == null) {
|
||||
PluginModel.registerRendererPlugin(ImagePlugin);
|
||||
PluginModel.registerRendererPlugin(MarkdownPlugin);
|
||||
PluginModel.registerRendererPlugin(JsonPlugin);
|
||||
PluginModel.registerRendererPlugin(OpenAIPlugin);
|
||||
(window as any).PluginModel = PluginModel;
|
||||
}
|
||||
|
||||
|
@ -63,12 +63,17 @@ const NewLineCharCode = "\n".charCodeAt(0);
|
||||
|
||||
class PacketDataBuffer extends PtyDataBuffer {
|
||||
parsePos : number;
|
||||
packets : OArr<Object>;
|
||||
callback : (any) => void;
|
||||
|
||||
constructor() {
|
||||
constructor(callback : (any) => void) {
|
||||
super();
|
||||
this.parsePos = 0;
|
||||
this.packets = mobx.observable.array([], {name: "packets"});
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
reset() : void {
|
||||
super.reset();
|
||||
this.parsePos = 0;
|
||||
}
|
||||
|
||||
processLine(line : string) {
|
||||
@ -94,7 +99,7 @@ class PacketDataBuffer extends PtyDataBuffer {
|
||||
}
|
||||
try {
|
||||
let packet = JSON.parse(packetStr);
|
||||
this.packets.push(packet);
|
||||
this.callback(packet);
|
||||
}
|
||||
catch (e) {
|
||||
console.log("invalid line packet (bad json)", line);
|
||||
|
27
src/sh2.less
27
src/sh2.less
@ -260,3 +260,30 @@ input[type=checkbox] {
|
||||
padding: 4px 4px 4px 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.openai-renderer {
|
||||
.openai-message {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
.openai-role {
|
||||
color: @term-bright-green;
|
||||
font-weight: bold;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.openai-role.openai-role-assistant {
|
||||
color: @term-bright-white;
|
||||
}
|
||||
|
||||
.openai-content-user {
|
||||
white-space: pre;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.openai-content-assistant {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import {sprintf} from "sprintf-js";
|
||||
import {boundMethod} from "autobind-decorator";
|
||||
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
|
||||
import type {RendererModelInitializeParams, TermOptsType, RendererContext, RendererOpts, SimpleBlobRendererComponent, RendererModelContainerApi, RendererPluginType, PtyDataType, RendererModel, RendererOptsUpdate, LineType, TermContextUnion, RendererContainerType} from "./types";
|
||||
import {PtyDataBuffer} from "./ptydata";
|
||||
import {PacketDataBuffer} from "./ptydata";
|
||||
import {debounce, throttle} from "throttle-debounce";
|
||||
|
||||
type OV<V> = mobx.IObservableValue<V>;
|
||||
@ -20,13 +20,8 @@ class SimpleBlobRendererModel {
|
||||
loading : OV<boolean>;
|
||||
loadError : OV<string> = mobx.observable.box(null, {name: "renderer-loadError"});
|
||||
ptyData : PtyDataType;
|
||||
updateHeight_debounced : (newHeight : number) => void;
|
||||
ptyDataSource : (termContext : TermContextUnion) => Promise<PtyDataType>;
|
||||
|
||||
constructor() {
|
||||
this.updateHeight_debounced = debounce(1000, this.updateHeight.bind(this));
|
||||
}
|
||||
|
||||
initialize(params : RendererModelInitializeParams) : void {
|
||||
this.loading = mobx.observable.box(true, {name: "renderer-loading"});
|
||||
this.isDone = mobx.observable.box(params.isDone, {name: "renderer-isDone"});
|
||||
@ -104,6 +99,7 @@ class SimpleBlobRenderer extends React.Component<{rendererContainer : RendererCo
|
||||
model : SimpleBlobRendererModel;
|
||||
wrapperDivRef : React.RefObject<any> = React.createRef();
|
||||
rszObs : ResizeObserver;
|
||||
updateHeight_debounced : (newHeight : number) => void;
|
||||
|
||||
constructor(props : any) {
|
||||
super(props);
|
||||
@ -111,6 +107,11 @@ class SimpleBlobRenderer extends React.Component<{rendererContainer : RendererCo
|
||||
this.model = new SimpleBlobRendererModel();
|
||||
this.model.initialize(initParams);
|
||||
rendererContainer.registerRenderer(cmdId, this.model);
|
||||
this.updateHeight_debounced = debounce(1000, this.updateHeight.bind(this));
|
||||
}
|
||||
|
||||
updateHeight(newHeight : number) : void {
|
||||
this.model.updateHeight(newHeight);
|
||||
}
|
||||
|
||||
handleResize(entries : ResizeObserverEntry[]) : void {
|
||||
@ -122,7 +123,7 @@ class SimpleBlobRenderer extends React.Component<{rendererContainer : RendererCo
|
||||
}
|
||||
if (!this.model.loading.get() && this.wrapperDivRef.current != null) {
|
||||
let height = this.wrapperDivRef.current.offsetHeight;
|
||||
this.model.updateHeight_debounced(height);
|
||||
this.updateHeight_debounced(height);
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,11 +162,17 @@ class SimpleBlobRenderer extends React.Component<{rendererContainer : RendererCo
|
||||
let height = this.model.savedHeight;
|
||||
return (<div ref={this.wrapperDivRef} style={{minHeight: height}}>...</div>);
|
||||
}
|
||||
let Comp = plugin.component;
|
||||
let Comp = plugin.simpleComponent;
|
||||
if (Comp == null) {
|
||||
<div ref={this.wrapperDivRef}>
|
||||
(no component found in plugin)
|
||||
</div>
|
||||
}
|
||||
let dataBlob = new Blob([model.ptyData.data]);
|
||||
let simpleModel = (model as SimpleBlobRendererModel);
|
||||
return (
|
||||
<div ref={this.wrapperDivRef}>
|
||||
<Comp data={dataBlob} context={model.context} opts={model.opts} savedHeight={this.model.savedHeight}/>
|
||||
<Comp data={dataBlob} context={simpleModel.context} opts={simpleModel.opts} savedHeight={simpleModel.savedHeight}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
25
src/types.ts
25
src/types.ts
@ -225,6 +225,7 @@ type CmdDataType = {
|
||||
cmdid : string,
|
||||
remote : RemotePtrType,
|
||||
cmdstr : string,
|
||||
rawcmdstr : string,
|
||||
festate : Record<string, string>,
|
||||
termopts : TermOptsType,
|
||||
origtermopts : TermOptsType,
|
||||
@ -381,12 +382,14 @@ type RendererPluginType = {
|
||||
name : string,
|
||||
rendererType : "simple" | "full",
|
||||
heightType : "rows" | "pixels",
|
||||
dataType : "json" | "blob" | "packet",
|
||||
dataType : "json" | "blob" | "model",
|
||||
collapseType : "hide" | "remove",
|
||||
hidePrompt? : boolean,
|
||||
globalCss? : string,
|
||||
mimeTypes? : string[],
|
||||
modelCtor? : RendererModel,
|
||||
component : SimpleBlobRendererComponent,
|
||||
modelCtor? : () => RendererModel,
|
||||
simpleComponent? : SimpleBlobRendererComponent,
|
||||
fullComponent? : FullRendererComponent,
|
||||
}
|
||||
|
||||
type RendererModelContainerApi = {
|
||||
@ -398,6 +401,7 @@ type RendererModelContainerApi = {
|
||||
type RendererModelInitializeParams = {
|
||||
context : RendererContext,
|
||||
isDone : boolean,
|
||||
rawCmd : WebCmd,
|
||||
savedHeight : number,
|
||||
opts : RendererOpts,
|
||||
api : RendererModelContainerApi,
|
||||
@ -412,7 +416,8 @@ type RendererModel = {
|
||||
updateOpts : (opts : RendererOptsUpdate) => void,
|
||||
setIsDone : () => void,
|
||||
receiveData : (pos : number, data : Uint8Array, reason? : string) => void,
|
||||
};
|
||||
updateHeight : (newHeight : number) => void,
|
||||
};
|
||||
|
||||
type SimpleBlobRendererComponent = React.ComponentType<{data : Blob, context : RendererContext, opts : RendererOpts, savedHeight : number}>;
|
||||
type SimpleJsonRendererComponent = React.ComponentType<{data : any, context : RendererContext, opts : RendererOpts, savedHeight : number}>;
|
||||
@ -523,7 +528,7 @@ type WebRemote = {
|
||||
};
|
||||
|
||||
type WebCmd = {
|
||||
screeid : string,
|
||||
screenid : string,
|
||||
lineid : string,
|
||||
remote : WebRemote,
|
||||
cmdstr : string,
|
||||
@ -590,4 +595,14 @@ type CommandRtnType = {
|
||||
|
||||
type LineHeightChangeCallbackType = (lineNum : number, newHeight : number, oldHeight : number) => void;
|
||||
|
||||
type OpenAIPacketType = {
|
||||
type : string,
|
||||
model : string,
|
||||
created : number,
|
||||
finish_reason : string,
|
||||
usage : Record<string, number>,
|
||||
index : number,
|
||||
text : string,
|
||||
};
|
||||
|
||||
export type {SessionDataType, LineType, RemoteType, RemoteStateType, RemoteInstanceType, HistoryItem, CmdRemoteStateType, FeCmdPacketType, TermOptsType, CmdStartPacketType, CmdDataType, ScreenDataType, ScreenOptsType, PtyDataUpdateType, ModelUpdateType, UpdateMessage, InfoType, CmdLineUpdateType, RemotePtrType, UIContextType, HistoryInfoType, HistoryQueryOpts, WatchScreenPacketType, TermWinSize, FeInputPacketType, RemoteInputPacketType, RemoteEditType, ContextMenuOpts, RendererContext, WindowSize, RendererModel, PtyDataType, BookmarkType, ClientDataType, PlaybookType, PlaybookEntryType, HistoryViewDataType, RenderModeType, AlertMessageType, HistorySearchParams, ScreenLinesType, FocusTypeStrs, HistoryTypeStrs, RendererOpts, RendererPluginType, SimpleBlobRendererComponent, RendererModelContainerApi, RendererModelInitializeParams, RendererOptsUpdate, ClientMigrationInfo, WebShareOpts, RemoteStatusTypeStrs, WebFullScreen, WebScreen, WebLine, WebCmd, RemoteTermContext, TermContextUnion, WebRemote, PtyDataUpdate, WebShareWSMessage, LineHeightChangeCallbackType, LineFactoryProps, LineInterface, RendererContainerType, RemoteViewType, CommandRtnType};
|
||||
|
205
src/view/openai.tsx
Normal file
205
src/view/openai.tsx
Normal file
@ -0,0 +1,205 @@
|
||||
import * as React from "react";
|
||||
import * as mobx from "mobx";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import cn from "classnames";
|
||||
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
|
||||
import * as T from "../types";
|
||||
import {debounce, throttle} from "throttle-debounce";
|
||||
import {boundMethod} from "autobind-decorator";
|
||||
import {sprintf} from "sprintf-js";
|
||||
import {PacketDataBuffer} from "../ptydata";
|
||||
import {Markdown} from "../elements";
|
||||
|
||||
type OV<V> = mobx.IObservableValue<V>;
|
||||
type OArr<V> = mobx.IObservableArray<V>;
|
||||
type OMap<K,V> = mobx.ObservableMap<K,V>;
|
||||
|
||||
type OpenAIOutputType = {
|
||||
model : string,
|
||||
created : number,
|
||||
finish_reason : string,
|
||||
message : string,
|
||||
};
|
||||
|
||||
class OpenAIRendererModel {
|
||||
context : T.RendererContext;
|
||||
opts : T.RendererOpts;
|
||||
isDone : OV<boolean>;
|
||||
api : T.RendererModelContainerApi;
|
||||
savedHeight : number;
|
||||
loading : OV<boolean>;
|
||||
loadError : OV<string> = mobx.observable.box(null, {name: "renderer-loadError"});
|
||||
updateHeight_debounced : (newHeight : number) => void;
|
||||
ptyDataSource : (termContext : T.TermContextUnion) => Promise<T.PtyDataType>;
|
||||
packetData : PacketDataBuffer;
|
||||
rawCmd : T.WebCmd;
|
||||
output : OV<OpenAIOutputType>;
|
||||
|
||||
constructor() {
|
||||
this.updateHeight_debounced = debounce(1000, this.updateHeight.bind(this));
|
||||
this.packetData = new PacketDataBuffer(this.packetCallback);
|
||||
this.output = mobx.observable.box(null, {name: "openai-output"});
|
||||
}
|
||||
|
||||
initialize(params : T.RendererModelInitializeParams) : void {
|
||||
this.loading = mobx.observable.box(true, {name: "renderer-loading"});
|
||||
this.isDone = mobx.observable.box(params.isDone, {name: "renderer-isDone"});
|
||||
this.context = params.context;
|
||||
this.opts = params.opts;
|
||||
this.api = params.api;
|
||||
this.savedHeight = params.savedHeight;
|
||||
this.ptyDataSource = params.ptyDataSource;
|
||||
this.rawCmd = params.rawCmd;
|
||||
if (this.isDone.get()) {
|
||||
setTimeout(() => this.reload(0), 10);
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
packetCallback(packetAny : any) {
|
||||
let packet : T.OpenAIPacketType = packetAny
|
||||
if (packet == null) {
|
||||
return;
|
||||
}
|
||||
if (packet.model != null && (packet.index ?? 0) == 0) {
|
||||
let output = {
|
||||
model: packet.model,
|
||||
created: packet.created,
|
||||
finish_reason: packet.finish_reason,
|
||||
message: (packet.text ?? ""),
|
||||
};
|
||||
mobx.action(() => {
|
||||
this.output.set(output);
|
||||
})();
|
||||
return;
|
||||
}
|
||||
if ((packet.index ?? 0) == 0) {
|
||||
mobx.action(() => {
|
||||
if (packet.finish_reason != null) {
|
||||
this.output.get().finish_reason = packet.finish_reason;
|
||||
}
|
||||
if (packet.text != null) {
|
||||
this.output.get().message += packet.text;
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
dispose() : void {
|
||||
return;
|
||||
}
|
||||
|
||||
giveFocus() : void {
|
||||
return;
|
||||
}
|
||||
|
||||
updateOpts(update : T.RendererOptsUpdate) : void {
|
||||
Object.assign(this.opts, update);
|
||||
}
|
||||
|
||||
updateHeight(newHeight : number) : void {
|
||||
if (this.savedHeight != newHeight) {
|
||||
this.savedHeight = newHeight;
|
||||
this.api.saveHeight(newHeight);
|
||||
}
|
||||
}
|
||||
|
||||
setIsDone() : void {
|
||||
if (this.isDone.get()) {
|
||||
return;
|
||||
}
|
||||
mobx.action(() => {
|
||||
this.isDone.set(true);
|
||||
})();
|
||||
this.reload(0);
|
||||
}
|
||||
|
||||
reload(delayMs : number) : void {
|
||||
mobx.action(() => {
|
||||
this.loading.set(true);
|
||||
})();
|
||||
let rtnp = this.ptyDataSource(this.context);
|
||||
if (rtnp == null) {
|
||||
console.log("no promise returned from ptyDataSource (openai renderer)", this.context);
|
||||
return;
|
||||
}
|
||||
rtnp.then((ptydata) => {
|
||||
setTimeout(() => {
|
||||
this.packetData.reset();
|
||||
this.receiveData(ptydata.pos, ptydata.data, "reload");
|
||||
mobx.action(() => {
|
||||
this.loading.set(false);
|
||||
this.loadError.set(null);
|
||||
})();
|
||||
}, delayMs);
|
||||
}).catch((e) => {
|
||||
console.log("error loading data", e);
|
||||
mobx.action(() => {
|
||||
this.loadError.set("error loading data: " + e);
|
||||
})();
|
||||
});
|
||||
}
|
||||
|
||||
receiveData(pos : number, data : Uint8Array, reason? : string) : void {
|
||||
this.packetData.receiveData(pos, data, reason);
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class OpenAIRenderer extends React.Component<{model : OpenAIRendererModel}> {
|
||||
renderPrompt(cmd : T.WebCmd) {
|
||||
let cmdStr = cmd.cmdstr.trim();
|
||||
if (cmdStr.startsWith("/openai")) {
|
||||
let spaceIdx = cmdStr.indexOf(" ");
|
||||
if (spaceIdx > 0) {
|
||||
cmdStr = cmdStr.substr(spaceIdx+1).trim();
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="openai-message">
|
||||
<span className="openai-role openai-role-user">[user]</span>
|
||||
<div className="openai-content-user">
|
||||
{cmdStr}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderOutput(cmd : T.WebCmd) {
|
||||
let output = this.props.model.output.get();
|
||||
let message = "";
|
||||
if (output != null) {
|
||||
message = output.message ?? "";
|
||||
}
|
||||
let model = this.props.model;
|
||||
let opts = model.opts;
|
||||
let maxWidth = opts.maxSize.width;
|
||||
let minWidth = opts.maxSize.width;
|
||||
if (minWidth > 1000) {
|
||||
minWidth = 1000;
|
||||
}
|
||||
return (
|
||||
<div className="openai-message">
|
||||
<div className="openai-role openai-role-assistant">[assistant]</div>
|
||||
<div className="openai-content-assistant">
|
||||
<div className="scroller" style={{maxHeight: opts.maxSize.height, minWidth: minWidth, width: "min-content", maxWidth: maxWidth}}>
|
||||
<Markdown text={message} style={{maxHeight: opts.maxSize.height}}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let model : OpenAIRendererModel = this.props.model;
|
||||
let cmd = model.rawCmd;
|
||||
return (
|
||||
<div className="renderer-container openai-renderer">
|
||||
{this.renderPrompt(cmd)}
|
||||
{this.renderOutput(cmd)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export {OpenAIRenderer, OpenAIRendererModel};
|
Loading…
Reference in New Issue
Block a user