Compare commits

..

3 Commits

Author SHA1 Message Date
pinks 82fdd0f21c feat: webui initial impl 2023-09-26 12:43:36 +02:00
pinks 70a9b1180d fix: sd gen request crash 2023-09-26 11:52:11 +02:00
pinks 7ebdf6eae4 fix: ignore edit message errors 2023-09-26 00:01:50 +02:00
18 changed files with 196 additions and 26 deletions

62
api/mod.ts Normal file
View File

@ -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;
}

BIN
api/webui/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

13
api/webui/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="/favicon.png" type="image/png">
<script type="module" src="/main.tsx"></script>
<title>Eris</title>
</head>
<body>
</body>
</html>

78
api/webui/main.tsx Normal file
View File

@ -0,0 +1,78 @@
/// <reference lib="dom" />
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<AppRouter>();
const trpcClient = trpc.createClient({
links: [
httpBatchLink({
url: "/api/trpc",
}),
],
});
const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true,
},
},
});
createRoot(document.body).render(
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</trpc.Provider>,
);
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 (
<FlipMove
typeName={"ul"}
className={tw("p-4")}
enterAnimation="fade"
leaveAnimation="fade"
>
{jobs.map((job) => (
<li key={job.id.join("/")} className={tw("")}>
{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}
</li>
))}
</FlipMove>
);
}

View File

@ -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 {

View File

@ -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();
@ -138,7 +138,7 @@ async function processGenerationJob(
state.replyMessage.message_id,
`Generating your prompt now... 0% using ${sdInstance.name || sdInstance.id}`,
{ maxAttempts: 1 },
);
).catch(() => undefined);
// reduce size if worker can't handle the resolution
const config = await getConfig();
@ -199,6 +199,10 @@ async function processGenerationJob(
throw new Error(`Unknown task type: ${state.task.type}`);
}
// we await the promise only after it finishes
// so we need to add catch callback to not crash the process before that
responsePromise.catch(() => undefined);
// poll for progress while the generation request is pending
do {
const progressResponse = await workerSdClient.GET("/sdapi/v1/progress", {

View File

@ -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";

View File

@ -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<ErisContext>) {
if (!ctx.from?.username) {

View File

@ -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<PngInfo> };

View File

@ -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";

View File

@ -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<ErisContext>(
"pnginfo",

View File

@ -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<ErisContext>(
"txt2img",

View File

@ -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/"
}
}

11
main.ts
View File

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