basic waveapp json -> react/html functionality sorta working

This commit is contained in:
sawka 2024-04-12 16:25:52 -07:00
parent 41fe49a54c
commit 6319b26924
7 changed files with 325 additions and 7 deletions

View File

@ -226,13 +226,12 @@
}
.ts,
.termopts,
.renderer {
.termopts {
display: flex;
}
.renderer .renderer-icon {
margin-right: 0.5em;
.renderer-icon {
margin-right: 0.2em;
}
.metapart-mono {

View File

@ -291,10 +291,10 @@ class LineHeader extends React.Component<{ screen: LineContainerType; line: Line
</div>
<If condition={!isBlank(renderer) && renderer != "terminal"}>
<div className="meta-divider">|</div>
<div className="renderer">
<div className="renderer-icon">
<i className="fa-sharp fa-solid fa-fill renderer-icon" />
{renderer}
</div>
<div className="renderer-name">{renderer}</div>
</If>
</div>
);

View File

@ -129,4 +129,56 @@ class PacketDataBuffer extends PtyDataBuffer {
}
}
export { PtyDataBuffer, PacketDataBuffer };
class JsonLinesDataBuffer extends PtyDataBuffer {
parsePos: number;
callback: (any) => void;
constructor(callback: (any) => void) {
super();
this.parsePos = 0;
this.callback = callback;
}
reset(): void {
super.reset();
this.parsePos = 0;
}
processLine(line: string) {
if (line.length == 0) {
return;
}
let jsonVal: any = null;
try {
jsonVal = JSON.parse(line.trim());
} catch (e) {
console.log("invalid json", line, e);
return;
}
if (jsonVal != null) {
this.callback(jsonVal);
}
}
parseData() {
for (let i = this.parsePos; i < this.dataSize; i++) {
let ch = this.rawData[i];
if (ch == NewLineCharCode) {
// line does *not* include the newline
let line = new TextDecoder().decode(
new Uint8Array(this.rawData.buffer, this.parsePos, i - this.parsePos)
);
this.parsePos = i + 1;
this.processLine(line);
}
}
return;
}
receiveData(pos: number, data: Uint8Array, reason?: string): void {
super.receiveData(pos, data, reason);
this.parseData();
}
}
export { PtyDataBuffer, PacketDataBuffer, JsonLinesDataBuffer };

View File

@ -9,6 +9,7 @@ import { CSVRenderer } from "./csv/csv";
import { OpenAIRenderer, OpenAIRendererModel } from "./openai/openai";
import { SimplePdfRenderer } from "./pdf/pdf";
import { SimpleMediaRenderer } from "./media/media";
import { WaveAppRenderer, WaveAppRendererModel } from "./waveapp/waveapp";
import { isBlank } from "@/util/util";
import { sprintf } from "sprintf-js";
@ -100,6 +101,18 @@ const PluginConfigs: RendererPluginType[] = [
mimeTypes: ["video/*", "audio/*"],
simpleComponent: SimpleMediaRenderer,
},
{
name: "waveapp",
rendererType: "full",
heightType: "pixels",
dataType: "model",
collapseType: "remove",
hidePrompt: true,
globalCss: null,
mimeTypes: ["application/x-waveapp"],
fullComponent: WaveAppRenderer,
modelCtor: () => new WaveAppRendererModel(),
},
];
class PluginModelClass {

View File

@ -0,0 +1 @@
# WaveApp Plugin

View File

@ -0,0 +1,6 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
.waveapp-renderer {
line-height: normal;
}

View File

@ -0,0 +1,247 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as React from "react";
import * as mobx from "mobx";
import * as mobxReact from "mobx-react";
import { debounce } from "throttle-debounce";
import { boundMethod } from "autobind-decorator";
import { JsonLinesDataBuffer } from "../core/ptydata";
import { Markdown } from "@/elements";
import * as ijson from "@/util/ijson";
import "./waveapp.less";
type WaveAppProps = {
lineId: string;
isSelected: boolean;
isFocused: boolean;
savedHeight: number;
initialData: any;
onPacket: (packetFn: (packet: any) => void) => void;
};
type WaveAppNode = {
tag: string;
props?: Record<string, any>;
children?: (WaveAppNode | string)[];
};
const TagMap: Record<string, React.ComponentType<{ node: WaveAppNode }>> = {};
function convertNodeToTag(node: WaveAppNode | string, idx?: number): JSX.Element | string {
if (node == null) {
return null;
}
if (idx == null) {
idx = 0;
}
if (typeof node === "string") {
return node;
}
let key = node.props?.key ?? "child-" + idx;
let TagComp = TagMap[node.tag];
if (!TagComp) {
return (
<div key={key} s>
Unknown tag:{node.tag}
</div>
);
}
return <TagComp key={key} node={node} />;
}
@mobxReact.observer
class WaveAppHtmlTag extends React.Component<{ node: WaveAppNode }, {}> {
render() {
let { tag, props, children } = this.props.node;
let divProps = {};
if (props != null) {
for (let [key, val] of Object.entries(props)) {
if (key.startsWith("on")) {
divProps[key] = (e: any) => {
console.log("handler", key, val);
};
} else {
divProps[key] = mobx.toJS(val);
}
}
}
let childrenComps = [];
if (children != null) {
for (let idx = 0; idx < children.length; idx++) {
let comp = convertNodeToTag(children[idx], idx);
if (comp != null) {
childrenComps.push(comp);
}
}
}
return React.createElement(tag, divProps, childrenComps);
}
}
TagMap["div"] = WaveAppHtmlTag;
TagMap["b"] = WaveAppHtmlTag;
TagMap["i"] = WaveAppHtmlTag;
TagMap["p"] = WaveAppHtmlTag;
TagMap["span"] = WaveAppHtmlTag;
TagMap["a"] = WaveAppHtmlTag;
TagMap["h1"] = WaveAppHtmlTag;
TagMap["h2"] = WaveAppHtmlTag;
TagMap["h3"] = WaveAppHtmlTag;
TagMap["h4"] = WaveAppHtmlTag;
TagMap["h5"] = WaveAppHtmlTag;
TagMap["h6"] = WaveAppHtmlTag;
TagMap["ul"] = WaveAppHtmlTag;
TagMap["ol"] = WaveAppHtmlTag;
TagMap["li"] = WaveAppHtmlTag;
class WaveAppRendererModel {
context: RendererContext;
opts: RendererOpts;
isDone: OV<boolean>;
api: RendererModelContainerApi;
savedHeight: number;
loading: OV<boolean>;
ptyDataSource: (termContext: TermContextUnion) => Promise<PtyDataType>;
packetData: JsonLinesDataBuffer;
rawCmd: WebCmd;
version: OV<number>;
loadError: OV<string> = mobx.observable.box(null, { name: "renderer-loadError" });
data: OV<any> = mobx.observable.box(null, { name: "renderer-data" });
constructor() {
this.packetData = new JsonLinesDataBuffer(this.packetCallback.bind(this));
this.version = mobx.observable.box(0);
}
initialize(params: 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;
setTimeout(() => this.reload(0), 10);
}
packetCallback(jsonVal: any) {
console.log("packet-callback", jsonVal);
try {
let data = this.data.get();
let newData = ijson.applyCommand(data, jsonVal);
console.log("got newdata", newData);
if (newData != data) {
mobx.action(() => {
this.data.set(newData);
})();
}
} catch (e) {
console.log("error adding data", e);
}
return;
}
dispose(): void {
return;
}
reload(delayMs: number): void {
mobx.action(() => {
this.loading.set(true);
this.loadError.set(null);
})();
let rtnp = this.ptyDataSource(this.context);
if (rtnp == null) {
console.log("no promise returned from ptyDataSource (waveapp renderer)", this.context);
return;
}
rtnp.then((ptydata) => {
setTimeout(() => {
this.packetData.reset();
this.receiveData(ptydata.pos, ptydata.data, "reload");
mobx.action(() => {
this.loading.set(false);
})();
}, delayMs);
}).catch((e) => {
console.log("error loading data", e);
mobx.action(() => {
this.loadError.set("error loading data: " + e);
})();
});
}
giveFocus(): void {
return;
}
updateOpts(opts: RendererOptsUpdate): void {
Object.assign(this.opts, opts);
}
setIsDone(): void {
if (this.isDone.get()) {
return;
}
mobx.action(() => {
this.isDone.set(true);
})();
}
receiveData(pos: number, data: Uint8Array, reason?: string): void {
this.packetData.receiveData(pos, data, reason);
}
updateHeight(newHeight: number): void {
if (this.savedHeight != newHeight) {
this.savedHeight = newHeight;
this.api.saveHeight(newHeight);
}
}
}
@mobxReact.observer
class WaveAppRenderer extends React.Component<{ model: WaveAppRendererModel }, {}> {
renderError() {
const model = this.props.model;
return <div className="load-error-text">{model.loadError.get()}</div>;
}
render() {
let model = this.props.model;
let styleVal = null;
if (model.loading.get() && model.savedHeight >= 0 && model.isDone) {
styleVal = {
height: model.savedHeight,
maxHeight: model.opts.maxSize.height,
};
} else {
styleVal = {
maxHeight: model.opts.maxSize.height,
};
}
let version = model.version.get();
let loadError = model.loadError.get();
if (loadError != null) {
return (
<div className="waveapp-renderer" style={styleVal}>
{this.renderError()}
</div>
);
}
let node = model.data.get();
if (node == null) {
return <div className="waveapp-renderer" style={styleVal} />;
}
return (
<div className="waveapp-renderer" style={styleVal}>
{convertNodeToTag(node)}
</div>
);
}
}
export { WaveAppRendererModel, WaveAppRenderer };