mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-05-14 19:25:37 +08:00
feat: warn when classroom speech server is unavailable
This commit is contained in:
@@ -1322,6 +1322,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
||||||
|
<v-dialog v-model="showSpeechServerWarning" max-width="620" persistent>
|
||||||
|
<div class="panel dialog speech-server-dialog">
|
||||||
|
<div class="speech-server-icon">
|
||||||
|
<v-icon size="34">mdi-alert-circle-outline</v-icon>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Sprachserver aktuell nicht erreichbar</p>
|
||||||
|
<h3 class="h3">Server-TTS ist gerade überlastet</h3>
|
||||||
|
<p class="muted speech-server-copy">
|
||||||
|
Unser Sprachserver ist aktuell wegen zu hoher Auslastung nicht erreichbar. Es kann ein bisschen dauern,
|
||||||
|
bis dieses Feature wieder verfügbar ist. Du kannst solange Browser TTS nutzen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="speech-server-actions">
|
||||||
|
<button
|
||||||
|
class="btn primary"
|
||||||
|
type="button"
|
||||||
|
:disabled="!browserTtsAvailable"
|
||||||
|
@click="enableBrowserTtsFromWarning"
|
||||||
|
>
|
||||||
|
<v-icon size="18">mdi-volume-high</v-icon>
|
||||||
|
Browser TTS nutzen
|
||||||
|
</button>
|
||||||
|
<button class="btn ghost" type="button" @click="showSpeechServerWarning=false">
|
||||||
|
Verstanden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p v-if="!browserTtsAvailable" class="muted small">
|
||||||
|
Dein Browser meldet aktuell keine Web-Speech-Unterstützung.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
<!-- TOAST -->
|
<!-- TOAST -->
|
||||||
<v-snackbar v-model="toast.show" timeout="2200" location="top" color="#22d3ee">
|
<v-snackbar v-model="toast.show" timeout="2200" location="top" color="#22d3ee">
|
||||||
<v-icon class="mr-2">mdi-trophy</v-icon>
|
<v-icon class="mr-2">mdi-trophy</v-icon>
|
||||||
@@ -2710,9 +2743,16 @@ const referenceOpen = ref(false)
|
|||||||
|
|
||||||
const toast = ref({show: false, text: ''})
|
const toast = ref({show: false, text: ''})
|
||||||
const showSettings = ref(false)
|
const showSettings = ref(false)
|
||||||
|
const showSpeechServerWarning = ref(false)
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
const isClient = typeof window !== 'undefined'
|
const isClient = typeof window !== 'undefined'
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
const browserTtsAvailable = computed(() => isClient && 'speechSynthesis' in window)
|
||||||
|
|
||||||
|
type SpeechServerHealth = {
|
||||||
|
configured: boolean
|
||||||
|
reachable: boolean
|
||||||
|
}
|
||||||
|
|
||||||
const ATC_SETTINGS_STORAGE_PREFIX = 'os_atc_settings_'
|
const ATC_SETTINGS_STORAGE_PREFIX = 'os_atc_settings_'
|
||||||
|
|
||||||
@@ -2769,6 +2809,20 @@ function persistLocalAtcSettings(config: LearnConfig) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkSpeechServerAvailability() {
|
||||||
|
try {
|
||||||
|
const health = await api.get<SpeechServerHealth>('/api/classroom/speech-server-health')
|
||||||
|
showSpeechServerWarning.value = Boolean(health.configured && !health.reachable)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to check speech server availability', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableBrowserTtsFromWarning() {
|
||||||
|
cfg.value.tts = true
|
||||||
|
showSpeechServerWarning.value = false
|
||||||
|
}
|
||||||
|
|
||||||
function loadLocalAtcSettings(): LearnConfigPatch | null {
|
function loadLocalAtcSettings(): LearnConfigPatch | null {
|
||||||
if (!isClient) return null
|
if (!isClient) return null
|
||||||
const key = getAtcSettingsStorageKey()
|
const key = getAtcSettingsStorageKey()
|
||||||
@@ -4576,6 +4630,7 @@ onMounted(() => {
|
|||||||
simbriefForm.userId = storedId
|
simbriefForm.userId = storedId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
void checkSpeechServerAvailability()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -6707,6 +6762,40 @@ onMounted(() => {
|
|||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.speech-server-dialog {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speech-server-icon {
|
||||||
|
width: 58px;
|
||||||
|
height: 58px;
|
||||||
|
border-radius: 18px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: #fbbf24;
|
||||||
|
background: color-mix(in srgb, #f59e0b 18%, transparent);
|
||||||
|
border: 1px solid color-mix(in srgb, #f59e0b 36%, transparent);
|
||||||
|
box-shadow: 0 18px 36px rgba(245, 158, 11, .16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.speech-server-copy {
|
||||||
|
margin-top: 10px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speech-server-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speech-server-actions .btn {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.sr-only {
|
.sr-only {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
|
|||||||
53
server/api/classroom/speech-server-health.get.ts
Normal file
53
server/api/classroom/speech-server-health.get.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { defineEventHandler } from 'h3'
|
||||||
|
import { requireUserSession } from '../../utils/auth'
|
||||||
|
import { getServerRuntimeConfig } from '../../utils/runtimeConfig'
|
||||||
|
|
||||||
|
type SpeechServerHealth = {
|
||||||
|
configured: boolean
|
||||||
|
reachable: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHealthUrls(baseUrl: string): string[] {
|
||||||
|
const trimmed = baseUrl.replace(/\/+$/, '')
|
||||||
|
return [
|
||||||
|
`${trimmed}/health`,
|
||||||
|
`${trimmed}/v1/models`,
|
||||||
|
trimmed,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function canReach(url: string): Promise<boolean> {
|
||||||
|
const controller = new AbortController()
|
||||||
|
const timeout = setTimeout(() => controller.abort(), 2500)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
signal: controller.signal,
|
||||||
|
})
|
||||||
|
return response.status < 500
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event): Promise<SpeechServerHealth> => {
|
||||||
|
await requireUserSession(event)
|
||||||
|
|
||||||
|
const { speachesBaseUrl } = getServerRuntimeConfig()
|
||||||
|
if (!speachesBaseUrl) {
|
||||||
|
return {
|
||||||
|
configured: false,
|
||||||
|
reachable: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Promise.all(buildHealthUrls(speachesBaseUrl).map(url => canReach(url)))
|
||||||
|
|
||||||
|
return {
|
||||||
|
configured: true,
|
||||||
|
reachable: results.some(Boolean),
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user