Merge branch 'fix/t12-laya-status-consistency' into develop (#12)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -776,6 +776,30 @@ h4 {
|
|||||||
gap: var(--space-sm);
|
gap: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.laya-component-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
margin-top: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
#laya-service-state {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.laya-start-command {
|
||||||
|
padding: var(--space-sm);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
overflow-x: auto;
|
||||||
|
user-select: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.laya-notice-action {
|
||||||
|
margin-left: var(--space-sm);
|
||||||
|
padding: 2px var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
.laya-progress {
|
.laya-progress {
|
||||||
margin-top: var(--space-md);
|
margin-top: var(--space-md);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
<title>nodecast-tv | IPTV Player</title>
|
<title>nodecast-tv | IPTV Player</title>
|
||||||
<meta name="description" content="Stream live TV, movies, and series with EPG support">
|
<meta name="description" content="Stream live TV, movies, and series with EPG support">
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||||
<link rel="stylesheet" href="css/main.css?v=4">
|
<link rel="stylesheet" href="css/main.css?v=5">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@800&display=swap"
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Outfit:wght@800&display=swap"
|
||||||
@ -485,9 +485,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="laya-model-panel" id="laya-model-panel">
|
<div class="laya-model-panel" id="laya-model-panel">
|
||||||
<div class="laya-model-heading">
|
<div class="laya-model-heading">
|
||||||
<div><strong>Local AI model · Laya</strong><div id="laya-model-status" class="setting-hint" role="status">Checking status…</div></div>
|
<div><strong>Local AI · Laya</strong><div id="laya-model-status" class="setting-hint" role="status">Checking status…</div></div>
|
||||||
<span id="laya-model-size" class="setting-hint"></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="laya-component-status"><span id="laya-model-state">Model: Checking…</span><span id="laya-model-size" class="setting-hint"></span></div>
|
||||||
|
<div id="laya-service-state">Local AI service: Checking…</div>
|
||||||
<div id="laya-model-message" class="setting-hint"></div>
|
<div id="laya-model-message" class="setting-hint"></div>
|
||||||
<div id="laya-download-progress" class="laya-progress" hidden>
|
<div id="laya-download-progress" class="laya-progress" hidden>
|
||||||
<div id="laya-progress-track" class="laya-progress-track" role="progressbar" aria-label="Local AI model download progress">
|
<div id="laya-progress-track" class="laya-progress-track" role="progressbar" aria-label="Local AI model download progress">
|
||||||
@ -500,8 +501,11 @@
|
|||||||
<div class="laya-model-actions">
|
<div class="laya-model-actions">
|
||||||
<button type="button" class="btn btn-primary" id="laya-download-model">Download model</button>
|
<button type="button" class="btn btn-primary" id="laya-download-model">Download model</button>
|
||||||
<button type="button" class="btn btn-ghost" id="laya-remove-model">Remove downloaded model</button>
|
<button type="button" class="btn btn-ghost" id="laya-remove-model">Remove downloaded model</button>
|
||||||
|
<button type="button" class="btn btn-ghost" id="laya-check-again" hidden>Check again</button>
|
||||||
|
<button type="button" class="btn btn-ghost" id="laya-copy-command" hidden>Copy start command</button>
|
||||||
<button type="button" class="btn btn-ghost" id="laya-use-standard" hidden>Use Standard</button>
|
<button type="button" class="btn btn-ghost" id="laya-use-standard" hidden>Use Standard</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="laya-start-command" class="laya-start-command" hidden><code>docker compose --profile laya up -d</code></div>
|
||||||
<p class="setting-hint">The model runs on this server. Channel names and metadata are not sent to an AI service.</p>
|
<p class="setting-hint">The model runs on this server. Channel names and metadata are not sent to an AI service.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1180,9 +1184,9 @@
|
|||||||
<script src="/js/components/VideoPlayer.js?v=5"></script>
|
<script src="/js/components/VideoPlayer.js?v=5"></script>
|
||||||
<script src="/js/services/channelLanguageDetection.js?v=1"></script>
|
<script src="/js/services/channelLanguageDetection.js?v=1"></script>
|
||||||
<script src="/js/services/channelLanguageFusion.js?v=1"></script>
|
<script src="/js/services/channelLanguageFusion.js?v=1"></script>
|
||||||
<script src="/js/services/layaStatusPresentation.js?v=1"></script>
|
<script src="/js/services/layaStatusPresentation.js?v=2"></script>
|
||||||
<script src="/js/services/channelVariantGrouping.js?v=4"></script>
|
<script src="/js/services/channelVariantGrouping.js?v=4"></script>
|
||||||
<script src="/js/components/ChannelList.js?v=7"></script>
|
<script src="/js/components/ChannelList.js?v=8"></script>
|
||||||
<script src="/js/components/SourceManager.js?v=2"></script>
|
<script src="/js/components/SourceManager.js?v=2"></script>
|
||||||
<script src="/js/components/EpgGuide.js?v=2"></script>
|
<script src="/js/components/EpgGuide.js?v=2"></script>
|
||||||
<script src="/js/pages/HomePage.js?v=2"></script>
|
<script src="/js/pages/HomePage.js?v=2"></script>
|
||||||
@ -1190,7 +1194,7 @@
|
|||||||
<script src="/js/pages/Guide.js?v=2"></script>
|
<script src="/js/pages/Guide.js?v=2"></script>
|
||||||
<script src="/js/pages/MoviesPage.js?v=4"></script>
|
<script src="/js/pages/MoviesPage.js?v=4"></script>
|
||||||
<script src="/js/pages/SeriesPage.js?v=1"></script>
|
<script src="/js/pages/SeriesPage.js?v=1"></script>
|
||||||
<script src="/js/pages/Settings.js?v=5"></script>
|
<script src="/js/pages/Settings.js?v=6"></script>
|
||||||
<script src="/js/pages/WatchPage.js?v=1"></script>
|
<script src="/js/pages/WatchPage.js?v=1"></script>
|
||||||
<script src="/js/app.js?v=4"></script>
|
<script src="/js/app.js?v=4"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@ -33,6 +33,7 @@ class ChannelList {
|
|||||||
this.channelLanguagePreferences = this.loadChannelLanguagePreferences();
|
this.channelLanguagePreferences = this.loadChannelLanguagePreferences();
|
||||||
this.layaLanguageResults = new Map();
|
this.layaLanguageResults = new Map();
|
||||||
this.languageDetectionNotice = '';
|
this.languageDetectionNotice = '';
|
||||||
|
this.languageDetectionFallback = false;
|
||||||
this._languageDetectionRequest = 0;
|
this._languageDetectionRequest = 0;
|
||||||
|
|
||||||
this.loadCollapsedState();
|
this.loadCollapsedState();
|
||||||
@ -335,6 +336,7 @@ class ChannelList {
|
|||||||
const safeMode = ['algo', 'laya', 'hybrid'].includes(mode) ? mode : 'algo';
|
const safeMode = ['algo', 'laya', 'hybrid'].includes(mode) ? mode : 'algo';
|
||||||
this.layaLanguageResults.clear();
|
this.layaLanguageResults.clear();
|
||||||
this.languageDetectionNotice = '';
|
this.languageDetectionNotice = '';
|
||||||
|
this.languageDetectionFallback = false;
|
||||||
if (this.channels.length > 0) {
|
if (this.channels.length > 0) {
|
||||||
this.render();
|
this.render();
|
||||||
void this.refreshLayaLanguages(safeMode);
|
void this.refreshLayaLanguages(safeMode);
|
||||||
@ -346,6 +348,7 @@ class ChannelList {
|
|||||||
if (mode === 'algo') {
|
if (mode === 'algo') {
|
||||||
this.layaLanguageResults.clear();
|
this.layaLanguageResults.clear();
|
||||||
this.languageDetectionNotice = '';
|
this.languageDetectionNotice = '';
|
||||||
|
this.languageDetectionFallback = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -359,6 +362,15 @@ class ChannelList {
|
|||||||
});
|
});
|
||||||
const results = new Map();
|
const results = new Map();
|
||||||
try {
|
try {
|
||||||
|
const status = await API.laya.getStatus();
|
||||||
|
if (!status?.modelPresent || !status?.runtimeAvailable) {
|
||||||
|
if (requestId !== this._languageDetectionRequest) return;
|
||||||
|
this.layaLanguageResults.clear();
|
||||||
|
this.languageDetectionFallback = true;
|
||||||
|
this.languageDetectionNotice = 'Local AI service is unavailable. Standard channel matching is being used.';
|
||||||
|
this.render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (let index = 0; index < descriptors.length; index += 64) {
|
for (let index = 0; index < descriptors.length; index += 64) {
|
||||||
const response = await API.laya.detectLanguages(descriptors.slice(index, index + 64));
|
const response = await API.laya.detectLanguages(descriptors.slice(index, index + 64));
|
||||||
(response.results || []).forEach(result => results.set(String(result.id), result));
|
(response.results || []).forEach(result => results.set(String(result.id), result));
|
||||||
@ -366,12 +378,12 @@ class ChannelList {
|
|||||||
if (requestId !== this._languageDetectionRequest) return;
|
if (requestId !== this._languageDetectionRequest) return;
|
||||||
this.layaLanguageResults = results;
|
this.layaLanguageResults = results;
|
||||||
this.languageDetectionNotice = '';
|
this.languageDetectionNotice = '';
|
||||||
|
this.languageDetectionFallback = false;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (requestId !== this._languageDetectionRequest) return;
|
if (requestId !== this._languageDetectionRequest) return;
|
||||||
this.layaLanguageResults.clear();
|
this.layaLanguageResults.clear();
|
||||||
this.languageDetectionNotice = mode === 'hybrid'
|
this.languageDetectionFallback = true;
|
||||||
? 'Local AI is unavailable; using standard channel matching.'
|
this.languageDetectionNotice = 'Local AI service is unavailable. Standard channel matching is being used.';
|
||||||
: 'Local AI is unavailable. Download or start the model in Settings.';
|
|
||||||
console.warn('[ChannelList] Local AI language detection unavailable:', err.message);
|
console.warn('[ChannelList] Local AI language detection unavailable:', err.message);
|
||||||
}
|
}
|
||||||
this.render();
|
this.render();
|
||||||
@ -405,7 +417,9 @@ class ChannelList {
|
|||||||
selectedVariants: this.selectedVariants,
|
selectedVariants: this.selectedVariants,
|
||||||
preferredLanguage: this.preferredLanguage,
|
preferredLanguage: this.preferredLanguage,
|
||||||
languagePreferences: this.channelLanguagePreferences,
|
languagePreferences: this.channelLanguagePreferences,
|
||||||
languageMode: window.app?.player?.settings?.languageDetectionMode || 'algo',
|
languageMode: this.languageDetectionFallback
|
||||||
|
? 'algo'
|
||||||
|
: (window.app?.player?.settings?.languageDetectionMode || 'algo'),
|
||||||
languageResults: this.layaLanguageResults,
|
languageResults: this.layaLanguageResults,
|
||||||
isFavorite: channel => this.isFavorite(channel.sourceId, channel.id),
|
isFavorite: channel => this.isFavorite(channel.sourceId, channel.id),
|
||||||
isHidden: channel => this.isHidden('channel', channel.sourceId, channel.streamId || channel.id)
|
isHidden: channel => this.isHidden('channel', channel.sourceId, channel.streamId || channel.id)
|
||||||
@ -420,6 +434,15 @@ class ChannelList {
|
|||||||
notice.className = 'laya-language-notice';
|
notice.className = 'laya-language-notice';
|
||||||
notice.setAttribute('role', 'status');
|
notice.setAttribute('role', 'status');
|
||||||
notice.textContent = this.languageDetectionNotice;
|
notice.textContent = this.languageDetectionNotice;
|
||||||
|
const settingsButton = document.createElement('button');
|
||||||
|
settingsButton.type = 'button';
|
||||||
|
settingsButton.className = 'btn btn-ghost laya-notice-action';
|
||||||
|
settingsButton.textContent = 'Open settings';
|
||||||
|
settingsButton.addEventListener('click', () => {
|
||||||
|
window.app?.navigateTo?.('settings');
|
||||||
|
window.app?.pages?.settings?.switchTab?.('player');
|
||||||
|
});
|
||||||
|
notice.appendChild(settingsButton);
|
||||||
this.container.appendChild(notice);
|
this.container.appendChild(notice);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -126,6 +126,8 @@ class SettingsPage {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.getElementById('laya-use-standard')?.addEventListener('click', () => this.useStandardLanguageDetection());
|
document.getElementById('laya-use-standard')?.addEventListener('click', () => this.useStandardLanguageDetection());
|
||||||
|
document.getElementById('laya-check-again')?.addEventListener('click', () => void this.loadLayaStatus());
|
||||||
|
document.getElementById('laya-copy-command')?.addEventListener('click', () => void this.copyLayaStartCommand());
|
||||||
document.getElementById('laya-show-details')?.addEventListener('click', () => {
|
document.getElementById('laya-show-details')?.addEventListener('click', () => {
|
||||||
const details = document.getElementById('laya-error-details');
|
const details = document.getElementById('laya-error-details');
|
||||||
const button = document.getElementById('laya-show-details');
|
const button = document.getElementById('laya-show-details');
|
||||||
@ -149,11 +151,16 @@ class SettingsPage {
|
|||||||
|
|
||||||
renderLayaStatus(status = {}) {
|
renderLayaStatus(status = {}) {
|
||||||
const statusElement = document.getElementById('laya-model-status');
|
const statusElement = document.getElementById('laya-model-status');
|
||||||
|
const modelStateElement = document.getElementById('laya-model-state');
|
||||||
|
const serviceStateElement = document.getElementById('laya-service-state');
|
||||||
const sizeElement = document.getElementById('laya-model-size');
|
const sizeElement = document.getElementById('laya-model-size');
|
||||||
const messageElement = document.getElementById('laya-model-message');
|
const messageElement = document.getElementById('laya-model-message');
|
||||||
const downloadButton = document.getElementById('laya-download-model');
|
const downloadButton = document.getElementById('laya-download-model');
|
||||||
const removeButton = document.getElementById('laya-remove-model');
|
const removeButton = document.getElementById('laya-remove-model');
|
||||||
const useStandardButton = document.getElementById('laya-use-standard');
|
const useStandardButton = document.getElementById('laya-use-standard');
|
||||||
|
const checkAgainButton = document.getElementById('laya-check-again');
|
||||||
|
const copyCommandButton = document.getElementById('laya-copy-command');
|
||||||
|
const startCommand = document.getElementById('laya-start-command');
|
||||||
const progress = document.getElementById('laya-download-progress');
|
const progress = document.getElementById('laya-download-progress');
|
||||||
const progressTrack = document.getElementById('laya-progress-track');
|
const progressTrack = document.getElementById('laya-progress-track');
|
||||||
const progressBar = document.getElementById('laya-progress-bar');
|
const progressBar = document.getElementById('laya-progress-bar');
|
||||||
@ -165,12 +172,17 @@ class SettingsPage {
|
|||||||
const mode = this.app.player?.settings?.languageDetectionMode || 'algo';
|
const mode = this.app.player?.settings?.languageDetectionMode || 'algo';
|
||||||
const presentation = LayaStatusPresentation.presentLayaStatus(status, mode);
|
const presentation = LayaStatusPresentation.presentLayaStatus(status, mode);
|
||||||
statusElement.textContent = presentation.statusLabel;
|
statusElement.textContent = presentation.statusLabel;
|
||||||
|
modelStateElement.textContent = presentation.modelLabel;
|
||||||
|
serviceStateElement.textContent = presentation.serviceLabel;
|
||||||
const size = status.sizeBytes || status.runtime?.modelSizeBytes;
|
const size = status.sizeBytes || status.runtime?.modelSizeBytes;
|
||||||
sizeElement.textContent = size ? this.formatBytes(size) : '';
|
sizeElement.textContent = size ? `· ${this.formatBytes(size)}` : '';
|
||||||
downloadButton.hidden = !presentation.showDownload;
|
downloadButton.hidden = !presentation.showDownload;
|
||||||
downloadButton.textContent = presentation.downloadLabel;
|
downloadButton.textContent = presentation.downloadLabel;
|
||||||
removeButton.hidden = !presentation.showRemove;
|
removeButton.hidden = !presentation.showRemove;
|
||||||
useStandardButton.hidden = !presentation.showUseStandard;
|
useStandardButton.hidden = !presentation.showUseStandard;
|
||||||
|
checkAgainButton.hidden = !presentation.showCheckAgain;
|
||||||
|
copyCommandButton.hidden = !presentation.showStartCommand;
|
||||||
|
startCommand.hidden = !presentation.showStartCommand;
|
||||||
downloadButton.disabled = presentation.busy || Boolean(this.layaOperationPending);
|
downloadButton.disabled = presentation.busy || Boolean(this.layaOperationPending);
|
||||||
removeButton.disabled = presentation.busy || Boolean(this.layaOperationPending);
|
removeButton.disabled = presentation.busy || Boolean(this.layaOperationPending);
|
||||||
|
|
||||||
@ -199,6 +211,7 @@ class SettingsPage {
|
|||||||
details.hidden = true;
|
details.hidden = true;
|
||||||
details.textContent = presentation.errorDetail;
|
details.textContent = presentation.errorDetail;
|
||||||
if (presentation.errorMessage) messageElement.textContent = presentation.errorMessage;
|
if (presentation.errorMessage) messageElement.textContent = presentation.errorMessage;
|
||||||
|
else if (presentation.availabilityMessage) messageElement.textContent = presentation.availabilityMessage;
|
||||||
else if (this.layaOperationPending) messageElement.textContent = this.layaOperationPending;
|
else if (this.layaOperationPending) messageElement.textContent = this.layaOperationPending;
|
||||||
else if (mode !== 'algo' && (!status.modelPresent || !status.runtimeAvailable)) {
|
else if (mode !== 'algo' && (!status.modelPresent || !status.runtimeAvailable)) {
|
||||||
messageElement.textContent = 'Local AI is not ready. Download the model and ensure the runtime is running. Live TV will remain available.';
|
messageElement.textContent = 'Local AI is not ready. Download the model and ensure the runtime is running. Live TV will remain available.';
|
||||||
@ -266,6 +279,24 @@ class SettingsPage {
|
|||||||
this.renderLayaStatus(this.layaStatus);
|
this.renderLayaStatus(this.layaStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async copyLayaStartCommand() {
|
||||||
|
const command = 'docker compose --profile laya up -d';
|
||||||
|
const button = document.getElementById('laya-copy-command');
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(command);
|
||||||
|
button.textContent = 'Copied';
|
||||||
|
} catch {
|
||||||
|
const code = document.querySelector('#laya-start-command code');
|
||||||
|
const selection = window.getSelection();
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(code);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
button.textContent = 'Command selected';
|
||||||
|
}
|
||||||
|
setTimeout(() => { button.textContent = 'Copy start command'; }, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
async initTranscodingSettings() {
|
async initTranscodingSettings() {
|
||||||
// Encoder settings
|
// Encoder settings
|
||||||
const hwEncoderSelect = document.getElementById('setting-hw-encoder');
|
const hwEncoderSelect = document.getElementById('setting-hw-encoder');
|
||||||
|
|||||||
@ -37,11 +37,24 @@
|
|||||||
const progressPercent = determined ? Math.max(0, Math.min(100, Math.round(percentValue))) : null;
|
const progressPercent = determined ? Math.max(0, Math.min(100, Math.round(percentValue))) : null;
|
||||||
const error = errorParts(status);
|
const error = errorParts(status);
|
||||||
const failed = phase === 'failed';
|
const failed = phase === 'failed';
|
||||||
|
const modelPresent = status.modelPresent === true;
|
||||||
|
const runtimeAvailable = status.runtimeAvailable === true;
|
||||||
|
const ready = modelPresent && runtimeAvailable && !busy && !failed;
|
||||||
|
const statusLabel = ready
|
||||||
|
? 'Ready'
|
||||||
|
: failed || busy
|
||||||
|
? PHASE_LABELS[phase]
|
||||||
|
: modelPresent && !runtimeAvailable
|
||||||
|
? 'Local AI service unavailable'
|
||||||
|
: !modelPresent
|
||||||
|
? 'Model not downloaded'
|
||||||
|
: PHASE_LABELS[phase];
|
||||||
return {
|
return {
|
||||||
phase,
|
phase,
|
||||||
statusLabel: status.modelPresent && phase === 'idle'
|
ready,
|
||||||
? (status.runtimeAvailable ? 'Model ready' : 'Model downloaded · runtime unavailable')
|
statusLabel,
|
||||||
: PHASE_LABELS[phase],
|
modelLabel: `Model: ${modelPresent ? 'Downloaded' : 'Not downloaded'}`,
|
||||||
|
serviceLabel: `Local AI service: ${runtimeAvailable ? 'Available' : 'Unavailable'}`,
|
||||||
busy,
|
busy,
|
||||||
showProgress: phase === 'downloading',
|
showProgress: phase === 'downloading',
|
||||||
progressIndeterminate: phase === 'downloading' && progressPercent === null,
|
progressIndeterminate: phase === 'downloading' && progressPercent === null,
|
||||||
@ -53,9 +66,14 @@
|
|||||||
errorMessage: failed ? (error.message || 'The local AI model could not be downloaded.') : '',
|
errorMessage: failed ? (error.message || 'The local AI model could not be downloaded.') : '',
|
||||||
errorDetail: failed ? error.detail : '',
|
errorDetail: failed ? error.detail : '',
|
||||||
downloadLabel: failed ? 'Retry download' : 'Download model',
|
downloadLabel: failed ? 'Retry download' : 'Download model',
|
||||||
showDownload: !busy && !status.modelPresent && (phase === 'idle' || failed),
|
showDownload: !busy && !modelPresent && (phase === 'idle' || failed),
|
||||||
showRemove: !busy && Boolean(status.modelPresent) && !failed,
|
showRemove: !busy && modelPresent,
|
||||||
showUseStandard: failed && mode !== 'algo'
|
showUseStandard: failed && mode !== 'algo',
|
||||||
|
showCheckAgain: !busy && modelPresent && !runtimeAvailable,
|
||||||
|
showStartCommand: !busy && modelPresent && !runtimeAvailable,
|
||||||
|
availabilityMessage: modelPresent && !runtimeAvailable
|
||||||
|
? 'The model is installed, but the Local AI service is not running. Standard channel matching is being used.'
|
||||||
|
: ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -41,3 +41,28 @@ test('ready models expose removal without download actions', () => {
|
|||||||
assert.equal(view.showRemove, true);
|
assert.equal(view.showRemove, true);
|
||||||
assert.equal(view.showDownload, false);
|
assert.equal(view.showDownload, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('installed model with unavailable service is not reported ready', () => {
|
||||||
|
const view = presentLayaStatus({
|
||||||
|
phase: 'idle',
|
||||||
|
modelPresent: true,
|
||||||
|
runtimeAvailable: false,
|
||||||
|
sizeBytes: 10
|
||||||
|
}, 'hybrid');
|
||||||
|
assert.equal(view.ready, false);
|
||||||
|
assert.equal(view.statusLabel, 'Local AI service unavailable');
|
||||||
|
assert.equal(view.modelLabel, 'Model: Downloaded');
|
||||||
|
assert.equal(view.serviceLabel, 'Local AI service: Unavailable');
|
||||||
|
assert.equal(view.showDownload, false);
|
||||||
|
assert.equal(view.showRemove, true);
|
||||||
|
assert.equal(view.showCheckAgain, true);
|
||||||
|
assert.equal(view.showStartCommand, true);
|
||||||
|
assert.equal(view.availabilityMessage, 'The model is installed, but the Local AI service is not running. Standard channel matching is being used.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('missing model keeps download available', () => {
|
||||||
|
const view = presentLayaStatus({ phase: 'idle', modelPresent: false, runtimeAvailable: false });
|
||||||
|
assert.equal(view.showDownload, true);
|
||||||
|
assert.equal(view.showRemove, false);
|
||||||
|
assert.equal(view.showCheckAgain, false);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user