From 6ffe0732e05828217a512c126a9f1f55a5dd1da6 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 5 May 2023 16:13:18 -0700 Subject: [PATCH] get chatgpt working inline --- src/comps.less | 6 + src/fullrenderer.tsx | 89 +++++++++++++++ src/linecomps.tsx | 254 +++++------------------------------------ src/lines.less | 44 +------ src/model.ts | 35 +++++- src/plugins.ts | 21 +++- src/ptydata.ts | 13 ++- src/sh2.less | 27 +++++ src/simplerenderer.tsx | 25 ++-- src/types.ts | 25 +++- src/view/openai.tsx | 205 +++++++++++++++++++++++++++++++++ 11 files changed, 453 insertions(+), 291 deletions(-) create mode 100644 src/fullrenderer.tsx create mode 100644 src/view/openai.tsx diff --git a/src/comps.less b/src/comps.less index 6cecbfcd..915afd58 100644 --- a/src/comps.less +++ b/src/comps.less @@ -326,6 +326,12 @@ body .xterm .xterm-viewport { color: #32afff; } + table { + tr th { + color: white; + } + } + ul { list-style-type: disc; list-style-position: outside; diff --git a/src/fullrenderer.tsx b/src/fullrenderer.tsx new file mode 100644 index 00000000..1182559c --- /dev/null +++ b/src/fullrenderer.tsx @@ -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 = mobx.IObservableValue; +type CV = mobx.IComputedValue; + +@mobxReact.observer +class FullRenderer extends React.Component<{rendererContainer : RendererContainerType, cmdId : string, plugin : RendererPluginType, onHeightChange : () => void, initParams : RendererModelInitializeParams}, {}> { + model : RendererModel; + wrapperDivRef : React.RefObject = 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) { +
+ (no component found in plugin) +
+ } + return ( +
+ +
+ ); + } +} + +export {FullRenderer}; + diff --git a/src/linecomps.tsx b/src/linecomps.tsx index 341be3b2..fd314bcd 100644 --- a/src/linecomps.tsx +++ b/src/linecomps.tsx @@ -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, onHeightChange : LineHeightChangeCallbackType, topBorder : boolean, renderMode : RenderModeType, overrideCollapsed : OV, noSelect? : boolean, showHints? : boolean}, {}> { - dataBuffer : PtyDataBuffer = new PtyDataBuffer(); - loading : OV = mobx.observable.box(null, {name: "loading"}); - loadError : OV = mobx.observable.box(null, {name: "loadError"}); - dataLines : OArr = mobx.observable.array([], {name: "dataLines"}); - dataPos : number = 0; - lineRef : React.RefObject = 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 ( -
- -
{formattedTime}
-
- ); - } - - 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 ( -
-
-
{formattedTime}
-
 
-
openai 
-
- ({termOpts.rows}x{termOpts.cols}) -
-
- -
-
-
- ); - } - - 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 ( -
- [user] -
{cmdStr}
-
- ); - } - - renderOutput(cmd : Cmd) { - let output = "...\nhello\nmore"; - return ( -
-
[assistant]
-
{output}
-
- ); - } - - 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 ( -
- [cmd not found '{line.cmdid}'] -
- ); - } - 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 ( -
-
-
- - {this.renderMetaWrap(cmd)} -
- {this.renderPrompt(cmd)} - {this.renderOutput(cmd)} -
- ); - } -} - @mobxReact.observer class LineCmd extends React.Component<{screen : LineContainerModel, line : LineType, width : number, staticRender : boolean, visible : OV, onHeightChange : LineHeightChangeCallbackType, topBorder : boolean, renderMode : RenderModeType, overrideCollapsed : OV, noSelect? : boolean, showHints? : boolean}, {}> { lineRef : React.RefObject = 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 ( -
-
- -
{formattedTime}
-
 
- -
{renderer} 
-
-
- ({termOpts.rows}x{termOpts.cols}) -
-
- -
-
- {this.renderCmdText(cmd)} -
- ); - } - 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 (
-
+
{this.renderMeta1(cmd)} - {this.renderCmdText(cmd)} + + {this.renderCmdText(cmd)} +
@@ -789,9 +587,12 @@ class LineCmd extends React.Component<{screen : LineContainerModel, line : LineT - + + + +
@@ -825,12 +626,9 @@ class Line extends React.Component<{screen : LineContainerModel, line : LineType if (line.linetype == "text") { return ; } - if (line.linetype == "cmd") { + if (line.linetype == "cmd" || line.linetype == "openai") { return ; } - if (line.linetype == "openai") { - return ; - } return
[invalid line type '{line.linetype}']
; } } diff --git a/src/lines.less b/src/lines.less index cc770d74..54b6cbc5 100644 --- a/src/lines.less +++ b/src/lines.less @@ -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; diff --git a/src/model.ts b/src/model.ts index 88f91304..c03275c3 100644 --- a/src/model.ts +++ b/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; @@ -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; } diff --git a/src/plugins.ts b/src/plugins.ts index d8043504..bd6223cc 100644 --- a/src/plugins.ts +++ b/src/plugins.ts @@ -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; } diff --git a/src/ptydata.ts b/src/ptydata.ts index ac395c91..a4f962ba 100644 --- a/src/ptydata.ts +++ b/src/ptydata.ts @@ -63,12 +63,17 @@ const NewLineCharCode = "\n".charCodeAt(0); class PacketDataBuffer extends PtyDataBuffer { parsePos : number; - packets : OArr; + 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); diff --git a/src/sh2.less b/src/sh2.less index 92b5da90..49a712d1 100644 --- a/src/sh2.less +++ b/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; + } + } +} diff --git a/src/simplerenderer.tsx b/src/simplerenderer.tsx index 552d8ac0..fa48710b 100644 --- a/src/simplerenderer.tsx +++ b/src/simplerenderer.tsx @@ -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 = mobx.IObservableValue; @@ -20,13 +20,8 @@ class SimpleBlobRendererModel { loading : OV; loadError : OV = mobx.observable.box(null, {name: "renderer-loadError"}); ptyData : PtyDataType; - updateHeight_debounced : (newHeight : number) => void; ptyDataSource : (termContext : TermContextUnion) => Promise; - 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 = 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 (
...
); } - let Comp = plugin.component; + let Comp = plugin.simpleComponent; + if (Comp == null) { +
+ (no component found in plugin) +
+ } let dataBlob = new Blob([model.ptyData.data]); + let simpleModel = (model as SimpleBlobRendererModel); return (
- +
); } diff --git a/src/types.ts b/src/types.ts index 93aeed37..a11d1b4a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -225,6 +225,7 @@ type CmdDataType = { cmdid : string, remote : RemotePtrType, cmdstr : string, + rawcmdstr : string, festate : Record, 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, + 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}; diff --git a/src/view/openai.tsx b/src/view/openai.tsx new file mode 100644 index 00000000..491fec25 --- /dev/null +++ b/src/view/openai.tsx @@ -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 = mobx.IObservableValue; +type OArr = mobx.IObservableArray; +type OMap = mobx.ObservableMap; + +type OpenAIOutputType = { + model : string, + created : number, + finish_reason : string, + message : string, +}; + +class OpenAIRendererModel { + context : T.RendererContext; + opts : T.RendererOpts; + isDone : OV; + api : T.RendererModelContainerApi; + savedHeight : number; + loading : OV; + loadError : OV = mobx.observable.box(null, {name: "renderer-loadError"}); + updateHeight_debounced : (newHeight : number) => void; + ptyDataSource : (termContext : T.TermContextUnion) => Promise; + packetData : PacketDataBuffer; + rawCmd : T.WebCmd; + output : OV; + + 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 ( +
+ [user] +
+ {cmdStr} +
+
+ ); + } + + 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 ( +
+
[assistant]
+
+
+ +
+
+
+ ); + } + + render() { + let model : OpenAIRendererModel = this.props.model; + let cmd = model.rawCmd; + return ( +
+ {this.renderPrompt(cmd)} + {this.renderOutput(cmd)} +
+ ); + } +} + +export {OpenAIRenderer, OpenAIRendererModel};