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(), ]);