💬 Commit message: Update 2026-02-15 05:04:35, 10 files, 374 lines
📁 Files changed: 10 📝 Lines changed: 374 • convert.sh • index-BQdjF_w0.css • index-CKJOpgtQ.js • index-DEHUU-Zz.js • index-Ljwp9hgM.css • index.html • App.vue • DownloadsPanel.vue • ModelCard.vue • app.ts
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Convert a safetensors model to q8_0 GGUF format
|
||||||
|
# Usage: ./scripts/convert.sh <input.safetensors>
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
echo "Usage: $0 <input.safetensors> [quantization]"
|
||||||
|
echo " quantization: f32, f16, q4_0, q5_0, q8_0 (default: q8_0)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
INPUT="$1"
|
||||||
|
QUANT="${2:-q8_0}"
|
||||||
|
|
||||||
|
if [[ ! -f "$INPUT" ]]; then
|
||||||
|
echo "Error: File not found: $INPUT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Derive output filename: model.safetensors -> model-q8_0.gguf
|
||||||
|
BASENAME=$(basename "$INPUT" .safetensors)
|
||||||
|
DIRNAME=$(dirname "$INPUT")
|
||||||
|
OUTPUT="${DIRNAME}/${BASENAME}-${QUANT}.gguf"
|
||||||
|
|
||||||
|
echo "==> Converting: $INPUT"
|
||||||
|
echo " Output: $OUTPUT"
|
||||||
|
echo " Quantization: $QUANT"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
sd -M convert -m "$INPUT" -o "$OUTPUT" --type "$QUANT"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "==> Done: $OUTPUT"
|
||||||
|
ls -lh "$OUTPUT"
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Tensors</title>
|
<title>Tensors</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CKJOpgtQ.js"></script>
|
<script type="module" crossorigin src="/assets/index-DEHUU-Zz.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BQdjF_w0.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Ljwp9hgM.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import { useAppStore } from '@/stores/app'
|
|||||||
import GenerateView from '@/components/GenerateView.vue'
|
import GenerateView from '@/components/GenerateView.vue'
|
||||||
import SearchView from '@/components/SearchView.vue'
|
import SearchView from '@/components/SearchView.vue'
|
||||||
import GalleryView from '@/components/GalleryView.vue'
|
import GalleryView from '@/components/GalleryView.vue'
|
||||||
|
import DownloadsPanel from '@/components/DownloadsPanel.vue'
|
||||||
|
|
||||||
const store = useAppStore()
|
const store = useAppStore()
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
store.loadModels()
|
store.loadModels()
|
||||||
|
store.pollDownloads() // Check for any active downloads on load
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -34,9 +36,31 @@ onMounted(() => {
|
|||||||
prepend-icon="mdi-image-multiple"
|
prepend-icon="mdi-image-multiple"
|
||||||
title="Gallery"
|
title="Gallery"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<v-divider class="my-2" />
|
||||||
|
|
||||||
|
<v-list-item
|
||||||
|
@click="store.showDownloadsPanel = true"
|
||||||
|
title="Downloads"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-badge
|
||||||
|
v-if="store.hasActiveDownloads"
|
||||||
|
:content="store.activeDownloads.length"
|
||||||
|
color="primary"
|
||||||
|
offset-x="-2"
|
||||||
|
offset-y="-2"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-download</v-icon>
|
||||||
|
</v-badge>
|
||||||
|
<v-icon v-else>mdi-download</v-icon>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
|
|
||||||
|
<DownloadsPanel />
|
||||||
|
|
||||||
<v-main>
|
<v-main>
|
||||||
<GenerateView v-if="store.currentView === 'generate'" />
|
<GenerateView v-if="store.currentView === 'generate'" />
|
||||||
<SearchView v-else-if="store.currentView === 'search'" />
|
<SearchView v-else-if="store.currentView === 'search'" />
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
|
||||||
|
const store = useAppStore()
|
||||||
|
|
||||||
|
// Start polling when panel opens or when there are active downloads
|
||||||
|
watch(() => store.showDownloadsPanel, (show) => {
|
||||||
|
if (show) {
|
||||||
|
store.startDownloadPolling()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Also poll on mount if there might be active downloads
|
||||||
|
onMounted(() => {
|
||||||
|
store.pollDownloads()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// Don't stop polling - let it continue for background tracking
|
||||||
|
})
|
||||||
|
|
||||||
|
// Auto-stop polling when no active downloads for a while
|
||||||
|
watch(() => store.hasActiveDownloads, (hasActive) => {
|
||||||
|
if (!hasActive && !store.showDownloadsPanel) {
|
||||||
|
// Give a grace period before stopping
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!store.hasActiveDownloads && !store.showDownloadsPanel) {
|
||||||
|
store.stopDownloadPolling()
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function formatBytes(bytes: number): string {
|
||||||
|
if (bytes >= 1024 * 1024 * 1024) return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB'
|
||||||
|
if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
||||||
|
if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
||||||
|
return bytes + ' B'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusColor(status: string): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'completed': return 'success'
|
||||||
|
case 'failed': return 'error'
|
||||||
|
case 'downloading': return 'primary'
|
||||||
|
default: return 'grey'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusIcon(status: string): string {
|
||||||
|
switch (status) {
|
||||||
|
case 'completed': return 'mdi-check-circle'
|
||||||
|
case 'failed': return 'mdi-alert-circle'
|
||||||
|
case 'downloading': return 'mdi-download'
|
||||||
|
default: return 'mdi-clock-outline'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-navigation-drawer
|
||||||
|
v-model="store.showDownloadsPanel"
|
||||||
|
location="right"
|
||||||
|
temporary
|
||||||
|
width="360"
|
||||||
|
>
|
||||||
|
<v-toolbar density="compact" color="surface">
|
||||||
|
<v-toolbar-title class="text-body-1">Downloads</v-toolbar-title>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn icon="mdi-close" variant="text" size="small" @click="store.showDownloadsPanel = false" />
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<v-list v-if="store.downloads.length > 0" density="compact">
|
||||||
|
<v-list-item
|
||||||
|
v-for="dl in store.downloads"
|
||||||
|
:key="dl.id"
|
||||||
|
:class="{ 'bg-surface-light': dl.status === 'downloading' }"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-icon :color="getStatusColor(dl.status)" class="mr-3">
|
||||||
|
{{ getStatusIcon(dl.status) }}
|
||||||
|
</v-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list-item-title class="text-body-2 font-weight-medium">
|
||||||
|
{{ dl.model_name || dl.filename }}
|
||||||
|
</v-list-item-title>
|
||||||
|
<v-list-item-subtitle class="text-caption">
|
||||||
|
{{ dl.version_name || '' }}
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
|
||||||
|
<!-- Progress bar for active downloads -->
|
||||||
|
<div v-if="dl.status === 'downloading'" class="mt-2">
|
||||||
|
<v-progress-linear
|
||||||
|
:model-value="dl.progress || 0"
|
||||||
|
color="primary"
|
||||||
|
height="6"
|
||||||
|
rounded
|
||||||
|
/>
|
||||||
|
<div class="d-flex justify-space-between text-caption text-grey mt-1">
|
||||||
|
<span>{{ dl.downloaded_str || formatBytes(dl.downloaded || 0) }} / {{ dl.total_str || formatBytes(dl.total || 0) }}</span>
|
||||||
|
<span class="text-primary">{{ dl.speed_str || '' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Queued status -->
|
||||||
|
<div v-else-if="dl.status === 'queued'" class="mt-2">
|
||||||
|
<v-progress-linear indeterminate color="grey" height="4" rounded />
|
||||||
|
<div class="text-caption text-grey mt-1">Queued...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Completed status -->
|
||||||
|
<div v-else-if="dl.status === 'completed'" class="text-caption text-success mt-1">
|
||||||
|
Downloaded to {{ dl.filename }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Failed status -->
|
||||||
|
<div v-else-if="dl.status === 'failed'" class="text-caption text-error mt-1">
|
||||||
|
{{ dl.error || 'Download failed' }}
|
||||||
|
</div>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<div v-else class="text-center text-grey pa-8">
|
||||||
|
<v-icon size="48" color="grey-darken-1">mdi-download-off</v-icon>
|
||||||
|
<p class="mt-4">No downloads</p>
|
||||||
|
</div>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
</template>
|
||||||
@@ -1,26 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onUnmounted } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import type { CivitaiModel } from '@/types'
|
import type { CivitaiModel } from '@/types'
|
||||||
import type { DownloadStatus } from '@/api/client'
|
|
||||||
import * as api from '@/api/client'
|
import * as api from '@/api/client'
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
model: CivitaiModel
|
model: CivitaiModel
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const store = useAppStore()
|
||||||
|
|
||||||
const showDialog = ref(false)
|
const showDialog = ref(false)
|
||||||
const loadingDetails = ref(false)
|
const loadingDetails = ref(false)
|
||||||
const modelDetails = ref<CivitaiModel | null>(null)
|
const modelDetails = ref<CivitaiModel | null>(null)
|
||||||
|
|
||||||
// Download tracking
|
|
||||||
const activeDownload = ref<DownloadStatus | null>(null)
|
|
||||||
const downloadingVersionId = ref<number | null>(null)
|
|
||||||
let pollInterval: ReturnType<typeof setInterval> | null = null
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (pollInterval) clearInterval(pollInterval)
|
|
||||||
})
|
|
||||||
|
|
||||||
const previewImage = computed(() => {
|
const previewImage = computed(() => {
|
||||||
const version = props.model.modelVersions?.[0]
|
const version = props.model.modelVersions?.[0]
|
||||||
return version?.images?.[0]?.url || ''
|
return version?.images?.[0]?.url || ''
|
||||||
@@ -53,41 +46,12 @@ async function openDetails() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function downloadVersion(versionId: number) {
|
async function downloadVersion(versionId: number) {
|
||||||
if (downloadingVersionId.value === versionId) return // Already downloading
|
if (store.isDownloading(versionId)) return // Already downloading
|
||||||
if (!confirm(`Download "${props.model.name}" to the server?`)) return
|
if (!confirm(`Download "${props.model.name}" to the server?`)) return
|
||||||
|
|
||||||
try {
|
const downloadId = await store.startDownload(versionId)
|
||||||
downloadingVersionId.value = versionId
|
if (!downloadId) {
|
||||||
const response = await api.downloadModel(undefined, versionId)
|
alert('Failed to start download')
|
||||||
|
|
||||||
// Start polling for progress
|
|
||||||
const downloadId = response.download_id
|
|
||||||
pollInterval = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
const status = await api.getDownloadStatus(downloadId)
|
|
||||||
activeDownload.value = status
|
|
||||||
|
|
||||||
if (status.status === 'completed' || status.status === 'failed') {
|
|
||||||
if (pollInterval) clearInterval(pollInterval)
|
|
||||||
pollInterval = null
|
|
||||||
|
|
||||||
if (status.status === 'failed') {
|
|
||||||
alert('Download failed: ' + (status.error || 'Unknown error'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep showing completed state briefly, then clear
|
|
||||||
setTimeout(() => {
|
|
||||||
activeDownload.value = null
|
|
||||||
downloadingVersionId.value = null
|
|
||||||
}, 3000)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to get download status:', e)
|
|
||||||
}
|
|
||||||
}, 500)
|
|
||||||
} catch (e: any) {
|
|
||||||
alert('Download failed: ' + e.message)
|
|
||||||
downloadingVersionId.value = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -144,26 +108,17 @@ async function downloadVersion(versionId: number) {
|
|||||||
</span>
|
</span>
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<!-- Download progress or button -->
|
<!-- Download progress or button -->
|
||||||
<template v-if="downloadingVersionId === model.modelVersions?.[0]?.id && activeDownload">
|
<template v-if="store.isDownloading(model.modelVersions?.[0]?.id || 0)">
|
||||||
<div class="download-progress">
|
<div class="download-progress">
|
||||||
<v-progress-linear
|
<v-progress-linear
|
||||||
:model-value="activeDownload.progress || 0"
|
:model-value="store.getDownloadProgress(model.modelVersions?.[0]?.id || 0)?.progress || 0"
|
||||||
:indeterminate="activeDownload.status === 'queued'"
|
:indeterminate="store.getDownloadProgress(model.modelVersions?.[0]?.id || 0)?.status === 'queued'"
|
||||||
:color="activeDownload.status === 'completed' ? 'success' : 'primary'"
|
color="primary"
|
||||||
height="6"
|
height="6"
|
||||||
rounded
|
rounded
|
||||||
/>
|
/>
|
||||||
<span class="text-caption text-grey">
|
<span class="text-caption text-grey">
|
||||||
<template v-if="activeDownload.status === 'completed'">
|
{{ store.getDownloadProgress(model.modelVersions?.[0]?.id || 0)?.progress?.toFixed(0) || 0 }}%
|
||||||
Done!
|
|
||||||
</template>
|
|
||||||
<template v-else-if="activeDownload.status === 'queued'">
|
|
||||||
Queued...
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
{{ activeDownload.downloaded_str }} / {{ activeDownload.total_str }}
|
|
||||||
<span class="text-primary ml-1">{{ activeDownload.speed_str }}</span>
|
|
||||||
</template>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -254,17 +209,17 @@ async function downloadVersion(versionId: number) {
|
|||||||
<v-list-item-title>{{ version.name }}</v-list-item-title>
|
<v-list-item-title>{{ version.name }}</v-list-item-title>
|
||||||
<v-list-item-subtitle>{{ version.baseModel }}</v-list-item-subtitle>
|
<v-list-item-subtitle>{{ version.baseModel }}</v-list-item-subtitle>
|
||||||
<template #append>
|
<template #append>
|
||||||
<template v-if="downloadingVersionId === version.id && activeDownload">
|
<template v-if="store.isDownloading(version.id)">
|
||||||
<div class="download-progress-dialog">
|
<div class="download-progress-dialog">
|
||||||
<v-progress-linear
|
<v-progress-linear
|
||||||
:model-value="activeDownload.progress || 0"
|
:model-value="store.getDownloadProgress(version.id)?.progress || 0"
|
||||||
:indeterminate="activeDownload.status === 'queued'"
|
:indeterminate="store.getDownloadProgress(version.id)?.status === 'queued'"
|
||||||
:color="activeDownload.status === 'completed' ? 'success' : 'primary'"
|
color="primary"
|
||||||
height="6"
|
height="6"
|
||||||
rounded
|
rounded
|
||||||
/>
|
/>
|
||||||
<span class="text-caption text-grey">
|
<span class="text-caption text-grey">
|
||||||
{{ activeDownload.progress?.toFixed(0) || 0 }}%
|
{{ store.getDownloadProgress(version.id)?.progress?.toFixed(0) || 0 }}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import type { Model, LoRA, ResolutionPreset } from '@/types'
|
import type { Model, LoRA, ResolutionPreset } from '@/types'
|
||||||
|
import type { DownloadStatus } from '@/api/client'
|
||||||
import * as api from '@/api/client'
|
import * as api from '@/api/client'
|
||||||
|
|
||||||
export const useAppStore = defineStore('app', () => {
|
export const useAppStore = defineStore('app', () => {
|
||||||
// Navigation
|
// Navigation
|
||||||
const currentView = ref<'generate' | 'search' | 'gallery'>('generate')
|
const currentView = ref<'generate' | 'search' | 'gallery'>('generate')
|
||||||
|
|
||||||
|
// Downloads
|
||||||
|
const downloads = ref<DownloadStatus[]>([])
|
||||||
|
const showDownloadsPanel = ref(false)
|
||||||
|
let downloadPollInterval: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
// Models
|
// Models
|
||||||
const models = ref<Model[]>([])
|
const models = ref<Model[]>([])
|
||||||
const loras = ref<LoRA[]>([])
|
const loras = ref<LoRA[]>([])
|
||||||
@@ -97,6 +103,57 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Downloads - computed
|
||||||
|
const activeDownloads = computed(() =>
|
||||||
|
downloads.value.filter(d => d.status === 'downloading' || d.status === 'queued')
|
||||||
|
)
|
||||||
|
const hasActiveDownloads = computed(() => activeDownloads.value.length > 0)
|
||||||
|
|
||||||
|
// Downloads - actions
|
||||||
|
async function pollDownloads() {
|
||||||
|
try {
|
||||||
|
const res = await api.getActiveDownloads()
|
||||||
|
downloads.value = res.downloads || []
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to poll downloads:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDownloadPolling() {
|
||||||
|
if (downloadPollInterval) return
|
||||||
|
pollDownloads() // Initial fetch
|
||||||
|
downloadPollInterval = setInterval(pollDownloads, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopDownloadPolling() {
|
||||||
|
if (downloadPollInterval) {
|
||||||
|
clearInterval(downloadPollInterval)
|
||||||
|
downloadPollInterval = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startDownload(versionId: number): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const response = await api.downloadModel(undefined, versionId)
|
||||||
|
startDownloadPolling()
|
||||||
|
showDownloadsPanel.value = true
|
||||||
|
return response.download_id
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('Failed to start download:', e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDownloading(versionId: number): boolean {
|
||||||
|
return downloads.value.some(
|
||||||
|
d => d.version_id === versionId && (d.status === 'downloading' || d.status === 'queued')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDownloadProgress(versionId: number): DownloadStatus | undefined {
|
||||||
|
return downloads.value.find(d => d.version_id === versionId)
|
||||||
|
}
|
||||||
|
|
||||||
async function switchModel(modelPath: string) {
|
async function switchModel(modelPath: string) {
|
||||||
if (modelPath === activeModel.value) return
|
if (modelPath === activeModel.value) return
|
||||||
|
|
||||||
@@ -169,8 +226,20 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
switchMessage,
|
switchMessage,
|
||||||
switchError,
|
switchError,
|
||||||
|
|
||||||
|
// Downloads
|
||||||
|
downloads,
|
||||||
|
activeDownloads,
|
||||||
|
hasActiveDownloads,
|
||||||
|
showDownloadsPanel,
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
loadModels,
|
loadModels,
|
||||||
switchModel,
|
switchModel,
|
||||||
|
startDownload,
|
||||||
|
pollDownloads,
|
||||||
|
startDownloadPolling,
|
||||||
|
stopDownloadPolling,
|
||||||
|
isDownloading,
|
||||||
|
getDownloadProgress,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user