get chatgpt working inline

This commit is contained in:
sawka 2023-05-05 16:13:18 -07:00
parent 162dd5a7a9
commit 6ffe0732e0
11 changed files with 453 additions and 291 deletions

View File

@ -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
View 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};

View File

@ -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>&nbsp;</div>
<div className="renderer"><i className="fa-sharp fa-solid fa-fill"/>openai&nbsp;</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>&nbsp;</div>
<If condition={!isBlank(renderer) && renderer != "terminal"}>
<div className="renderer"><i className="fa-sharp fa-solid fa-fill"/>{renderer}&nbsp;</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>;
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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;
}
}
}

View File

@ -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>
);
}

View File

@ -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
View 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};