-
-
Notifications
You must be signed in to change notification settings - Fork 269
feat: previousJobId and previousImage for follow-up media edits #927
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c1e5b1b
45518bf
88f018e
1e18bd5
9710fdd
d2dd0c4
3fdee4e
49e9d02
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # AgentsRoom: personal files (not committed to git) | ||
| *-personal.json | ||
| agents-local.json | ||
| sessions/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| [ | ||
| { | ||
| "role": "fullstack", | ||
| "model": "opus", | ||
| "customName": "Full-Stack Developer", | ||
| "isPersonal": false, | ||
| "id": "agent-1776361243376-3sekdc", | ||
| "claudeSessionId": "96773a93-be2a-45a9-a732-ceb224d3d0e5" | ||
| } | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| { | ||
| "folders": [], | ||
| "prompts": [] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| --- | ||
| '@tanstack/ai': minor | ||
| '@tanstack/ai-openai': minor | ||
| '@tanstack/ai-gemini': minor | ||
| '@tanstack/ai-grok': minor | ||
| '@tanstack/ai-fal': minor | ||
| '@tanstack/ai-client': minor | ||
| --- | ||
|
|
||
| feat: first-class follow-up edits for generated media. | ||
|
|
||
| `generateVideo({ ..., previousJobId })` edits a previously generated video instead of generating from scratch. Callers always pass the prior generation's job id; adapters decide how to consume it via a `VideoAdapter` edit-kind map: | ||
|
|
||
| - `'job'` — reference the id server-side (OpenAI Sora 2 / Sora 2 Pro remix; Gemini Omni Flash, which maps `previousJobId` onto the Interactions API's `previous_interaction_id` wire field — that field is omitted from Omni `modelOptions`) | ||
| - `'media'` — resolve the finished clip via `getVideoUrl(previousJobId)` (xAI `grok-imagine-video` → `/videos/edits`; fal video-to-video endpoints such as `xai/grok-imagine-video/edit-video` and Seedance 2.0 reference-to-video). Fal generate endpoints with a known edit sibling (e.g. Grok text/image-to-video) resolve on the generate model, then submit to the edit endpoint. | ||
|
|
||
| Non-editing models (Veo, `grok-imagine-video-1.5`) reject `previousJobId` at compile time. Sora remix and Grok edits accept only a prompt — `size` / `duration` / media inputs are rejected because the output inherits them from the source video. | ||
|
|
||
| `generateImage({ ..., previousImage })` is the image-side counterpart: pass a prior result's `GeneratedImage` (or an array, or the whole result) and it is prepended to the prompt as an image part, flowing through each adapter's existing edit path; type-gated to models that accept image inputs. | ||
|
|
||
| Breaking for hand-rolled (non-`BaseVideoAdapter`) `VideoAdapter` implementations: the interface gains `supportedEditKind(): 'job' | 'media' | undefined` (and a 7th, defaulted `TModelEditByName` generic — existing 6-argument instantiations keep compiling). `BaseVideoAdapter` supplies a default returning `undefined`, plus `resolvePreviousJobUrl(previousJobId)`. New exports include `VideoEditKind`, `ModelEditKindByName`, `VideoPreviousJobIdForAdapter`, `ImagePreviousSource`, `ImagePreviousImageForModel`, `generatedImageToImagePart`, `generatedVideoUrlToVideoPart`. Client wire types: `VideoGenerateInput.previousJobId`, `ImageGenerateInput.previousImage`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -383,6 +383,7 @@ And returns: | |
| | `prompt` | `string \| MediaPromptPart[]` | Description of the video to generate (required). A plain string, or — on models that support conditioned generation — an ordered array of content parts interleaving text with image / video / audio inputs. See [Image-to-Video](#image-to-video) below. | | ||
| | `size` | `string` | Video resolution in WIDTHxHEIGHT format | | ||
| | `duration` | `number` | Video duration in seconds (maps to `seconds` parameter in API) | | ||
| | `previousJobId?` | `string` | Prior generation's job id to edit instead of generating from scratch. Only offered (at compile time) for models that support follow-up edits — see [Editing Generated Videos](#editing-generated-videos-previousjobid). | | ||
| | `modelOptions?` | `object` | Model-specific options (renamed from `providerOptions`) | | ||
|
|
||
| ## Image-to-Video | ||
|
|
@@ -489,6 +490,91 @@ The API uses the `seconds` parameter. Allowed values: | |
| - `8` seconds (default) | ||
| - `12` seconds | ||
|
|
||
| ## Editing Generated Videos (previousJobId) | ||
|
|
||
| Models that support **follow-up runs** can edit a previously generated | ||
| video instead of generating from scratch: pass the prior generation's | ||
| job id as `previousJobId` and describe the change in the prompt. The | ||
| canonical call is the same for every provider. | ||
|
|
||
| **Server:** | ||
|
|
||
| ```typescript ignore | ||
| import { generateVideo, getVideoJobStatus } from '@tanstack/ai' | ||
| import { openaiVideo } from '@tanstack/ai-openai' | ||
|
|
||
| const adapter = openaiVideo('sora-2') | ||
|
|
||
| // Turn 1: generate | ||
| const first = await generateVideo({ adapter, prompt: 'A city street at dusk' }) | ||
| // …poll first.jobId to completion… | ||
|
|
||
| // Turn 2: edit the result | ||
| const edited = await generateVideo({ | ||
| adapter, | ||
| prompt: 'Make it rain', | ||
| previousJobId: first.jobId, | ||
| }) | ||
| ``` | ||
|
|
||
| **Client** — pass the completed job's id through the hook; your server | ||
| forwards it to `generateVideo`: | ||
|
|
||
| ```tsx | ||
| import { useGenerateVideo, fetchServerSentEvents } from '@tanstack/ai-react' | ||
|
|
||
| function VideoEditor() { | ||
| const { generate, result, isLoading } = useGenerateVideo({ | ||
| connection: fetchServerSentEvents('/api/generate/video'), | ||
| }) | ||
|
|
||
| const handleEdit = () => { | ||
| if (!result?.jobId) return | ||
| void generate({ | ||
| prompt: 'Make it rain', | ||
| previousJobId: result.jobId, | ||
| }) | ||
|
Comment on lines
+520
to
+536
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Add the server endpoint half of the client example. The example says the server forwards 🤖 Prompt for AI AgentsSource: Coding guidelines
Comment on lines
+531
to
+536
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Use the hook’s job id instead of The documented hook exposes the job identifier separately ( 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| return ( | ||
| <button onClick={handleEdit} disabled={isLoading || !result}> | ||
| Edit | ||
| </button> | ||
| ) | ||
| } | ||
| ``` | ||
|
|
||
| Each model declares **how** it consumes that job id via | ||
| `adapter.supportedEditKind()`: | ||
|
|
||
| | Kind | How the adapter uses `previousJobId` | Providers | | ||
| | --- | --- | --- | | ||
| | `'job'` | References the prior job server-side | OpenAI Sora 2 / Sora 2 Pro (remix), Gemini Omni Flash | | ||
| | `'media'` | Resolves the finished clip via `getVideoUrl(previousJobId)` | xAI `grok-imagine-video` (`/videos/edits`), fal video-to-video endpoints (`xai/grok-imagine-video/edit-video`, `fal-ai/wan/v2.7/edit-video`, Seedance 2.0 reference-to-video) | | ||
|
|
||
| Models without follow-up support (Veo, `grok-imagine-video-1.5`, fal | ||
| text/image-to-video endpoints without a known edit sibling) reject | ||
| `previousJobId` at compile time. | ||
|
|
||
| Provider-specific constraints: | ||
|
|
||
| - **OpenAI Sora (remix)** accepts only a text prompt — the output inherits | ||
| the source video's size and duration, so `size`, `duration`, and image | ||
| parts are rejected when `previousJobId` is set. | ||
| - **Grok `/videos/edits`** inherits duration and aspect ratio from the | ||
| source (capped at 720p, input truncated to 8 seconds); `size` / `duration` | ||
| options are rejected when `previousJobId` is set. The resolved source rides | ||
| `video_url` (public URL, base64 `data:` URI, or `file_id`). | ||
| - **Gemini Omni Flash** maps `previousJobId` onto the Interactions API's | ||
| `previous_interaction_id` wire field internally. That field is **not** | ||
| exposed on Omni `modelOptions` — use `previousJobId` only. | ||
| - **fal** resolves `previousJobId` via `getVideoUrl` on the generate | ||
| model, then routes the URL onto the edit endpoint's video input | ||
| (`video_url`, or the endpoint's list field — Seedance 2.0's | ||
| reference-to-video takes `video_urls`). Generate endpoints with a known | ||
| edit sibling (e.g. Grok text/image-to-video → `edit-video`) do this | ||
| automatically. | ||
|
|
||
| ## Model Options | ||
|
|
||
| ### OpenAI Model Options | ||
|
|
@@ -618,10 +704,10 @@ the Files API first). | |
|
|
||
| #### Conversational video editing | ||
|
|
||
| Omni's headline capability is iterative refinement: pass the interaction id | ||
| of a prior generation (its `jobId`) as | ||
| `modelOptions.previous_interaction_id` and describe the change — the model | ||
| edits the video while preserving everything you didn't mention: | ||
| Omni's headline capability is iterative refinement: pass a prior | ||
| generation's `jobId` (its interaction id) as `previousJobId` and describe the | ||
| change — the model edits the video while preserving everything you didn't | ||
| mention (see [Editing Generated Videos](#editing-generated-videos-previousjobid)): | ||
|
|
||
| ```typescript ignore | ||
| import { generateVideo } from '@tanstack/ai' | ||
|
|
@@ -641,12 +727,15 @@ const first = await generateVideo({ | |
| const second = await generateVideo({ | ||
| adapter, | ||
| prompt: 'Make the violin invisible', | ||
| modelOptions: { previous_interaction_id: first.jobId }, | ||
| previousJobId: first.jobId, | ||
| }) | ||
| ``` | ||
|
|
||
| `modelOptions` also passes through the Interactions API's request fields | ||
| (e.g. `generation_config.video_config.task` to pin | ||
| The adapter maps `previousJobId` onto the Interactions API's | ||
| `previous_interaction_id` wire field. That field is not available on Omni | ||
| `modelOptions` — always use `previousJobId`. `modelOptions` still passes | ||
| through the Interactions API's other request fields (e.g. | ||
| `generation_config.video_config.task` to pin | ||
| `'text_to_video' | 'image_to_video' | 'reference_to_video' | 'edit'` | ||
| instead of letting the model infer the task mode). | ||
|
|
||
|
|
@@ -708,6 +797,8 @@ adapter.snapDuration(99) // 15 | |
|
|
||
| Generated clips include an audio track. When the job completes, the adapter reports `usage.unitsBilled` (billed seconds of video) and `usage.cost` (exact USD cost as returned by the API) on the result. | ||
|
|
||
| `grok-imagine-video` can also edit a previously generated clip via [`previousJobId`](#editing-generated-videos-previousjobid) — pass the prior job id and an edit prompt; the adapter resolves the finished clip and posts to xAI's `/videos/edits` endpoint, then polls like any other job. The output inherits duration and aspect ratio from the source (capped at 720p, input truncated to 8 seconds). | ||
|
|
||
| ## Response Types | ||
|
|
||
| > **Note:** The interfaces below are the underlying adapter-level types. The `getVideoJobStatus()` helper returns a single merged object, `{ status, progress?, url?, error?, usage? }` — it does not return `jobId` or `expiresAt`. | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,7 +1,7 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useRef, useState } from 'react' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ImageIcon, Loader2, Plus, Shuffle, X } from 'lucide-react' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { ImageIcon, Loader2, Plus, Shuffle, Wand2, X } from 'lucide-react' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { ImageGenerationResult } from '@tanstack/ai' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { MediaPrompt } from '@tanstack/ai/client' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { GeneratedImage, MediaPrompt } from '@tanstack/ai/client' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { generateImageFn } from '@/lib/server-functions' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getRandomImagePrompt } from '@/lib/prompts' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -37,6 +37,7 @@ export default function ImageGenerator({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [isLoading, setIsLoading] = useState(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [results, setResults] = useState<Record<string, ModelResult>>({}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [images, setImages] = useState<Array<AttachedMedia>>([]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [editPrompts, setEditPrompts] = useState<Record<string, string>>({}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fileInputRef = useRef<HTMLInputElement>(null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const currentModel = IMAGE_MODELS.find((m) => m.id === selectedModel) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -64,6 +65,40 @@ export default function ImageGenerator({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setImages((prev) => prev.filter((image) => image.id !== id)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Follow-up edit of a generated image: the prior image rides `previousImage` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * and the server prepends it to the prompt as an image input for the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * model's edit path. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleEditImage = async (modelId: string, image: GeneratedImage) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const editPrompt = editPrompts[modelId]?.trim() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!editPrompt) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setResults((prev) => ({ ...prev, [modelId]: { status: 'loading' } })) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const response = await generateImageFn({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data: { prompt: editPrompt, model: modelId, previousImage: image }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setResults((prev) => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...prev, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [modelId]: { status: 'success', result: response }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setEditPrompts((prev) => ({ ...prev, [modelId]: '' })) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const edited = response.images[0] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (edited) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onImageGenerated?.(getImageSrc(edited)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setResults((prev) => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...prev, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [modelId]: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: 'error', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: err instanceof Error ? err.message : 'Failed to edit image', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleGenerate = async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!prompt.trim()) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const builtPrompt = buildPrompt() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -313,6 +348,45 @@ export default function ImageGenerator({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| — multiply by the endpoint unit price for USD cost | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {model?.editable && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex gap-2"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="text" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value={editPrompts[modelId] ?? ''} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={(e) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setEditPrompts((prev) => ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...prev, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [modelId]: e.target.value, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onKeyDown={(e) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (e.key === 'Enter') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| handleEditImage( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| modelId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| modelResult.result!.images[0]!, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| placeholder="Describe an edit — e.g. 'make it night time'..." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| disabled={isLoading} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="flex-1 px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white text-sm placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent disabled:opacity-50" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| handleEditImage( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| modelId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| modelResult.result!.images[0]!, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+362
to
+379
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Remove the redundant Inside the 🧹 Proposed fix onKeyDown={(e) => {
if (e.key === 'Enter')
handleEditImage(
modelId,
- modelResult.result!.images[0]!,
+ modelResult.result.images[0]!,
)
}} onClick={() =>
handleEditImage(
modelId,
- modelResult.result!.images[0]!,
+ modelResult.result.images[0]!,
)
}📝 Committable suggestion
Suggested change
🧰 Tools🪛 ESLint[error] 366-366: This assertion is unnecessary since the receiver accepts the original type of the expression. ( [error] 377-377: This assertion is unnecessary since the receiver accepts the original type of the expression. ( 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| disabled={ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isLoading || !editPrompts[modelId]?.trim() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="px-4 py-2 bg-purple-600 hover:bg-purple-700 disabled:bg-gray-700 disabled:cursor-not-allowed text-white text-sm font-medium rounded-lg transition-colors flex items-center gap-1.5" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Wand2 className="w-4 h-4" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Edit | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Include the server endpoint half of this example.
The client snippet sends a wire-friendly
{ url? }/{ b64Json? }value and explicitly says the server must narrow it, but no route showing that conversion is provided. Add a minimal server endpoint that validates the payload and passes aGeneratedImagetogenerateImage.🤖 Prompt for AI Agents
Source: Coding guidelines