108 | {/* Time labels */}
109 |
110 | 0:00
111 |
112 | {formatDuration(selectedDuration)}
113 |
114 | {formatDuration(duration)}
115 |
116 |
117 | {/* Track */}
118 |
122 | {/* Unselected region (before) */}
123 |
127 |
128 | {/* Selected region */}
129 |
handleMouseDown(e, 'range')}
139 | />
140 |
141 | {/* Unselected region (after) */}
142 |
146 |
147 | {/* Start handle */}
148 |
handleMouseDown(e, 'start')}
155 | >
156 |
159 |
160 |
161 | {/* End handle */}
162 |
handleMouseDown(e, 'end')}
169 | >
170 |
173 |
174 |
175 |
176 | {/* Time inputs */}
177 |
178 |
179 | Start:
180 | {formatDuration(startTime)}
181 |
182 |
183 | End:
184 | {formatDuration(endTime)}
185 |
186 |
187 |
188 | )
189 | }
190 |
--------------------------------------------------------------------------------
/src/components/playground/DynamicForm.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo, useEffect, useState, useRef } from 'react'
2 | import type { Model } from '@/types/model'
3 | import { schemaToFormFields, getDefaultValues, type FormFieldConfig } from '@/lib/schemaToForm'
4 | import { FormField } from './FormField'
5 | import { ScrollArea } from '@/components/ui/scroll-area'
6 | import { cn } from '@/lib/utils'
7 |
8 | interface DynamicFormProps {
9 | model: Model
10 | values: Record
11 | validationErrors?: Record
12 | onChange: (key: string, value: unknown) => void
13 | onSetDefaults: (defaults: Record) => void
14 | onFieldsChange?: (fields: FormFieldConfig[]) => void
15 | disabled?: boolean
16 | onUploadingChange?: (isUploading: boolean) => void
17 | }
18 |
19 | export function DynamicForm({
20 | model,
21 | values,
22 | validationErrors = {},
23 | onChange,
24 | onSetDefaults,
25 | onFieldsChange,
26 | disabled = false,
27 | onUploadingChange
28 | }: DynamicFormProps) {
29 | // Track which hidden fields are enabled
30 | const [enabledHiddenFields, setEnabledHiddenFields] = useState>(new Set())
31 |
32 | // Track if we've initialized defaults for this model instance
33 | const initializedRef = useRef(null)
34 |
35 | // Extract schema from model
36 | const fields = useMemo(() => {
37 | // The API returns schema in api_schema.api_schemas[0].request_schema
38 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
39 | const apiSchemas = (model.api_schema as any)?.api_schemas as Array<{
40 | type: string
41 | request_schema?: {
42 | properties?: Record
43 | required?: string[]
44 | 'x-order-properties'?: string[]
45 | }
46 | }> | undefined
47 |
48 | const requestSchema = apiSchemas?.find(s => s.type === 'model_run')?.request_schema
49 | if (!requestSchema?.properties) {
50 | return []
51 | }
52 | return schemaToFormFields(
53 | requestSchema.properties as Record,
54 | requestSchema.required || [],
55 | requestSchema['x-order-properties']
56 | )
57 | }, [model])
58 |
59 | // Reset enabled hidden fields when model changes
60 | useEffect(() => {
61 | setEnabledHiddenFields(new Set())
62 | }, [model.model_id])
63 |
64 | // Register fields and set defaults when model changes
65 | useEffect(() => {
66 | onFieldsChange?.(fields)
67 |
68 | // Only set defaults if this is a new model (not just remount)
69 | // Check if we already have values for this model
70 | const hasExistingValues = Object.keys(values).some(key =>
71 | values[key] !== undefined && values[key] !== '' &&
72 | !(Array.isArray(values[key]) && values[key].length === 0)
73 | )
74 |
75 | // Set defaults only if model changed AND no existing values
76 | if (initializedRef.current !== model.model_id && !hasExistingValues) {
77 | const defaults = getDefaultValues(fields)
78 | onSetDefaults(defaults)
79 | }
80 | initializedRef.current = model.model_id
81 | // eslint-disable-next-line react-hooks/exhaustive-deps
82 | }, [fields, model.model_id, onFieldsChange, onSetDefaults])
83 |
84 | // Toggle a hidden field
85 | const toggleHiddenField = (fieldName: string) => {
86 | setEnabledHiddenFields(prev => {
87 | const next = new Set(prev)
88 | if (next.has(fieldName)) {
89 | next.delete(fieldName)
90 | // Clear the value when disabling
91 | onChange(fieldName, undefined)
92 | } else {
93 | next.add(fieldName)
94 | }
95 | return next
96 | })
97 | }
98 |
99 |
100 | if (fields.length === 0) {
101 | return (
102 |
103 |
No configurable parameters for this model.
104 |
You can run this model directly.
105 |
106 | )
107 | }
108 |
109 | return (
110 |
111 |
112 | {fields.map((field) => {
113 | // Hidden fields render with a toggle
114 | if (field.hidden) {
115 | const isEnabled = enabledHiddenFields.has(field.name)
116 | return (
117 |
118 |
119 |
137 | {field.description && !isEnabled && (
138 |
{field.description}
139 | )}
140 |
141 | {isEnabled && (
142 |
143 | onChange(field.name, value)}
147 | disabled={disabled}
148 | error={validationErrors[field.name]}
149 | modelType={model.type}
150 | imageValue={field.name === 'prompt' ? (values['image'] as string) : undefined}
151 | hideLabel
152 | formValues={values}
153 | onUploadingChange={onUploadingChange}
154 | />
155 |
156 | )}
157 |
158 | )
159 | }
160 |
161 | // Regular visible fields
162 | return (
163 |
onChange(field.name, value)}
168 | disabled={disabled}
169 | error={validationErrors[field.name]}
170 | modelType={model.type}
171 | imageValue={field.name === 'prompt' ? (values['image'] as string) : undefined}
172 | formValues={values}
173 | onUploadingChange={onUploadingChange}
174 | />
175 | )
176 | })}
177 |
178 |
179 | )
180 | }
181 |
--------------------------------------------------------------------------------
/src/hooks/useImageEraserWorker.ts:
--------------------------------------------------------------------------------
1 | import { useRef, useCallback, useEffect } from 'react'
2 | import type { ProgressDetail } from '@/types/progress'
3 | import { getDownloadTimeoutMs } from '@/stores/settingsStore'
4 |
5 | interface WorkerMessage {
6 | type: 'phase' | 'progress' | 'ready' | 'result' | 'error' | 'disposed'
7 | payload?: unknown
8 | }
9 |
10 | interface PhasePayload {
11 | phase: string
12 | id: number
13 | }
14 |
15 | interface ProgressPayload {
16 | phase: string
17 | progress: number
18 | detail?: ProgressDetail
19 | id: number
20 | }
21 |
22 | interface ReadyPayload {
23 | id: number
24 | }
25 |
26 | interface ResultPayload {
27 | data: Float32Array
28 | width: number
29 | height: number
30 | id: number
31 | }
32 |
33 | export interface EraserResult {
34 | data: Float32Array
35 | width: number
36 | height: number
37 | }
38 |
39 | interface UseImageEraserWorkerOptions {
40 | onPhase?: (phase: string) => void
41 | onProgress?: (phase: string, progress: number, detail?: ProgressDetail) => void
42 | onReady?: () => void
43 | onError?: (error: string) => void
44 | }
45 |
46 | export function useImageEraserWorker(options: UseImageEraserWorkerOptions = {}) {
47 | const workerRef = useRef(null)
48 | const callbacksRef = useRef