Skip to content

Commit

Permalink
rip macro
Browse files Browse the repository at this point in the history
  • Loading branch information
actualwitch committed Jan 19, 2025
1 parent a6774ff commit 9c66269
Show file tree
Hide file tree
Showing 28 changed files with 646 additions and 181 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ concurrency:
cancel-in-progress: false

env:
BUILD_PATH: spa
BUILD_PATH: static

jobs:
build:
Expand All @@ -30,11 +30,11 @@ jobs:
id: pages
uses: actions/configure-pages@v5
- run: BASE_URL="/experiment" bun run build:spa
- run: cp ./spa/index.html ./spa/404.html
- run: cp ./static/index.html ./static/404.html
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: spa
path: static

deploy:
environment:
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.DS_Store
node_modules
build
spa
static
*.bun-build
magick

Expand Down
Binary file modified bun.lockb
Binary file not shown.
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,8 @@
"@types/react": "latest",
"@types/react-dom": "latest",
"@types/selenium-webdriver": "latest",
"ink": "latest",
"playwright": "latest",
"prettier": "latest",
"selenium-webdriver": "latest"
"prettier": "latest"
},
"peerDependencies": {
"typescript": "next"
Expand Down
16 changes: 6 additions & 10 deletions scripts/build.bin.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import { $ } from "bun";
import { VERSION } from "../src/const/dynamic";

const TARGETS = ["linux-x64", "linux-arm64", "windows-x64", "darwin-x64", "darwin-arm64"];
import { revisionAtom } from "../src/atoms/common";
import { store } from "../src/store";

$`rm -rf ./build`;

// Build all targets
await Promise.all(
TARGETS.map(
(target) =>
$`bun build --compile --minify --target=bun-${target}-modern ./src/entry/server.tsx --outfile ./build/experiment-${VERSION}-${target}`,
),
);
const revision = await store.get(revisionAtom);

for (const target of ["linux-x64", "linux-arm64", "windows-x64", "darwin-x64", "darwin-arm64"]) {
await $`bun build --compile --minify --target=bun-${target}-modern ./src/entry/server.tsx --outfile ./build/experiment-${revision}-${target}`;
}
28 changes: 12 additions & 16 deletions scripts/build.spa.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import { $ } from "bun";
import { name, description, iconResolutions } from "../src/const";
import { getHtml } from "../src/entry/_handlers";
import { name, description, iconResolutions, staticDir } from "../src/const";
import { getStaticHtml } from "../src/entry/_handlers";
import { getManifest } from "../src/feature/pwa/manifest";
import { ROUTES } from "../src/feature/router";
import { assignToWindow } from "../src/utils/hydration";
import { store } from "../src/store";
import { clientScriptAtom } from "../src/atoms/server";

// why does this work but running it from Bun.build fails? #justbunthings
await $`bun build ./src/entry/client.tsx --outdir ./spa --minify`;
// const buildResult = await Bun.build({
// entrypoints: ["./src/entry/client.tsx"],
// outdir: "./spa",
// });
// if (!buildResult.success) {
// throw new AggregateError(buildResult.logs, "Build failed");
// }

const result = await store.get(clientScriptAtom);
if (result.isNothing) {
throw new Error("could not build frontend");
}
const baseUrl = process.env.BASE_URL;
const regex = /\/\w*/gm;

Expand All @@ -23,20 +19,20 @@ for (const route of ROUTES) {
const pathname = path === "/" ? "index" : path.slice(1);
const fullUrl = `${baseUrl ?? ""}${path}`;

const html = getHtml(
const html = await getStaticHtml(
fullUrl,
[assignToWindow("REALM", `"SPA"`), baseUrl && assignToWindow("BASE_URL", `"${baseUrl}"`)],
baseUrl,
);

await Bun.write(`./spa/${pathname}.html`, html);
await Bun.write(`./${staticDir}/${pathname}.html`, html);
}

await Bun.write(
"./spa/manifest.json",
`./${staticDir}/manifest.json`,
JSON.stringify(getManifest(name, description, iconResolutions, baseUrl), null, 2),
);

for (const res of iconResolutions) {
await $`cp ./.github/assets/experiment-${res}.png ./spa`;
await $`cp ./.github/assets/experiment-${res}.png ./${staticDir}`;
}
20 changes: 19 additions & 1 deletion src/atoms/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Result } from "true-myth";
import { createFileStorage, getStoragePath, resolve, spawn } from "../utils";
import { divergentAtom, entangledAtom } from "../utils/entanglement";
import { getRealm, hasBackend } from "../utils/realm";
import { author } from "../const";
import { author, version } from "../const";
import type { _Message, SerialExperiment, ExperimentWithMeta, Message } from "../types";
import { modelLabels, type ProviderType } from "../feature/inference/types";

Expand Down Expand Up @@ -257,3 +257,21 @@ export const localCertAndKeyAtom = atom(async () => {
}
return Result.ok({ key: `${getStoragePath()}/cert.key`, cert: `${getStoragePath()}/key.cert` });
});

