Skip to content
Merged
3 changes: 3 additions & 0 deletions src/components/common/UrlInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { ValidationState } from '@/utils/validationUtil'
const props = defineProps<{
modelValue: string
validateUrlFn?: (url: string) => Promise<boolean>
disableValidation?: boolean
}>()

const emit = defineEmits<{
Expand Down Expand Up @@ -101,6 +102,8 @@ const defaultValidateUrl = async (url: string): Promise<boolean> => {
}

const validateUrl = async (value: string) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[performance] high Priority

Issue: Race condition in URL validation - multiple simultaneous validation requests can cause state inconsistencies
Context: validateUrl function can be called multiple times concurrently without proper cancellation handling
Suggestion: Implement request cancellation using AbortController and debounce validation calls

if (props.disableValidation) return

if (validationState.value === ValidationState.LOADING) return

const url = cleanInput(value)
Expand Down
36 changes: 36 additions & 0 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -2076,6 +2076,35 @@
"noModelsInFolder": "No {type} available in this folder",
"searchAssetsPlaceholder": "Type to search...",
"uploadModel": "Upload model",
"uploadModelFromCivitai": "Upload a model from Civitai",
"uploadModelFailedToRetrieveMetadata": "Failed to retrieve metadata. Please check the link and try again.",
"onlyCivitaiUrlsSupported": "Only Civitai URLs are supported",
"uploadModelDescription1": "Paste a Civitai model download link to add it to your library.",
"uploadModelDescription2": "Only links from https://civitai.com are supported at the moment",
"uploadModelDescription3": "Max file size: 1 GB",
"civitaiLinkLabel": "Civitai model download link",
"civitaiLinkPlaceholder": "Paste link here",
"civitaiLinkExample": "Example: https://civitai.com/api/download/models/833921?type=Model&format=SafeTensor",
"confirmModelDetails": "Confirm Model Details",
"fileName": "File Name",
"fileSize": "File Size",
"modelName": "Model Name",
"modelNamePlaceholder": "Enter a name for this model",
"tags": "Tags",
"tagsPlaceholder": "e.g., models, checkpoint",
"tagsHelp": "Separate tags with commas",
"upload": "Upload",
"uploadingModel": "Uploading model...",
"uploadSuccess": "Model uploaded successfully!",
"uploadFailed": "Upload failed",
"modelAssociatedWithLink": "The model associated with the link you provided:",
"modelTypeSelectorLabel": "What type of model is this?",
"modelTypeSelectorPlaceholder": "Select model type",
"selectModelType": "Select model type",
"notSureLeaveAsIs": "Not sure? Just leave this as is",
"modelUploaded": "Model uploaded! 🎉",
"findInLibrary": "Find it in the {type} section of the models library.",
"finish": "Finish",
"allModels": "All Models",
"allCategory": "All {category}",
"unknown": "Unknown",
Expand All @@ -2087,6 +2116,13 @@
"sortZA": "Z-A",
"sortRecent": "Recent",
"sortPopular": "Popular",
"errorFileTooLarge": "File exceeds the maximum allowed size limit",
"errorFormatNotAllowed": "Only SafeTensor format is allowed",
"errorUnsafePickleScan": "CivitAI detected potentially unsafe code in this file",
"errorUnsafeVirusScan": "CivitAI detected malware or suspicious content in this file",
"errorModelTypeNotSupported": "This model type is not supported",
"errorUnknown": "An unexpected error occurred",
"errorUploadFailed": "Failed to upload asset. Please try again.",
"ariaLabel": {
"assetCard": "{name} - {type} asset",
"loadingAsset": "Loading asset"
Expand Down
15 changes: 14 additions & 1 deletion src/platform/assets/components/AssetBrowserModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,14 @@ import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import AssetFilterBar from '@/platform/assets/components/AssetFilterBar.vue'
import AssetGrid from '@/platform/assets/components/AssetGrid.vue'
import UploadModelDialog from '@/platform/assets/components/UploadModelDialog.vue'
import UploadModelDialogHeader from '@/platform/assets/components/UploadModelDialogHeader.vue'
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
import { useAssetBrowser } from '@/platform/assets/composables/useAssetBrowser'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { assetService } from '@/platform/assets/services/assetService'
import { formatCategoryLabel } from '@/platform/assets/utils/categoryLabel'
import { useDialogStore } from '@/stores/dialogStore'
import { useModelToNodeStore } from '@/stores/modelToNodeStore'
import { OnCloseKey } from '@/types/widgetTypes'

Expand All @@ -92,6 +95,7 @@ const props = defineProps<{
}>()

const { t } = useI18n()
const dialogStore = useDialogStore()

const emit = defineEmits<{
'asset-select': [asset: AssetDisplayItem]
Expand Down Expand Up @@ -189,6 +193,15 @@ const { flags } = useFeatureFlags()
const isUploadButtonEnabled = computed(() => flags.modelUploadButtonEnabled)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[architecture] low Priority

Issue: Feature flag tightly coupled to component - modelUploadButtonEnabled flag is directly accessed in component
Context: Feature flags should be abstracted through business logic layer rather than UI components checking flags directly
Suggestion: Create a composable or service that encapsulates the upload feature availability logic


function handleUploadClick() {
// Will be implemented in the future commit
dialogStore.showDialog({
key: 'upload-model',
headerComponent: UploadModelDialogHeader,
component: UploadModelDialog,
props: {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[quality] low Priority

Issue: Inconsistent prop validation - some components lack proper prop validation and defaults
Context: Components should have consistent prop validation for better developer experience and runtime safety
Suggestion: Add proper prop validation with defaults and validators where appropriate, especially for onUploadSuccess callback prop

onUploadSuccess: async () => {
await execute()
}
}
})
}
</script>
58 changes: 58 additions & 0 deletions src/platform/assets/components/UploadModelConfirmation.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<template>
<div class="flex flex-col gap-4">
<!-- Model Info Section -->
<div class="flex flex-col gap-2">
<p class="text-sm text-muted m-0">
{{ $t('assetBrowser.modelAssociatedWithLink') }}
</p>
<p class="text-sm mt-0">
{{ metadata?.name || metadata?.filename }}
</p>
</div>

<!-- Model Type Selection -->
<div class="flex flex-col gap-2">
<label class="text-sm text-muted">
{{ $t('assetBrowser.modelTypeSelectorLabel') }}
</label>
<SingleSelect
v-model="selectedModelType"
:label="
isLoading
? $t('g.loading')
: $t('assetBrowser.modelTypeSelectorPlaceholder')
"
:options="modelTypes"
:disabled="isLoading"
/>
<div class="flex items-center gap-2 text-sm text-muted">
<i class="icon-[lucide--info]" />
<span>{{ $t('assetBrowser.notSureLeaveAsIs') }}</span>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import SingleSelect from '@/components/input/SingleSelect.vue'
import { useModelTypes } from '@/platform/assets/composables/useModelTypes'
import type { AssetMetadata } from '@/platform/assets/schemas/assetSchema'
const props = defineProps<{
modelValue: string | undefined
metadata: AssetMetadata | null
}>()
const emit = defineEmits<{
'update:modelValue': [value: string | undefined]
}>()
const { modelTypes, isLoading } = useModelTypes()
const selectedModelType = computed({
get: () => props.modelValue ?? null,
set: (value: string | null) => emit('update:modelValue', value ?? undefined)
})
</script>
108 changes: 108 additions & 0 deletions src/platform/assets/components/UploadModelDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<template>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[architecture] medium Priority

Issue: Single Responsibility Principle violation - UploadModelDialog handles wizard logic, validation, API calls, and state management
Context: This component has too many responsibilities making it harder to test and maintain
Suggestion: Extract wizard state management into a composable (useUploadWizard) and separate business logic from presentation

<div class="upload-model-dialog flex flex-col justify-between gap-6 p-4 pt-6">
<!-- Step 1: Enter URL -->
<UploadModelUrlInput
v-if="currentStep === 1"
v-model="wizardData.url"
:error="uploadError"
/>

<!-- Step 2: Confirm Metadata -->
<UploadModelConfirmation
v-else-if="currentStep === 2"
v-model="selectedModelType"
:metadata="wizardData.metadata"
/>

<!-- Step 3: Upload Progress -->
<UploadModelProgress
v-else-if="currentStep === 3"
:status="uploadStatus"
:error="uploadError"
:metadata="wizardData.metadata"
:model-type="selectedModelType"
/>

<!-- Navigation Footer -->
<UploadModelFooter
:current-step="currentStep"
:is-fetching-metadata="isFetchingMetadata"
:is-uploading="isUploading"
:can-fetch-metadata="canFetchMetadata"
:can-upload-model="canUploadModel"
:upload-status="uploadStatus"
@back="goToPreviousStep"
@fetch-metadata="handleFetchMetadata"
@upload="handleUploadModel"
@close="handleClose"
/>
</div>
</template>

<script setup lang="ts">
import { onMounted } from 'vue'

import UploadModelConfirmation from '@/platform/assets/components/UploadModelConfirmation.vue'
import UploadModelFooter from '@/platform/assets/components/UploadModelFooter.vue'
import UploadModelProgress from '@/platform/assets/components/UploadModelProgress.vue'
import UploadModelUrlInput from '@/platform/assets/components/UploadModelUrlInput.vue'
import { useModelTypes } from '@/platform/assets/composables/useModelTypes'
import { useUploadModelWizard } from '@/platform/assets/composables/useUploadModelWizard'
import { useDialogStore } from '@/stores/dialogStore'

const dialogStore = useDialogStore()
const { modelTypes, fetchModelTypes } = useModelTypes()

const emit = defineEmits<{
'upload-success': []
}>()

const {
currentStep,
isFetchingMetadata,
isUploading,
uploadStatus,
uploadError,
wizardData,
selectedModelType,
canFetchMetadata,
canUploadModel,
fetchMetadata,
uploadModel,
goToPreviousStep
} = useUploadModelWizard(modelTypes)

async function handleFetchMetadata() {
await fetchMetadata()
}

async function handleUploadModel() {
const success = await uploadModel()
if (success) {
emit('upload-success')
}
}

function handleClose() {
dialogStore.closeDialog({ key: 'upload-model' })
}

onMounted(() => {
fetchModelTypes()
})
</script>

<style scoped>
.upload-model-dialog {
width: 90vw;
max-width: 800px;
min-height: 400px;
}

@media (min-width: 640px) {
.upload-model-dialog {
width: auto;
min-width: 600px;
}
}
</style>
12 changes: 12 additions & 0 deletions src/platform/assets/components/UploadModelDialogHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<template>
<div class="flex items-center gap-3 px-4 py-2 font-bold">
<span>{{ $t('assetBrowser.uploadModelFromCivitai') }}</span>
<span
class="rounded-full bg-white px-1.5 py-0 text-xxs font-medium uppercase text-black"
>
{{ $t('g.beta') }}
</span>
</div>
</template>

<script setup lang="ts"></script>
72 changes: 72 additions & 0 deletions src/platform/assets/components/UploadModelFooter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<template>
<div class="flex justify-end gap-2">
<TextButton
v-if="currentStep !== 1 && currentStep !== 3"
:label="$t('g.back')"
type="secondary"
size="md"
:disabled="isFetchingMetadata || isUploading"
@click="emit('back')"
/>
<span v-else />

<IconTextButton
v-if="currentStep === 1"
:label="$t('g.continue')"
type="primary"
size="md"
:disabled="!canFetchMetadata || isFetchingMetadata"
@click="emit('fetchMetadata')"
>
<template #icon>
<i
v-if="isFetchingMetadata"
class="icon-[lucide--loader-circle] animate-spin"
/>
</template>
</IconTextButton>
<IconTextButton
v-else-if="currentStep === 2"
:label="$t('assetBrowser.upload')"
type="primary"
size="md"
:disabled="!canUploadModel || isUploading"
@click="emit('upload')"
>
<template #icon>
<i
v-if="isUploading"
class="icon-[lucide--loader-circle] animate-spin"
/>
</template>
</IconTextButton>
<TextButton
v-else-if="currentStep === 3 && uploadStatus === 'success'"
:label="$t('assetBrowser.finish')"
type="primary"
size="md"
@click="emit('close')"
/>
</div>
</template>

<script setup lang="ts">
import IconTextButton from '@/components/button/IconTextButton.vue'
import TextButton from '@/components/button/TextButton.vue'

defineProps<{
currentStep: number
isFetchingMetadata: boolean
isUploading: boolean
canFetchMetadata: boolean
canUploadModel: boolean
uploadStatus: 'idle' | 'uploading' | 'success' | 'error'
}>()

const emit = defineEmits<{
(e: 'back'): void
(e: 'fetchMetadata'): void
(e: 'upload'): void
(e: 'close'): void
}>()
</script>
Loading
Loading