mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-27 11:22:03 +00:00
Merge pull request #52366 from NousResearch/bb/pet-gen-variant-remix
feat(pets): remix a draft into a fresh round
This commit is contained in:
commit
818f03cdd8
8 changed files with 134 additions and 29 deletions
|
|
@ -1,5 +1,7 @@
|
|||
import { PixelEggSprite } from '@/components/pet/pixel-egg-sprite'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Codicon } from '@/components/ui/codicon'
|
||||
import { Tip } from '@/components/ui/tooltip'
|
||||
import { useI18n } from '@/i18n'
|
||||
import { PawPrint } from '@/lib/icons'
|
||||
import { selectableCardClass } from '@/lib/selectable-card'
|
||||
|
|
@ -13,11 +15,21 @@ interface DraftGridProps {
|
|||
hasDrafts: boolean
|
||||
onCancel: () => void
|
||||
onHatch: () => void
|
||||
onRemix: (draft: { index: number; dataUri: string }) => void
|
||||
onSelect: (index: number) => void
|
||||
selected: number | null
|
||||
}
|
||||
|
||||
export function DraftGrid({ drafts, generating, hasDrafts, onCancel, onHatch, onSelect, selected }: DraftGridProps) {
|
||||
export function DraftGrid({
|
||||
drafts,
|
||||
generating,
|
||||
hasDrafts,
|
||||
onCancel,
|
||||
onHatch,
|
||||
onRemix,
|
||||
onSelect,
|
||||
selected
|
||||
}: DraftGridProps) {
|
||||
const { t } = useI18n()
|
||||
const copy = t.commandCenter.generatePet
|
||||
|
||||
|
|
@ -43,32 +55,56 @@ export function DraftGrid({ drafts, generating, hasDrafts, onCancel, onHatch, on
|
|||
const isSelected = draft != null && selected === draft.index
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'relative flex aspect-[192/208] items-center justify-center overflow-hidden',
|
||||
selectableCardClass({ active: isSelected, prominent: true })
|
||||
<div className="group relative aspect-[192/208]" key={draft ? `draft-${draft.index}` : `slot-${i}`}>
|
||||
<button
|
||||
className={cn(
|
||||
'absolute inset-0 flex items-center justify-center overflow-hidden',
|
||||
selectableCardClass({ active: isSelected, prominent: true })
|
||||
)}
|
||||
disabled={draft == null}
|
||||
onClick={() => draft != null && onSelect(draft.index)}
|
||||
type="button"
|
||||
>
|
||||
{draft != null ? (
|
||||
// Hatches into place as each draft streams back.
|
||||
<img
|
||||
alt=""
|
||||
className="pet-reveal size-full object-contain p-1.5"
|
||||
draggable={false}
|
||||
src={draft.dataUri}
|
||||
/>
|
||||
) : (
|
||||
// Incubating: a creme egg bouncing on its contact shadow.
|
||||
<div className="relative z-10 flex flex-col items-center">
|
||||
<PixelEggSprite index={i} mode="bounce" size={48} />
|
||||
<span className="pet-egg-shadow pet-egg-shadow--sm" style={{ marginTop: '-0.3rem' }} />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Remix: branch a new round off this look. Revealed on hover/focus. */}
|
||||
{draft != null && !generating && (
|
||||
<Tip label={copy.remix}>
|
||||
<Button
|
||||
aria-label={copy.remix}
|
||||
className={cn(
|
||||
'absolute right-1 top-1 z-20',
|
||||
'text-(--ui-text-tertiary) opacity-10 transition',
|
||||
'hover:bg-transparent hover:text-foreground focus-visible:opacity-100 group-hover:opacity-100'
|
||||
)}
|
||||
onClick={event => {
|
||||
event.stopPropagation()
|
||||
onRemix(draft)
|
||||
}}
|
||||
size="icon-xs"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<Codicon name="git-branch" size={12} />
|
||||
</Button>
|
||||
</Tip>
|
||||
)}
|
||||
disabled={draft == null}
|
||||
key={draft ? `draft-${draft.index}` : `slot-${i}`}
|
||||
onClick={() => draft != null && onSelect(draft.index)}
|
||||
type="button"
|
||||
>
|
||||
{draft != null ? (
|
||||
// Hatches into place as each draft streams back.
|
||||
<img
|
||||
alt=""
|
||||
className="pet-reveal size-full object-contain p-1.5"
|
||||
draggable={false}
|
||||
src={draft.dataUri}
|
||||
/>
|
||||
) : (
|
||||
// Incubating: a creme egg bouncing on its contact shadow.
|
||||
<div className="relative z-10 flex flex-col items-center">
|
||||
<PixelEggSprite index={i} mode="bounce" size={48} />
|
||||
<span className="pet-egg-shadow pet-egg-shadow--sm" style={{ marginTop: '-0.3rem' }} />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { useStore } from '@nanostores/react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import { useGatewayRequest } from '@/app/gateway/hooks/use-gateway-request'
|
||||
import { SETTINGS_ROUTE } from '@/app/routes'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { ConfirmDialog } from '@/components/ui/confirm-dialog'
|
||||
import { DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { GenerateButton } from '@/components/ui/generate-button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
|
@ -20,6 +21,7 @@ import {
|
|||
$petGenPreview,
|
||||
$petGenRefImage,
|
||||
$petGenRefName,
|
||||
$petGenRemixConfirmed,
|
||||
$petGenSelected,
|
||||
$petGenStage,
|
||||
$petGenStatus,
|
||||
|
|
@ -31,7 +33,8 @@ import {
|
|||
discardDrafts,
|
||||
discardHatched,
|
||||
generateDrafts,
|
||||
hatchSelected
|
||||
hatchSelected,
|
||||
markRemixConfirmed
|
||||
} from '@/store/pet-generate'
|
||||
|
||||
import { DraftGrid } from './components/draft-grid'
|
||||
|
|
@ -68,6 +71,9 @@ export function PetGenerateContent() {
|
|||
const refName = useStore($petGenRefName)
|
||||
const fileRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// The draft awaiting the one-time "remix regenerates" confirmation.
|
||||
const [remixPending, setRemixPending] = useState<{ dataUri: string } | null>(null)
|
||||
|
||||
// Probe backend availability on open — and again whenever the content
|
||||
// remounts (e.g. after returning from the providers settings), so adding a
|
||||
// key flips the setup card to the prompt with no manual refresh.
|
||||
|
|
@ -139,6 +145,27 @@ export function PetGenerateContent() {
|
|||
void generateDrafts(requestGateway, { prompt: example })
|
||||
}
|
||||
|
||||
// A remix re-runs generation grounded on an existing draft — same prompt, stay
|
||||
// on step 2 — so the user explores variations without starting over.
|
||||
const runRemix = (draft: { dataUri: string }) => {
|
||||
void generateDrafts(requestGateway, { prompt: prompt.trim(), referenceImage: draft.dataUri })
|
||||
}
|
||||
|
||||
// Slow, and it replaces the current drafts — so confirm once, then remember it.
|
||||
const remixDraft = (draft: { dataUri: string }) => {
|
||||
if (busy) {
|
||||
return
|
||||
}
|
||||
|
||||
if ($petGenRemixConfirmed.get()) {
|
||||
runRemix(draft)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
setRemixPending(draft)
|
||||
}
|
||||
|
||||
// Hatch the selected draft. The user can pick one before the rest stream in —
|
||||
// if so, abort the remaining generations first (keeping the drafts we have).
|
||||
// The prompt is grounding text, not a label; the user names it on reveal.
|
||||
|
|
@ -283,11 +310,27 @@ export function PetGenerateContent() {
|
|||
hasDrafts={hasDrafts}
|
||||
onCancel={discardDrafts}
|
||||
onHatch={hatch}
|
||||
onRemix={remixDraft}
|
||||
onSelect={index => $petGenSelected.set(index)}
|
||||
selected={selected}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
confirmLabel={copy.remix}
|
||||
description={copy.remixConfirmBody}
|
||||
onClose={() => setRemixPending(null)}
|
||||
onConfirm={() => {
|
||||
markRemixConfirmed()
|
||||
|
||||
if (remixPending) {
|
||||
runRemix(remixPending)
|
||||
}
|
||||
}}
|
||||
open={remixPending !== null}
|
||||
title={copy.remixConfirmTitle}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -799,6 +799,10 @@ export const en: Translations = {
|
|||
staleBackend: 'Update Hermes to generate pets.',
|
||||
backgroundHint: 'You can close this — Hermes will notify you when it’s done.',
|
||||
slowProviderHint: 'This can take several minutes',
|
||||
remix: 'Remix',
|
||||
remixConfirmTitle: 'Remix this look?',
|
||||
remixConfirmBody:
|
||||
'This generates a fresh set of drafts using this one as the starting point. It can take several minutes.',
|
||||
genericError: 'Generation failed — try again or pick a suggestion.',
|
||||
referenceImageTooLarge: 'Reference image is too large. Use one under 16 MB.',
|
||||
referenceImageInvalid: 'Could not read that reference image. Try a PNG, JPG, WebP, or GIF.',
|
||||
|
|
|
|||
|
|
@ -917,6 +917,9 @@ export const ja = defineLocale({
|
|||
staleBackend: 'ペットを生成するには Hermes を更新してください。',
|
||||
backgroundHint: 'このウィンドウは閉じても大丈夫です。完了したら Hermes が通知します。',
|
||||
slowProviderHint: '数分かかることがあります',
|
||||
remix: 'リミックス',
|
||||
remixConfirmTitle: 'この見た目でリミックスしますか?',
|
||||
remixConfirmBody: 'これを起点に新しい候補を生成します。数分かかることがあります。',
|
||||
genericError: '生成に失敗しました。もう一度試すか、候補を選んでください。',
|
||||
referenceImageTooLarge: '参照画像が大きすぎます。16 MB 未満の画像を使ってください。',
|
||||
referenceImageInvalid: '参照画像を読み込めませんでした。PNG/JPG/WebP/GIF を試してください。',
|
||||
|
|
|
|||
|
|
@ -703,6 +703,9 @@ export interface Translations {
|
|||
staleBackend: string
|
||||
backgroundHint: string
|
||||
slowProviderHint: string
|
||||
remix: string
|
||||
remixConfirmTitle: string
|
||||
remixConfirmBody: string
|
||||
genericError: string
|
||||
referenceImageTooLarge: string
|
||||
referenceImageInvalid: string
|
||||
|
|
|
|||
|
|
@ -889,6 +889,9 @@ export const zhHant = defineLocale({
|
|||
staleBackend: '請更新 Hermes 以生成寵物。',
|
||||
backgroundHint: '你可以關閉此視窗——完成後 Hermes 會通知你。',
|
||||
slowProviderHint: '這可能需要幾分鐘',
|
||||
remix: '混合生成',
|
||||
remixConfirmTitle: '以此造型混合生成?',
|
||||
remixConfirmBody: '將以此造型為起點生成一組新草圖,可能需要幾分鐘。',
|
||||
genericError: '生成失敗——請重試或選一個建議。',
|
||||
referenceImageTooLarge: '參考圖片過大。請使用小於 16 MB 的圖片。',
|
||||
referenceImageInvalid: '無法讀取該參考圖片。請嘗試 PNG、JPG、WebP 或 GIF。',
|
||||
|
|
|
|||
|
|
@ -987,6 +987,9 @@ export const zh: Translations = {
|
|||
staleBackend: '请更新 Hermes 以生成宠物。',
|
||||
backgroundHint: '你可以关闭此窗口——完成后 Hermes 会通知你。',
|
||||
slowProviderHint: '这可能需要几分钟',
|
||||
remix: '混合生成',
|
||||
remixConfirmTitle: '以此造型混合生成?',
|
||||
remixConfirmBody: '将以此造型为起点生成一组新草图,可能需要几分钟。',
|
||||
genericError: '生成失败——请重试或选择一个建议。',
|
||||
referenceImageTooLarge: '参考图过大。请使用小于 16 MB 的图片。',
|
||||
referenceImageInvalid: '无法读取该参考图。请尝试 PNG、JPG、WebP 或 GIF。',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { atom } from 'nanostores'
|
||||
|
||||
import { persistString, storedString } from '@/lib/storage'
|
||||
import { persistBoolean, persistString, storedBoolean, storedString } from '@/lib/storage'
|
||||
import { $gateway } from '@/store/gateway'
|
||||
import { dispatchNativeNotification } from '@/store/native-notifications'
|
||||
import { notify } from '@/store/notifications'
|
||||
|
|
@ -118,6 +118,7 @@ export interface PetGenProvider {
|
|||
}
|
||||
|
||||
const PROVIDER_KEY = 'hermes.desktop.petgen.provider'
|
||||
const REMIX_CONFIRMED_KEY = 'hermes.desktop.petgen.remixConfirmed'
|
||||
|
||||
/** Reference-capable providers available to pick (from `pet.generate.status`). */
|
||||
export const $petGenProviders = atom<PetGenProvider[]>([])
|
||||
|
|
@ -130,6 +131,15 @@ export function setPetGenProvider(name: string): void {
|
|||
persistString(PROVIDER_KEY, name || null)
|
||||
}
|
||||
|
||||
/** Whether the user has acknowledged the one-time "remix regenerates" notice. */
|
||||
export const $petGenRemixConfirmed = atom(storedBoolean(REMIX_CONFIRMED_KEY, false))
|
||||
|
||||
/** Remember that the remix notice has been shown so we don't ask again. */
|
||||
export function markRemixConfirmed(): void {
|
||||
$petGenRemixConfirmed.set(true)
|
||||
persistBoolean(REMIX_CONFIRMED_KEY, true)
|
||||
}
|
||||
|
||||
/** Probe whether generation is possible (a reference-capable backend exists). */
|
||||
export async function checkPetGenAvailable(request: GatewayRequest): Promise<void> {
|
||||
try {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue