-
Notifications
You must be signed in to change notification settings - Fork 424
[feat] Add Civitai model upload wizard #6694
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
Changes from all commits
422d9a7
0f0faba
60765b4
78afe71
73ecd06
15d5093
e83b92b
624c63a
f5564fd
803beb2
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 |
|---|---|---|
|
|
@@ -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' | ||
|
|
||
|
|
@@ -92,6 +95,7 @@ const props = defineProps<{ | |
| }>() | ||
|
|
||
| const { t } = useI18n() | ||
| const dialogStore = useDialogStore() | ||
|
|
||
| const emit = defineEmits<{ | ||
| 'asset-select': [asset: AssetDisplayItem] | ||
|
|
@@ -189,6 +193,15 @@ const { flags } = useFeatureFlags() | |
| const isUploadButtonEnabled = computed(() => flags.modelUploadButtonEnabled) | ||
|
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. [architecture] low Priority Issue: Feature flag tightly coupled to component - modelUploadButtonEnabled flag is directly accessed in component |
||
|
|
||
| function handleUploadClick() { | ||
| // Will be implemented in the future commit | ||
| dialogStore.showDialog({ | ||
| key: 'upload-model', | ||
| headerComponent: UploadModelDialogHeader, | ||
| component: UploadModelDialog, | ||
| props: { | ||
|
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. [quality] low Priority Issue: Inconsistent prop validation - some components lack proper prop validation and defaults |
||
| onUploadSuccess: async () => { | ||
| await execute() | ||
| } | ||
| } | ||
| }) | ||
| } | ||
| </script> | ||
| 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 }} | ||
christian-byrne marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| </p> | ||
| </div> | ||
|
|
||
| <!-- Model Type Selection --> | ||
| <div class="flex flex-col gap-2"> | ||
| <label class="text-sm text-muted"> | ||
| {{ $t('assetBrowser.modelTypeSelectorLabel') }} | ||
| </label> | ||
| <SingleSelect | ||
luke-mino-altherr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| <template> | ||
|
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. [architecture] medium Priority Issue: Single Responsibility Principle violation - UploadModelDialog handles wizard logic, validation, API calls, and state management |
||
| <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 { | ||
luke-mino-altherr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| width: 90vw; | ||
| max-width: 800px; | ||
| min-height: 400px; | ||
| } | ||
|
|
||
| @media (min-width: 640px) { | ||
| .upload-model-dialog { | ||
| width: auto; | ||
| min-width: 600px; | ||
| } | ||
| } | ||
| </style> | ||
| 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> |
| 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> |
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.
[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