export const revisionAtom = entangledAtom(
"revision",
atom(async (get) => {
const result = await spawn("git", ["rev-parse", "HEAD"]);
const hash = result.map((hash) => hash.slice(0, 6));
const revision = [version, hash.unwrapOr(undefined)].filter(Boolean).join("-");
return revision;
}),
);

export const debugAtom = entangledAtom(
"debug",
atom(() => {
if (getRealm() !== "server") return false;
return process.env.DEBUG === "true";
}),
);
28 changes: 28 additions & 0 deletions src/atoms/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { atom } from "jotai";
import { getRealm } from "../utils/realm";
import { Maybe } from "true-myth";
import { resolve, spawn } from "../utils";
import { clientFile, staticDir } from "../const";

export const clientScriptAtom = atom(async () => {
if (getRealm() !== "server") return Maybe.nothing();
const result = await spawn("bun", [
"build",
"./src/entry/client.tsx",
"--outdir",
`./${staticDir}`,
"--minify",
]);
if (result.isErr) {
console.error(result.error);
return Maybe.nothing();
}
const fs = await resolve("fs/promises");
const readFile = fs.map((fs) => fs.readFile);
if (readFile.isErr) {
console.error(readFile.error);
return Maybe.nothing();
}
const file = await readFile.value(`./${staticDir}/${clientFile}`, "utf8");
return Maybe.just(file);
});
10 changes: 10 additions & 0 deletions src/components/FavIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,13 @@ export const FavIcon = ({ children }: PropsWithChildren) => {
/>
);
};

export const ExperimentIcon = ({ children }: { children: string }) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<text y=".9em" font-size="90">
{children}
</text>
</svg>
);
};
10 changes: 0 additions & 10 deletions src/const/_macro.ts

This file was deleted.

9 changes: 0 additions & 9 deletions src/const/dynamic.ts

This file was deleted.

8 changes: 3 additions & 5 deletions src/const/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import project from "../../package.json";

export const { name, description, author, version } = project;

export const clientFile = "/client.js";
export const staticDir = "static";

export const schema = "http";
export const hostname = "localhost";
export const port = 5173;
export const url = `${schema}://${hostname}:${port}`;

export const name = project.name;
export const description = project.description;
export const author = project.author;
export const version = project.version;

export const iconResolutions = [128, 192, 256, 512, 1024];

export const TRIANGLE = "▴";
3 changes: 3 additions & 0 deletions src/entry/__snapshots__/_test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Bun Snapshot v1, https://goo.gl/fbAQLP

