mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 07:05:43 -03:00
checkpoint
This commit is contained in:
@@ -40,6 +40,7 @@ class ApiRoutes:
|
|||||||
app.router.add_post('/api/fetch-all-civitai', routes.fetch_all_civitai)
|
app.router.add_post('/api/fetch-all-civitai', routes.fetch_all_civitai)
|
||||||
app.router.add_get('/ws/fetch-progress', ws_manager.handle_connection)
|
app.router.add_get('/ws/fetch-progress', ws_manager.handle_connection)
|
||||||
app.router.add_get('/api/lora-roots', routes.get_lora_roots)
|
app.router.add_get('/api/lora-roots', routes.get_lora_roots)
|
||||||
|
app.router.add_get('/api/folders', routes.get_folders)
|
||||||
app.router.add_get('/api/civitai/versions/{model_id}', routes.get_civitai_versions)
|
app.router.add_get('/api/civitai/versions/{model_id}', routes.get_civitai_versions)
|
||||||
app.router.add_post('/api/download-lora', routes.download_lora)
|
app.router.add_post('/api/download-lora', routes.download_lora)
|
||||||
app.router.add_post('/api/settings', routes.update_settings)
|
app.router.add_post('/api/settings', routes.update_settings)
|
||||||
@@ -521,6 +522,13 @@ class ApiRoutes:
|
|||||||
'roots': config.loras_roots
|
'roots': config.loras_roots
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async def get_folders(self, request: web.Request) -> web.Response:
|
||||||
|
"""Get all folders in the cache"""
|
||||||
|
cache = await self.scanner.get_cached_data()
|
||||||
|
return web.json_response({
|
||||||
|
'folders': cache.folders
|
||||||
|
})
|
||||||
|
|
||||||
async def get_civitai_versions(self, request: web.Request) -> web.Response:
|
async def get_civitai_versions(self, request: web.Request) -> web.Response:
|
||||||
"""Get available versions for a Civitai model with local availability info"""
|
"""Get available versions for a Civitai model with local availability info"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -180,7 +180,6 @@ class RecipeRoutes:
|
|||||||
|
|
||||||
# Extract metadata from the image using ExifUtils
|
# Extract metadata from the image using ExifUtils
|
||||||
user_comment = ExifUtils.extract_user_comment(temp_path)
|
user_comment = ExifUtils.extract_user_comment(temp_path)
|
||||||
print(f"User comment: {user_comment}", file=sys.stderr)
|
|
||||||
|
|
||||||
# If no metadata found, return a more specific error
|
# If no metadata found, return a more specific error
|
||||||
if not user_comment:
|
if not user_comment:
|
||||||
@@ -191,7 +190,6 @@ class RecipeRoutes:
|
|||||||
|
|
||||||
# Parse the recipe metadata
|
# Parse the recipe metadata
|
||||||
metadata = ExifUtils.parse_recipe_metadata(user_comment)
|
metadata = ExifUtils.parse_recipe_metadata(user_comment)
|
||||||
print(f"Metadata: {metadata}", file=sys.stderr)
|
|
||||||
|
|
||||||
# Look for Civitai resources in the metadata
|
# Look for Civitai resources in the metadata
|
||||||
civitai_resources = metadata.get('loras', [])
|
civitai_resources = metadata.get('loras', [])
|
||||||
@@ -220,7 +218,6 @@ class RecipeRoutes:
|
|||||||
|
|
||||||
# Get additional info from Civitai
|
# Get additional info from Civitai
|
||||||
civitai_info = await self.civitai_client.get_model_version_info(model_version_id)
|
civitai_info = await self.civitai_client.get_model_version_info(model_version_id)
|
||||||
print(f"Civitai info: {civitai_info}", file=sys.stderr)
|
|
||||||
|
|
||||||
# Check if this LoRA exists locally by SHA256 hash
|
# Check if this LoRA exists locally by SHA256 hash
|
||||||
exists_locally = False
|
exists_locally = False
|
||||||
|
|||||||
@@ -3,6 +3,341 @@
|
|||||||
margin: var(--space-2) 0;
|
margin: var(--space-2) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* File Input Styles */
|
||||||
|
.file-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-wrapper input[type="file"] {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
background: var(--lora-accent);
|
||||||
|
color: var(--lora-text);
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-button:hover {
|
||||||
|
background: oklch(from var(--lora-accent) l c h / 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-wrapper:hover .file-input-button {
|
||||||
|
background: oklch(from var(--lora-accent) l c h / 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Recipe Details Layout */
|
||||||
|
.recipe-details-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 200px 1fr;
|
||||||
|
gap: var(--space-3);
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-image-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--lora-surface);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-image img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-form-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tags Input Styles */
|
||||||
|
.tag-input-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-input-container input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: var(--space-1);
|
||||||
|
min-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: var(--lora-surface);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-tag i {
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-tag i:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--lora-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-tags {
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.6;
|
||||||
|
font-size: 0.9em;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LoRAs List Styles */
|
||||||
|
.loras-list {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin: var(--space-2) 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lora-item {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-2);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
background: var(--bg-color);
|
||||||
|
margin: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lora-item.exists-locally {
|
||||||
|
background: oklch(var(--lora-accent) / 0.05);
|
||||||
|
border-left: 4px solid var(--lora-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lora-item.missing-locally {
|
||||||
|
border-left: 4px solid var(--lora-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lora-thumbnail {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lora-thumbnail img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lora-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lora-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lora-content h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: var(--text-color);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lora-info {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lora-info .base-model {
|
||||||
|
background: oklch(var(--lora-accent) / 0.1);
|
||||||
|
color: var(--lora-accent);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lora-version {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weight-badge {
|
||||||
|
background: var(--lora-surface);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Missing LoRAs List */
|
||||||
|
.missing-loras-list {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin: var(--space-2) 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: var(--space-1);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
background: var(--lora-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.missing-lora-item {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-1);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.missing-lora-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.missing-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--lora-error);
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.missing-badge i {
|
||||||
|
margin-right: 4px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lora-count-info {
|
||||||
|
font-size: 0.85em;
|
||||||
|
opacity: 0.8;
|
||||||
|
font-weight: normal;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Location Selection Styles */
|
||||||
|
.location-selection {
|
||||||
|
margin: var(--space-2) 0;
|
||||||
|
padding: var(--space-2);
|
||||||
|
background: var(--lora-surface);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reuse folder browser and path preview styles from download-modal.css */
|
||||||
|
.folder-browser {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
padding: var(--space-1);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-item {
|
||||||
|
padding: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-item:hover {
|
||||||
|
background: var(--lora-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-item.selected {
|
||||||
|
background: oklch(var(--lora-accent) / 0.1);
|
||||||
|
border: 1px solid var(--lora-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-preview {
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
padding: var(--space-2);
|
||||||
|
background: var(--bg-color);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
border: 1px dashed var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-preview label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 0.9em;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-display {
|
||||||
|
padding: var(--space-1);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
opacity: 0.85;
|
||||||
|
background: var(--lora-surface);
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input Group Styles */
|
||||||
.input-group {
|
.input-group {
|
||||||
margin-bottom: var(--space-2);
|
margin-bottom: var(--space-2);
|
||||||
}
|
}
|
||||||
@@ -29,447 +364,30 @@
|
|||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Image Upload Styles */
|
/* Modal Actions */
|
||||||
.image-upload-container {
|
.modal-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
justify-content: flex-end;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
margin-bottom: var(--space-3);
|
margin-top: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-preview {
|
/* Dark theme adjustments */
|
||||||
width: 100%;
|
[data-theme="dark"] .lora-item {
|
||||||
height: 200px;
|
|
||||||
border: 2px dashed var(--border-color);
|
|
||||||
border-radius: var(--border-radius-sm);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--bg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-preview img {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-preview .placeholder {
|
|
||||||
color: var(--text-color);
|
|
||||||
opacity: 0.5;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-input-wrapper {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-input-wrapper input[type="file"] {
|
|
||||||
opacity: 0;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-input-wrapper .file-input-button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
background: var(--lora-surface);
|
background: var(--lora-surface);
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: var(--border-radius-xs);
|
|
||||||
color: var(--text-color);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-input-wrapper:hover .file-input-button {
|
[data-theme="dark"] .recipe-tag {
|
||||||
background: var(--lora-surface-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Recipe Details Styles */
|
|
||||||
.recipe-details-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.recipe-name-container {
|
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.recipe-name-container label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recipe-name-container input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: var(--border-radius-xs);
|
|
||||||
background: var(--bg-color);
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags-section {
|
|
||||||
margin-bottom: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags-section label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-input-container {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-input-container input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: var(--border-radius-xs);
|
|
||||||
background: var(--bg-color);
|
|
||||||
color: var(--text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags-container {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 8px;
|
|
||||||
min-height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recipe-tag {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
background: var(--lora-surface);
|
|
||||||
color: var(--text-color);
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: var(--border-radius-xs);
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recipe-tag i {
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0.7;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recipe-tag i:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-tags {
|
|
||||||
color: var(--text-color);
|
|
||||||
opacity: 0.5;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* LoRAs List Styles */
|
|
||||||
.loras-list {
|
|
||||||
max-height: 300px;
|
|
||||||
overflow-y: auto;
|
|
||||||
margin: var(--space-2) 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lora-item {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--space-2);
|
|
||||||
padding: var(--space-2);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: var(--border-radius-sm);
|
|
||||||
background: var(--bg-color);
|
|
||||||
margin: 1px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lora-item.exists-locally {
|
|
||||||
background: oklch(var(--lora-accent) / 0.05);
|
|
||||||
border-left: 4px solid var(--lora-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lora-item.missing-locally {
|
|
||||||
background: oklch(var(--lora-error) / 0.05);
|
|
||||||
border-left: 4px solid var(--lora-error);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lora-thumbnail {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
border-radius: var(--border-radius-xs);
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--bg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lora-thumbnail img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lora-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lora-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lora-content h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1em;
|
|
||||||
color: var(--text-color);
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lora-info {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lora-info .base-model {
|
|
||||||
background: oklch(var(--lora-accent) / 0.1);
|
|
||||||
color: var(--lora-accent);
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: var(--border-radius-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.weight-badge {
|
|
||||||
background: var(--lora-surface);
|
|
||||||
color: var(--text-color);
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: var(--border-radius-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lora-meta {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
font-size: 0.85em;
|
|
||||||
color: var(--text-color);
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lora-meta span {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Status Badges */
|
|
||||||
.local-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
background: var(--lora-accent);
|
|
||||||
color: var(--lora-text);
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: var(--border-radius-xs);
|
|
||||||
font-size: 0.8em;
|
|
||||||
font-weight: 500;
|
|
||||||
white-space: nowrap;
|
|
||||||
flex-shrink: 0;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.local-badge i {
|
|
||||||
margin-right: 4px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.missing-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
background: var(--lora-error);
|
|
||||||
color: var(--lora-text);
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: var(--border-radius-xs);
|
|
||||||
font-size: 0.8em;
|
|
||||||
font-weight: 500;
|
|
||||||
white-space: nowrap;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.missing-badge i {
|
|
||||||
margin-right: 4px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.local-path {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
right: 0;
|
|
||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: var(--border-radius-xs);
|
|
||||||
padding: var(--space-1);
|
|
||||||
margin-top: 4px;
|
|
||||||
font-size: 0.9em;
|
|
||||||
color: var(--text-color);
|
|
||||||
white-space: normal;
|
|
||||||
word-break: break-all;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
z-index: 1;
|
|
||||||
min-width: 200px;
|
|
||||||
max-width: 300px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.local-badge:hover .local-path {
|
/* Responsive adjustments */
|
||||||
display: block;
|
@media (max-width: 768px) {
|
||||||
}
|
.recipe-details-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
/* Missing LoRAs List */
|
.recipe-image-container {
|
||||||
.missing-loras-list {
|
height: 150px;
|
||||||
max-height: 150px;
|
}
|
||||||
overflow-y: auto;
|
|
||||||
margin: var(--space-2) 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.missing-lora-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px;
|
|
||||||
background: oklch(var(--lora-error) / 0.05);
|
|
||||||
border-left: 4px solid var(--lora-error);
|
|
||||||
border-radius: var(--border-radius-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lora-name {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lora-type {
|
|
||||||
font-size: 0.9em;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Folder Browser Styles */
|
|
||||||
.folder-browser {
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: var(--border-radius-xs);
|
|
||||||
padding: var(--space-1);
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal Header Styles - Updated to match download-modal */
|
|
||||||
.modal-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
/* align-items: center; */
|
|
||||||
margin-bottom: var(--space-3);
|
|
||||||
padding-bottom: var(--space-2);
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-modal {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0.7;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
padding: 0 5px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-modal:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Recipe Details Layout */
|
|
||||||
.recipe-details-layout {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--space-3);
|
|
||||||
margin-bottom: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.recipe-image-container {
|
|
||||||
flex: 0 0 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recipe-image {
|
|
||||||
width: 100%;
|
|
||||||
height: 200px;
|
|
||||||
border-radius: var(--border-radius-sm);
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
background: var(--bg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.recipe-image img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recipe-form-container {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Simplify file input for step 1 */
|
|
||||||
.file-input-wrapper {
|
|
||||||
margin: var(--space-3) auto;
|
|
||||||
max-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Update LoRA item styles to include version */
|
|
||||||
.lora-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lora-version {
|
|
||||||
font-size: 0.85em;
|
|
||||||
color: var(--text-color);
|
|
||||||
opacity: 0.7;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lora-count-info {
|
|
||||||
font-size: 0.85em;
|
|
||||||
font-weight: normal;
|
|
||||||
color: var(--text-color);
|
|
||||||
opacity: 0.8;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,12 @@ export class ImportManager {
|
|||||||
this.loadingManager = new LoadingManager();
|
this.loadingManager = new LoadingManager();
|
||||||
this.folderClickHandler = null;
|
this.folderClickHandler = null;
|
||||||
this.updateTargetPath = this.updateTargetPath.bind(this);
|
this.updateTargetPath = this.updateTargetPath.bind(this);
|
||||||
|
|
||||||
|
// 添加对注入样式的引用
|
||||||
|
this.injectedStyles = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
showImportModal() {
|
showImportModal() {
|
||||||
console.log('Showing import modal...');
|
|
||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
// Check if modal exists
|
// Check if modal exists
|
||||||
const modal = document.getElementById('importModal');
|
const modal = document.getElementById('importModal');
|
||||||
@@ -37,13 +39,26 @@ export class ImportManager {
|
|||||||
modalManager.showModal('importModal', null, () => {
|
modalManager.showModal('importModal', null, () => {
|
||||||
// Cleanup handler when modal closes
|
// Cleanup handler when modal closes
|
||||||
this.cleanupFolderBrowser();
|
this.cleanupFolderBrowser();
|
||||||
|
|
||||||
|
// 移除任何强制样式
|
||||||
|
this.removeInjectedStyles();
|
||||||
});
|
});
|
||||||
this.resetSteps();
|
this.resetSteps();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加移除注入样式的方法
|
||||||
|
removeInjectedStyles() {
|
||||||
|
if (this.injectedStyles && this.injectedStyles.parentNode) {
|
||||||
|
this.injectedStyles.parentNode.removeChild(this.injectedStyles);
|
||||||
|
this.injectedStyles = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resetSteps() {
|
resetSteps() {
|
||||||
document.querySelectorAll('.import-step').forEach(step => step.style.display = 'none');
|
// 移除可能存在的强制样式
|
||||||
document.getElementById('uploadStep').style.display = 'block';
|
this.removeInjectedStyles();
|
||||||
|
|
||||||
|
this.showStep('uploadStep');
|
||||||
|
|
||||||
// Reset file input
|
// Reset file input
|
||||||
const fileInput = document.getElementById('recipeImageUpload');
|
const fileInput = document.getElementById('recipeImageUpload');
|
||||||
@@ -147,8 +162,7 @@ export class ImportManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showRecipeDetailsStep() {
|
showRecipeDetailsStep() {
|
||||||
document.getElementById('uploadStep').style.display = 'none';
|
this.showStep('detailsStep');
|
||||||
document.getElementById('detailsStep').style.display = 'block';
|
|
||||||
|
|
||||||
// Set default recipe name from image filename
|
// Set default recipe name from image filename
|
||||||
const recipeName = document.getElementById('recipeName');
|
const recipeName = document.getElementById('recipeName');
|
||||||
@@ -277,6 +291,8 @@ export class ImportManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Proceeding from details, missing LoRAs:', this.missingLoras.length);
|
||||||
|
|
||||||
// If we have missing LoRAs, go to location step
|
// If we have missing LoRAs, go to location step
|
||||||
if (this.missingLoras.length > 0) {
|
if (this.missingLoras.length > 0) {
|
||||||
this.proceedToLocation();
|
this.proceedToLocation();
|
||||||
@@ -287,239 +303,242 @@ export class ImportManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async proceedToLocation() {
|
async proceedToLocation() {
|
||||||
document.getElementById('detailsStep').style.display = 'none';
|
// 先移除可能已有的样式
|
||||||
document.getElementById('locationStep').style.display = 'block';
|
this.removeInjectedStyles();
|
||||||
|
|
||||||
|
// 添加强制CSS覆盖
|
||||||
|
this.injectedStyles = document.createElement('style');
|
||||||
|
this.injectedStyles.innerHTML = `
|
||||||
|
#locationStep {
|
||||||
|
display: block !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
position: static !important;
|
||||||
|
z-index: 10000 !important;
|
||||||
|
width: auto !important;
|
||||||
|
height: auto !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(this.injectedStyles);
|
||||||
|
console.log('Added override CSS to force visibility');
|
||||||
|
|
||||||
|
this.showStep('locationStep');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.loadingManager.showSimpleLoading('Loading download options...');
|
// Fetch LoRA roots
|
||||||
|
const rootsResponse = await fetch('/api/lora-roots');
|
||||||
const response = await fetch('/api/lora-roots');
|
if (!rootsResponse.ok) {
|
||||||
if (!response.ok) {
|
throw new Error(`Failed to fetch LoRA roots: ${rootsResponse.status}`);
|
||||||
throw new Error('Failed to fetch LoRA roots');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const rootsData = await rootsResponse.json();
|
||||||
const loraRoot = document.getElementById('importLoraRoot');
|
const loraRoot = document.getElementById('importLoraRoot');
|
||||||
|
if (loraRoot) {
|
||||||
// Check if we have roots
|
loraRoot.innerHTML = rootsData.roots.map(root =>
|
||||||
if (!data.roots || data.roots.length === 0) {
|
`<option value="${root}">${root}</option>`
|
||||||
throw new Error('No LoRA root directories configured');
|
).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate roots dropdown
|
// Fetch folders
|
||||||
loraRoot.innerHTML = data.roots.map(root =>
|
const foldersResponse = await fetch('/api/folders');
|
||||||
`<option value="${root}">${root}</option>`
|
if (!foldersResponse.ok) {
|
||||||
).join('');
|
throw new Error(`Failed to fetch folders: ${foldersResponse.status}`);
|
||||||
|
|
||||||
// Initialize folder browser after loading roots
|
|
||||||
await this.initializeFolderBrowser();
|
|
||||||
|
|
||||||
// Display missing LoRAs
|
|
||||||
const missingLorasList = document.getElementById('missingLorasList');
|
|
||||||
if (missingLorasList) {
|
|
||||||
missingLorasList.innerHTML = this.missingLoras.map(lora => `
|
|
||||||
<div class="missing-lora-item">
|
|
||||||
<div class="lora-name">${lora.name}</div>
|
|
||||||
<div class="lora-type">${lora.type || 'lora'}</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update target path display
|
const foldersData = await foldersResponse.json();
|
||||||
this.updateTargetPath();
|
const folderBrowser = document.getElementById('importFolderBrowser');
|
||||||
|
if (folderBrowser) {
|
||||||
|
folderBrowser.innerHTML = foldersData.folders.map(folder =>
|
||||||
|
folder ? `<div class="folder-item" data-folder="${folder}">${folder}</div>` : ''
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize folder browser after loading data
|
||||||
|
this.initializeFolderBrowser();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in proceedToLocation:', error);
|
console.error('Error in API calls:', error);
|
||||||
showToast(error.message, 'error');
|
showToast(error.message, 'error');
|
||||||
// Go back to details step on error
|
|
||||||
this.backToDetails();
|
|
||||||
} finally {
|
|
||||||
this.loadingManager.hide();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
backToUpload() {
|
backToUpload() {
|
||||||
document.getElementById('detailsStep').style.display = 'none';
|
this.showStep('uploadStep');
|
||||||
document.getElementById('uploadStep').style.display = 'block';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
backToDetails() {
|
backToDetails() {
|
||||||
document.getElementById('locationStep').style.display = 'none';
|
this.showStep('detailsStep');
|
||||||
document.getElementById('detailsStep').style.display = 'block';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveRecipe() {
|
async saveRecipe() {
|
||||||
|
if (!this.recipeName) {
|
||||||
|
showToast('Please enter a recipe name', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// If we're in the location step, we need to download missing LoRAs first
|
// First save the recipe
|
||||||
if (document.getElementById('locationStep').style.display !== 'none') {
|
|
||||||
const loraRoot = document.getElementById('importLoraRoot').value;
|
|
||||||
const newFolder = document.getElementById('importNewFolder').value.trim();
|
|
||||||
|
|
||||||
if (!loraRoot) {
|
|
||||||
showToast('Please select a LoRA root directory', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct relative path
|
|
||||||
let targetFolder = '';
|
|
||||||
if (this.selectedFolder) {
|
|
||||||
targetFolder = this.selectedFolder;
|
|
||||||
}
|
|
||||||
if (newFolder) {
|
|
||||||
targetFolder = targetFolder ?
|
|
||||||
`${targetFolder}/${newFolder}` : newFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show loading with progress bar for download
|
|
||||||
this.loadingManager.show('Downloading missing LoRAs...', 0);
|
|
||||||
|
|
||||||
// Setup WebSocket for progress updates
|
|
||||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
|
||||||
const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/fetch-progress`);
|
|
||||||
ws.onmessage = (event) => {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
if (data.status === 'progress') {
|
|
||||||
this.loadingManager.setProgress(data.progress);
|
|
||||||
this.loadingManager.setStatus(`Downloading: ${data.progress}%`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Download missing LoRAs
|
|
||||||
const downloadResponse = await fetch('/api/recipes/download-missing-loras', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
loras: this.missingLoras,
|
|
||||||
lora_root: loraRoot,
|
|
||||||
relative_path: targetFolder
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!downloadResponse.ok) {
|
|
||||||
throw new Error(await downloadResponse.text());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update missing LoRAs with downloaded paths
|
|
||||||
const downloadResult = await downloadResponse.json();
|
|
||||||
this.recipeData.loras = this.recipeData.loras.map(lora => {
|
|
||||||
const downloaded = downloadResult.downloaded.find(d => d.id === lora.id);
|
|
||||||
if (downloaded) {
|
|
||||||
return {
|
|
||||||
...lora,
|
|
||||||
existsLocally: true,
|
|
||||||
localPath: downloaded.localPath
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return lora;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now save the recipe
|
|
||||||
this.loadingManager.showSimpleLoading('Saving recipe...');
|
this.loadingManager.showSimpleLoading('Saving recipe...');
|
||||||
|
|
||||||
// Create form data for recipe save
|
// Create form data for save request
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('image', this.recipeImage);
|
formData.append('image', this.recipeImage);
|
||||||
formData.append('name', this.recipeName);
|
formData.append('name', this.recipeName);
|
||||||
formData.append('tags', JSON.stringify(this.recipeTags));
|
formData.append('tags', JSON.stringify(this.recipeTags));
|
||||||
formData.append('recipe_data', JSON.stringify(this.recipeData));
|
formData.append('metadata', JSON.stringify(this.recipeData));
|
||||||
|
|
||||||
// Save recipe
|
// Send save request
|
||||||
const saveResponse = await fetch('/api/recipes/save', {
|
const response = await fetch('/api/recipes/save', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!saveResponse.ok) {
|
const result = await response.json();
|
||||||
throw new Error(await saveResponse.text());
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || 'Failed to save recipe');
|
||||||
}
|
}
|
||||||
|
|
||||||
showToast('Recipe saved successfully', 'success');
|
// Show success message for recipe save
|
||||||
|
showToast(`Recipe "${this.recipeName}" saved successfully`, 'success');
|
||||||
|
|
||||||
|
// Check if we need to download LoRAs
|
||||||
|
if (this.missingLoras.length > 0) {
|
||||||
|
// For download, we need to validate the target path
|
||||||
|
const loraRoot = document.getElementById('importLoraRoot')?.value;
|
||||||
|
if (!loraRoot) {
|
||||||
|
throw new Error('Please select a LoRA root directory');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build target path
|
||||||
|
let targetPath = loraRoot;
|
||||||
|
if (this.selectedFolder) {
|
||||||
|
targetPath += '/' + this.selectedFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFolder = document.getElementById('importNewFolder')?.value?.trim();
|
||||||
|
if (newFolder) {
|
||||||
|
targetPath += '/' + newFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up WebSocket for progress updates
|
||||||
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||||
|
const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/fetch-progress`);
|
||||||
|
|
||||||
|
// Download missing LoRAs sequentially
|
||||||
|
this.loadingManager.show('Downloading LoRAs...', 0);
|
||||||
|
|
||||||
|
let completedDownloads = 0;
|
||||||
|
for (let i = 0; i < this.missingLoras.length; i++) {
|
||||||
|
const lora = this.missingLoras[i];
|
||||||
|
|
||||||
|
// Update overall progress
|
||||||
|
this.loadingManager.setStatus(`Downloading LoRA ${i+1}/${this.missingLoras.length}: ${lora.name}`);
|
||||||
|
|
||||||
|
// Set up progress tracking for current download
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.status === 'progress') {
|
||||||
|
// Calculate overall progress: completed files + current file progress
|
||||||
|
const overallProgress = Math.floor(
|
||||||
|
(completedDownloads + data.progress/100) / this.missingLoras.length * 100
|
||||||
|
);
|
||||||
|
this.loadingManager.setProgress(overallProgress);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Download the LoRA
|
||||||
|
const response = await fetch('/api/download-lora', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
download_url: lora.downloadUrl,
|
||||||
|
lora_root: loraRoot,
|
||||||
|
relative_path: targetPath.replace(loraRoot + '/', '')
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error(`Failed to download LoRA ${lora.name}: ${errorText}`);
|
||||||
|
// Continue with next download
|
||||||
|
} else {
|
||||||
|
completedDownloads++;
|
||||||
|
}
|
||||||
|
} catch (downloadError) {
|
||||||
|
console.error(`Error downloading LoRA ${lora.name}:`, downloadError);
|
||||||
|
// Continue with next download
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close WebSocket
|
||||||
|
ws.close();
|
||||||
|
|
||||||
|
// Show final completion message
|
||||||
|
if (completedDownloads === this.missingLoras.length) {
|
||||||
|
showToast(`All ${completedDownloads} LoRAs downloaded successfully`, 'success');
|
||||||
|
} else {
|
||||||
|
showToast(`Downloaded ${completedDownloads} of ${this.missingLoras.length} LoRAs`, 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal and reload recipes
|
||||||
modalManager.closeModal('importModal');
|
modalManager.closeModal('importModal');
|
||||||
|
|
||||||
// Reload recipes
|
// Refresh the recipe list if needed
|
||||||
window.location.reload();
|
if (typeof refreshRecipes === 'function') {
|
||||||
|
refreshRecipes();
|
||||||
|
} else {
|
||||||
|
// Fallback to reloading the page
|
||||||
|
resetAndReload();
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('Error saving recipe:', error);
|
||||||
showToast(error.message, 'error');
|
showToast(error.message, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
this.loadingManager.hide();
|
this.loadingManager.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new method to handle folder selection
|
initializeFolderBrowser() {
|
||||||
async initializeFolderBrowser() {
|
|
||||||
const folderBrowser = document.getElementById('importFolderBrowser');
|
const folderBrowser = document.getElementById('importFolderBrowser');
|
||||||
if (!folderBrowser) return;
|
if (!folderBrowser) return;
|
||||||
|
|
||||||
// Cleanup existing handler if any
|
// Cleanup existing handler if any
|
||||||
this.cleanupFolderBrowser();
|
this.cleanupFolderBrowser();
|
||||||
|
|
||||||
try {
|
// Create new handler
|
||||||
// Get the selected root
|
this.folderClickHandler = (event) => {
|
||||||
const loraRoot = document.getElementById('importLoraRoot').value;
|
const folderItem = event.target.closest('.folder-item');
|
||||||
if (!loraRoot) {
|
if (!folderItem) return;
|
||||||
folderBrowser.innerHTML = '<div class="empty-folder">Please select a LoRA root directory</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch folders for the selected root
|
if (folderItem.classList.contains('selected')) {
|
||||||
const response = await fetch(`/api/folders?root=${encodeURIComponent(loraRoot)}`);
|
folderItem.classList.remove('selected');
|
||||||
if (!response.ok) {
|
this.selectedFolder = '';
|
||||||
throw new Error('Failed to fetch folders');
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
// Display folders
|
|
||||||
if (data.folders && data.folders.length > 0) {
|
|
||||||
folderBrowser.innerHTML = data.folders.map(folder => `
|
|
||||||
<div class="folder-item" data-folder="${folder}">
|
|
||||||
<i class="fas fa-folder"></i> ${folder}
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
} else {
|
} else {
|
||||||
folderBrowser.innerHTML = '<div class="empty-folder">No folders found</div>';
|
folderBrowser.querySelectorAll('.folder-item').forEach(f =>
|
||||||
|
f.classList.remove('selected'));
|
||||||
|
folderItem.classList.add('selected');
|
||||||
|
this.selectedFolder = folderItem.dataset.folder;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new handler
|
// Update path display after folder selection
|
||||||
this.folderClickHandler = (event) => {
|
this.updateTargetPath();
|
||||||
const folderItem = event.target.closest('.folder-item');
|
};
|
||||||
if (!folderItem) return;
|
|
||||||
|
|
||||||
if (folderItem.classList.contains('selected')) {
|
// Add the new handler
|
||||||
folderItem.classList.remove('selected');
|
folderBrowser.addEventListener('click', this.folderClickHandler);
|
||||||
this.selectedFolder = '';
|
|
||||||
} else {
|
|
||||||
folderBrowser.querySelectorAll('.folder-item').forEach(f =>
|
|
||||||
f.classList.remove('selected'));
|
|
||||||
folderItem.classList.add('selected');
|
|
||||||
this.selectedFolder = folderItem.dataset.folder;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update path display after folder selection
|
|
||||||
this.updateTargetPath();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add the new handler
|
|
||||||
folderBrowser.addEventListener('click', this.folderClickHandler);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error initializing folder browser:', error);
|
|
||||||
folderBrowser.innerHTML = `<div class="error-message">Error: ${error.message}</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add event listeners for path updates
|
// Add event listeners for path updates
|
||||||
const loraRoot = document.getElementById('importLoraRoot');
|
const loraRoot = document.getElementById('importLoraRoot');
|
||||||
const newFolder = document.getElementById('importNewFolder');
|
const newFolder = document.getElementById('importNewFolder');
|
||||||
|
|
||||||
loraRoot.addEventListener('change', async () => {
|
if (loraRoot) loraRoot.addEventListener('change', this.updateTargetPath);
|
||||||
await this.initializeFolderBrowser();
|
if (newFolder) newFolder.addEventListener('input', this.updateTargetPath);
|
||||||
this.updateTargetPath();
|
|
||||||
});
|
|
||||||
|
|
||||||
newFolder.addEventListener('input', this.updateTargetPath);
|
|
||||||
|
|
||||||
|
console.log('Initializing folder browser...');
|
||||||
// Update initial path
|
// Update initial path
|
||||||
this.updateTargetPath();
|
this.updateTargetPath();
|
||||||
}
|
}
|
||||||
@@ -541,13 +560,12 @@ export class ImportManager {
|
|||||||
if (newFolder) newFolder.removeEventListener('input', this.updateTargetPath);
|
if (newFolder) newFolder.removeEventListener('input', this.updateTargetPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new method to update target path
|
|
||||||
updateTargetPath() {
|
updateTargetPath() {
|
||||||
const pathDisplay = document.getElementById('importTargetPathDisplay');
|
const pathDisplay = document.getElementById('importTargetPathDisplay');
|
||||||
if (!pathDisplay) return;
|
if (!pathDisplay) return;
|
||||||
|
|
||||||
const loraRoot = document.getElementById('importLoraRoot')?.value || '';
|
const loraRoot = document.getElementById('importLoraRoot')?.value || '';
|
||||||
const newFolder = document.getElementById('importNewFolder')?.value.trim() || '';
|
const newFolder = document.getElementById('importNewFolder')?.value?.trim() || '';
|
||||||
|
|
||||||
let fullPath = loraRoot || 'Select a LoRA root directory';
|
let fullPath = loraRoot || 'Select a LoRA root directory';
|
||||||
|
|
||||||
@@ -562,4 +580,38 @@ export class ImportManager {
|
|||||||
|
|
||||||
pathDisplay.innerHTML = `<span class="path-text">${fullPath}</span>`;
|
pathDisplay.innerHTML = `<span class="path-text">${fullPath}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showStep(stepId) {
|
||||||
|
// 隐藏所有步骤
|
||||||
|
document.querySelectorAll('.import-step').forEach(step => {
|
||||||
|
step.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示目标步骤
|
||||||
|
const targetStep = document.getElementById(stepId);
|
||||||
|
if (targetStep) {
|
||||||
|
// 强制显示目标步骤 - 使用 !important 覆盖任何其他CSS规则
|
||||||
|
targetStep.setAttribute('style', 'display: block !important');
|
||||||
|
|
||||||
|
// 调试信息
|
||||||
|
console.log(`Showing step: ${stepId}`);
|
||||||
|
const rect = targetStep.getBoundingClientRect();
|
||||||
|
console.log('Step dimensions:', {
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
|
top: rect.top,
|
||||||
|
left: rect.left,
|
||||||
|
visible: rect.width > 0 && rect.height > 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// 强制重新计算布局
|
||||||
|
targetStep.offsetHeight;
|
||||||
|
|
||||||
|
// 滚动模态内容到顶部
|
||||||
|
const modalContent = document.querySelector('#importModal .modal-content');
|
||||||
|
if (modalContent) {
|
||||||
|
modalContent.scrollTop = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,106 +1,102 @@
|
|||||||
<div id="importModal" class="modal">
|
<div id="importModal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<button class="close" onclick="modalManager.closeModal('importModal')">×</button>
|
||||||
<h2>Import Recipe</h2>
|
<h2>Import Recipe</h2>
|
||||||
<span class="close-modal" onclick="modalManager.closeModal('importModal')">×</span>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<!-- Step 1: Upload Image -->
|
|
||||||
<div id="uploadStep" class="import-step">
|
|
||||||
<p>Upload an image with LoRA metadata to import as a recipe.</p>
|
|
||||||
|
|
||||||
|
<!-- Step 1: Upload Image -->
|
||||||
|
<div class="import-step" id="uploadStep">
|
||||||
|
<p>Upload an image with LoRA metadata to import as a recipe.</p>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="recipeImageUpload">Select Image:</label>
|
||||||
<div class="file-input-wrapper">
|
<div class="file-input-wrapper">
|
||||||
<input type="file" id="recipeImageUpload" accept="image/*" onchange="importManager.handleImageUpload(event)">
|
<input type="file" id="recipeImageUpload" accept="image/*" onchange="importManager.handleImageUpload(event)">
|
||||||
<div class="file-input-button">
|
<div class="file-input-button">
|
||||||
<i class="fas fa-upload"></i> Select Image
|
<i class="fas fa-upload"></i> Select Image
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="error-message" id="uploadError"></div>
|
||||||
<div id="uploadError" class="error-message"></div>
|
|
||||||
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button class="secondary-btn" onclick="modalManager.closeModal('importModal')">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 2: Recipe Details -->
|
<div class="modal-actions">
|
||||||
<div id="detailsStep" class="import-step" style="display: none;">
|
<button class="secondary-btn" onclick="modalManager.closeModal('importModal')">Cancel</button>
|
||||||
<div class="recipe-details-layout">
|
</div>
|
||||||
<div class="recipe-image-container">
|
</div>
|
||||||
<div id="recipeImagePreview" class="recipe-image"></div>
|
|
||||||
|
<!-- Step 2: Recipe Details -->
|
||||||
|
<div class="import-step" id="detailsStep" style="display: none;">
|
||||||
|
<div class="recipe-details-layout">
|
||||||
|
<div class="recipe-image-container">
|
||||||
|
<div id="recipeImagePreview" class="recipe-image"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="recipe-form-container">
|
||||||
|
<div class="input-group">
|
||||||
|
<label for="recipeName">Recipe Name</label>
|
||||||
|
<input type="text" id="recipeName" placeholder="Enter recipe name"
|
||||||
|
onchange="importManager.handleRecipeNameChange(event)">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="recipe-form-container">
|
<div class="input-group">
|
||||||
<div class="recipe-name-container">
|
<label>Tags (optional)</label>
|
||||||
<label for="recipeName">Recipe Name</label>
|
<div class="tag-input-container">
|
||||||
<input type="text" id="recipeName" placeholder="Enter recipe name"
|
<input type="text" id="tagInput" placeholder="Add a tag">
|
||||||
onchange="importManager.handleRecipeNameChange(event)">
|
<button class="secondary-btn" onclick="importManager.addTag()">
|
||||||
|
<i class="fas fa-plus"></i> Add
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="tagsContainer" class="tags-container">
|
||||||
<div class="tags-section">
|
<div class="empty-tags">No tags added</div>
|
||||||
<label>Tags (optional)</label>
|
|
||||||
<div class="tag-input-container">
|
|
||||||
<input type="text" id="tagInput" placeholder="Add a tag">
|
|
||||||
<button class="secondary-btn" onclick="importManager.addTag()">
|
|
||||||
<i class="fas fa-plus"></i> Add
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="tagsContainer" class="tags-container">
|
|
||||||
<div class="empty-tags">No tags added</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="loras-section">
|
<div class="input-group">
|
||||||
<label>LoRAs in this Recipe <span id="loraCountInfo" class="lora-count-info">(0/0 in library)</span></label>
|
<label>LoRAs in this Recipe <span id="loraCountInfo" class="lora-count-info">(0/0 in library)</span></label>
|
||||||
<div id="lorasList" class="loras-list">
|
<div id="lorasList" class="loras-list">
|
||||||
<!-- LoRAs will be populated here -->
|
<!-- LoRAs will be populated here -->
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button class="secondary-btn" onclick="importManager.backToUpload()">Back</button>
|
|
||||||
<button class="primary-btn" onclick="importManager.proceedFromDetails()">Next</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 3: Download Location (if needed) -->
|
<div class="modal-actions">
|
||||||
<div id="locationStep" class="import-step" style="display: none;">
|
<button class="secondary-btn" onclick="importManager.backToUpload()">Back</button>
|
||||||
<p>The following LoRAs are not in your library and need to be downloaded:</p>
|
<button class="primary-btn" onclick="importManager.proceedFromDetails()">Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="missingLorasList" class="missing-loras-list">
|
<!-- Step 3: Download Location (if needed) -->
|
||||||
<!-- Missing LoRAs will be populated here -->
|
<div class="import-step" id="locationStep" style="display: none;">
|
||||||
|
<div class="location-selection">
|
||||||
|
<!-- Move path preview to top -->
|
||||||
|
<div class="path-preview">
|
||||||
|
<label>Download Location Preview:</label>
|
||||||
|
<div class="path-display" id="importTargetPathDisplay">
|
||||||
|
<span class="path-text">Select a LoRA root directory</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label for="importLoraRoot">Select LoRA Root Directory</label>
|
<label>Select LoRA Root:</label>
|
||||||
<select id="importLoraRoot">
|
<select id="importLoraRoot"></select>
|
||||||
<!-- LoRA roots will be populated here -->
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label>Select Folder (optional)</label>
|
<label>Target Folder:</label>
|
||||||
<div id="importFolderBrowser" class="folder-browser">
|
<div class="folder-browser" id="importFolderBrowser">
|
||||||
<!-- Folders will be populated here -->
|
<!-- Folders will be populated here -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label for="importNewFolder">Create New Folder (optional)</label>
|
<label for="importNewFolder">New Folder (optional):</label>
|
||||||
<input type="text" id="importNewFolder" placeholder="Enter folder name">
|
<input type="text" id="importNewFolder" placeholder="Enter folder name">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="path-preview">
|
<div class="modal-actions">
|
||||||
<label>Download Location:</label>
|
<button class="secondary-btn" onclick="importManager.backToDetails()">Back</button>
|
||||||
<div id="importTargetPathDisplay" class="path-display"></div>
|
<button class="primary-btn" onclick="importManager.saveRecipe()">Download & Save Recipe</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button class="secondary-btn" onclick="importManager.backToDetails()">Back</button>
|
|
||||||
<button class="primary-btn" onclick="importManager.saveRecipe()">Download & Save Recipe</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user