diff --git a/api/mod.ts b/api/mod.ts
new file mode 100644
index 0000000..191c8bd
--- /dev/null
+++ b/api/mod.ts
@@ -0,0 +1,62 @@
+import { initTRPC } from "@trpc/server";
+import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
+import { serveDir } from "std/http/file_server.ts";
+import { transform } from "swc";
+import { generationQueue } from "../app/generationQueue.ts";
+
+const t = initTRPC.create();
+
+export const appRouter = t.router({
+ ping: t.procedure.query(() => "pong"),
+ getAllGenerationJobs: t.procedure.query(() => {
+ return generationQueue.getAllJobs();
+ }),
+});
+
+export type AppRouter = typeof appRouter;
+
+const webuiRoot = new URL("./webui/", import.meta.url);
+
+export async function serveApi() {
+ const server = Deno.serve({ port: 8000 }, async (request) => {
+ const requestPath = new URL(request.url).pathname;
+ const filePath = webuiRoot.pathname + requestPath;
+ const fileExt = filePath.split("/").pop()?.split(".").pop()?.toLowerCase();
+ const fileExists = await Deno.stat(filePath).then((stat) => stat.isFile).catch(() => false);
+
+ if (requestPath.startsWith("/api/trpc/")) {
+ return fetchRequestHandler({
+ endpoint: "/api/trpc",
+ req: request,
+ router: appRouter,
+ createContext: () => ({}),
+ });
+ }
+
+ if (fileExists) {
+ if (fileExt === "ts" || fileExt === "tsx") {
+ const file = await Deno.readTextFile(filePath);
+ const result = await transform(file, {
+ jsc: {
+ parser: {
+ syntax: "typescript",
+ tsx: fileExt === "tsx",
+ },
+ target: "es2022",
+ },
+ });
+ return new Response(result.code, {
+ status: 200,
+ headers: {
+ "Content-Type": "application/javascript",
+ },
+ });
+ }
+ }
+ return serveDir(request, {
+ fsRoot: webuiRoot.pathname,
+ });
+ });
+
+ await server.finished;
+}
diff --git a/api/webui/favicon.png b/api/webui/favicon.png
new file mode 100644
index 0000000..90ecd66
Binary files /dev/null and b/api/webui/favicon.png differ
diff --git a/api/webui/index.html b/api/webui/index.html
new file mode 100644
index 0000000..347e2f0
--- /dev/null
+++ b/api/webui/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+ Eris
+
+
+
+
+
diff --git a/api/webui/main.tsx b/api/webui/main.tsx
new file mode 100644
index 0000000..84be409
--- /dev/null
+++ b/api/webui/main.tsx
@@ -0,0 +1,78 @@
+///
+import { QueryClient, QueryClientProvider } from "https://esm.sh/@tanstack/react-query@4.35.3";
+import { httpBatchLink } from "https://esm.sh/@trpc/client@10.38.4/links/httpBatchLink";
+import { createTRPCReact } from "https://esm.sh/@trpc/react-query@10.38.4";
+import { defineConfig, injectGlobal, install, tw } from "https://esm.sh/@twind/core@1.1.3";
+import presetTailwind from "https://esm.sh/@twind/preset-tailwind@1.1.4";
+import { createRoot } from "https://esm.sh/react-dom@18.2.0/client";
+import FlipMove from "https://esm.sh/react-flip-move@3.0.5";
+import React from "https://esm.sh/react@18.2.0";
+import type { AppRouter } from "../mod.ts";
+
+const twConfig = defineConfig({
+ presets: [presetTailwind()],
+});
+
+install(twConfig);
+
+injectGlobal`
+ html {
+ @apply h-full bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100;
+ }
+ body {
+ @apply flex min-h-full flex-col items-stretch;
+ }
+`;
+
+export const trpc = createTRPCReact();
+
+const trpcClient = trpc.createClient({
+ links: [
+ httpBatchLink({
+ url: "/api/trpc",
+ }),
+ ],
+});
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ suspense: true,
+ },
+ },
+});
+
+createRoot(document.body).render(
+
+
+
+
+ ,
+);
+
+function App() {
+ const allJobs = trpc.getAllGenerationJobs.useQuery(undefined, { refetchInterval: 1000 });
+ const processingJobs = (allJobs.data ?? [])
+ .filter((job) => new Date(job.lockUntil) > new Date()).map((job) => ({ ...job, index: 0 }));
+ const waitingJobs = (allJobs.data ?? [])
+ .filter((job) => new Date(job.lockUntil) <= new Date())
+ .map((job, index) => ({ ...job, index: index + 1 }));
+ const jobs = [...processingJobs, ...waitingJobs];
+
+ return (
+
+ {jobs.map((job) => (
+
+ {job.index}. {job.state.from.first_name} {job.state.from.last_name}{" "}
+ {job.state.from.username} {job.state.from.language_code}{" "}
+ {((job.state.progress ?? 0) * 100).toFixed(0)}% {job.state.sdInstanceId}
+
+ ))}
+
+ );
+}
diff --git a/sd/SdError.ts b/app/SdError.ts
similarity index 100%
rename from sd/SdError.ts
rename to app/SdError.ts
diff --git a/app/config.ts b/app/config.ts
index d3be5b8..d1d0197 100644
--- a/app/config.ts
+++ b/app/config.ts
@@ -1,4 +1,4 @@
-import * as SdApi from "../sd/sdApi.ts";
+import * as SdApi from "./sdApi.ts";
import { db } from "./db.ts";
export interface ConfigData {
diff --git a/app/generationQueue.ts b/app/generationQueue.ts
index 1903d90..b1c2a33 100644
--- a/app/generationQueue.ts
+++ b/app/generationQueue.ts
@@ -2,19 +2,19 @@ import { promiseState } from "async";
import { Chat, Message, User } from "grammy_types";
import { JobData, Queue, Worker } from "kvmq";
import createOpenApiClient from "openapi_fetch";
-import { delay } from "std/async";
-import { decode, encode } from "std/encoding/base64";
-import { getLogger } from "std/log";
+import { delay } from "std/async/delay.ts";
+import { decode, encode } from "std/encoding/base64.ts";
+import { getLogger } from "std/log/mod.ts";
import { ulid } from "ulid";
import { bot } from "../bot/mod.ts";
-import { SdError } from "../sd/SdError.ts";
-import { PngInfo } from "../sd/parsePngInfo.ts";
-import * as SdApi from "../sd/sdApi.ts";
+import { PngInfo } from "../bot/parsePngInfo.ts";
import { formatOrdinal } from "../utils/formatOrdinal.ts";
import { formatUserChat } from "../utils/formatUserChat.ts";
+import { SdError } from "./SdError.ts";
import { getConfig, SdInstanceData } from "./config.ts";
import { db, fs } from "./db.ts";
import { SdGenerationInfo } from "./generationStore.ts";
+import * as SdApi from "./sdApi.ts";
import { uploadQueue } from "./uploadQueue.ts";
const logger = () => getLogger();
diff --git a/sd/sdApi.ts b/app/sdApi.ts
similarity index 100%
rename from sd/sdApi.ts
rename to app/sdApi.ts
diff --git a/app/uploadQueue.ts b/app/uploadQueue.ts
index c63a2ea..e49ae07 100644
--- a/app/uploadQueue.ts
+++ b/app/uploadQueue.ts
@@ -3,8 +3,8 @@ import { InputFile, InputMediaBuilder } from "grammy";
import { bold, fmt } from "grammy_parse_mode";
import { Chat, Message, User } from "grammy_types";
import { Queue } from "kvmq";
-import { format } from "std/fmt/duration";
-import { getLogger } from "std/log";
+import { format } from "std/fmt/duration.ts";
+import { getLogger } from "std/log/mod.ts";
import { bot } from "../bot/mod.ts";
import { formatUserChat } from "../utils/formatUserChat.ts";
import { db, fs } from "./db.ts";
diff --git a/bot/broadcastCommand.ts b/bot/broadcastCommand.ts
index 54edcc3..0712472 100644
--- a/bot/broadcastCommand.ts
+++ b/bot/broadcastCommand.ts
@@ -1,10 +1,10 @@
import { CommandContext } from "grammy";
import { bold, fmt, FormattedString } from "grammy_parse_mode";
-import { distinctBy } from "std/collections";
+import { distinctBy } from "std/collections/distinct_by.ts";
import { getConfig } from "../app/config.ts";
import { generationStore } from "../app/generationStore.ts";
-import { ErisContext, logger } from "./mod.ts";
import { formatUserChat } from "../utils/formatUserChat.ts";
+import { ErisContext, logger } from "./mod.ts";
export async function broadcastCommand(ctx: CommandContext) {
if (!ctx.from?.username) {
diff --git a/bot/img2imgCommand.ts b/bot/img2imgCommand.ts
index b4f9d4d..16a27ac 100644
--- a/bot/img2imgCommand.ts
+++ b/bot/img2imgCommand.ts
@@ -1,11 +1,11 @@
import { CommandContext } from "grammy";
import { StatelessQuestion } from "grammy_stateless_question";
-import { maxBy } from "std/collections";
+import { maxBy } from "std/collections/max_by.ts";
import { getConfig } from "../app/config.ts";
import { generationQueue } from "../app/generationQueue.ts";
-import { parsePngInfo, PngInfo } from "../sd/parsePngInfo.ts";
import { formatUserChat } from "../utils/formatUserChat.ts";
import { ErisContext, logger } from "./mod.ts";
+import { parsePngInfo, PngInfo } from "./parsePngInfo.ts";
type QuestionState = { fileId?: string; params?: Partial };
diff --git a/bot/mod.ts b/bot/mod.ts
index 653f939..3239c47 100644
--- a/bot/mod.ts
+++ b/bot/mod.ts
@@ -1,7 +1,7 @@
import { Api, Bot, Context, RawApi, session, SessionFlavor } from "grammy";
import { FileFlavor, hydrateFiles } from "grammy_files";
import { hydrateReply, ParseModeFlavor } from "grammy_parse_mode";
-import { getLogger } from "std/log";
+import { getLogger } from "std/log/mod.ts";
import { getConfig, setConfig } from "../app/config.ts";
import { formatUserChat } from "../utils/formatUserChat.ts";
import { broadcastCommand } from "./broadcastCommand.ts";
diff --git a/sd/parsePngInfo.test.ts b/bot/parsePngInfo.test.ts
similarity index 100%
rename from sd/parsePngInfo.test.ts
rename to bot/parsePngInfo.test.ts
diff --git a/sd/parsePngInfo.ts b/bot/parsePngInfo.ts
similarity index 100%
rename from sd/parsePngInfo.ts
rename to bot/parsePngInfo.ts
diff --git a/bot/pnginfoCommand.ts b/bot/pnginfoCommand.ts
index 1294917..d44ed81 100644
--- a/bot/pnginfoCommand.ts
+++ b/bot/pnginfoCommand.ts
@@ -1,8 +1,8 @@
import { CommandContext } from "grammy";
import { bold, fmt } from "grammy_parse_mode";
import { StatelessQuestion } from "grammy_stateless_question";
-import { getPngInfo, parsePngInfo } from "../sd/parsePngInfo.ts";
import { ErisContext } from "./mod.ts";
+import { getPngInfo, parsePngInfo } from "./parsePngInfo.ts";
export const pnginfoQuestion = new StatelessQuestion(
"pnginfo",
diff --git a/bot/txt2imgCommand.ts b/bot/txt2imgCommand.ts
index c4f10a0..459e4c0 100644
--- a/bot/txt2imgCommand.ts
+++ b/bot/txt2imgCommand.ts
@@ -2,9 +2,9 @@ import { CommandContext } from "grammy";
import { StatelessQuestion } from "grammy_stateless_question";
import { getConfig } from "../app/config.ts";
import { generationQueue } from "../app/generationQueue.ts";
-import { getPngInfo, parsePngInfo, PngInfo } from "../sd/parsePngInfo.ts";
import { formatUserChat } from "../utils/formatUserChat.ts";
import { ErisContext, logger } from "./mod.ts";
+import { getPngInfo, parsePngInfo, PngInfo } from "./parsePngInfo.ts";
export const txt2imgQuestion = new StatelessQuestion(
"txt2img",
diff --git a/deno.jsonc b/deno.json
similarity index 61%
rename from deno.jsonc
rename to deno.json
index 3cdfde7..e5a4494 100644
--- a/deno.jsonc
+++ b/deno.json
@@ -2,20 +2,26 @@
"tasks": {
"start": "deno run --unstable --allow-env --allow-read --allow-write --allow-net main.ts"
},
+ "compilerOptions": {
+ "jsx": "react"
+ },
"fmt": {
"lineWidth": 100
},
"imports": {
- "std/log": "https://deno.land/std@0.201.0/log/mod.ts",
- "std/async": "https://deno.land/std@0.201.0/async/mod.ts",
- "std/fmt/duration": "https://deno.land/std@0.202.0/fmt/duration.ts",
- "std/collections": "https://deno.land/std@0.202.0/collections/mod.ts",
- "std/encoding/base64": "https://deno.land/std@0.202.0/encoding/base64.ts",
+ "std/dotenv/": "https://deno.land/std@0.201.0/dotenv/",
+ "std/log/": "https://deno.land/std@0.201.0/log/",
+ "std/async/": "https://deno.land/std@0.201.0/async/",
+ "std/fmt/": "https://deno.land/std@0.202.0/fmt/",
+ "std/collections/": "https://deno.land/std@0.202.0/collections/",
+ "std/encoding/": "https://deno.land/std@0.202.0/encoding/",
+ "std/http/": "https://deno.land/std@0.202.0/http/",
"async": "https://deno.land/x/async@v2.0.2/mod.ts",
"ulid": "https://deno.land/x/ulid@v0.3.0/mod.ts",
"indexed_kv": "https://deno.land/x/indexed_kv@v0.4.0/mod.ts",
"kvmq": "https://deno.land/x/kvmq@v0.2.0/mod.ts",
"kvfs": "https://deno.land/x/kvfs@v0.1.0/mod.ts",
+ "swc": "https://deno.land/x/swc@0.2.1/mod.ts",
"grammy": "https://lib.deno.dev/x/grammy@1/mod.ts",
"grammy_types": "https://lib.deno.dev/x/grammy_types@3/mod.ts",
"grammy_autoquote": "https://lib.deno.dev/x/grammy_autoquote@1/mod.ts",
@@ -25,6 +31,8 @@
"file_type": "https://esm.sh/file-type@18.5.0",
"png_chunks_extract": "https://esm.sh/png-chunks-extract@1.0.0",
"png_chunk_text": "https://esm.sh/png-chunk-text@1.0.0",
- "openapi_fetch": "https://esm.sh/openapi-fetch@0.7.6"
+ "openapi_fetch": "https://esm.sh/openapi-fetch@0.7.6",
+ "@trpc/server": "https://esm.sh/@trpc/server@10.38.4",
+ "@trpc/server/": "https://esm.sh/@trpc/server@10.38.4/"
}
}
diff --git a/main.ts b/main.ts
index 1c37ebc..261baec 100644
--- a/main.ts
+++ b/main.ts
@@ -1,18 +1,23 @@
-import "https://deno.land/std@0.201.0/dotenv/load.ts";
-import { handlers, setup } from "std/log";
+import "std/dotenv/load.ts";
+import { ConsoleHandler } from "std/log/handlers.ts";
+import { setup } from "std/log/mod.ts";
+import { serveApi } from "./api/mod.ts";
import { runAllTasks } from "./app/mod.ts";
import { bot } from "./bot/mod.ts";
+// setup logging
setup({
handlers: {
- console: new handlers.ConsoleHandler("DEBUG"),
+ console: new ConsoleHandler("DEBUG"),
},
loggers: {
default: { level: "DEBUG", handlers: ["console"] },
},
});
+// run parts of the app
await Promise.all([
bot.start(),
runAllTasks(),
+ serveApi(),
]);