Compare commits
25 Commits
Author | SHA1 | Date |
---|---|---|
Akiru | 51849e73cb | |
Akiru | 9c607dd6c5 | |
Akiru | 555272fbdd | |
Akiru | d266b8e342 | |
Akiru | e45ad93229 | |
Akiru | ed40a902ef | |
Akiru | 8930544485 | |
Akiru | b512e2e97c | |
Akiru | 898ed1b2db | |
Akiru | 45a5fe98b9 | |
Akiru | 1e2d402c3b | |
Akiru | 14e2e71ee1 | |
Akiru | 2ae1f3b9f1 | |
Akiru | eaff7c0772 | |
Akiru | 9f75621c70 | |
Akiru | b7a551afc7 | |
Akiru | c19f8c8f71 | |
Akiru | a7e41b63df | |
Akiru | 04b0f9bdf5 | |
Akiru | 29161fa58a | |
Akiru | 128c4312d4 | |
Akiru | 85bd9b5861 | |
Akiru | aefbc63a46 | |
Akiru | ffff9c87ef | |
Akiru | d9ff25e04e |
27
README.md
27
README.md
|
@ -1,17 +1,12 @@
|
|||
# Eris the Bot
|
||||
|
||||
[![Website](https://img.shields.io/website?url=https%3A%2F%2Feris.lisq.eu%2F)](https://eris.lisq.eu/)
|
||||
![Unique users](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Feris.lisq.eu%2Fapi%2Fstats&query=%24.userCount&label=unique%20users)
|
||||
![Generated images](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Feris.lisq.eu%2Fapi%2Fstats&query=%24.imageCount&label=images%20generated)
|
||||
![Processed steps](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Feris.lisq.eu%2Fapi%2Fstats&query=%24.stepCount&label=steps%20processed)
|
||||
![Painted pixels](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Feris.lisq.eu%2Fapi%2Fstats&query=%24.pixelCount&label=pixels%20painted)
|
||||
# Nyx the Bot
|
||||
Fork of Eris the Bot https://eris.lisq.eu
|
||||
|
||||
Telegram bot for generating images from text.
|
||||
|
||||
## Requirements
|
||||
|
||||
- [Deno](https://deno.land/)
|
||||
- [Stable Diffusion WebUI](https://github.com/AUTOMATIC1111/stable-diffusion-webui/)
|
||||
- [Deno](https://deno.land/) (for the bot server)
|
||||
- [Stable Diffusion WebUI](https://github.com/AUTOMATIC1111/stable-diffusion-webui/) (for the worker that generates images)
|
||||
|
||||
## Options
|
||||
|
||||
|
@ -20,17 +15,23 @@ You can put these in `.env` file or pass them as environment variables.
|
|||
- `TG_BOT_TOKEN` - Telegram bot token. Get yours from [@BotFather](https://t.me/BotFather).
|
||||
Required.
|
||||
- `DENO_KV_PATH` - [Deno KV](https://deno.land/api?s=Deno.openKv&unstable) database file path. A
|
||||
temporary file is used by default.
|
||||
temporary file is used by default. Example: /opt/data/botdata.kv
|
||||
- `LOG_LEVEL` - [Log level](https://deno.land/std@0.201.0/log/mod.ts?s=LogLevels). Default: `INFO`.
|
||||
|
||||
## Running
|
||||
|
||||
1. Start Eris: `deno task start`
|
||||
2. Visit [Eris WebUI](http://localhost:5999/) and login via Telegram.
|
||||
2. Visit [Eris WebUI](http://localhost:8443/) and login via Telegram.
|
||||
3. Promote yourself to admin in the Eris WebUI.
|
||||
4. Start Stable Diffusion WebUI: `./webui.sh --api` (in SD WebUI directory)
|
||||
5. Add a new worker in the Eris WebUI.
|
||||
|
||||
## This fork requires the use of webhooks.
|
||||
|
||||
1. You need a reverse Proxy and HTTPS certificate set up that proxies all requests from a domain on port 443 (e.g. nyx.akiru.de) to the backend of this bot (:8443).
|
||||
2. Change the Webhook URL to your own one in /mod/bot.ts - There are also console log statements you can uncomment for troubleshooting.
|
||||
3. Make sure the DNS and firewall are set up for the webhook to be reachable, because otherwise the bot fails to start.
|
||||
|
||||
## Codegen
|
||||
|
||||
The Stable Diffusion API types are auto-generated. To regenerate them, first start your SD WebUI
|
||||
|
@ -38,8 +39,8 @@ with `--nowebui --api`, and then run `deno task generate`
|
|||
|
||||
## Project structure
|
||||
|
||||
- `/api` - Eris API served at `http://localhost:5999/api/`.
|
||||
- `/api` - Eris API served at `http://localhost:8443/api/`.
|
||||
- `/app` - Queue handling and other core processes.
|
||||
- `/bot` - Handling bot commands and other updates from Telegram API.
|
||||
- `/ui` - Eris WebUI frontend files served at `http://localhost:5999/`.
|
||||
- `/ui` - Eris WebUI frontend files served at `http://localhost:8443/`.
|
||||
- `/util` - Utility functions shared by other parts.
|
||||
|
|
|
@ -2,11 +2,15 @@ import { route } from "reroute";
|
|||
import { serveSpa } from "serve_spa";
|
||||
import { api } from "./serveApi.ts";
|
||||
import { fromFileUrl } from "std/path/mod.ts";
|
||||
// New function that handles the webhook
|
||||
import { handleWebhook } from "../bot/mod.ts";
|
||||
|
||||
|
||||
export async function serveUi() {
|
||||
const server = Deno.serve({ port: 5999 }, (request) =>
|
||||
const server = Deno.serve({ port: 8443 }, (request) =>
|
||||
route(request, {
|
||||
"/api/*": (request) => api.fetch(request),
|
||||
"/webhook": handleWebhook, // Create the webhook route handle
|
||||
"/*": (request) =>
|
||||
serveSpa(request, {
|
||||
fsRoot: fromFileUrl(new URL("../ui/", import.meta.url)),
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import { ErisContext } from "./mod.ts";
|
||||
import { omitUndef } from "../utils/omitUndef.ts";
|
||||
import { botDescription } from "./mod.ts"; // Import the description
|
||||
|
||||
export async function helpCommand(ctx: ErisContext) {
|
||||
await ctx.reply(
|
||||
botDescription, // Send the stored description
|
||||
omitUndef({
|
||||
reply_to_message_id: ctx.message?.message_id,
|
||||
allow_sending_without_reply: true,
|
||||
disable_web_page_preview: true, // Disable link previews
|
||||
parse_mode: "Markdown"
|
||||
}),
|
||||
);
|
||||
}
|
100
bot/mod.ts
100
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 { run, sequentialize } from "grammy_runner";
|
||||
import { sequentialize } from "grammy_runner";
|
||||
import { error, info, warning } from "std/log/mod.ts";
|
||||
import { sessions } from "../api/sessionsRoute.ts";
|
||||
import { formatUserChat } from "../utils/formatUserChat.ts";
|
||||
|
@ -12,6 +12,17 @@ import { img2imgCommand, img2imgQuestion } from "./img2imgCommand.ts";
|
|||
import { pnginfoCommand, pnginfoQuestion } from "./pnginfoCommand.ts";
|
||||
import { queueCommand } from "./queueCommand.ts";
|
||||
import { txt2imgCommand, txt2imgQuestion } from "./txt2imgCommand.ts";
|
||||
import { helpCommand } from "./helpCommand.ts";
|
||||
import { setConfig, getConfig } from "../app/config.ts";
|
||||
|
||||
// Set the new configuration
|
||||
await setConfig({ maxUserJobs: 1, maxJobs: 500 });
|
||||
|
||||
// Fetch the updated configuration
|
||||
const updatedConfig = await getConfig();
|
||||
|
||||
// Log the updated configuration to the console
|
||||
console.log("Updated Configuration:", updatedConfig);
|
||||
|
||||
interface SessionData {
|
||||
chat: ErisChatData;
|
||||
|
@ -107,18 +118,64 @@ bot.use(async (ctx, next) => {
|
|||
}
|
||||
});
|
||||
|
||||
bot.api.setMyShortDescription("I can generate furry images from text");
|
||||
bot.api.setMyDescription(
|
||||
"I can generate furry images from text. " +
|
||||
"Send /txt2img to generate an image.",
|
||||
);
|
||||
bot.api.setMyCommands([
|
||||
// Declare the bot description variable
|
||||
export const botDescription = `I can generate furry images from text.
|
||||
\`/txt2img Nyx\`
|
||||
|
||||
If you want landscape or portrait:
|
||||
\`/txt2img Nyx, Size: 576x832\`
|
||||
\`/txt2img Nyx, Size: 832x576\`
|
||||
|
||||
This bot uses the following model and will render nsfw and sfw:
|
||||
\`EasyFluffV11.2.safetensors [821628644e]\`
|
||||
|
||||
There is no loRA support.
|
||||
|
||||
Please read about our terms of use:
|
||||
https://nyx.akiru.de/disclaimer
|
||||
|
||||
You can support Nyx by sending her a coffee :3
|
||||
ko-fi.com/nyxthebot`;
|
||||
|
||||
// Short description constant
|
||||
const botShortDescription = `Generate furry images from text.
|
||||
Use /help for more information.
|
||||
https://ko-fi.com/nyxthebot
|
||||
https://nyx.akiru.de`;
|
||||
|
||||
// Command descriptions
|
||||
const botCommands = [
|
||||
{ command: "txt2img", description: "Generate image from text" },
|
||||
{ command: "img2img", description: "Generate image from image" },
|
||||
{ command: "pnginfo", description: "Try to extract prompt from raw file" },
|
||||
{ command: "queue", description: "Show the current queue" },
|
||||
{ command: "cancel", description: "Cancel all your requests" },
|
||||
]);
|
||||
{ command: "help", description: "Show bot description" },
|
||||
];
|
||||
|
||||
// Wrap the calls in try-catch for error handling
|
||||
async function setupBotCommands() {
|
||||
try {
|
||||
await bot.api.setMyShortDescription(botShortDescription);
|
||||
} catch (err) {
|
||||
error(`Failed to set short description: ${err.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await bot.api.setMyDescription(botDescription);
|
||||
} catch (err) {
|
||||
error(`Failed to set description: ${err.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await bot.api.setMyCommands(botCommands);
|
||||
} catch (err) {
|
||||
error(`Failed to set commands: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Call the setup function
|
||||
setupBotCommands();
|
||||
|
||||
bot.command("start", async (ctx) => {
|
||||
if (ctx.match) {
|
||||
|
@ -167,13 +224,34 @@ bot.command("queue", queueCommand);
|
|||
|
||||
bot.command("cancel", cancelCommand);
|
||||
|
||||
bot.command("help", helpCommand);
|
||||
|
||||
bot.command("broadcast", broadcastCommand);
|
||||
|
||||
bot.command("crash", () => {
|
||||
throw new Error("Crash command used");
|
||||
});
|
||||
|
||||
export async function runBot() {
|
||||
const runner = run(bot, { runner: { silent: true } });
|
||||
await runner.task();
|
||||
// Set up the webhook in the telegram API and initialize the bot
|
||||
await bot.api.setWebhook('https://nyx.akiru.de/webhook');
|
||||
await bot.init();
|
||||
|
||||
// Function to handle incoming webhook requests
|
||||
export async function handleWebhook(req: Request): Promise<Response> {
|
||||
try {
|
||||
const body = await req.json();
|
||||
// console.log("Received webhook data:", JSON.stringify(body, null, 2));
|
||||
|
||||
// Log before processing update
|
||||
// console.log("Processing update through handleUpdate...");
|
||||
await bot.handleUpdate(body); // Process the update
|
||||
// Log after processing update
|
||||
// console.log("Update processed successfully.");
|
||||
|
||||
return new Response(JSON.stringify({ status: 'ok' }), { headers: { 'Content-Type': 'application/json' } });
|
||||
} catch (error) {
|
||||
// Detailed error logging
|
||||
console.error("Error in handleWebhook:", error);
|
||||
return new Response("Error", { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
7
main.ts
7
main.ts
|
@ -4,7 +4,9 @@ import { ConsoleHandler } from "std/log/handlers.ts";
|
|||
import { LevelName, setup } from "std/log/mod.ts";
|
||||
import { serveUi } from "./api/mod.ts";
|
||||
import { runAllTasks } from "./app/mod.ts";
|
||||
import { runBot } from "./bot/mod.ts";
|
||||
// runbot shouldn't be needed for webhooks?
|
||||
// import { runBot } from "./bot/mod.ts";
|
||||
import "./bot/mod.ts";
|
||||
|
||||
const logLevel = Deno.env.get("LOG_LEVEL")?.toUpperCase() as LevelName ?? "INFO";
|
||||
|
||||
|
@ -20,7 +22,8 @@ setup({
|
|||
|
||||
// run parts of the app
|
||||
await Promise.all([
|
||||
runBot(),
|
||||
// runbot shouldn't be needed for webhooks?
|
||||
// runBot(),
|
||||
runAllTasks(),
|
||||
serveUi(),
|
||||
]);
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
These scripts exist to automatically restart the bot in case of errors and connectivity issues. They should either be run as systemd services or cronjobs.
|
|
@ -0,0 +1,29 @@
|
|||
#!/bin/bash
|
||||
|
||||
# This script has to be run as systemd service. It checks journalctl output for errors and restarts the bot automatically when they occur too often.
|
||||
|
||||
# Define the regex pattern to search for occurrences of "GrammyError" and "sendMediaGroup"
|
||||
ERROR_PATTERN="GrammyError.*sendMediaGroup"
|
||||
|
||||
# Initialize the counter for consecutive occurrences
|
||||
error_count=0
|
||||
|
||||
# Monitor the journalctl output
|
||||
journalctl -xe -f | while read line; do
|
||||
# Check if the line contains the pattern
|
||||
if echo "$line" | grep -qE "$ERROR_PATTERN"; then
|
||||
# Increment the error counter
|
||||
((error_count++))
|
||||
|
||||
# Check if the error has occurred 4 times in a row
|
||||
if [ $error_count -eq 4 ]; then
|
||||
# Restart the bot service
|
||||
systemctl restart nyxthebot
|
||||
# Reset the error counter
|
||||
error_count=0
|
||||
fi
|
||||
else
|
||||
# Reset the error counter if the line does not contain the error
|
||||
error_count=0
|
||||
fi
|
||||
done
|
|
@ -0,0 +1,30 @@
|
|||
#!/bin/bash
|
||||
|
||||
# When the bot gets eepy sleepy, this script will restart it. It happens, when no jobs come in for multiple minutes.
|
||||
# The script interfaces with the REST API of the bot and checks if the image count has changed.
|
||||
# Run this script as cronjob all 4-5 minutes.
|
||||
|
||||
# Define the URL and the stats file path
|
||||
URL="https://YOUR_URL/api/stats"
|
||||
STATS_FILE="YOUR_FILE_PATH/stats.txt"
|
||||
|
||||
# Fetch the data and extract imageCount
|
||||
imageCount=$(curl -s "$URL" | jq '.imageCount')
|
||||
|
||||
# Check if stats file exists and read the last value
|
||||
if [ -f "$STATS_FILE" ]; then
|
||||
lastValue=$(tail -n 1 "$STATS_FILE")
|
||||
else
|
||||
lastValue=""
|
||||
fi
|
||||
|
||||
# Save the new value to the file
|
||||
echo "$imageCount" >> "$STATS_FILE"
|
||||
|
||||
# Keep only the last 2 values in the file
|
||||
tail -n 2 "$STATS_FILE" > "$STATS_FILE.tmp" && mv "$STATS_FILE.tmp" "$STATS_FILE"
|
||||
|
||||
# Compare the two values and restart the service if they are the same
|
||||
if [ "$lastValue" == "$imageCount" ]; then
|
||||
systemctl restart nyxthebot
|
||||
fi
|
|
@ -8,6 +8,7 @@ import { StatsPage } from "./StatsPage.tsx";
|
|||
import { WorkersPage } from "./WorkersPage.tsx";
|
||||
import { fetchApi, handleResponse } from "./apiClient.ts";
|
||||
import { useLocalStorage } from "./useLocalStorage.ts";
|
||||
import { DisclaimerPage } from "./DisclaimerPage.tsx";
|
||||
|
||||
export function App() {
|
||||
// store session ID in the local storage
|
||||
|
@ -39,6 +40,7 @@ export function App() {
|
|||
<Route path="/workers" element={<WorkersPage sessionId={sessionId} />} />
|
||||
<Route path="/queue" element={<QueuePage />} />
|
||||
<Route path="/settings" element={<SettingsPage sessionId={sessionId} />} />
|
||||
<Route path="/disclaimer" element={<DisclaimerPage />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -73,21 +73,12 @@ export function AppHeader(props: {
|
|||
|
||||
{/* tabs */}
|
||||
<nav className="flex-grow self-stretch flex items-stretch justify-center gap-2">
|
||||
<NavTab to="/">
|
||||
Stats
|
||||
</NavTab>
|
||||
<NavTab to="/admins">
|
||||
Admins
|
||||
</NavTab>
|
||||
<NavTab to="/workers">
|
||||
Workers
|
||||
</NavTab>
|
||||
<NavTab to="/queue">
|
||||
Queue
|
||||
</NavTab>
|
||||
<NavTab to="/settings">
|
||||
Settings
|
||||
</NavTab>
|
||||
<NavTab to="/">Stats</NavTab>
|
||||
<NavTab to="/admins">Admins</NavTab>
|
||||
<NavTab to="/workers">Workers</NavTab>
|
||||
<NavTab to="/queue">Queue</NavTab>
|
||||
<NavTab to="/settings">Settings</NavTab>
|
||||
<NavTab to="/disclaimer">Disclaimer</NavTab>
|
||||
</nav>
|
||||
|
||||
{/* loading indicator */}
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
import React from "react";
|
||||
|
||||
export function DisclaimerPage() {
|
||||
return (
|
||||
<div className="my-16 flex flex-col gap-8 text-zinc-600 dark:text-zinc-400">
|
||||
<h2 className="text-center text-3xl font-bold">Service Agreement and Disclaimer</h2>
|
||||
|
||||
{ /* Each section is now enclosed in a styled block */ }
|
||||
<section className="bg-zinc-100 dark:bg-zinc-700 p-4 rounded-md shadow-sm">
|
||||
<h3 className="text-2xl font-semibold mb-4">About the Service</h3>
|
||||
<p className="text-xl">
|
||||
This website and <a href="https://t.me/NyxTheBot" className="text-blue-600 hover:text-blue-800">@NyxTheBot</a> are privately hosted services, operated as a non-commercial hobby project. They offer a service for generating images from text inputs using the Stable Diffusion model via a Telegram bot <a href="https://t.me/NyxTheBot" className="text-blue-600 hover:text-blue-800">@NyxTheBot</a>. This service is provided free of charge, without any commercial intent, and relies solely on voluntary tips for its operating costs. You can support this project with a tip at <a href="https://ko-fi.com/nyxthebot" className="text-blue-600 hover:text-blue-800">ko-fi.com/nyxthebot</a>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="bg-zinc-100 dark:bg-zinc-700 p-4 rounded-md shadow-sm">
|
||||
<h3 className="text-2xl font-semibold mb-4">Disclaimer of Warranties and Limitation of Liability</h3>
|
||||
<p className="text-xl">
|
||||
THIS WEBSITE AND ASSOCIATED SERVICES ARE PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE AND SERVICE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="bg-zinc-100 dark:bg-zinc-700 p-4 rounded-md shadow-sm">
|
||||
<h3 className="text-2xl font-semibold mb-4">Open Source</h3>
|
||||
<p className="text-xl mb-2">
|
||||
This service is open source. The source code can be explored and utilized for your own projects.
|
||||
</p>
|
||||
<ul className="list-disc pl-6">
|
||||
<li className="text-xl">
|
||||
Find the source code at <a href="https://git.foxo.me/Akiru/nyx" className="text-blue-600 hover:text-blue-800">git.foxo.me/Akiru/nyx</a>
|
||||
</li>
|
||||
<li className="text-xl">
|
||||
Originally forked from <a href="https://git.foxo.me/pinks/eris" className="text-blue-600 hover:text-blue-800">git.foxo.me/pinks/eris</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="bg-zinc-100 dark:bg-zinc-700 p-4 rounded-md shadow-sm">
|
||||
<h3 className="text-2xl font-semibold mb-4">User Responsibilities</h3>
|
||||
<p className="text-xl">
|
||||
Users are solely responsible for the inputs they provide and the resulting outputs. Users must ensure that their use of the service complies with all applicable laws and regulations. The service should not be used for generating content that is considered illegal or harmful in applicable jurisdictions or interpretations of law. Content generated by this service is fictitious, generated by a randomized computing process, and any similarity to existing persons, objects, entities or situations is unintentional, regardless of input by the user.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="bg-zinc-100 dark:bg-zinc-700 p-4 rounded-md shadow-sm">
|
||||
<h3 className="text-2xl font-semibold mb-4">Data Handling and Privacy</h3>
|
||||
<p className="text-xl">
|
||||
In the course of using this service, specific data may be collected and stored. This includes associating your Telegram username with the prompts you provide to the bot and the resulting images generated. Our collection and use of this data is solely for the purpose of providing and improving the service. We are committed to handling your data responsibly and safely.
|
||||
</p>
|
||||
<p className="text-xl">
|
||||
Please be aware that your interactions with the bot, including the prompts you submit and the images generated, are logged and may be stored for service optimization, troubleshooting, and enhancing user experience. We endeavor to protect the privacy and integrity of your data, and ensure that it is used only for the intended purposes of this service. However, since the service integrates with third parties (i.e. Telegram and potentially services local to you for data transport and storage), your use will also be subject to those third parties' data handling and privacy policies that are not within our scope.
|
||||
</p>
|
||||
<p className="text-xl">
|
||||
We respect your privacy and are dedicated to safeguarding your personal information. If you have any concerns or questions about how your data is handled, please feel free to contact us.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="bg-zinc-100 dark:bg-zinc-700 p-4 rounded-md shadow-sm">
|
||||
<h3 className="text-2xl font-semibold mb-4">External Links</h3>
|
||||
<p className="text-xl">
|
||||
This website may contain links to external websites that are not operated by us. We have no control over the content and practices of these sites and cannot accept responsibility or liability for their respective privacy policies.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="bg-zinc-100 dark:bg-zinc-700 p-4 rounded-md shadow-sm">
|
||||
<h3 className="text-2xl font-semibold mb-4">Amendments to the Agreement</h3>
|
||||
<p className="text-xl">
|
||||
The host reserves the right to make changes to this service agreement at any time without notice.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -7,7 +7,7 @@ export function StatsPage() {
|
|||
const getGlobalStats = useSWR(
|
||||
["/stats", {}] as const,
|
||||
(args) => fetchApi(...args).then(handleResponse),
|
||||
{ refreshInterval: 2_000 },
|
||||
{ refreshInterval: 1_00 },
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -18,13 +18,13 @@ export function StatsPage() {
|
|||
className="font-bold text-zinc-700 dark:text-zinc-300"
|
||||
value={getGlobalStats.data?.pixelStepCount ?? 0}
|
||||
digits={15}
|
||||
transitionDurationMs={3_000}
|
||||
transitionDurationMs={1_00}
|
||||
/>
|
||||
<Counter
|
||||
className="text-base"
|
||||
value={(getGlobalStats.data?.pixelStepsPerMinute ?? 0) / 60}
|
||||
digits={9}
|
||||
transitionDurationMs={2_000}
|
||||
transitionDurationMs={1_00}
|
||||
postfix="/s"
|
||||
/>
|
||||
</p>
|
||||
|
@ -34,13 +34,13 @@ export function StatsPage() {
|
|||
className="font-bold text-zinc-700 dark:text-zinc-300"
|
||||
value={getGlobalStats.data?.pixelCount ?? 0}
|
||||
digits={15}
|
||||
transitionDurationMs={3_000}
|
||||
transitionDurationMs={1_00}
|
||||
/>
|
||||
<Counter
|
||||
className="text-base"
|
||||
value={(getGlobalStats.data?.pixelsPerMinute ?? 0) / 60}
|
||||
digits={9}
|
||||
transitionDurationMs={2_000}
|
||||
transitionDurationMs={1_00}
|
||||
postfix="/s"
|
||||
/>
|
||||
</p>
|
||||
|
@ -51,14 +51,14 @@ export function StatsPage() {
|
|||
className="font-bold text-zinc-700 dark:text-zinc-300"
|
||||
value={getGlobalStats.data?.stepCount ?? 0}
|
||||
digits={9}
|
||||
transitionDurationMs={3_000}
|
||||
transitionDurationMs={1_00}
|
||||
/>
|
||||
<Counter
|
||||
className="text-base"
|
||||
value={(getGlobalStats.data?.stepsPerMinute ?? 0) / 60}
|
||||
digits={3}
|
||||
fractionDigits={3}
|
||||
transitionDurationMs={2_000}
|
||||
transitionDurationMs={1_00}
|
||||
postfix="/s"
|
||||
/>
|
||||
</p>
|
||||
|
@ -68,14 +68,14 @@ export function StatsPage() {
|
|||
className="font-bold text-zinc-700 dark:text-zinc-300"
|
||||
value={getGlobalStats.data?.imageCount ?? 0}
|
||||
digits={9}
|
||||
transitionDurationMs={3_000}
|
||||
transitionDurationMs={1_00}
|
||||
/>
|
||||
<Counter
|
||||
className="text-base"
|
||||
value={(getGlobalStats.data?.imagesPerMinute ?? 0) / 60}
|
||||
digits={3}
|
||||
fractionDigits={3}
|
||||
transitionDurationMs={2_000}
|
||||
transitionDurationMs={1_00}
|
||||
postfix="/s"
|
||||
/>
|
||||
</p>
|
||||
|
@ -86,7 +86,7 @@ export function StatsPage() {
|
|||
className="font-bold text-zinc-700 dark:text-zinc-300"
|
||||
value={getGlobalStats.data?.userCount ?? 0}
|
||||
digits={6}
|
||||
transitionDurationMs={1_500}
|
||||
transitionDurationMs={1_00}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -5,6 +5,7 @@
|
|||
<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>
|
||||
<script async src="/es-module-shims.js"></script>
|
||||
<title>Eris</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
Loading…
Reference in New Issue