mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 23:50:08 +00:00
[feature] Add text and PDF file upload support
This commit is contained in:
136
src/composables/widgets/useTextUploadWidget.ts
Normal file
136
src/composables/widgets/useTextUploadWidget.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import type { IComboWidget } from '@comfyorg/litegraph/dist/types/widgets'
|
||||
|
||||
import { useNodeDragAndDrop } from '@/composables/node/useNodeDragAndDrop'
|
||||
import { useNodeFileInput } from '@/composables/node/useNodeFileInput'
|
||||
import { useNodePaste } from '@/composables/node/useNodePaste'
|
||||
import { t } from '@/i18n'
|
||||
import { api } from '@/scripts/api'
|
||||
import type { ComfyWidgetConstructor } from '@/scripts/widgets'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { addToComboValues } from '@/utils/litegraphUtil'
|
||||
|
||||
// Support only txt and pdf files
|
||||
const isTextFile = (file: File): boolean =>
|
||||
file.type === 'text/plain' || file.name.toLowerCase().endsWith('.txt')
|
||||
|
||||
const isPdfFile = (file: File): boolean =>
|
||||
file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')
|
||||
|
||||
const isSupportedFile = (file: File): boolean =>
|
||||
isTextFile(file) || isPdfFile(file)
|
||||
|
||||
// Upload a text file and return the file path
|
||||
async function uploadTextFile(file: File): Promise<string | null> {
|
||||
try {
|
||||
const body = new FormData()
|
||||
body.append('image', file) // Using standard field name for compatibility
|
||||
|
||||
const resp = await api.fetchApi('/upload/image', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
|
||||
if (resp.status === 200) {
|
||||
const data = await resp.json()
|
||||
let path = data.name
|
||||
if (data.subfolder) {
|
||||
path = data.subfolder + '/' + path
|
||||
}
|
||||
return path
|
||||
} else {
|
||||
useToastStore().addAlert(resp.status + ' - ' + resp.statusText)
|
||||
}
|
||||
} catch (error) {
|
||||
useToastStore().addAlert(String(error))
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const useTextUploadWidget = (): ComfyWidgetConstructor => {
|
||||
return (node, inputName, inputData) => {
|
||||
// Early return with empty button if no valid inputs
|
||||
if (
|
||||
!node.widgets ||
|
||||
!inputData[1] ||
|
||||
typeof inputData[1].textInputName !== 'string'
|
||||
) {
|
||||
const emptyButton = node.addWidget('button', inputName, '', () => {})
|
||||
return { widget: emptyButton }
|
||||
}
|
||||
|
||||
// Get the name of the text input widget from the spec
|
||||
const textInputName = inputData[1].textInputName as string
|
||||
|
||||
// Find the combo widget that will store the file path
|
||||
const textWidget = node.widgets.find((w) => w.name === textInputName) as
|
||||
| IComboWidget
|
||||
| undefined
|
||||
|
||||
if (!textWidget) {
|
||||
console.error(`Text widget with name "${textInputName}" not found`)
|
||||
const fallbackButton = node.addWidget('button', inputName, '', () => {})
|
||||
return { widget: fallbackButton }
|
||||
}
|
||||
|
||||
// Ensure options and values are initialized
|
||||
if (!textWidget.options) {
|
||||
textWidget.options = { values: [] }
|
||||
} else if (!textWidget.options.values) {
|
||||
textWidget.options.values = []
|
||||
}
|
||||
|
||||
// Handle the file upload
|
||||
const handleFileUpload = async (files: File[]): Promise<File[]> => {
|
||||
if (!files.length) return files
|
||||
|
||||
const file = files[0]
|
||||
if (!isSupportedFile(file)) {
|
||||
useToastStore().addAlert(t('toastMessages.invalidFileType'))
|
||||
return files
|
||||
}
|
||||
|
||||
const path = await uploadTextFile(file)
|
||||
if (path && textWidget) {
|
||||
addToComboValues(textWidget, path)
|
||||
textWidget.value = path
|
||||
if (textWidget.callback) {
|
||||
textWidget.callback(path)
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// Set up file input for upload button
|
||||
const { openFileSelection } = useNodeFileInput(node, {
|
||||
accept: '.txt,.pdf,text/plain,application/pdf',
|
||||
onSelect: handleFileUpload
|
||||
})
|
||||
|
||||
// Set up drag and drop
|
||||
useNodeDragAndDrop(node, {
|
||||
fileFilter: isSupportedFile,
|
||||
onDrop: handleFileUpload
|
||||
})
|
||||
|
||||
// Set up paste
|
||||
useNodePaste(node, {
|
||||
fileFilter: isSupportedFile,
|
||||
onPaste: handleFileUpload
|
||||
})
|
||||
|
||||
// Create upload button widget
|
||||
const uploadWidget = node.addWidget(
|
||||
'button',
|
||||
inputName,
|
||||
'',
|
||||
openFileSelection,
|
||||
{ serialize: false }
|
||||
)
|
||||
|
||||
uploadWidget.label = t('g.choose_file_to_upload')
|
||||
|
||||
return { widget: uploadWidget }
|
||||
}
|
||||
}
|
||||
@@ -18,5 +18,6 @@ import './simpleTouchSupport'
|
||||
import './slotDefaults'
|
||||
import './uploadAudio'
|
||||
import './uploadImage'
|
||||
import './uploadText'
|
||||
import './webcamCapture'
|
||||
import './widgetInputs'
|
||||
|
||||
50
src/extensions/core/uploadText.ts
Normal file
50
src/extensions/core/uploadText.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
ComfyNodeDef,
|
||||
InputSpec,
|
||||
isComboInputSpecV1
|
||||
} from '@/schemas/nodeDefSchema'
|
||||
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
// Adds an upload button to text/pdf nodes
|
||||
|
||||
const isTextUploadComboInput = (inputSpec: InputSpec) => {
|
||||
const [inputName, inputOptions] = inputSpec
|
||||
if (!inputOptions) return false
|
||||
|
||||
const isUploadInput = inputOptions['text_upload'] === true
|
||||
|
||||
return (
|
||||
isUploadInput && (isComboInputSpecV1(inputSpec) || inputName === 'COMBO')
|
||||
)
|
||||
}
|
||||
|
||||
const createUploadInput = (
|
||||
textInputName: string,
|
||||
textInputOptions: InputSpec
|
||||
): InputSpec => [
|
||||
'TEXTUPLOAD',
|
||||
{
|
||||
...textInputOptions[1],
|
||||
textInputName
|
||||
}
|
||||
]
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Comfy.UploadText',
|
||||
beforeRegisterNodeDef(_nodeType, nodeData: ComfyNodeDef) {
|
||||
const { input } = nodeData ?? {}
|
||||
const { required } = input ?? {}
|
||||
if (!required) return
|
||||
|
||||
const found = Object.entries(required).find(([_, input]) =>
|
||||
isTextUploadComboInput(input)
|
||||
)
|
||||
|
||||
// If text/pdf combo input found, attach upload input
|
||||
if (found) {
|
||||
const [inputName, inputSpec] = found
|
||||
required.upload = createUploadInput(inputName, inputSpec)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -76,6 +76,7 @@ export const zComboInputOptions = zBaseInputOptions.extend({
|
||||
allow_batch: z.boolean().optional(),
|
||||
video_upload: z.boolean().optional(),
|
||||
animated_image_upload: z.boolean().optional(),
|
||||
text_upload: z.boolean().optional(),
|
||||
options: z.array(zComboOption).optional(),
|
||||
remote: zRemoteWidgetConfig.optional(),
|
||||
/** Whether the widget is a multi-select widget. */
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useImageUploadWidget } from '@/composables/widgets/useImageUploadWidget
|
||||
import { useIntWidget } from '@/composables/widgets/useIntWidget'
|
||||
import { useMarkdownWidget } from '@/composables/widgets/useMarkdownWidget'
|
||||
import { useStringWidget } from '@/composables/widgets/useStringWidget'
|
||||
import { useTextUploadWidget } from '@/composables/widgets/useTextUploadWidget'
|
||||
import { t } from '@/i18n'
|
||||
import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration'
|
||||
import type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
@@ -289,5 +290,6 @@ export const ComfyWidgets: Record<string, ComfyWidgetConstructor> = {
|
||||
STRING: transformWidgetConstructorV2ToV1(useStringWidget()),
|
||||
MARKDOWN: transformWidgetConstructorV2ToV1(useMarkdownWidget()),
|
||||
COMBO: transformWidgetConstructorV2ToV1(useComboWidget()),
|
||||
IMAGEUPLOAD: useImageUploadWidget()
|
||||
IMAGEUPLOAD: useImageUploadWidget(),
|
||||
TEXTUPLOAD: useTextUploadWidget()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user