exports[`ssr should render static html 1`] = `"<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/><link rel="preload" as="script" fetchPriority="low" href="/client.js"/><link rel="manifest" href="manifest.json"/><link rel="icon" href="experiment-512.png"/><title>Experiment</title><meta name="description" content="🔬 Experiment is a LLM chat UI with advanced tool use debugging facilities."/><link rel="icon" href="data:image/svg+xml,&lt;svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22&gt;&lt;text y=%22.9em%22 font-size=%2290%22&gt;🔬&lt;/text&gt;&lt;/svg&gt;"/></head><body><!--$--><style data-emotion="css j0pw3e">.css-j0pw3e{font-size:16px;line-height:1.5;margin-bottom:24px;margin-bottom:0;display:grid;grid-template-columns:278px 1fr 320px;height:100svh;}@media (max-width: 920px){.css-j0pw3e{position:relative;overflow:hidden;grid-template-columns:1fr;}}</style><div class="css-j0pw3e"><style data-emotion="css 198xo76">.css-198xo76{display:contents;}@media (max-width: 920px){.css-198xo76{display:none;}}</style><div class="css-198xo76"><style data-emotion="css 352ebn">.css-352ebn{padding:24px;overflow:auto;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;height:100%;}.css-352ebn ul{list-style:none;padding:0;}.css-352ebn input{width:-moz-available;width:-webkit-fill-available;}.css-352ebn a{-webkit-text-decoration:none;text-decoration:none;}.css-352ebn a[aria-current="page"]{-webkit-text-decoration:underline;text-decoration:underline;text-underline-offset:4px;}</style><nav class="css-352ebn"><header><style data-emotion="css 15xv5ui">.css-15xv5ui{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;}</style><h2 class="css-15xv5ui">🔬<!-- --> <a aria-current="page" class="active" href="/" data-discover="true">Experiment</a></h2><h2 class="css-15xv5ui">⛴️<!-- --> <a class="" href="/import" data-discover="true">Import</a></h2><h2 class="css-15xv5ui">🛠️<!-- --> <a class="" href="/parameters" data-discover="true">Parameters</a></h2></header><style data-emotion="css 1d30q9z">.css-1d30q9z{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;}.css-1d30q9z.css-1d30q9z ul{padding-left:0;}</style><div class="css-1d30q9z"><div></div><h3 class="css-15xv5ui">History</h3><ul></ul></div><style data-emotion="css hgjmlq">.css-hgjmlq{text-align:center;opacity:0.25;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;}</style><footer class="css-hgjmlq">© ∞ <!-- -->▴<!-- --> <!-- -->0.3.0-16d6d8</footer></nav></div><style data-emotion="css h7qzbv">.css-h7qzbv{display:contents;}@media (min-width: 921px){.css-h7qzbv{display:none;}}</style><div class="css-h7qzbv"><style data-emotion="css 1yea4yg">.css-1yea4yg{position:absolute;cursor:pointer;left:24px;top:24px;z-index:1;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;}.css-1yea4yg span{-webkit-text-decoration:underline;text-decoration:underline;text-decoration-thickness:auto;text-underline-offset:4px;text-shadow:#FFFFFF 1px 2px 14px,#FFFFFF 0px 0px 24px;}@media (prefers-color-scheme: dark){.css-1yea4yg span{text-shadow:#000000 1px 2px 14px,#000000 0px 0px 24px;}}</style><h2 class="css-1yea4yg">🔬<!-- --> <span>Experiment</span></h2><style data-emotion="css urdo9c">.css-urdo9c{position:absolute;right:24px;top:24px;z-index:3;margin-top:4px;}</style><div class="css-urdo9c"><style data-emotion="css kb9jd3">.css-kb9jd3:not(:disabled){box-shadow:2px 2px 8px #00000020;text-shadow:1px 0px 1px #00000024,-1px 0px 1px #FFFFFFb8;}.css-kb9jd3:not(:disabled):hover{box-shadow:0px 1px 8px 2px #1a1a1a24;}.css-kb9jd3:not(:disabled):active{-webkit-transform:translate(0px, 1px);-moz-transform:translate(0px, 1px);-ms-transform:translate(0px, 1px);transform:translate(0px, 1px);}@media (prefers-color-scheme: dark){.css-kb9jd3:not(:disabled){box-shadow:2px 2px 8px #ececec21;}.css-kb9jd3:not(:disabled):hover{box-shadow:0px 1px 14px 4px #ececec52;}}</style><button type="button" class="css-kb9jd3">⍇</button></div></div><div class="css-h7qzbv"><style data-emotion="css n4x7c9">.css-n4x7c9{position:absolute;top:0;right:0;bottom:0;left:0;background-color:#FFFFFF;z-index:2;-webkit-transition:-webkit-transform 100ms ease-out;transition:transform 100ms ease-out;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-transform:translateX(-100%);-moz-transform:translateX(-100%);-ms-transform:translateX(-100%);transform:translateX(-100%);}.css-n4x7c9>*{width:100%;}@media (prefers-color-scheme: dark){.css-n4x7c9{background-color:#000000;}}</style><aside from="right" class="css-n4x7c9"><nav class="css-352ebn"><header><h2 class="css-15xv5ui">🔬<!-- --> <a aria-current="page" class="active" href="/" data-discover="true">Experiment</a></h2><h2 class="css-15xv5ui">⛴️<!-- --> <a class="" href="/import" data-discover="true">Import</a></h2><h2 class="css-15xv5ui">🛠️<!-- --> <a class="" href="/parameters" data-discover="true">Parameters</a></h2></header><div class="css-1d30q9z"><div></div><h3 class="css-15xv5ui">History</h3><ul></ul></div><footer class="css-hgjmlq">© ∞ <!-- -->▴<!-- --> <!-- -->0.3.0-16d6d8</footer></nav></aside></div><style data-emotion="css j910es">.css-j910es{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:24px;}.css-j910es.css-j910es{overflow-x:hidden;}.css-j910es a{color:color(display-p3 0.9 0.66 0.81);-webkit-text-decoration:underline;text-decoration:underline;cursor:pointer;}.css-j910es a:hover{color:#9B59D0;}@media (max-width: 920px){.css-j910es{padding-top:80px;}}</style><div class="css-j910es"><h2>Welcome to Experiment</h2><p>To start inference, add a token on <a class="" href="/parameters" data-discover="true">Parameters</a> page. You can also explore a .csv file on the <a class="" href="/import" data-discover="true">Import</a> page.</p></div><style data-emotion="css-global 1hrimtb">*,*::before,*::after{box-sizing:border-box;}*{margin:0;}img,picture,video,canvas,svg{display:block;max-width:100%;}input,button,textarea,select{font:inherit;color:inherit;}p,h1,h2,h3,h4,h5,h6{overflow-wrap:break-word;}p{text-wrap:pretty;}h1,h2,h3,h4,h5,h6{text-wrap:balance;}#root,#__next{isolation:isolate;}select{-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;appearance:none;}body{font-size:16px;line-height:1.5;}h1{font-size:39.808px;line-height:1.2057877813504823;margin-bottom:24px;}h2{font-size:33.184px;line-height:1.0848601735776278;margin-bottom:24px;}h3{font-size:27.648px;line-height:1.3020833333333333;margin-bottom:24px;}h4{font-size:23.04px;line-height:1.0416666666666667;margin-bottom:24px;}h5{font-size:19.2px;line-height:1.25;margin-bottom:24px;}h6{font-size:16px;line-height:1.5;margin-bottom:24px;}p,ol,ul,pre{font-size:16px;line-height:1.5;margin-bottom:24px;}html{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;}:root{background-color:#FFFFFF;color:#000000;font-family:Charter,'Bitstream Charter','Sitka Text',Cambria,serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol','Noto Color Emoji';font-weight:normal;-webkit-hyphens:auto;-moz-hyphens:auto;-ms-hyphens:auto;hyphens:auto;}:root ul,:root ol{margin-bottom:4px;padding-left:36px;}:root table{border-collapse:collapse;}:root th{border-bottom:1px solid #000000;}:root th,:root td{padding:0 6px 2.4000000000000004px;}:root pre{font-size:0.75em;background-color:#00000010;padding:8px 12px;border-radius:8px;overflow-x:scroll;}:root pre code{background:none;}:root style{display:none;}::selection{background-color:#ff98ef80;}a{color:inherit;-webkit-transition:color 0.1s ease-out;transition:color 0.1s ease-out;}a:hover{color:#9B59D0;}input,code,button{font:inherit;}body{overflow-x:hidden;}button:not(:disabled){background-color:#dddddd;}button:not(:disabled):hover{background-color:color(display-p3 0 0 0 / 0.19);}button[disabled]{background-color:#b5b5b50f;color:#00000054;cursor:not-allowed;}button,input[type="file"]::file-selector-button{font-size:16px;line-height:1.5;margin-bottom:24px;padding:4px 13px;margin:0;-webkit-transition:background-color 0.1s ease-out,box-shadow 0.1s ease-out,-webkit-transform 0.1s cubic-bezier(0.18, 0.89, 0.32, 1.28);transition:background-color 0.1s ease-out,box-shadow 0.1s ease-out,transform 0.1s cubic-bezier(0.18, 0.89, 0.32, 1.28);border:1px solid transparent;color:#000000;border-radius:6px;cursor:pointer;-webkit-transform:translate(0px, 0px);-moz-transform:translate(0px, 0px);-ms-transform:translate(0px, 0px);transform:translate(0px, 0px);}button[disabled],input[type="file"]::file-selector-button[disabled]{background-color:#b5b5b50f;color:#e6e6e6;cursor:not-allowed;border-color:#8888880f;}button+button{margin-left:6px;}input:not([type="file"]){background-color:#eeeeee;}input[type="file"]{width:94px;}@media (prefers-color-scheme: dark){:root{background-color:#000000;color:#FFFFFF;}:root th{border-bottom:1px solid #FFFFFF;}:root pre{background-color:#FFFFFF30;}button:not(:disabled):hover{background-color:#FFFFFF;}button[disabled]{background-color:transparent;box-shadow:none;border-color:#FFFFFF54;color:#FFFFFF54;}input{color:#000000;}}</style></div><!--/$--><!--$--><script>window[Symbol.for("hydration")] = JSON.parse("{\\"experiment\\":[],\\"experimentIds\\":[],\\"tokens\\":{},\\"rendererMode\\":\\"markdown\\",\\"debug\\":false,\\"temp\\":0,\\"is running\\":false,\\"selected-experiment\\":[]}");</script><!--/$--><script src="/client.js" async=""></script></body></html>"`;
43 changes: 18 additions & 25 deletions src/entry/_handlers.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,35 @@
import type { Server } from "bun";
import { renderToReadableStream, renderToString } from "react-dom/server";
import { renderToReadableStream } from "react-dom/server";
import { prerenderToNodeStream as prerender } from "react-dom/static";
import { StaticRouter } from "react-router";

