Initial commit — ComfyUI Discord bot + web UI

Full source for the-third-rev: Discord bot (discord.py), FastAPI web UI
(React/TS/Vite/Tailwind), ComfyUI integration, generation history DB,
preset manager, workflow inspector, and all supporting modules.

Excluded from tracking: .env, invite_tokens.json, *.db (SQLite),
current-workflow-changes.json, user_settings/, presets/, logs/,
web-static/ (build output), frontend/node_modules/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Khoa (Revenovich) Tran Gia
2026-03-02 09:55:48 +07:00
commit 1ed3c9ec4b
82 changed files with 20693 additions and 0 deletions

169
commands/history.py Normal file
View File

@@ -0,0 +1,169 @@
"""
commands/history.py
===================
History management commands for the Discord ComfyUI bot.
This module contains commands for viewing and retrieving past generation
results from the bot's history.
"""
from __future__ import annotations
import logging
from io import BytesIO
from typing import Optional
import discord
from discord.ext import commands
from config import MAX_IMAGES_PER_RESPONSE
from discord_utils import require_comfy_client, truncate_text, convert_image_bytes_to_discord_files
logger = logging.getLogger(__name__)
def setup_history_commands(bot):
"""
Register history management commands with the bot.
Parameters
----------
bot : commands.Bot
The Discord bot instance.
"""
@bot.command(name="history", extras={"category": "History"})
@require_comfy_client
async def history_command(ctx: commands.Context) -> None:
"""
Show a list of recently generated prompts.
The bot keeps a rolling history of the last few generations. Each
entry lists the prompt id along with the positive and negative
prompt texts. You can retrieve the images from a previous
generation with the ``ttr!gethistory <prompt_id>`` command.
"""
hist = bot.comfy.get_history()
if not hist:
await ctx.reply(
"No history available yet. Generate something first!",
mention_author=False,
)
return
# Build a human readable list
lines = ["Here are the most recent generations (oldest first):"]
for entry in hist:
pid = entry.get("prompt_id", "unknown")
prompt = entry.get("prompt") or ""
neg = entry.get("negative_prompt") or ""
# Truncate long prompts for readability
lines.append(
f"• ID: {pid} | prompt: '{truncate_text(prompt, 60)}' | negative: '{truncate_text(neg, 60)}'"
)
await ctx.reply("\n".join(lines), mention_author=False)
@bot.command(name="get-history", aliases=["gethistory", "gh"], extras={"category": "History"})
@require_comfy_client
async def get_history_command(ctx: commands.Context, *, arg: str = "") -> None:
"""
Retrieve images from a previous generation, or search history by keyword.
Usage:
ttr!gethistory <prompt_id_or_index>
ttr!gethistory search:<keyword>
Provide either the prompt id returned in the generation response
(shown in `ttr!history`) or the 1based index into the history
list. The bot will fetch the images associated with that
generation and resend them. If no images are found, you will be
notified.
Use ``search:<keyword>`` to filter history by prompt text, checkpoint
name, seed value, or any other override field.
"""
if not arg:
await ctx.reply(
"Please provide a prompt id, history index, or `search:<keyword>`. See `ttr!history` for a list.",
mention_author=False,
)
return
# Handle search:<keyword>
lower_arg = arg.lower()
if lower_arg.startswith("search:"):
keyword = arg[len("search:"):].strip()
if not keyword:
await ctx.reply("Please provide a keyword after `search:`.", mention_author=False)
return
from generation_db import search_history_for_user, get_history as db_get_history
# Use get_history for Discord since Discord bot doesn't have per-user context like the web UI
hist = db_get_history(limit=50)
matches = [
e for e in hist
if keyword.lower() in str(e.get("overrides", {})).lower()
]
if not matches:
await ctx.reply(f"No history entries matching `{keyword}`.", mention_author=False)
return
lines = [f"**History matching `{keyword}`** ({len(matches)} result(s))"]
for entry in matches[:10]:
pid = entry.get("prompt_id", "unknown")
overrides = entry.get("overrides") or {}
prompt = str(overrides.get("prompt") or "")
lines.append(
f"• `{pid[:12]}…` | {truncate_text(prompt, 60) if prompt else '(no prompt)'}"
)
if len(matches) > 10:
lines.append(f"_(showing first 10 of {len(matches)})_")
await ctx.reply("\n".join(lines), mention_author=False)
return
# Determine whether arg refers to an index or an id
target_id: Optional[str] = None
hist = bot.comfy.get_history()
# If arg is a digit, interpret as 1based index
if arg.isdigit():
idx = int(arg) - 1
if idx < 0 or idx >= len(hist):
await ctx.reply(
f"Index out of range. There are {len(hist)} entries in history.",
mention_author=False,
)
return
target_id = hist[idx]["prompt_id"]
else:
# Otherwise treat as an explicit prompt id
target_id = arg.strip()
try:
images = await bot.comfy.fetch_history_images(target_id)
if not images:
await ctx.reply(
f"No images found for prompt id `{target_id}`.",
mention_author=False,
)
return
files = []
for idx, img_bytes in enumerate(images):
if idx >= MAX_IMAGES_PER_RESPONSE:
break
file_obj = BytesIO(img_bytes)
file_obj.seek(0)
files.append(discord.File(file_obj, filename=f"history_{target_id}_{idx+1}.png"))
await ctx.reply(
content=f"Here are the images for prompt id `{target_id}`:",
files=files,
mention_author=False,
)
except Exception as exc:
logger.exception("Failed to fetch history for %s", target_id)
await ctx.reply(
f"An error occurred: {type(exc).__name__}: {exc}",
mention_author=False,
)