Compare commits

...

2 Commits

Author SHA1 Message Date
pinks 4f2371fa8b exactOptionalPropertyTypes 2023-10-19 23:37:03 +02:00
pinks f020499f4d dialog animations 2023-10-19 03:18:56 +02:00
20 changed files with 169 additions and 96 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; userId?: number | undefined;
} }
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,6 +12,7 @@ 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"> & {
@ -105,7 +106,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` } };
} }
@ -126,7 +127,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` } };
} }
@ -136,7 +137,7 @@ export const workersRoute = createPathFilter({
JSON.stringify(body.data) JSON.stringify(body.data)
}`, }`,
); );
await workerInstance.update(body.data); await workerInstance.update(omitUndef(body.data));
return { return {
status: 200, status: 200,
body: { type: "application/json", data: await getWorkerData(workerInstance) }, body: { type: "application/json", data: await getWorkerData(workerInstance) },
@ -152,7 +153,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` } };
} }
@ -169,7 +170,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` } };
} }
@ -200,7 +201,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,6 +11,7 @@ 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";
@ -161,7 +162,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(
{ ...config.defaultParams, ...state.task.params }, omitUndef({ ...config.defaultParams, ...state.task.params }),
1024 * 1024, 1024 * 1024,
); );
function limitSize( function limitSize(
@ -182,18 +183,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: { body: omitUndef({
...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: { body: omitUndef({
...config.defaultParams, ...config.defaultParams,
...state.task.params, ...state.task.params,
...size, ...size,
@ -209,7 +210,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; // TODO: change to workerInstanceKey sdInstanceId?: string | undefined; // TODO: change to workerInstanceKey
info?: SdGenerationInfo; info?: SdGenerationInfo | undefined;
startDate?: Date; startDate?: Date | undefined;
endDate?: Date; endDate?: Date | undefined;
} }
/** /**

View File

@ -1,19 +1,42 @@
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 storage. * Memoizes the function result in KV store.
*/ */
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?: { options?: KvMemoizeOptions<A, R>,
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
@ -34,9 +57,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 }); await options.override.set(key, args, result, expireIn != null ? { expireIn } : {});
} else { } else {
await db.set([...key, ...args], result, { expireIn }); await db.set([...key, ...args], result, expireIn != null ? { 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,4 +1,5 @@
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) {
@ -7,8 +8,11 @@ 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(`Cancelled ${userJobs.length} jobs`, { await ctx.reply(
reply_to_message_id: ctx.message?.message_id, `Cancelled ${userJobs.length} jobs`,
allow_sending_without_reply: true, omitUndef({
}); reply_to_message_id: ctx.message?.message_id,
allow_sending_without_reply: true,
}),
);
} }

View File

@ -6,6 +6,7 @@ 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";
@ -19,11 +20,11 @@ interface SessionData {
} }
interface ErisChatData { interface ErisChatData {
language?: string; language?: string | undefined;
} }
interface ErisUserData { interface ErisUserData {
params?: Record<string, string>; params?: Record<string, string> | undefined;
} }
export type ErisContext = export type ErisContext =
@ -94,10 +95,13 @@ bot.use(async (ctx, next) => {
await next(); await next();
} catch (err) { } catch (err) {
try { try {
await ctx.reply(`Handling update failed: ${err}`, { await ctx.reply(
reply_to_message_id: ctx.message?.message_id, `Handling update failed: ${err}`,
allow_sending_without_reply: true, omitUndef({
}); reply_to_message_id: ctx.message?.message_id,
allow_sending_without_reply: true,
}),
);
} catch { } catch {
throw err; throw err;
} }
@ -122,24 +126,33 @@ 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("Login failed: Invalid session ID", { await ctx.reply(
reply_to_message_id: ctx.message?.message_id, "Login failed: Invalid session 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("Login successful! You can now return to the WebUI.", { await ctx.reply(
reply_to_message_id: ctx.message?.message_id, "Login successful! You can now return to the WebUI.",
}); omitUndef({
reply_to_message_id: ctx.message?.message_id,
}),
);
return; return;
} }
await ctx.reply("Hello! Use the /txt2img command to generate an image", { await ctx.reply(
reply_to_message_id: ctx.message?.message_id, "Hello! Use the /txt2img command to generate an image",
}); omitUndef({
reply_to_message_id: ctx.message?.message_id,
}),
);
}); });
bot.command("txt2img", txt2imgCommand); bot.command("txt2img", txt2imgCommand);

View File

@ -25,7 +25,11 @@ interface PngInfoExtra extends PngInfo {
upscale?: number; upscale?: number;
} }
export function parsePngInfo(pngInfo: string, baseParams?: Partial<PngInfo>, shouldParseSeed?: boolean): Partial<PngInfo> { export function parsePngInfo(
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> = {};
@ -34,7 +38,7 @@ export function parsePngInfo(pngInfo: string, baseParams?: Partial<PngInfo>, sho
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 [, param, value] = paramValuePair; const [_match, param = "", value = ""] = paramValuePair;
switch (param.replace(/\s+/u, "").toLowerCase()) { switch (param.replace(/\s+/u, "").toLowerCase()) {
case "positiveprompt": case "positiveprompt":
case "positive": case "positive":
@ -67,7 +71,7 @@ export function parsePngInfo(pngInfo: string, baseParams?: Partial<PngInfo>, sho
case "size": case "size":
case "resolution": { case "resolution": {
part = "params"; part = "params";
const [width, height] = value.trim() const [width = 0, height = 0] = 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);
@ -103,9 +107,11 @@ export function parsePngInfo(pngInfo: string, baseParams?: Partial<PngInfo>, sho
part = "params"; part = "params";
if (shouldParseSeed) { if (shouldParseSeed) {
const seed = Number(value.trim()); const seed = Number(value.trim());
params.seed = seed; if (Number.isFinite(seed)) {
break; params.seed = seed;
}
} }
break;
} }
case "model": case "model":
case "modelhash": case "modelhash":

View File

@ -1,6 +1,7 @@
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";
@ -23,11 +24,13 @@ 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 }, {
parse_mode: "Markdown", reply_markup: { force_reply: true, selective: true },
reply_to_message_id: ctx.message?.message_id, parse_mode: "Markdown",
}, reply_to_message_id: ctx.message?.message_id,
} as const,
),
); );
return; return;
} }
@ -46,8 +49,11 @@ 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(paramsText.text, { await ctx.reply(
entities: paramsText.entities, paramsText.text,
reply_to_message_id: ctx.message?.message_id, omitUndef({
}); entities: paramsText.entities,
reply_to_message_id: ctx.message?.message_id,
}),
);
} }

