feat(models): show Civitai model/version ids in model modal, always-on hash/id search

- Model modal hash footnote now shows Civitai model id and version id
  (right-aligned, quick-copy buttons); hidden for non-Civitai models
- Hash/id exact search (sha256/autov2/autov3/civitai ids) is now always
  on: the search-options "hash" toggle is removed and the search_hash
  query param is silently ignored for API compatibility
- Footnote render condition relaxed so autov3-only and id-only models
  still show the line
- i18n: 4 new keys translated in all 9 locales; filters.hash key removed
This commit is contained in:
Will Miao
2026-10-03 09:46:46 +08:00
parent 515469054c
commit f8aba393fe
22 changed files with 254 additions and 167 deletions
@@ -292,6 +292,15 @@
font-size: 0.8em;
}
/* Civitai model/version ids — pushed to the right end of the footnote line */
.hash-footnote .civitai-id-group {
display: inline-flex;
align-items: baseline;
flex-wrap: wrap;
gap: 4px 8px;
margin-left: auto;
}
.hash-footnote .hash-copy-btn {
display: inline-flex;
align-items: center;
-3
View File
@@ -1566,9 +1566,6 @@ export class BaseModelApiClient {
if (pageState.searchOptions.creator !== undefined) {
params.append('search_creator', pageState.searchOptions.creator.toString());
}
if (pageState.searchOptions.hash !== undefined) {
params.append('search_hash', pageState.searchOptions.hash.toString());
}
}
}
+45 -4
View File
@@ -355,7 +355,9 @@ export async function showModelModal(model, modelType) {
const escapedFolderPath = escapeHtml((modelWithFullData.file_path || '').replace(/[^/]+$/, '') || 'N/A');
// De-emphasized hash display: a borderless full-width footnote line below
// the info grid — sha256 middle-truncated (first 10 + last 6), autov3 in
// full (12 chars); the full value is copied via data-hash.
// full (12 chars); the full value is copied via data-hash. Civitai model /
// version ids join the same line on the right when the model comes from
// the Civitai ecosystem (a model card corresponds to one Civitai version).
const modelSha256 = modelWithFullData.sha256 || '';
const modelAutov3 = modelWithFullData.autov3 || '';
const truncatedSha256 = modelSha256.length > 16
@@ -383,8 +385,47 @@ export async function showModelModal(model, modelType) {
</button>
</span>`);
}
const hashesMarkup = modelSha256 && hashEntries.length ? `
<div class="hash-footnote" aria-label="${translate('modals.model.metadata.hashes', {}, 'Hashes')}">${hashEntries.join('<span class="hash-sep">·</span>')}
const normalizeCivitaiId = (value) => {
if (value === undefined || value === null) return '';
const normalized = String(value).trim();
// "0" is used as a placeholder for unknown ids in some metadata sources
return normalized && normalized !== '0' ? normalized : '';
};
const civitaiInfo = modelWithFullData.civitai || {};
const footnoteModelId = normalizeCivitaiId(civitaiInfo.modelId ?? civitaiInfo.model_id);
const footnoteVersionId = normalizeCivitaiId(civitaiInfo.id);
const copyCivitaiIdTitle = translate('modals.model.actions.copyCivitaiId', {}, 'Copy Civitai ID');
const civitaiIdCopiedToast = escapeAttribute(translate('modals.model.actions.civitaiIdCopied', {}, 'Civitai ID copied to clipboard'));
const civitaiIdEntries = [];
if (footnoteModelId) {
civitaiIdEntries.push(`
<span class="hash-entry civitai-id-entry">
<span class="hash-kind">${translate('modals.model.metadata.civitaiModelId', {}, 'Model ID')}</span>
<span class="model-hash-value" title="${escapeAttribute(footnoteModelId)}">${escapeHtml(footnoteModelId)}</span>
<button class="hash-copy-btn" data-action="copy-hash" data-hash="${escapeAttribute(footnoteModelId)}" data-toast="${civitaiIdCopiedToast}" title="${copyCivitaiIdTitle}">
<i class="fas fa-copy"></i>
</button>
</span>`);
}
if (footnoteVersionId) {
civitaiIdEntries.push(`
<span class="hash-entry civitai-id-entry">
<span class="hash-kind">${translate('modals.model.metadata.civitaiVersionId', {}, 'Version ID')}</span>
<span class="model-hash-value" title="${escapeAttribute(footnoteVersionId)}">${escapeHtml(footnoteVersionId)}</span>
<button class="hash-copy-btn" data-action="copy-hash" data-hash="${escapeAttribute(footnoteVersionId)}" data-toast="${civitaiIdCopiedToast}" title="${copyCivitaiIdTitle}">
<i class="fas fa-copy"></i>
</button>
</span>`);
}
const footnoteParts = [];
if (hashEntries.length) {
footnoteParts.push(hashEntries.join('<span class="hash-sep">·</span>'));
}
if (civitaiIdEntries.length) {
footnoteParts.push(`<span class="civitai-id-group">${civitaiIdEntries.join('<span class="hash-sep">·</span>')}</span>`);
}
const hashesMarkup = footnoteParts.length ? `
<div class="hash-footnote" aria-label="${translate('modals.model.metadata.hashes', {}, 'Hashes')}">${footnoteParts.join('')}
</div>` : '';
const useNewIcons = state.global.settings.use_new_license_icons !== false;
const licenseIcons = useNewIcons
@@ -975,7 +1016,7 @@ function setupEventHandlers(filePath, modelType) {
break;
case 'copy-hash':
if (target.dataset.hash) {
copyToClipboard(target.dataset.hash, 'Hash copied to clipboard');
copyToClipboard(target.dataset.hash, target.dataset.toast || 'Hash copied to clipboard');
}
break;
}
-1
View File
@@ -304,7 +304,6 @@ export class SearchManager {
pageState.searchOptions.modelname = options.modelname || false;
pageState.searchOptions.tags = options.tags || false;
pageState.searchOptions.creator = options.creator || false;
pageState.searchOptions.hash = options.hash || false;
}
}
-4
View File
@@ -123,7 +123,6 @@ export const state = {
modelname: true,
tags: false,
creator: false,
hash: false,
recursive: getStorageItem(`${MODEL_TYPES.LORA}_recursiveSearch`, true),
},
filters: {
@@ -190,7 +189,6 @@ export const state = {
filename: true,
modelname: true,
creator: false,
hash: false,
recursive: getStorageItem(`${MODEL_TYPES.CHECKPOINT}_recursiveSearch`, true),
},
filters: {
@@ -230,7 +228,6 @@ export const state = {
modelname: true,
tags: false,
creator: false,
hash: false,
recursive: getStorageItem(`${MODEL_TYPES.EMBEDDING}_recursiveSearch`, true),
},
filters: {
@@ -268,7 +265,6 @@ export const state = {
modelname: true,
tags: false,
creator: false,
hash: false,
recursive: getStorageItem(`${MODEL_TYPES.OTHER}_recursiveSearch`, true),
},
filters: {