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:
196
frontend/src/pages/PresetsPage.tsx
Normal file
196
frontend/src/pages/PresetsPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user