update harmony demo

This commit is contained in:
Dominik Kundel 2025-08-12 23:23:57 -07:00
parent 52176bfbec
commit 9b2dd0a2bf
No known key found for this signature in database
9 changed files with 1095 additions and 66 deletions

View file

@ -12,8 +12,11 @@
"@openai/harmony": "file:../../javascript",
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-query": "^5.83.0",
"class-variance-authority": "^0.7.1",
@ -721,6 +724,12 @@
"resolved": "../../javascript",
"link": true
},
"node_modules/@radix-ui/number": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
"integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
"license": "MIT"
},
"node_modules/@radix-ui/primitive": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
@ -780,6 +789,32 @@
}
}
},
"node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
@ -810,6 +845,21 @@
}
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
@ -837,6 +887,46 @@
}
}
},
"node_modules/@radix-ui/react-focus-guards": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
"integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-focus-scope": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
@ -878,6 +968,43 @@
}
}
},
"node_modules/@radix-ui/react-popover": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz",
"integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.10",
"@radix-ui/react-focus-guards": "1.1.2",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.7",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-controllable-state": "1.2.2",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
@ -981,6 +1108,80 @@
}
}
},
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz",
"integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz",
"integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==",
"license": "MIT",
"dependencies": {
"@radix-ui/number": "1.1.1",
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.10",
"@radix-ui/react-focus-guards": "1.1.2",
"@radix-ui/react-focus-scope": "1.1.7",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.7",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-layout-effect": "1.1.1",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-visually-hidden": "1.2.3",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
@ -1028,6 +1229,36 @@
}
}
},
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz",
"integrity": "sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-roving-focus": "1.1.10",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz",
@ -1568,6 +1799,18 @@
"@types/react": "^19.0.0"
}
},
"node_modules/aria-hidden": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
"integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001727",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
@ -1687,6 +1930,12 @@
"node": ">=8"
}
},
"node_modules/detect-node-es": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
"node_modules/enhanced-resolve": {
"version": "5.18.2",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz",
@ -1701,6 +1950,15 @@
"node": ">=10.13.0"
}
},
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@ -2176,6 +2434,75 @@
"react": "^19.1.0"
}
},
"node_modules/react-remove-scroll": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
"integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==",
"license": "MIT",
"dependencies": {
"react-remove-scroll-bar": "^2.3.7",
"react-style-singleton": "^2.2.3",
"tslib": "^2.1.0",
"use-callback-ref": "^1.3.3",
"use-sidecar": "^1.1.3"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-remove-scroll-bar": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
"license": "MIT",
"dependencies": {
"react-style-singleton": "^2.2.2",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-style-singleton": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
"license": "MIT",
"dependencies": {
"get-nonce": "^1.0.0",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/scheduler": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
@ -2362,6 +2689,49 @@
"dev": true,
"license": "MIT"
},
"node_modules/use-callback-ref": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
"integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/use-sidecar": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
"integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
"license": "MIT",
"dependencies": {
"detect-node-es": "^1.1.0",
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/yallist": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",

View file

@ -7,14 +7,18 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"build:wasm": "cd ../../ && wasm-pack build --target web --out-dir javascript/dist/web --features wasm-binding --no-default-features && wasm-pack build --target nodejs --out-dir javascript/dist/node --features wasm-binding --no-default-features"
},
"dependencies": {
"@openai/harmony": "file:../../javascript",
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-query": "^5.83.0",
"class-variance-authority": "^0.7.1",

View file

@ -13,10 +13,23 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import {
load_harmony_encoding,
type JsHarmonyEncoding,
type Message as HarmonyMessage,
initSync as initHarmony,
JsStreamableParser,
} from "@openai/harmony";
@ -213,18 +226,58 @@ function HighlightedTokens({
);
}
type Message = {
role: "assistant" | "user" | "system" | "developer" | "tool";
channel?: string;
recipient?: string;
content_type?: string;
content: {
type: "text";
text: string;
}[];
};
type Message = HarmonyMessage;
function MessageItem({
message,
onUpdateContent,
onUpdateHeader,
}: {
message: Message;
onUpdateContent?: (contentIndex: number, newText: string) => void;
onUpdateHeader?: (updates: Partial<Message>) => void;
}) {
const [isHeaderOpen, setIsHeaderOpen] = useState(false);
const [draft, setDraft] = useState<Message>(message);
useEffect(() => {
if (isHeaderOpen) {
setDraft(message);
}
}, [isHeaderOpen, message]);
const commitHeaderUpdates = () => {
const normalize = (val?: string) =>
val && val.trim().length > 0 ? val.trim() : undefined;
const nextAuthor = {
role: draft.author.role,
name: normalize(draft.author?.name),
} as Message["author"];
let nextRecipient = normalize(draft.recipient);
if (nextAuthor.role === "tool" && !nextRecipient)
nextRecipient = "assistant";
onUpdateHeader?.({
author: nextAuthor,
channel: normalize(draft.channel),
recipient: nextRecipient,
content_type: normalize(draft.content_type),
});
};
const adjustTextareaSize = (el: HTMLTextAreaElement) => {
const style = window.getComputedStyle(el);
const lineHeight = parseFloat(style.lineHeight || "16");
const paddingTop = parseFloat(style.paddingTop || "0");
const paddingBottom = parseFloat(style.paddingBottom || "0");
const oneLineHeight = lineHeight + paddingTop + paddingBottom;
const maxHeight = lineHeight * 5 + paddingTop + paddingBottom;
el.style.height = "auto";
const desired = Math.max(oneLineHeight, el.scrollHeight);
const nextHeight = Math.min(desired, maxHeight);
el.style.height = `${nextHeight}px`;
el.style.overflowY = desired > maxHeight ? "auto" : "hidden";
};
function MessageItem({ message }: { message: Message }) {
return (
<div className="text-sm border rounded-md p-1">
<div className="border border-dashed border-gray-300 rounded-md">
@ -241,49 +294,198 @@ function MessageItem({ message }: { message: Message }) {
</CollapsibleTrigger>
</div>
<CollapsibleContent>
<div className="p-2 flex flex-col gap-1 mt-1">
<dl className="text-xs flex gap-1">
<dt>role:</dt>
<dd className="font-semibold bg-blue-300/50 px-1 rounded-sm">
{message.role}
</dd>
</dl>
{message.channel && (
<dl className="text-xs flex gap-1">
<dt>content:</dt>
<dd className="font-semibold bg-blue-300/50 px-1 rounded-sm">
{message.channel}
</dd>
</dl>
)}
{message.recipient && (
<dl className="text-xs flex gap-1">
<dt>recipient:</dt>
<dd className="font-semibold bg-blue-300/50 px-1 rounded-sm">
{message.recipient}
</dd>
</dl>
)}
{message.content_type && (
<dl className="text-xs flex gap-1">
<dt>content_type:</dt>
<dd className="font-semibold bg-blue-300/50 px-1 rounded-sm">
{message.content_type}
</dd>
</dl>
)}
</div>
<Popover
open={isHeaderOpen}
onOpenChange={(open) => {
if (!open && isHeaderOpen) {
commitHeaderUpdates();
}
setIsHeaderOpen(open);
}}
>
<PopoverTrigger asChild>
<div className="p-2 flex flex-col gap-1 mt-1 cursor-pointer hover:bg-gray-50 rounded-sm">
<dl className="text-xs flex gap-1 items-center">
<dt>role:</dt>
<dd className="font-semibold bg-blue-300/50 px-1 rounded-sm">
{message.author?.role === "tool" && message.author?.name
? `${message.author.role} (${message.author.name})`
: message.author?.role}
</dd>
</dl>
{message.channel && (
<dl className="text-xs flex gap-1 items-center">
<dt>channel:</dt>
<dd className="font-semibold bg-blue-300/50 px-1 rounded-sm">
{message.channel}
</dd>
</dl>
)}
{message.recipient && (
<dl className="text-xs flex gap-1 items-center">
<dt>recipient:</dt>
<dd className="font-semibold bg-blue-300/50 px-1 rounded-sm">
{message.recipient}
</dd>
</dl>
)}
{message.content_type && (
<dl className="text-xs flex gap-1 items-center">
<dt>content_type:</dt>
<dd className="font-semibold bg-blue-300/50 px-1 rounded-sm">
{message.content_type}
</dd>
</dl>
)}
</div>
</PopoverTrigger>
<PopoverContent className="w-80">
<div className="flex items-center gap-1 mb-3">
{(
[
"user",
"assistant",
"developer",
"system",
"tool",
] as const
).map((r) => (
<button
key={r}
className={cn(
"text-xs px-2 py-1 rounded-sm border",
draft.author?.role === r
? "bg-gray-200 border-gray-300"
: "border-transparent hover:bg-gray-50"
)}
onClick={() =>
setDraft((prev) => ({
...prev,
author: { role: r, name: prev.author?.name } as any,
}))
}
>
{r}
</button>
))}
</div>
{draft.author?.role === "assistant" && (
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-1">
<label className="text-xs text-gray-500">Channel</label>
<Select
value={draft.channel}
onValueChange={(val) =>
setDraft((p) => ({ ...p, channel: val }))
}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select channel" />
</SelectTrigger>
<SelectContent>
<SelectItem value="analysis">analysis</SelectItem>
<SelectItem value="commentary">commentary</SelectItem>
<SelectItem value="final">final</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs text-gray-500">Recipient</label>
<input
className="border rounded-md px-2 py-1 text-sm outline-none focus:ring-2 focus:ring-blue-300"
value={draft.recipient ?? ""}
placeholder="e.g. functions.lookup_weather"
onChange={(e) => {
const value = e.currentTarget.value;
setDraft((p) => ({ ...p, recipient: value }));
}}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs text-gray-500">
Content type
</label>
<input
className="border rounded-md px-2 py-1 text-sm outline-none focus:ring-2 focus:ring-blue-300"
value={draft.content_type ?? ""}
placeholder="e.g. json"
onChange={(e) => {
const value = e.currentTarget.value;
setDraft((p) => ({ ...p, content_type: value }));
}}
/>
</div>
</div>
)}
{draft.author?.role === "tool" && (
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-1">
<label className="text-xs text-gray-500">Tool name</label>
<input
className="border rounded-md px-2 py-1 text-sm outline-none focus:ring-2 focus:ring-blue-300"
value={draft.author?.name ?? ""}
placeholder="e.g. functions.get_weather"
onChange={(e) => {
const value = e.currentTarget.value;
setDraft((p) => ({
...p,
author: { role: p.author.role, name: value } as any,
}));
}}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs text-gray-500">Channel</label>
<Select
value={draft.channel}
onValueChange={(val) =>
setDraft((p) => ({ ...p, channel: val }))
}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select channel" />
</SelectTrigger>
<SelectContent>
<SelectItem value="analysis">analysis</SelectItem>
<SelectItem value="commentary">commentary</SelectItem>
<SelectItem value="final">final</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs text-gray-500">Recipient</label>
<input
className="border rounded-md px-2 py-1 text-sm outline-none focus:ring-2 focus:ring-blue-300"
value={draft.recipient ?? ""}
placeholder="assistant"
onChange={(e) => {
const value = e.currentTarget.value;
setDraft((p) => ({ ...p, recipient: value }));
}}
/>
</div>
</div>
)}
</PopoverContent>
</Popover>
</CollapsibleContent>
</Collapsible>
</div>
<div className="text-sm flex flex-col gap-4 mt-3 mb-1">
{message.content.map((content: any, idx: number) => (
<pre
className="bg-gray-100/50 p-4 rounded-md text-xs overflow-x-scroll"
key={idx}
>
{content.text}
</pre>
<textarea
key={`${idx}-${content?.text ?? ""}`}
className="bg-gray-100/50 p-3 rounded-md text-base w-full resize-y focus:outline-none focus:ring-2 focus:ring-blue-300"
rows={1}
defaultValue={content.text}
ref={(el) => {
if (el) adjustTextareaSize(el);
}}
onInput={(e) => adjustTextareaSize(e.currentTarget)}
onBlur={(e) => onUpdateContent?.(idx, e.currentTarget.value)}
/>
))}
</div>
</div>
@ -328,12 +530,15 @@ function useTokens(text: string | undefined) {
const tokens = Array.from(encoding.encode(text, encoding.specialTokens()));
// @ts-ignore
const tokenTexts = tokens.map((t) => encoding.decodeUtf8([t]));
const parser = new JsStreamableParser(encoding, "user");
const parser = new JsStreamableParser(encoding);
for (const token of tokens) {
parser.process(token);
}
const messages = JSON.parse(parser.messages).map((m: any) => ({
...m,
const messages: Message[] = JSON.parse(parser.messages).map((m: any) => ({
author: { role: m.role, name: m.name },
channel: m.channel,
recipient: m.recipient,
content_type: m.content_type,
content:
typeof m.content === "string"
? [{ type: "text", text: m.content }]
@ -348,16 +553,84 @@ function useTokens(text: string | undefined) {
});
}, [text, encoding]);
const convertTokensToAll = (tokens: number[]) => {
if (!encoding) return;
// @ts-ignore
const tokenTexts = tokens.map((t) => encoding.decodeUtf8([t]));
const parser = new JsStreamableParser(encoding);
for (const token of tokens) parser.process(token);
const messages: Message[] = JSON.parse(parser.messages).map((m: any) => ({
author: { role: m.role, name: m.name },
channel: m.channel,
recipient: m.recipient,
content_type: m.content_type,
content:
typeof m.content === "string"
? [{ type: "text", text: m.content }]
: m.content,
}));
setTokenData({ tokens: Array.from(tokens), tokenTexts, messages });
};
const convertTextToAll = (newText: string) => {
if (!encoding) return;
const tokens = Array.from(
encoding.encode(newText, encoding.specialTokens())
);
convertTokensToAll(tokens);
};
return {
...tokenData,
highlightedIndex,
setHighlightedIndex,
setTokens: (newTokens: number[]) => convertTokensToAll(newTokens),
setTokenTexts: (newTokenTexts: string | string[]) =>
convertTextToAll(
Array.isArray(newTokenTexts) ? newTokenTexts.join("") : newTokenTexts
),
setMessages: (newMessages: Message[]) => {
// Flatten author for WASM (serde(flatten) expects top-level role/name)
const wasmMessages = newMessages.map((m) => ({
role: m.author.role,
name: m.author.name,
channel: m.channel,
recipient: m.recipient,
content_type: m.content_type,
content: (m.content || []).map((c: any) =>
typeof c === "string" ? { type: "text", text: c } : c
),
}));
const tokenIds = encoding?.renderConversation(
{
messages: wasmMessages as any,
},
{
auto_drop_analysis: false,
}
);
if (!tokenIds) return;
const tokenIdsArray = Array.from(tokenIds);
// @ts-ignore
const tokenTexts = tokenIdsArray.map((t) => encoding?.decodeUtf8([t]));
setTokenData({
tokens: tokenIdsArray,
tokenTexts: tokenTexts.filter((t) => t !== undefined),
messages: newMessages,
});
},
};
}
function App() {
const [text, setText] = useState(
`<|start|>user<|message|>What is the weather in SF?<|end|><|start|>assistant<|channel|>analysis<|message|>User asks: “What is the weather in SF?” We need to use lookup_weather tool.<|end|><|start|>assistant to=functions.lookup_weather<|channel|>commentary <|constrain|>json<|message|>{"location": "San Francisco"}<|end|><|start|>assistant`
`<|start|>user<|message|>What is the weather in SF?<|end|>` +
`<|start|>assistant<|channel|>analysis<|message|>User asks: “What is the weather in SF?” We need to use lookup_weather tool.<|end|>` +
`<|start|>assistant<|channel|>commentary to=functions.lookup_weather<|constrain|>json<|message|>{"location": "San Francisco"}<|end|>` +
`<|start|>assistant`
// ""
);
const {
tokens,
@ -365,6 +638,8 @@ function App() {
highlightedIndex,
setHighlightedIndex,
messages,
setTokens,
setMessages,
} = useTokens(text);
const [wrapWhitespace, setWrapWhitespace] = useState(true);
@ -416,14 +691,61 @@ function App() {
<h3 className="text-sm font-semibold text-gray-700 mb-2">Messages</h3>
<div className="flex flex-col gap-4">
{messages.map((message: Message, idx: number) => (
<MessageItem key={idx} message={message} />
<MessageItem
key={idx}
message={message}
onUpdateContent={(contentIndex, newText) => {
const updatedMessages = messages.map(
(existingMessage: Message, messageIndex: number) => {
if (messageIndex !== idx) return existingMessage;
const newContent = (existingMessage.content || []).map(
(c: any, ci: number) =>
ci === contentIndex ? { ...c, text: newText } : c
);
return { ...existingMessage, content: newContent };
}
);
setMessages(updatedMessages);
}}
onUpdateHeader={(updates) => {
const updatedMessages = messages.map(
(existingMessage: Message, messageIndex: number) => {
if (messageIndex !== idx) return existingMessage;
const next: Message = {
...existingMessage,
...updates,
author: {
...(existingMessage.author || { role: "user" }),
...(updates as any).author,
},
} as Message;
const normalize = (val?: string) =>
val && val.length > 0 ? val : undefined;
next.channel = normalize(next.channel);
next.recipient = normalize(next.recipient);
next.content_type = normalize(next.content_type);
return next;
}
);
setMessages(updatedMessages);
}}
/>
))}
{/* <Button
<Button
variant="outline"
className="border border-dashed border-gray-300 rounded-md text-center py-8 text-sm text-gray-500 cursor-pointer"
onClick={() =>
setMessages([
...messages,
{
author: { role: "user" },
content: [{ type: "text", text: "" }],
} as Message,
])
}
>
<PlusIcon /> Add new message
</Button> */}
</Button>
</div>
</div>
<div className="flex flex-col gap-4">
@ -454,9 +776,13 @@ function App() {
onHover={setHighlightedIndex}
wrapWhitespace={wrapWhitespace}
highlightTokens={highlightTokens}
editable={true}
onEdit={(text) =>
typeof text === "string" && setTokens(JSON.parse(text))
}
/>
</div>
{/* <div className="flex flex-col md:flex-row gap-2">
<div className="flex flex-col md:flex-row gap-2">
<div className="flex items-center space-x-2">
<Switch
id="wrap-whitespace"
@ -473,7 +799,7 @@ function App() {
/>
<Label htmlFor="highlight-tokens">Highlight Tokens</Label>
</div>
</div> */}
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View file

@ -0,0 +1,185 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View file

@ -0,0 +1,66 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
className
)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent }

View file

@ -116,7 +116,6 @@ pub async fn load_harmony_encoding(name: HarmonyEncodingName) -> anyhow::Result<
FormattingToken::EndMessageDoneSampling,
FormattingToken::EndMessageAssistantToTool,
]),
conversation_has_function_tools: Arc::new(AtomicBool::new(false)),
})
}
}

View file

@ -411,6 +411,28 @@ where
.map_err(LoadError::CoreBPECreationFailed)
}
#[cfg(target_arch = "wasm32")]
pub fn load_encoding_from_bytes<S, TS>(
bytes: &[u8],
expected_hash: Option<&str>,
special_tokens: S,
pattern: &str,
) -> Result<CoreBPE, LoadError>
where
S: IntoIterator<Item = (TS, Rank)>,
TS: Into<String>,
{
let reader = std::io::BufReader::new(bytes);
let encoder =
load_tiktoken_vocab(reader, expected_hash).map_err(LoadError::InvalidTiktokenVocabFile)?;
CoreBPE::new(
encoder,
special_tokens.into_iter().map(|(k, v)| (k.into(), v)),
pattern,
)
.map_err(LoadError::CoreBPECreationFailed)
}
/// This returns the path to a file containing the data at `url`. If the file is
/// cached, it is used. Otherwise, the file is downloaded and cached.
#[cfg(not(target_arch = "wasm32"))]

View file

@ -253,10 +253,18 @@ pub struct JsStreamableParser {
#[wasm_bindgen]
impl JsStreamableParser {
#[wasm_bindgen(constructor)]
pub fn new(encoding: &JsHarmonyEncoding, role: &str) -> Result<JsStreamableParser, JsValue> {
let parsed_role = Role::try_from(role)
.map_err(|_| JsValue::from_str(&format!("unknown role: {role}")))?;
let inner = StreamableParser::new(encoding.inner.clone(), Some(parsed_role))
pub fn new(
encoding: &JsHarmonyEncoding,
role: Option<String>,
) -> Result<JsStreamableParser, JsValue> {
let parsed_role = match role {
Some(r) => Some(
Role::try_from(r.as_str())
.map_err(|_| JsValue::from_str(&format!("unknown role: {r}")))?,
),
None => None,
};
let inner = StreamableParser::new(encoding.inner.clone(), parsed_role)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(Self { inner })
}
@ -344,8 +352,9 @@ pub async fn load_harmony_encoding(
let parsed: HarmonyEncodingName = name
.parse::<HarmonyEncodingName>()
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let encoding =
inner_load_harmony_encoding(parsed).map_err(|e| JsValue::from_str(&e.to_string()))?;
let encoding = inner_load_harmony_encoding(parsed)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(JsHarmonyEncoding { inner: encoding })
}