import { log } from "../utils/logger";
import { Shell } from "../root";
import { publish, subscribe, type Update } from "../utils/æther";
import { eventStream } from "../utils/eventStream";
import { getClientAsString } from "./_macro" with { type: "macro" };
import { getManifest } from "../feature/pwa/manifest";
import { clientFile, description, iconResolutions, name } from "../const";
import type { Nullish } from "../types";
import { clientScriptAtom } from "../atoms/server";
import { store } from "../store";

export const getHtml = (location: string, additionalScripts?: Array<string | Nullish>, baseUrl?: string) => {
const html = renderToString(
export const getStaticHtml = async (
location: string,
additionalScripts?: Array<string | Nullish>,
baseUrl?: string,
): Promise<string> => {
const { prelude } = await prerender(
<StaticRouter location={location} basename={baseUrl}>
<Shell bootstrap additionalScripts={additionalScripts} baseUrl={baseUrl} />
</StaticRouter>,
);
return `<!DOCTYPE html>${html}`;
};

export const getStaticHtml = async (location: string, additionalScripts?: Array<string | Nullish>, baseUrl?: string) => {
const { prelude } = await prerender(
<StaticRouter location={location} basename={baseUrl}>
<Shell additionalScripts={additionalScripts} baseUrl={baseUrl} />
</StaticRouter>,
{
bootstrapScripts: [clientFile],
},
);
return new Promise((resolve, reject) => {
let data = '';
prelude.on('data', chunk => {
let data = "";
prelude.on("data", (chunk) => {
data += chunk;
});
prelude.on('end', () => resolve(data));
prelude.on('error', reject);
prelude.on("end", () => resolve(data));
prelude.on("error", reject);
});
};

Expand All @@ -54,10 +47,10 @@ export const doStatic = async (request: Request) => {
});
}
if (url.pathname === clientFile) {
response = new Response(await getClientAsString(), {
headers: {
"Content-Type": "application/javascript",
},
const clientScript = await store.get(clientScriptAtom);
response = clientScript.match({
Just: (script) => new Response(script, { headers: { "Content-Type": "application/javascript" } }),
Nothing: () => new Response("KO", { status: 500 }),
});
}
if (response) {
Expand All @@ -69,7 +62,7 @@ export const doStatic = async (request: Request) => {
export const doSSR = async (request: Request) => {
const url = new URL(request.url);
log("SSR", request.url);
return new Response(getHtml(url.pathname), {
return new Response(await getStaticHtml(url.pathname), {
headers: {
"Content-Type": "text/html",
},
Expand Down
Loading

0 comments on commit 9c66269

Please sign in to comment.