mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-15 01:48:06 +00:00
Compare commits
6 Commits
v1.26.13-d
...
feature/te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3126fde24 | ||
|
|
1989561fb3 | ||
|
|
6f1f489e1a | ||
|
|
ce16b3a4eb | ||
|
|
6fd2051f5c | ||
|
|
06a36d6869 |
218
src/composables/widgets/useTextUploadWidget.ts
Normal file
218
src/composables/widgets/useTextUploadWidget.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { isComboWidget } from '@comfyorg/litegraph'
|
||||
|
||||
import { useNodeDragAndDrop } from '@/composables/node/useNodeDragAndDrop'
|
||||
import { useNodeFileInput } from '@/composables/node/useNodeFileInput'
|
||||
import { useNodePaste } from '@/composables/node/useNodePaste'
|
||||
import { useValueTransform } from '@/composables/useValueTransform'
|
||||
import { t } from '@/i18n'
|
||||
import type {
|
||||
CustomInputSpec,
|
||||
InputSpec
|
||||
} from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { api } from '@/scripts/api'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { addToComboValues } from '@/utils/litegraphUtil'
|
||||
|
||||
const SUPPORTED_FILE_TYPES = ['text/plain', 'application/pdf', '.txt', '.pdf']
|
||||
const TEXT_FILE_UPLOAD = 'TEXT_FILE_UPLOAD'
|
||||
|
||||
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
|
||||
* @param file - The file to upload
|
||||
* @returns The file path or null if the upload fails
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload multiple text files and return array of file paths
|
||||
* @param files - The files to upload
|
||||
* @returns The file paths or an empty array if the upload fails
|
||||
*/
|
||||
async function uploadTextFiles(files: File[]): Promise<string[]> {
|
||||
const uploadPromises = files.map(uploadTextFile)
|
||||
const results = await Promise.all(uploadPromises)
|
||||
return results.filter((path) => path !== null)
|
||||
}
|
||||
|
||||
interface TextUploadInputSpec extends CustomInputSpec {
|
||||
type: 'TEXT_FILE_UPLOAD'
|
||||
textInputName: string
|
||||
allow_batch?: boolean
|
||||
}
|
||||
|
||||
export const isTextUploadInputSpec = (
|
||||
inputSpec: InputSpec
|
||||
): inputSpec is TextUploadInputSpec => {
|
||||
return (
|
||||
inputSpec.type === TEXT_FILE_UPLOAD &&
|
||||
'textInputName' in inputSpec &&
|
||||
typeof inputSpec.textInputName === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a disabled button widget when text upload configuration is invalid
|
||||
*/
|
||||
const createDisabledUploadButton = (node: any, inputName: string) =>
|
||||
node.addWidget('button', inputName, '', () => {})
|
||||
|
||||
/**
|
||||
* Create a text upload widget using V2 input spec
|
||||
*/
|
||||
export const useTextUploadWidget = () => {
|
||||
const widgetConstructor: ComfyWidgetConstructorV2 = (
|
||||
node,
|
||||
inputSpec: InputSpec
|
||||
) => {
|
||||
if (!isTextUploadInputSpec(inputSpec)) {
|
||||
throw new Error(`Invalid input spec for text upload: ${inputSpec}`)
|
||||
}
|
||||
|
||||
// Get configuration from input spec
|
||||
const textInputName = inputSpec.textInputName
|
||||
const allow_batch = inputSpec.allow_batch === true
|
||||
|
||||
// Find the combo widget that will store the file path(s)
|
||||
const foundWidget = node.widgets?.find((w) => w.name === textInputName)
|
||||
|
||||
if (!foundWidget || !isComboWidget(foundWidget)) {
|
||||
console.error(
|
||||
`Text widget with name "${textInputName}" not found or is not a combo widget`
|
||||
)
|
||||
return createDisabledUploadButton(node, inputSpec.name)
|
||||
}
|
||||
|
||||
const textWidget = foundWidget
|
||||
|
||||
// Ensure options and values are initialized
|
||||
if (!textWidget.options) {
|
||||
textWidget.options = { values: [] }
|
||||
} else if (!textWidget.options.values) {
|
||||
textWidget.options.values = []
|
||||
}
|
||||
|
||||
// Types for internal handling
|
||||
type InternalValue = string | string[] | null | undefined
|
||||
type ExposedValue = string | string[]
|
||||
|
||||
const initialFile = allow_batch ? [] : ''
|
||||
|
||||
// Transform function to handle batch vs single file outputs
|
||||
const transform = (internalValue: InternalValue): ExposedValue => {
|
||||
if (!internalValue) return initialFile
|
||||
if (Array.isArray(internalValue))
|
||||
return allow_batch ? internalValue : internalValue[0] || ''
|
||||
return internalValue
|
||||
}
|
||||
|
||||
// Set up value transform on the widget
|
||||
Object.defineProperty(
|
||||
textWidget,
|
||||
'value',
|
||||
useValueTransform(transform, initialFile)
|
||||
)
|
||||
|
||||
// Handle the file upload
|
||||
const handleFileUpload = async (files: File[]): Promise<File[]> => {
|
||||
if (!files.length) return files
|
||||
|
||||
// Filter supported files
|
||||
const supportedFiles = files.filter(isSupportedFile)
|
||||
if (!supportedFiles.length) {
|
||||
useToastStore().addAlert(t('toastMessages.invalidFileType'))
|
||||
return files
|
||||
}
|
||||
|
||||
if (!allow_batch && supportedFiles.length > 1) {
|
||||
useToastStore().addAlert('Only single file upload is allowed')
|
||||
return files
|
||||
}
|
||||
|
||||
const filesToUpload = allow_batch ? supportedFiles : [supportedFiles[0]]
|
||||
const paths = await uploadTextFiles(filesToUpload)
|
||||
|
||||
if (paths.length && textWidget) {
|
||||
paths.forEach((path) => addToComboValues(textWidget, path))
|
||||
// @ts-expect-error litegraph combo value type does not support arrays yet
|
||||
textWidget.value = allow_batch ? paths : paths[0]
|
||||
if (textWidget.callback) {
|
||||
textWidget.callback(allow_batch ? paths : paths[0])
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// Set up file input for upload button
|
||||
const { openFileSelection } = useNodeFileInput(node, {
|
||||
accept: SUPPORTED_FILE_TYPES.join(','),
|
||||
allow_batch,
|
||||
onSelect: handleFileUpload
|
||||
})
|
||||
|
||||
// Set up drag and drop
|
||||
useNodeDragAndDrop(node, {
|
||||
fileFilter: isSupportedFile,
|
||||
onDrop: handleFileUpload
|
||||
})
|
||||
|
||||
// Set up paste
|
||||
useNodePaste(node, {
|
||||
fileFilter: isSupportedFile,
|
||||
allow_batch,
|
||||
onPaste: handleFileUpload
|
||||
})
|
||||
|
||||
// Create upload button widget
|
||||
const uploadWidget = node.addWidget(
|
||||
'button',
|
||||
inputSpec.name,
|
||||
'',
|
||||
openFileSelection,
|
||||
{ serialize: false }
|
||||
)
|
||||
|
||||
uploadWidget.label = allow_batch
|
||||
? t('g.choose_files_to_upload')
|
||||
: t('g.choose_file_to_upload')
|
||||
|
||||
return uploadWidget
|
||||
}
|
||||
|
||||
return widgetConstructor
|
||||
}
|
||||
@@ -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_file_upload'] === true
|
||||
|
||||
return (
|
||||
isUploadInput && (isComboInputSpecV1(inputSpec) || inputName === 'COMBO')
|
||||
)
|
||||
}
|
||||
|
||||
const createUploadInput = (
|
||||
textInputName: string,
|
||||
textInputOptions: InputSpec
|
||||
): InputSpec => [
|
||||
'TEXT_FILE_UPLOAD',
|
||||
{
|
||||
...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)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -86,6 +86,7 @@
|
||||
"control_after_generate": "control after generate",
|
||||
"control_before_generate": "control before generate",
|
||||
"choose_file_to_upload": "choose file to upload",
|
||||
"choose_files_to_upload": "choose files to upload",
|
||||
"capture": "capture",
|
||||
"nodes": "Nodes",
|
||||
"community": "Community",
|
||||
@@ -1274,7 +1275,9 @@
|
||||
"failedToPurchaseCredits": "Failed to purchase credits: {error}",
|
||||
"unauthorizedDomain": "Your domain {domain} is not authorized to use this service. Please contact {email} to add your domain to the whitelist.",
|
||||
"useApiKeyTip": "Tip: Can't access normal login? Use the Comfy API Key option.",
|
||||
"nothingSelected": "Nothing selected"
|
||||
"nothingSelected": "Nothing selected",
|
||||
"invalidFileType": "Invalid file type",
|
||||
"onlySingleFile": "Only single file upload is allowed"
|
||||
},
|
||||
"auth": {
|
||||
"apiKey": {
|
||||
|
||||
@@ -254,6 +254,7 @@
|
||||
"capture": "captura",
|
||||
"category": "Categoría",
|
||||
"choose_file_to_upload": "elige archivo para subir",
|
||||
"choose_files_to_upload": "elige archivos para subir",
|
||||
"clear": "Limpiar",
|
||||
"close": "Cerrar",
|
||||
"color": "Color",
|
||||
@@ -1349,6 +1350,7 @@
|
||||
"fileLoadError": "No se puede encontrar el flujo de trabajo en {fileName}",
|
||||
"fileUploadFailed": "Error al subir el archivo",
|
||||
"interrupted": "La ejecución ha sido interrumpida",
|
||||
"invalidFileType": "Tipo de archivo no válido",
|
||||
"migrateToLitegraphReroute": "Los nodos de reroute se eliminarán en futuras versiones. Haz clic para migrar a reroute nativo de litegraph.",
|
||||
"no3dScene": "No hay escena 3D para aplicar textura",
|
||||
"no3dSceneToExport": "No hay escena 3D para exportar",
|
||||
@@ -1357,6 +1359,7 @@
|
||||
"nothingSelected": "Nada seleccionado",
|
||||
"nothingToGroup": "Nada para agrupar",
|
||||
"nothingToQueue": "Nada para poner en cola",
|
||||
"onlySingleFile": "Solo se permite subir un archivo",
|
||||
"pendingTasksDeleted": "Tareas pendientes eliminadas",
|
||||
"pleaseSelectNodesToGroup": "Por favor, seleccione los nodos (u otros grupos) para crear un grupo para",
|
||||
"pleaseSelectOutputNodes": "Por favor, selecciona los nodos de salida",
|
||||
|
||||
@@ -254,6 +254,7 @@
|
||||
"capture": "capture",
|
||||
"category": "Catégorie",
|
||||
"choose_file_to_upload": "choisissez le fichier à télécharger",
|
||||
"choose_files_to_upload": "choisissez des fichiers à télécharger",
|
||||
"clear": "Effacer",
|
||||
"close": "Fermer",
|
||||
"color": "Couleur",
|
||||
@@ -1349,6 +1350,7 @@
|
||||
"fileLoadError": "Impossible de trouver le flux de travail dans {fileName}",
|
||||
"fileUploadFailed": "Échec du téléchargement du fichier",
|
||||
"interrupted": "L'exécution a été interrompue",
|
||||
"invalidFileType": "Type de fichier invalide",
|
||||
"migrateToLitegraphReroute": "Les nœuds de reroute seront supprimés dans les futures versions. Cliquez pour migrer vers le reroute natif de litegraph.",
|
||||
"no3dScene": "Aucune scène 3D pour appliquer la texture",
|
||||
"no3dSceneToExport": "Aucune scène 3D à exporter",
|
||||
@@ -1357,6 +1359,7 @@
|
||||
"nothingSelected": "Aucune sélection",
|
||||
"nothingToGroup": "Rien à regrouper",
|
||||
"nothingToQueue": "Rien à ajouter à la file d’attente",
|
||||
"onlySingleFile": "Le téléchargement d’un seul fichier est autorisé",
|
||||
"pendingTasksDeleted": "Tâches en attente supprimées",
|
||||
"pleaseSelectNodesToGroup": "Veuillez sélectionner les nœuds (ou autres groupes) pour créer un groupe pour",
|
||||
"pleaseSelectOutputNodes": "Veuillez sélectionner les nœuds de sortie",
|
||||
|
||||
@@ -254,6 +254,7 @@
|
||||
"capture": "キャプチャ",
|
||||
"category": "カテゴリ",
|
||||
"choose_file_to_upload": "アップロードするファイルを選択",
|
||||
"choose_files_to_upload": "アップロードするファイルを選択",
|
||||
"clear": "クリア",
|
||||
"close": "閉じる",
|
||||
"color": "色",
|
||||
@@ -1349,6 +1350,7 @@
|
||||
"fileLoadError": "{fileName}でワークフローが見つかりません",
|
||||
"fileUploadFailed": "ファイルのアップロードに失敗しました",
|
||||
"interrupted": "実行が中断されました",
|
||||
"invalidFileType": "無効なファイルタイプです",
|
||||
"migrateToLitegraphReroute": "将来のバージョンではRerouteノードが削除されます。litegraph-native rerouteに移行するにはクリックしてください。",
|
||||
"no3dScene": "テクスチャを適用する3Dシーンがありません",
|
||||
"no3dSceneToExport": "エクスポートする3Dシーンがありません",
|
||||
@@ -1357,6 +1359,7 @@
|
||||
"nothingSelected": "選択されていません",
|
||||
"nothingToGroup": "グループ化するものがありません",
|
||||
"nothingToQueue": "キューに追加する項目がありません",
|
||||
"onlySingleFile": "ファイルは1つだけアップロードできます",
|
||||
"pendingTasksDeleted": "保留中のタスクが削除されました",
|
||||
"pleaseSelectNodesToGroup": "グループを作成するためのノード(または他のグループ)を選択してください",
|
||||
"pleaseSelectOutputNodes": "出力ノードを選択してください",
|
||||
|
||||
@@ -254,6 +254,7 @@
|
||||
"capture": "캡처",
|
||||
"category": "카테고리",
|
||||
"choose_file_to_upload": "업로드할 파일 선택",
|
||||
"choose_files_to_upload": "업로드할 파일 선택",
|
||||
"clear": "지우기",
|
||||
"close": "닫기",
|
||||
"color": "색상",
|
||||
@@ -1349,6 +1350,7 @@
|
||||
"fileLoadError": "{fileName}에서 워크플로우를 찾을 수 없습니다",
|
||||
"fileUploadFailed": "파일 업로드에 실패했습니다",
|
||||
"interrupted": "실행이 중단되었습니다",
|
||||
"invalidFileType": "잘못된 파일 형식입니다",
|
||||
"migrateToLitegraphReroute": "향후 버전에서는 Reroute 노드가 제거됩니다. LiteGraph 에서 자체 제공하는 경유점으로 변환하려면 클릭하세요.",
|
||||
"no3dScene": "텍스처를 적용할 3D 장면이 없습니다",
|
||||
"no3dSceneToExport": "내보낼 3D 장면이 없습니다",
|
||||
@@ -1357,6 +1359,7 @@
|
||||
"nothingSelected": "선택된 항목이 없습니다",
|
||||
"nothingToGroup": "그룹화할 항목이 없습니다",
|
||||
"nothingToQueue": "대기열에 추가할 항목이 없습니다",
|
||||
"onlySingleFile": "파일은 하나만 업로드할 수 있습니다",
|
||||
"pendingTasksDeleted": "보류 중인 작업이 삭제되었습니다",
|
||||
"pleaseSelectNodesToGroup": "그룹을 만들기 위해 노드(또는 다른 그룹)를 선택해 주세요",
|
||||
"pleaseSelectOutputNodes": "출력 노드를 선택해 주세요",
|
||||
|
||||
@@ -254,6 +254,7 @@
|
||||
"capture": "захват",
|
||||
"category": "Категория",
|
||||
"choose_file_to_upload": "выберите файл для загрузки",
|
||||
"choose_files_to_upload": "выберите файлы для загрузки",
|
||||
"clear": "Очистить",
|
||||
"close": "Закрыть",
|
||||
"color": "Цвет",
|
||||
@@ -1349,6 +1350,7 @@
|
||||
"fileLoadError": "Не удалось найти рабочий процесс в {fileName}",
|
||||
"fileUploadFailed": "Не удалось загрузить файл",
|
||||
"interrupted": "Выполнение было прервано",
|
||||
"invalidFileType": "Недопустимый тип файла",
|
||||
"migrateToLitegraphReroute": "Узлы перенаправления будут удалены в будущих версиях. Нажмите, чтобы перейти на litegraph-native reroute.",
|
||||
"no3dScene": "Нет 3D сцены для применения текстуры",
|
||||
"no3dSceneToExport": "Нет 3D сцены для экспорта",
|
||||
@@ -1357,6 +1359,7 @@
|
||||
"nothingSelected": "Ничего не выбрано",
|
||||
"nothingToGroup": "Нечего группировать",
|
||||
"nothingToQueue": "Нет заданий в очереди",
|
||||
"onlySingleFile": "Разрешена загрузка только одного файла",
|
||||
"pendingTasksDeleted": "Ожидающие задачи удалены",
|
||||
"pleaseSelectNodesToGroup": "Пожалуйста, выберите узлы (или другие группы) для создания группы",
|
||||
"pleaseSelectOutputNodes": "Пожалуйста, выберите выходные узлы",
|
||||
|
||||
@@ -254,6 +254,7 @@
|
||||
"capture": "捕获",
|
||||
"category": "类别",
|
||||
"choose_file_to_upload": "选择要上传的文件",
|
||||
"choose_files_to_upload": "选择要上传的文件",
|
||||
"clear": "清除",
|
||||
"close": "关闭",
|
||||
"color": "颜色",
|
||||
@@ -1349,6 +1350,7 @@
|
||||
"fileLoadError": "无法在 {fileName} 中找到工作流",
|
||||
"fileUploadFailed": "文件上传失败",
|
||||
"interrupted": "执行已被中断",
|
||||
"invalidFileType": "文件类型无效",
|
||||
"migrateToLitegraphReroute": "将来的版本中将删除重定向节点。点击以迁移到litegraph-native重定向。",
|
||||
"no3dScene": "没有3D场景可以应用纹理",
|
||||
"no3dSceneToExport": "没有3D场景可以导出",
|
||||
@@ -1357,6 +1359,7 @@
|
||||
"nothingSelected": "未选择任何内容",
|
||||
"nothingToGroup": "没有可分组的内容",
|
||||
"nothingToQueue": "没有可加入队列的内容",
|
||||
"onlySingleFile": "只允许上传单个文件",
|
||||
"pendingTasksDeleted": "待处理任务已删除",
|
||||
"pleaseSelectNodesToGroup": "请选取节点(或其他组)以创建分组",
|
||||
"pleaseSelectOutputNodes": "请选择输出节点",
|
||||
|
||||
@@ -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_file_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(),
|
||||
TEXT_FILE_UPLOAD: transformWidgetConstructorV2ToV1(useTextUploadWidget())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user