View File

@ -1,16 +1,20 @@
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 { getFlagEmoji } from "../utils/getFlagEmoji.ts";
import { ErisContext } from "./mod.ts";
import { workerInstanceStore } from "../app/workerInstanceStore.ts"; import { workerInstanceStore } from "../app/workerInstanceStore.ts";
import { getFlagEmoji } from "../utils/getFlagEmoji.ts";
import { omitUndef } from "../utils/omitUndef.ts";
import { ErisContext } from "./mod.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(formattedMessage, { const queueMessage = await ctx.replyFmt(
disable_notification: true, formattedMessage,
reply_to_message_id: ctx.message?.message_id, omitUndef({
}); 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,7 +5,9 @@
"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": {
"jsx": "react" "exactOptionalPropertyTypes": true,
"jsx": "react",
"noUncheckedIndexedAccess": true
}, },
"fmt": { "fmt": {
"lineWidth": 100 "lineWidth": 100

View File

@ -34,7 +34,8 @@ export function AppHeader(
); );
const bot = useSWR( const bot = useSWR(
['bot',"GET",{}] as const, (args) => fetchApi(...args).then(handleResponse), ["bot", "GET", {}] as const,
(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 }) { function CounterDigit(props: { value: number; transitionDurationMs?: number | undefined }) {
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; fractionDigits?: number | undefined;
transitionDurationMs?: number; transitionDurationMs?: number | undefined;
className?: string; className?: string | undefined;
postfix?: string; postfix?: string | undefined;
}) { }) {
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" className="dialog animate-pop-in backdrop-animate-fade-in"
ref={createWorkerModalRef} ref={createWorkerModalRef}
> >
<form <form
@ -159,7 +159,7 @@ export function WorkersPage(props: { sessionId?: string }) {
); );
} }
function WorkerListItem(props: { worker: WorkerData; sessionId?: string }) { function WorkerListItem(props: { worker: WorkerData; sessionId: string | undefined }) {
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 }) {
)} )}
</p> </p>
<dialog <dialog
className="dialog" className="dialog animate-pop-in backdrop-animate-fade-in"
ref={editWorkerModalRef} ref={editWorkerModalRef}
> >
<form <form
@ -293,7 +293,7 @@ function WorkerListItem(props: { worker: WorkerData; sessionId?: string }) {
</form> </form>
</dialog> </dialog>
<dialog <dialog
className="dialog" className="dialog animate-pop-in backdrop-animate-fade-in"
ref={deleteWorkerModalRef} ref={deleteWorkerModalRef}
> >
<form <form

View File

@ -27,14 +27,11 @@ 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-stripes-scroll 0.5s linear infinite; animation: bg-scroll 0.5s linear infinite;
background-size: 40px 40px; background-size: 40px 40px;
} }
@keyframes bg-stripes-scroll { @keyframes bg-scroll {
0% { to {
background-position: 0 40px;
}
100% {
background-position: 40px 0; background-position: 40px 0;
} }
} }
@ -63,11 +60,11 @@ injectGlobal`
opacity 0s; opacity 0s;
} }
.animate-fade-in { .backdrop-animate-fade-in::backdrop {
animation: fade-in 0.3s ease-out forwards; animation: fade-in 0.3s ease-out forwards;
} }
@keyframes fade-in { @keyframes fade-in {
0% { from {
opacity: 0; opacity: 0;
} }
} }
@ -76,13 +73,13 @@ 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 {
0% { from {
transform: scale(0.8); transform: scale(0.8);
opacity: 0; opacity: 0;
} }
} }
} }
@layer components { @layer components {
.link { .link {
@apply text-sky-600 dark:text-sky-500 rounded-sm focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-sky-600 dark:focus:outline-sky-500; @apply text-sky-600 dark:text-sky-500 rounded-sm focus:outline focus:outline-2 focus:outline-offset-1 focus:outline-sky-600 dark:focus:outline-sky-500;
@ -134,8 +131,8 @@ injectGlobal`
} }
.dialog { .dialog {
@apply animate-pop-in backdrop:animate-fade-in overflow-hidden overflow-y-auto rounded-md @apply overflow-hidden overflow-y-auto rounded-md
bg-zinc-100 text-zinc-900 shadow-lg backdrop:bg-black/20 dark:bg-zinc-800 dark:text-zinc-100; bg-zinc-100 text-zinc-900 shadow-lg backdrop:bg-black/30 dark:bg-zinc-800 dark:text-zinc-100;
} }
} }
`; `;

View File

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

11
utils/omitUndef.ts Normal file
View File

@ -0,0 +1,11 @@
/**
* 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;
}