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

View File

@@ -0,0 +1,196 @@
import React, { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { listPresets, savePreset, getPreset, loadPreset, deletePreset, PresetMeta } from '../api/client'
export default function PresetsPage() {
const qc = useQueryClient()
const [newName, setNewName] = useState('')
const [newDescription, setNewDescription] = useState('')
const [savingError, setSavingError] = useState('')
const [expanded, setExpanded] = useState<string | null>(null)
const [message, setMessage] = useState<string | null>(null)
const { data, isLoading } = useQuery({ queryKey: ['presets'], queryFn: listPresets })
const { data: presetDetail } = useQuery({
queryKey: ['preset', expanded],
queryFn: () => getPreset(expanded!),
enabled: !!expanded,
})
const saveMut = useMutation({
mutationFn: ({ name, description }: { name: string; description: string }) =>
savePreset(name, description || undefined),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['presets'] })
setNewName('')
setNewDescription('')
setSavingError('')
setMessage('Preset saved.')
},
onError: (err) => setSavingError(err instanceof Error ? err.message : String(err)),
})
const loadMut = useMutation({
mutationFn: (name: string) => loadPreset(name),
onSuccess: (_, name) => {
qc.invalidateQueries({ queryKey: ['state'] })
qc.invalidateQueries({ queryKey: ['workflowInputs'] })
setMessage(`Loaded preset: ${name}`)
},
})
const deleteMut = useMutation({
mutationFn: (name: string) => deletePreset(name),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['presets'] })
setMessage('Preset deleted.')
},
})
return (
<div className="max-w-xl mx-auto space-y-6">
<h1 className="text-xl font-bold text-gray-800 dark:text-gray-100">Presets</h1>
{message && (
<div className="text-sm text-blue-600 dark:text-blue-400">{message}</div>
)}
{/* Save current state */}
<div className="space-y-2">
<div className="flex flex-col sm:flex-row gap-2">
<input
type="text"
value={newName}
onChange={e => setNewName(e.target.value)}
placeholder="Preset name"
className="flex-1 border border-gray-300 dark:border-gray-600 rounded px-3 py-2 text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
onClick={() => { setSavingError(''); saveMut.mutate({ name: newName, description: newDescription }) }}
disabled={!newName.trim() || saveMut.isPending}
className="bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded px-3 py-2 text-sm font-medium"
>
Save current state
</button>
</div>
<input
type="text"
value={newDescription}
onChange={e => setNewDescription(e.target.value)}
placeholder="Description (optional)"
className="w-full border border-gray-300 dark:border-gray-600 rounded px-3 py-2 text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{savingError && <p className="text-red-500 text-sm">{savingError}</p>}
{/* Preset list */}
{isLoading ? (
<div className="text-sm text-gray-400">Loading presets</div>
) : (data?.presets ?? []).length === 0 ? (
<p className="text-sm text-gray-400">No presets saved yet.</p>
) : (
<ul className="divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded">
{(data?.presets ?? []).map((preset: PresetMeta) => (
<li key={preset.name} className="px-3 py-2 space-y-1">
<div className="flex items-center justify-between">
<button
onClick={() => setExpanded(expanded === preset.name ? null : preset.name)}
className="text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 text-left"
>
{preset.name}
{preset.owner && (
<span className="ml-2 text-xs text-gray-400 dark:text-gray-500 font-normal">
{preset.owner}
</span>
)}
</button>
<div className="flex gap-2">
<button
onClick={() => { setMessage(null); loadMut.mutate(preset.name) }}
disabled={loadMut.isPending}
className="text-xs bg-green-600 hover:bg-green-700 disabled:opacity-50 text-white rounded px-2 py-1"
>
Load
</button>
<button
onClick={() => { setMessage(null); deleteMut.mutate(preset.name) }}
className="text-xs bg-red-600 hover:bg-red-700 text-white rounded px-2 py-1"
>
Delete
</button>
</div>
</div>
{preset.description && (
<p className="text-xs italic text-gray-400 dark:text-gray-500">{preset.description}</p>
)}
{expanded === preset.name && presetDetail && (
<PresetDetail data={presetDetail} />
)}
</li>
))}
</ul>
)}
</div>
)
}
function PresetDetail({ data }: { data: Record<string, unknown> }) {
const state = (data.state ?? {}) as Record<string, unknown>
const { prompt, negative_prompt, seed, ...otherOverrides } = state
const hasOther = Object.keys(otherOverrides).length > 0
return (
<div className="mt-2 space-y-2 text-xs">
{prompt != null && (
<div>
<span className="font-semibold text-gray-600 dark:text-gray-400">Prompt</span>
<p className="mt-0.5 bg-gray-50 dark:bg-gray-800 rounded p-2 text-gray-700 dark:text-gray-300 whitespace-pre-wrap break-words">
{String(prompt)}
</p>
</div>
)}
{negative_prompt != null && (
<div>
<span className="font-semibold text-gray-600 dark:text-gray-400">Negative prompt</span>
<p className="mt-0.5 bg-gray-50 dark:bg-gray-800 rounded p-2 text-gray-700 dark:text-gray-300 whitespace-pre-wrap break-words">
{String(negative_prompt)}
</p>
</div>
)}
{seed != null && (
<div className="flex gap-2 items-baseline">
<span className="font-semibold text-gray-600 dark:text-gray-400">Seed</span>
<span className="font-mono text-gray-700 dark:text-gray-300">
{String(seed)}
{seed === -1 && <span className="ml-1 text-gray-400">(random)</span>}
</span>
</div>
)}
{hasOther && (
<div>
<span className="font-semibold text-gray-600 dark:text-gray-400">Other overrides</span>
<table className="mt-0.5 w-full text-xs border-collapse">
<tbody>
{Object.entries(otherOverrides).map(([k, v]) => (
<tr key={k} className="border-b border-gray-100 dark:border-gray-700">
<td className="py-0.5 pr-3 font-mono text-gray-500 dark:text-gray-400 whitespace-nowrap">{k}</td>
<td className="py-0.5 text-gray-700 dark:text-gray-300 break-all">{JSON.stringify(v)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{!!data.workflow && (
<div className="text-green-600 dark:text-green-400">Includes workflow template</div>
)}
<details>
<summary className="cursor-pointer text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">Raw JSON</summary>
<pre className="mt-1 text-xs bg-gray-100 dark:bg-gray-800 rounded p-2 overflow-auto max-h-48 text-gray-600 dark:text-gray-400">
{JSON.stringify(data, null, 2)}
</pre>
</details>
</div>
)
}