Compare commits

..

No commits in common. "4f2371fa8baf4eb8652a83597ca1aed6a034e492" and "11d8a66c18da6f3c6e345fdbaee0ce5a2967632a" have entirely different histories.

20 changed files with 95 additions and 168 deletions

View File

@ -1,7 +1,7 @@
import { route } from "reroute"; import { route } from "reroute";
import { serveSpa } from "serve_spa"; import { serveSpa } from "serve_spa";
import { serveApi } from "./serveApi.ts"; import { serveApi } from "./serveApi.ts";
import { fromFileUrl } from "std/path/mod.ts"; import { fromFileUrl } from "std/path/mod.ts"
export async function serveUi() { export async function serveUi() {
const server = Deno.serve({ port: 5999 }, (request) => const server = Deno.serve({ port: 5999 }, (request) =>

View File

@ -4,7 +4,7 @@ import { ulid } from "ulid";
export const sessions = new Map<string, Session>(); export const sessions = new Map<string, Session>();
export interface Session { export interface Session {
userId?: number | undefined; userId?: number;
} }
export const sessionsRoute = createPathFilter({ export const sessionsRoute = createPathFilter({
@ -24,7 +24,7 @@ export const sessionsRoute = createPathFilter({
GET: createEndpoint( GET: createEndpoint(
{ query: null, body: null }, { query: null, body: null },
async ({ params }) => { async ({ params }) => {
const id = params.sessionId!; const id = params.sessionId;
const session = sessions.get(id); const session = sessions.get(id);
if (!session) { if (!session) {
return { status: 401, body: { type: "text/plain", data: "Session not found" } }; return { status: 401, body: { type: "text/plain", data: "Session not found" } };

View File

@ -7,7 +7,7 @@ export const usersRoute = createPathFilter({
GET: createEndpoint( GET: createEndpoint(
{ query: null, body: null }, { query: null, body: null },
async ({ params }) => { async ({ params }) => {
const chat = await bot.api.getChat(params.userId!); const chat = await bot.api.getChat(params.userId);
if (chat.type !== "private") { if (chat.type !== "private") {
throw new Error("Chat is not private"); throw new Error("Chat is not private");
} }
@ -36,7 +36,7 @@ export const usersRoute = createPathFilter({
GET: createEndpoint( GET: createEndpoint(
{ query: null, body: null }, { query: null, body: null },
async ({ params }) => { async ({ params }) => {
const chat = await bot.api.getChat(params.userId!); const chat = await bot.api.getChat(params.userId);
if (chat.type !== "private") { if (chat.type !== "private") {
throw new Error("Chat is not private"); throw new Error("Chat is not private");
} }

View File

@ -12,7 +12,6 @@ import {
workerInstanceStore, workerInstanceStore,
} from "../app/workerInstanceStore.ts"; } from "../app/workerInstanceStore.ts";
import { getAuthHeader } from "../utils/getAuthHeader.ts"; import { getAuthHeader } from "../utils/getAuthHeader.ts";
import { omitUndef } from "../utils/omitUndef.ts";
import { withUser } from "./withUser.ts"; import { withUser } from "./withUser.ts";
export type WorkerData = Omit<WorkerInstance, "sdUrl" | "sdAuth"> & { export type WorkerData = Omit<WorkerInstance, "sdUrl" | "sdAuth"> & {
@ -106,7 +105,7 @@ export const workersRoute = createPathFilter({
GET: createEndpoint( GET: createEndpoint(
{ query: null, body: null }, { query: null, body: null },
async ({ params }) => { async ({ params }) => {
const workerInstance = await workerInstanceStore.getById(params.workerId!); const workerInstance = await workerInstanceStore.getById(params.workerId);
if (!workerInstance) { if (!workerInstance) {
return { status: 404, body: { type: "text/plain", data: `Worker not found` } }; return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
} }
@ -127,7 +126,7 @@ export const workersRoute = createPathFilter({
}, },
}, },
async ({ params, query, body }) => { async ({ params, query, body }) => {
const workerInstance = await workerInstanceStore.getById(params.workerId!); const workerInstance = await workerInstanceStore.getById(params.workerId);
if (!workerInstance) { if (!workerInstance) {
return { status: 404, body: { type: "text/plain", data: `Worker not found` } }; return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
} }
@ -137,7 +136,7 @@ export const workersRoute = createPathFilter({
JSON.stringify(body.data) JSON.stringify(body.data)
}`, }`,
); );
await workerInstance.update(omitUndef(body.data)); await workerInstance.update(body.data);
return { return {
status: 200, status: 200,
body: { type: "application/json", data: await getWorkerData(workerInstance) }, body: { type: "application/json", data: await getWorkerData(workerInstance) },
@ -153,7 +152,7 @@ export const workersRoute = createPathFilter({
body: null, body: null,
}, },
async ({ params, query }) => { async ({ params, query }) => {
const workerInstance = await workerInstanceStore.getById(params.workerId!); const workerInstance = await workerInstanceStore.getById(params.workerId);
if (!workerInstance) { if (!workerInstance) {
return { status: 404, body: { type: "text/plain", data: `Worker not found` } }; return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
} }
@ -170,7 +169,7 @@ export const workersRoute = createPathFilter({
GET: createEndpoint( GET: createEndpoint(
{ query: null, body: null }, { query: null, body: null },
async ({ params }) => { async ({ params }) => {
const workerInstance = await workerInstanceStore.getById(params.workerId!); const workerInstance = await workerInstanceStore.getById(params.workerId);
if (!workerInstance) { if (!workerInstance) {
return { status: 404, body: { type: "text/plain", data: `Worker not found` } }; return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
} }
@ -201,7 +200,7 @@ export const workersRoute = createPathFilter({
GET: createEndpoint( GET: createEndpoint(
{ query: null, body: null }, { query: null, body: null },
async ({ params }) => { async ({ params }) => {
const workerInstance = await workerInstanceStore.getById(params.workerId!); const workerInstance = await workerInstanceStore.getById(params.workerId);
if (!workerInstance) { if (!workerInstance) {
return { status: 404, body: { type: "text/plain", data: `Worker not found` } }; return { status: 404, body: { type: "text/plain", data: `Worker not found` } };
} }

View File

@ -11,7 +11,6 @@ import { PngInfo } from "../bot/parsePngInfo.ts";
import { formatOrdinal } from "../utils/formatOrdinal.ts"; import { formatOrdinal } from "../utils/formatOrdinal.ts";
import { formatUserChat } from "../utils/formatUserChat.ts"; import { formatUserChat } from "../utils/formatUserChat.ts";
import { getAuthHeader } from "../utils/getAuthHeader.ts"; import { getAuthHeader } from "../utils/getAuthHeader.ts";
import { omitUndef } from "../utils/omitUndef.ts";
import { SdError } from "./SdError.ts"; import { SdError } from "./SdError.ts";
import { getConfig } from "./config.ts"; import { getConfig } from "./config.ts";
import { db, fs } from "./db.ts"; import { db, fs } from "./db.ts";
@ -162,7 +161,7 @@ async function processGenerationJob(
// reduce size if worker can't handle the resolution // reduce size if worker can't handle the resolution
const size = limitSize( const size = limitSize(
omitUndef({ ...config.defaultParams, ...state.task.params }), { ...config.defaultParams, ...state.task.params },
1024 * 1024, 1024 * 1024,
); );
function limitSize( function limitSize(
@ -183,18 +182,18 @@ async function processGenerationJob(
// start generating the image // start generating the image
const responsePromise = state.task.type === "txt2img" const responsePromise = state.task.type === "txt2img"
? workerSdClient.POST("/sdapi/v1/txt2img", { ? workerSdClient.POST("/sdapi/v1/txt2img", {
body: omitUndef({ body: {
...config.defaultParams, ...config.defaultParams,
...state.task.params, ...state.task.params,
...size, ...size,
negative_prompt: state.task.params.negative_prompt negative_prompt: state.task.params.negative_prompt
? state.task.params.negative_prompt ? state.task.params.negative_prompt
: config.defaultParams?.negative_prompt, : config.defaultParams?.negative_prompt,
}), },
}) })
: state.task.type === "img2img" : state.task.type === "img2img"
? workerSdClient.POST("/sdapi/v1/img2img", { ? workerSdClient.POST("/sdapi/v1/img2img", {
body: omitUndef({ body: {
...config.defaultParams, ...config.defaultParams,
...state.task.params, ...state.task.params,
...size, ...size,
@ -210,7 +209,7 @@ async function processGenerationJob(
).then((resp) => resp.arrayBuffer()), ).then((resp) => resp.arrayBuffer()),
), ),
], ],
}), },
}) })
: undefined; : undefined;

View File

@ -5,10 +5,10 @@ import { db } from "./db.ts";
export interface GenerationSchema { export interface GenerationSchema {
from: User; from: User;
chat: Chat; chat: Chat;
sdInstanceId?: string | undefined; // TODO: change to workerInstanceKey sdInstanceId?: string; // TODO: change to workerInstanceKey
info?: SdGenerationInfo | undefined; info?: SdGenerationInfo;
startDate?: Date | undefined; startDate?: Date;
endDate?: Date | undefined; endDate?: Date;
} }
/** /**

View File

@ -1,42 +1,19 @@
export interface KvMemoizeOptions<A extends Deno.KvKey, R> {
/**
* The time in milliseconds until the cached result expires.
*/
expireIn?: ((result: R, ...args: A) => number) | number | undefined;
/**
* Whether to recalculate the result if it was already cached.
*
* Runs whenever the result is retrieved from the cache.
*/
shouldRecalculate?: ((result: R, ...args: A) => boolean) | undefined;
/**
* Whether to cache the result after computing it.
*
* Runs whenever a new result is computed.
*/
shouldCache?: ((result: R, ...args: A) => boolean) | undefined;
/**
* Override the default KV store functions.
*/
override?: {
set: (
key: Deno.KvKey,
args: A,
value: R,
options: { expireIn?: number },
) => Promise<void>;
get: (key: Deno.KvKey, args: A) => Promise<R | undefined>;
};
}
/** /**
* Memoizes the function result in KV store. * Memoizes the function result in KV storage.
*/ */
export function kvMemoize<A extends Deno.KvKey, R>( export function kvMemoize<A extends Deno.KvKey, R>(
db: Deno.Kv, db: Deno.Kv,
key: Deno.KvKey, key: Deno.KvKey,
fn: (...args: A) => Promise<R>, fn: (...args: A) => Promise<R>,
options?: KvMemoizeOptions<A, R>, options?: {
expireIn?: number | ((result: R, ...args: A) => number);
shouldRecalculate?: (result: R, ...args: A) => boolean;
shouldCache?: (result: R, ...args: A) => boolean;
override?: {
set: (key: Deno.KvKey, args: A, value: R, options: { expireIn?: number }) => Promise<void>;
get: (key: Deno.KvKey, args: A) => Promise<R | undefined>;
};
},
): (...args: A) => Promise<R> { ): (...args: A) => Promise<R> {
return async (...args) => { return async (...args) => {
const cachedResult = options?.override?.get const cachedResult = options?.override?.get
@ -57,9 +34,9 @@ export function kvMemoize<A extends Deno.KvKey, R>(
if (options?.shouldCache?.(result, ...args) ?? (result != null)) { if (options?.shouldCache?.(result, ...args) ?? (result != null)) {
if (options?.override?.set) { if (options?.override?.set) {
await options.override.set(key, args, result, expireIn != null ? { expireIn } : {}); await options.override.set(key, args, result, { expireIn });
} else { } else {
await db.set([...key, ...args], result, expireIn != null ? { expireIn } : {}); await db.set([...key, ...args], result, { expireIn });
} }
} }

View File

@ -92,7 +92,7 @@ export async function processUploadQueue() {
// send caption in separate message if it couldn't fit // send caption in separate message if it couldn't fit
if (caption.text.length > 1024 && caption.text.length <= 4096) { if (caption.text.length > 1024 && caption.text.length <= 4096) {
await bot.api.sendMessage(state.chat.id, caption.text, { await bot.api.sendMessage(state.chat.id, caption.text, {
reply_to_message_id: resultMessages[0]!.message_id, reply_to_message_id: resultMessages[0].message_id,
allow_sending_without_reply: true, allow_sending_without_reply: true,
entities: caption.entities, entities: caption.entities,
maxWait: 60, maxWait: 60,

View File

@ -1,5 +1,4 @@
import { generationQueue } from "../app/generationQueue.ts"; import { generationQueue } from "../app/generationQueue.ts";
import { omitUndef } from "../utils/omitUndef.ts";
import { ErisContext } from "./mod.ts"; import { ErisContext } from "./mod.ts";
export async function cancelCommand(ctx: ErisContext) { export async function cancelCommand(ctx: ErisContext) {
@ -8,11 +7,8 @@ export async function cancelCommand(ctx: ErisContext) {
.filter((job) => job.lockUntil < new Date()) .filter((job) => job.lockUntil < new Date())
.filter((j) => j.state.from.id === ctx.from?.id); .filter((j) => j.state.from.id === ctx.from?.id);
for (const job of userJobs) await generationQueue.deleteJob(job.id); for (const job of userJobs) await generationQueue.deleteJob(job.id);
await ctx.reply( await ctx.reply(`Cancelled ${userJobs.length} jobs`, {
`Cancelled ${userJobs.length} jobs`, reply_to_message_id: ctx.message?.message_id,
omitUndef({ allow_sending_without_reply: true,
reply_to_message_id: ctx.message?.message_id, });
allow_sending_without_reply: true,
}),
);
} }

View File

@ -6,7 +6,6 @@ import { error, info, warning } from "std/log/mod.ts";
import { sessions } from "../api/sessionsRoute.ts"; import { sessions } from "../api/sessionsRoute.ts";
import { getConfig, setConfig } from "../app/config.ts"; import { getConfig, setConfig } from "../app/config.ts";
import { formatUserChat } from "../utils/formatUserChat.ts"; import { formatUserChat } from "../utils/formatUserChat.ts";
import { omitUndef } from "../utils/omitUndef.ts";
import { broadcastCommand } from "./broadcastCommand.ts"; import { broadcastCommand } from "./broadcastCommand.ts";
import { cancelCommand } from "./cancelCommand.ts"; import { cancelCommand } from "./cancelCommand.ts";
import { img2imgCommand, img2imgQuestion } from "./img2imgCommand.ts"; import { img2imgCommand, img2imgQuestion } from "./img2imgCommand.ts";
@ -20,11 +19,11 @@ interface SessionData {
} }
interface ErisChatData { interface ErisChatData {
language?: string | undefined; language?: string;
} }
interface ErisUserData { interface ErisUserData {
params?: Record<string, string> | undefined; params?: Record<string, string>;
} }
export type ErisContext = export type ErisContext =
@ -95,13 +94,10 @@ bot.use(async (ctx, next) => {
await next(); await next();
} catch (err) { } catch (err) {
try { try {
await ctx.reply( await ctx.reply(`Handling update failed: ${err}`, {
`Handling update failed: ${err}`, reply_to_message_id: ctx.message?.message_id,
omitUndef({ allow_sending_without_reply: true,
reply_to_message_id: ctx.message?.message_id, });
allow_sending_without_reply: true,
}),
);
} catch { } catch {
throw err; throw err;
} }
@ -126,33 +122,24 @@ bot.command("start", async (ctx) => {
const id = ctx.match.trim(); const id = ctx.match.trim();
const session = sessions.get(id); const session = sessions.get(id);
if (session == null) { if (session == null) {
await ctx.reply( await ctx.reply("Login failed: Invalid session ID", {
"Login failed: Invalid session ID", reply_to_message_id: ctx.message?.message_id,
omitUndef({ });
reply_to_message_id: ctx.message?.message_id,
}),
);
return; return;
} }
session.userId = ctx.from?.id; session.userId = ctx.from?.id;
sessions.set(id, session); sessions.set(id, session);
info(`User ${formatUserChat(ctx)} logged in`); info(`User ${formatUserChat(ctx)} logged in`);
// TODO: show link to web ui // TODO: show link to web ui
await ctx.reply( await ctx.reply("Login successful! You can now return to the WebUI.", {
"Login successful! You can now return to the WebUI.", reply_to_message_id: ctx.message?.message_id,
omitUndef({ });
reply_to_message_id: ctx.message?.message_id,
}),
);
return; return;
} }
await ctx.reply( await ctx.reply("Hello! Use the /txt2img command to generate an image", {
"Hello! Use the /txt2img command to generate an image", reply_to_message_id: ctx.message?.message_id,
omitUndef({ });
reply_to_message_id: ctx.message?.message_id,
}),
);
}); });
bot.command("txt2img", txt2imgCommand); bot.command("txt2img", txt2imgCommand);

View File

@ -25,11 +25,7 @@ interface PngInfoExtra extends PngInfo {
upscale?: number; upscale?: number;
} }
export function parsePngInfo( export function parsePngInfo(pngInfo: string, baseParams?: Partial<PngInfo>, shouldParseSeed?: boolean): Partial<PngInfo> {
pngInfo: string,
baseParams?: Partial<PngInfo>,
shouldParseSeed?: boolean,
): Partial<PngInfo> {
const tags = pngInfo.split(/[,;]+|\.+\s|\n/u); const tags = pngInfo.split(/[,;]+|\.+\s|\n/u);
let part: "prompt" | "negative_prompt" | "params" = "prompt"; let part: "prompt" | "negative_prompt" | "params" = "prompt";
const params: Partial<PngInfoExtra> = {}; const params: Partial<PngInfoExtra> = {};
@ -38,7 +34,7 @@ export function parsePngInfo(
for (const tag of tags) { for (const tag of tags) {
const paramValuePair = tag.trim().match(/^(\w+\s*\w*):\s+(.*)$/u); const paramValuePair = tag.trim().match(/^(\w+\s*\w*):\s+(.*)$/u);
if (paramValuePair) { if (paramValuePair) {
const [_match, param = "", value = ""] = paramValuePair; const [, param, value] = paramValuePair;
switch (param.replace(/\s+/u, "").toLowerCase()) { switch (param.replace(/\s+/u, "").toLowerCase()) {
case "positiveprompt": case "positiveprompt":
case "positive": case "positive":
@ -71,7 +67,7 @@ export function parsePngInfo(
case "size": case "size":
case "resolution": { case "resolution": {
part = "params"; part = "params";
const [width = 0, height = 0] = value.trim() const [width, height] = value.trim()
.split(/\s*[x,]\s*/u, 2) .split(/\s*[x,]\s*/u, 2)
.map((v) => v.trim()) .map((v) => v.trim())
.map(Number); .map(Number);
@ -107,11 +103,9 @@ export function parsePngInfo(
part = "params"; part = "params";
if (shouldParseSeed) { if (shouldParseSeed) {
const seed = Number(value.trim()); const seed = Number(value.trim());
if (Number.isFinite(seed)) { params.seed = seed;
params.seed = seed; break;
}
} }
break;
} }
case "model": case "model":
case "modelhash": case "modelhash":

View File

@ -1,7 +1,6 @@
import { CommandContext } from "grammy"; import { CommandContext } from "grammy";
import { bold, fmt } from "grammy_parse_mode"; import { bold, fmt } from "grammy_parse_mode";
import { StatelessQuestion } from "grammy_stateless_question"; import { StatelessQuestion } from "grammy_stateless_question";
import { omitUndef } from "../utils/omitUndef.ts";
import { ErisContext } from "./mod.ts"; import { ErisContext } from "./mod.ts";
import { getPngInfo, parsePngInfo } from "./parsePngInfo.ts"; import { getPngInfo, parsePngInfo } from "./parsePngInfo.ts";
@ -24,13 +23,11 @@ async function pnginfo(ctx: ErisContext, includeRepliedTo: boolean): Promise<voi
await ctx.reply( await ctx.reply(
"Please send me a PNG file." + "Please send me a PNG file." +
pnginfoQuestion.messageSuffixMarkdown(), pnginfoQuestion.messageSuffixMarkdown(),
omitUndef( {
{ reply_markup: { force_reply: true, selective: true },
reply_markup: { force_reply: true, selective: true }, parse_mode: "Markdown",
parse_mode: "Markdown", reply_to_message_id: ctx.message?.message_id,
reply_to_message_id: ctx.message?.message_id, },
} as const,
),
); );
return; return;
} }
@ -49,11 +46,8 @@ async function pnginfo(ctx: ErisContext, includeRepliedTo: boolean): Promise<voi
params.width && params.height ? fmt`${bold("Size")}: ${params.width}x${params.height}` : "", params.width && params.height ? fmt`${bold("Size")}: ${params.width}x${params.height}` : "",
]); ]);
await ctx.reply( await ctx.reply(paramsText.text, {
paramsText.text, entities: paramsText.entities,
omitUndef({ reply_to_message_id: ctx.message?.message_id,
entities: paramsText.entities, });
reply_to_message_id: ctx.message?.message_id,
}),
);
} }

View File

@ -1,20 +1,16 @@
import { CommandContext } from "grammy"; import { CommandContext } from "grammy";
import { bold, fmt } from "grammy_parse_mode"; import { bold, fmt } from "grammy_parse_mode";
import { activeGenerationWorkers, generationQueue } from "../app/generationQueue.ts"; import { activeGenerationWorkers, generationQueue } from "../app/generationQueue.ts";
import { workerInstanceStore } from "../app/workerInstanceStore.ts";
import { getFlagEmoji } from "../utils/getFlagEmoji.ts"; import { getFlagEmoji } from "../utils/getFlagEmoji.ts";
import { omitUndef } from "../utils/omitUndef.ts";
import { ErisContext } from "./mod.ts"; import { ErisContext } from "./mod.ts";
import { workerInstanceStore } from "../app/workerInstanceStore.ts";
export async function queueCommand(ctx: CommandContext<ErisContext>) { export async function queueCommand(ctx: CommandContext<ErisContext>) {
let formattedMessage = await getMessageText(); let formattedMessage = await getMessageText();
const queueMessage = await ctx.replyFmt( const queueMessage = await ctx.replyFmt(formattedMessage, {
formattedMessage, disable_notification: true,
omitUndef({ reply_to_message_id: ctx.message?.message_id,
disable_notification: true, });
reply_to_message_id: ctx.message?.message_id,
}),
);
handleFutureUpdates().catch(() => undefined); handleFutureUpdates().catch(() => undefined);
async function getMessageText() { async function getMessageText() {

View File

@ -5,9 +5,7 @@
"start": "deno run --unstable --allow-env --allow-read --allow-write --allow-net main.ts" "start": "deno run --unstable --allow-env --allow-read --allow-write --allow-net main.ts"
}, },
"compilerOptions": { "compilerOptions": {
"exactOptionalPropertyTypes": true, "jsx": "react"
"jsx": "react",
"noUncheckedIndexedAccess": true
}, },
"fmt": { "fmt": {
"lineWidth": 100 "lineWidth": 100

View File

@ -34,8 +34,7 @@ export function AppHeader(
); );
const bot = useSWR( const bot = useSWR(
["bot", "GET", {}] as const, ['bot',"GET",{}] as const, (args) => fetchApi(...args).then(handleResponse),
(args) => fetchApi(...args).then(handleResponse),
); );
const userPhoto = useSWR( const userPhoto = useSWR(

View File

@ -1,7 +1,7 @@
import { cx } from "@twind/core"; import { cx } from "@twind/core";
import React from "react"; import React from "react";
function CounterDigit(props: { value: number; transitionDurationMs?: number | undefined }) { function CounterDigit(props: { value: number; transitionDurationMs?: number }) {
const { value, transitionDurationMs = 1500 } = props; const { value, transitionDurationMs = 1500 } = props;
const rads = -(Math.floor(value) % 1_000_000) * 2 * Math.PI * 0.1; const rads = -(Math.floor(value) % 1_000_000) * 2 * Math.PI * 0.1;
@ -36,10 +36,10 @@ const CounterText = (props: { children: React.ReactNode }) => (
export function Counter(props: { export function Counter(props: {
value: number; value: number;
digits: number; digits: number;
fractionDigits?: number | undefined; fractionDigits?: number;
transitionDurationMs?: number | undefined; transitionDurationMs?: number;
className?: string | undefined; className?: string;
postfix?: string | undefined; postfix?: string;
}) { }) {
const { value, digits, fractionDigits = 0, transitionDurationMs, className, postfix } = props; const { value, digits, fractionDigits = 0, transitionDurationMs, className, postfix } = props;

View File

@ -43,7 +43,7 @@ export function WorkersPage(props: { sessionId?: string }) {
</button> </button>
)} )}
<dialog <dialog
className="dialog animate-pop-in backdrop-animate-fade-in" className="dialog"
ref={createWorkerModalRef} ref={createWorkerModalRef}
> >
<form <form
@ -159,7 +159,7 @@ export function WorkersPage(props: { sessionId?: string }) {
); );
} }
function WorkerListItem(props: { worker: WorkerData; sessionId: string | undefined }) { function WorkerListItem(props: { worker: WorkerData; sessionId?: string }) {
const { worker, sessionId } = props; const { worker, sessionId } = props;
const editWorkerModalRef = useRef<HTMLDialogElement>(null); const editWorkerModalRef = useRef<HTMLDialogElement>(null);
const deleteWorkerModalRef = useRef<HTMLDialogElement>(null); const deleteWorkerModalRef = useRef<HTMLDialogElement>(null);
@ -231,7 +231,7 @@ function WorkerListItem(props: { worker: WorkerData; sessionId: string | undefin
)} )}
</p> </p>
<dialog <dialog
className="dialog animate-pop-in backdrop-animate-fade-in" className="dialog"
ref={editWorkerModalRef} ref={editWorkerModalRef}
> >
<form <form
@ -293,7 +293,7 @@ function WorkerListItem(props: { worker: WorkerData; sessionId: string | undefin
</form> </form>
</dialog> </dialog>
<dialog <dialog
className="dialog animate-pop-in backdrop-animate-fade-in" className="dialog"
ref={deleteWorkerModalRef} ref={deleteWorkerModalRef}
> >
<form <form

View File

@ -27,11 +27,14 @@ injectGlobal`
rgba(0, 0, 0, 0.1) 14px, rgba(0, 0, 0, 0.1) 14px,
rgba(0, 0, 0, 0.1) 28px rgba(0, 0, 0, 0.1) 28px
); );
animation: bg-scroll 0.5s linear infinite; animation: bg-stripes-scroll 0.5s linear infinite;
background-size: 40px 40px; background-size: 40px 40px;
} }
@keyframes bg-scroll { @keyframes bg-stripes-scroll {
to { 0% {
background-position: 0 40px;
}
100% {
background-position: 40px 0; background-position: 40px 0;
} }
} }
@ -60,11 +63,11 @@ injectGlobal`
opacity 0s; opacity 0s;
} }
.backdrop-animate-fade-in::backdrop { .animate-fade-in {
animation: fade-in 0.3s ease-out forwards; animation: fade-in 0.3s ease-out forwards;
} }
@keyframes fade-in { @keyframes fade-in {
from { 0% {
opacity: 0; opacity: 0;
} }
} }
@ -73,7 +76,7 @@ injectGlobal`
animation: pop-in 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards; animation: pop-in 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
} }
@keyframes pop-in { @keyframes pop-in {
from { 0% {
transform: scale(0.8); transform: scale(0.8);
opacity: 0; opacity: 0;
} }
@ -131,8 +134,8 @@ injectGlobal`
} }
.dialog { .dialog {
@apply overflow-hidden overflow-y-auto rounded-md @apply animate-pop-in backdrop:animate-fade-in overflow-hidden overflow-y-auto rounded-md
bg-zinc-100 text-zinc-900 shadow-lg backdrop:bg-black/30 dark:bg-zinc-800 dark:text-zinc-100; bg-zinc-100 text-zinc-900 shadow-lg backdrop:bg-black/20 dark:bg-zinc-800 dark:text-zinc-100;
} }
} }
`; `;

View File

@ -1,11 +1,7 @@
import { Chat, User } from "grammy_types"; import { Chat, User } from "grammy_types";
export function formatUserChat( export function formatUserChat(
ctx: { ctx: { from?: User; chat?: Chat; workerInstanceKey?: string },
from?: User | undefined;
chat?: Chat | undefined;
workerInstanceKey?: string | undefined;
},
) { ) {
const msg: string[] = []; const msg: string[] = [];
if (ctx.from) { if (ctx.from) {

View File

@ -1,11 +0,0 @@
/**
* Removes all undefined properties from an object.
*/
export function omitUndef<O extends object | undefined>(object: O):
& { [K in keyof O as undefined extends O[K] ? never : K]: O[K] }
& { [K in keyof O as undefined extends O[K] ? K : never]?: O[K] & ({} | null) } {
if (object == undefined) return object as never;
return Object.fromEntries(
Object.entries(object).filter(([, v]) => v !== undefined),
) as never;
}