TTs whisper

This commit is contained in:
2026-06-18 13:45:32 +02:00
parent 0e7fbbfdca
commit 9a23863320
57 changed files with 10430 additions and 253 deletions

67
quibot-web/README.md Normal file
View File

@@ -0,0 +1,67 @@
# Quibot Web
Interfície web per controlar el robit de la Quibot. Construida amb [Nuxt 3](https://nuxt.com/) i [Vue 3](https://vuejs.org/).
## Requisits previs
- [Node.js](https://nodejs.org/) (versió 20 o superior)
- npm, yarn o pnpm
## Instal·lació
```bash
npm install
```
## Execució en mode desenvolupament
```bash
npm run dev
```
L'aplicació estarà disponible a `http://localhost:3000`.
## Configuració
L'aplicació es configura mitjançant variables d'entorn:
| Variable | Descripció | Valor per defecte |
|-------------------|----------------------------------|------------------------|
| `QUIBOT_BASE_URL` | URL del backend del Quibot | `http://quibot:8000` |
| `QUIBOT_TOKEN` | Token d'autenticació del backend | `MY_SECRET_TOKEN` |
## Comandos disponibles
| Comando | Descripció |
|-----------------|--------------------------------------------|
| `npm run dev` | Inicia el servidor de desenvolupament |
| `npm run build` | Compila l'aplicació per a producció |
| `npm run generate` | Genera l'aplicació estàtica |
| `npm run preview` | Previsualitza la versió de producció |
## Funcionalitat
L'interfície permet enviar comandes al motor del Quibot:
- **Step Forward** Mou el motor cap endavant
- **Step Backwards** Mou el motor cap enrere
- **Stop** Atura el motor
## Estructura del projecte
```
quibot-web/
├── app/
│ └── app.vue # Pàgina principal amb els controls del motor
├── server/
│ └── api/motor/ # Rutes API del servidor
│ ├── stop.post.ts # Comanda d'aturada
│ └── step/[direction].post.ts # Comandes de moviment
├── public/
│ └── favicon.ico # Icona del navegador
├── nuxt.config.ts # Configuració del Nuxt
├── package.json
└── tsconfig.json # Configuració de TypeScript
```
## Llicència

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,149 @@
export default {
header: {
subtitle: 'Monitor de Control del Robot',
online: 'EN LÍNIA',
offline: 'DESCONNECTAT',
},
panels: {
blockQueue: 'Cua de Blocs',
motionControls: 'Controls de Moviment',
eyeControls: 'Controls dels Ulls',
gestureSensor: 'Sensor de Gestos',
},
blocks: {
empty: 'No hi ha blocs a la cua',
clear: 'Buidar Cua',
queued: 'A l\'espera',
processing: 'Procesant',
},
blockActions: {
rd: 'Avançar fins al creuament',
gn: 'Girar a la dreta 90\u00B0',
bu: 'Girar a l\'esquerra 90\u00B0',
ye: 'Agafar / Extrair líquid',
og: 'Deixar / Dispensar líquid',
vt: 'Pausa sorpresa',
},
colors: {
red: 'Vermell',
green: 'Verd',
blue: 'Blau',
yellow: 'Groc',
orange: 'Taronja',
violet: 'Violeta',
},
motion: {
forward: 'Endavant',
backward: 'Enrere',
left: 'Esquerra',
right: 'Dreta',
sending: 'Enviant: {dir}',
sent: 'Comanda de motor enviada: {dir}',
failed: 'La comanda ha fallat',
},
motionToast: {
success: 'Moviment {dir}',
},
eyes: {
shapeLabel: 'Color dels Ulls',
actionsLabel: 'Accions',
applyShape: 'Aplicar Forma',
applyColor: 'Aplicar Color',
eyeOn: 'Encendre',
eyeOff: 'Apagar',
toastSetShape: 'Forma dels ulls establerta: {shape}',
toastFailedShape: 'No s\'ha pogut establir la forma dels ulls',
toastSetColor: 'Color dels ulls establert: {color}',
toastFailedColor: 'No s\'ha pogut establir el color dels ulls',
toastOn: 'Ulls encesos',
toastOff: 'Ulls apagats',
toastFailedOn: 'No s\'han pogut encendre els ulls',
toastFailedOff: 'No s\'han pogut apagar els ulls',
},
eyeShapes: {
open: 'Oberts',
fw: 'Endavant',
down: 'Baix',
gesture: 'Gest',
},
eyeColors: {
white: 'Blanc',
red: 'Vermell',
green: 'Verd',
blue: 'Blau',
yellow: 'Groc',
orange: 'Taronja',
purple: 'Lila',
cyan: 'Cian',
black: 'Apagat',
},
gestures: {
modeLabel: 'Mode de Funcionament',
blockMode: 'Mode Blocs',
gestureMode: 'Mode Gestos',
detectedLabel: 'Gestos Detectats',
empty: 'Encara no s\'han detectat gestos',
reference: 'Referència de Gestos',
clearLog: 'Buidar Registre',
toggleToast: 'Canviar mode',
},
gestureNames: {
forward: 'Empènyer Endavant',
left: 'Ona Esquerra',
right: 'Ona Dreta',
up: 'Ona Amunt',
down: 'Ona Avall',
clockwise: 'Cercle CW',
anticlockwise: 'Cercle CCW',
wave: 'Ona (Commute)',
},
gestureActions: {
forward: 'Avançar creuament',
right: 'Girar a la dreta 90\u00B0',
left: 'Girar a l\'esquerra 90\u00B0',
up: 'Agafar / Extreure',
down: 'Deixar / Dispensar',
clockwise: 'Inactiu',
anticlockwise: 'Inactiu',
wave: 'Canviar mode',
},
gestureRef: {
forward: 'Empènyer Endavant',
left: 'Ona Esquerra',
right: 'Ona Dreta',
up: 'Ona Amunt',
down: 'Ona Avall',
clockwise: 'Cercle CW',
anticlockwise: 'Cercle CCW',
wave: 'Ona (Ambdues)',
},
modes: {
block: 'Mode Blocs',
gesture: 'Mode Gestos',
},
toast: {
cleared: 'Cua buidada',
switched: 'Canviat a mode {mode}',
},
theme: {
light: 'Canviar a mode clar',
dark: 'Canviar a mode fosc',
},
settings: {
title: 'Configuració',
save: 'Desar',
saved: 'Configuració desada',
theme: {
label: 'Tema',
dark: 'Fosc',
light: 'Clar',
},
language: {
label: 'Idioma',
},
piUrl: {
label: 'URL del Raspberry Pi',
placeholder: 'http://raspberrypi.local:8000',
},
},
}

View File

@@ -0,0 +1,149 @@
export default {
header: {
subtitle: 'Robot Control Monitor',
online: 'ONLINE',
offline: 'OFFLINE',
},
panels: {
blockQueue: 'Block Queue',
motionControls: 'Motion Controls',
eyeControls: 'Eye Controls',
gestureSensor: 'Gesture Sensor',
},
blocks: {
empty: 'No blocks in queue',
clear: 'Clear Queue',
queued: 'Queued',
processing: 'Processing',
},
blockActions: {
rd: 'Advance to crossing',
gn: 'Turn right 90\u00B0',
bu: 'Turn left 90\u00B0',
ye: 'Take / Extract liquid',
og: 'Leave / Dispense liquid',
vt: 'Surprise pause',
},
colors: {
red: 'Red',
green: 'Green',
blue: 'Blue',
yellow: 'Yellow',
orange: 'Orange',
violet: 'Violet',
},
motion: {
forward: 'Forward',
backward: 'Back',
left: 'Left',
right: 'Right',
sending: 'Sending: {dir}',
sent: 'Motor command sent: {dir}',
failed: 'Command failed',
},
motionToast: {
success: 'Motion {dir}',
},
eyes: {
shapeLabel: 'Eye Color',
actionsLabel: 'Actions',
applyShape: 'Apply Shape',
applyColor: 'Apply Color',
eyeOn: 'Turn On',
eyeOff: 'Turn Off',
toastSetShape: 'Eye shape set: {shape}',
toastFailedShape: 'Failed to set eye shape',
toastSetColor: 'Eye color set: {color}',
toastFailedColor: 'Failed to set eye color',
toastOn: 'Eyes turned on',
toastOff: 'Eyes turned off',
toastFailedOn: 'Failed to turn eyes on',
toastFailedOff: 'Failed to turn eyes off',
},
eyeShapes: {
open: 'Open',
fw: 'Forward',
down: 'Down',
gesture: 'Gesture',
},
eyeColors: {
white: 'White',
red: 'Red',
green: 'Green',
blue: 'Blue',
yellow: 'Yellow',
orange: 'Orange',
purple: 'Purple',
cyan: 'Cyan',
black: 'Off',
},
gestures: {
modeLabel: 'Operating Mode',
blockMode: 'Block Mode',
gestureMode: 'Gesture Mode',
detectedLabel: 'Detected Gestures',
empty: 'No gestures detected yet',
reference: 'Gesture Reference',
clearLog: 'Clear Log',
toggleToast: 'Toggle mode',
},
gestureNames: {
forward: 'Push Forward',
left: 'Wave Left',
right: 'Wave Right',
up: 'Wave Up',
down: 'Wave Down',
clockwise: 'Circle CW',
anticlockwise: 'Circle CCW',
wave: 'Wave (Toggle)',
},
gestureActions: {
forward: 'Advance crossing',
right: 'Turn right 90\u00B0',
left: 'Turn left 90\u00B0',
up: 'Take / Extract',
down: 'Leave / Dispense',
clockwise: 'Idle',
anticlockwise: 'Idle',
wave: 'Toggle mode',
},
gestureRef: {
forward: 'Push Forward',
left: 'Wave Left',
right: 'Wave Right',
up: 'Wave Up',
down: 'Wave Down',
clockwise: 'Circle CW',
anticlockwise: 'Circle CCW',
wave: 'Wave (Both)',
},
modes: {
block: 'Block Mode',
gesture: 'Gesture Mode',
},
toast: {
cleared: 'Queue cleared',
switched: 'Switched to {mode} mode',
},
theme: {
light: 'Switch to light mode',
dark: 'Switch to dark mode',
},
settings: {
title: 'Settings',
save: 'Save',
saved: 'Settings saved',
theme: {
label: 'Theme',
dark: 'Dark',
light: 'Light',
},
language: {
label: 'Language',
},
piUrl: {
label: 'Raspberry Pi URL',
placeholder: 'http://raspberrypi.local:8000',
},
},
}

View File

@@ -0,0 +1,149 @@
export default {
header: {
subtitle: 'Monitor de Control del Robot',
online: 'EN LÍNEA',
offline: 'DESCONNECTADO',
},
panels: {
blockQueue: 'Cola de Bloques',
motionControls: 'Controles de Movimiento',
eyeControls: 'Controles de los Ojos',
gestureSensor: 'Sensor de Gestos',
},
blocks: {
empty: 'No hay bloques en la cola',
clear: 'Borrar Cola',
queued: 'En espera',
processing: 'Procesando',
},
blockActions: {
rd: 'Avanzar hasta el cruce',
gn: 'Girar a la derecha 90\u00B0',
bu: 'Girar a la izquierda 90\u00B0',
ye: 'Tomar / Extraer líquido',
og: 'Dejar / Dispensar líquido',
vt: 'Pausa sorpresa',
},
colors: {
red: 'Rojo',
green: 'Verde',
blue: 'Azul',
yellow: 'Amarillo',
orange: 'Naranja',
violet: 'Violeta',
},
motion: {
forward: 'Adelante',
backward: 'Atrás',
left: 'Izquierda',
right: 'Derecha',
sending: 'Enviando: {dir}',
sent: 'Comando de motor enviado: {dir}',
failed: 'El comando falló',
},
motionToast: {
success: 'Movimiento {dir}',
},
eyes: {
shapeLabel: 'Color de los Ojos',
actionsLabel: 'Acciones',
applyShape: 'Aplicar Forma',
applyColor: 'Aplicar Color',
eyeOn: 'Encender',
eyeOff: 'Apagar',
toastSetShape: 'Forma de ojos establecida: {shape}',
toastFailedShape: 'No se pudo establecer la forma de los ojos',
toastSetColor: 'Color de ojos establecido: {color}',
toastFailedColor: 'No se pudo establecer el color de los ojos',
toastOn: 'Ojos encendidos',
toastOff: 'Ojos apagados',
toastFailedOn: 'No se pudieron encender los ojos',
toastFailedOff: 'No se pudieron apagar los ojos',
},
eyeShapes: {
open: 'Abiertos',
fw: 'Adelante',
down: 'Abajo',
gesture: 'Gesto',
},
eyeColors: {
white: 'Blanco',
red: 'Rojo',
green: 'Verde',
blue: 'Azul',
yellow: 'Amarillo',
orange: 'Naranja',
purple: 'Morado',
cyan: 'Cian',
black: 'Apagado',
},
gestures: {
modeLabel: 'Modo de Funcionamiento',
blockMode: 'Modo Bloques',
gestureMode: 'Modo Gestos',
detectedLabel: 'Gestos Detectados',
empty: 'Aún no se han detectado gestos',
reference: 'Referencia de Gestos',
clearLog: 'Borrar Registro',
toggleToast: 'Cambiar modo',
},
gestureNames: {
forward: 'Empujar Adelante',
left: 'Onda Izquierda',
right: 'Onda Derecha',
up: 'Onda Arriba',
down: 'Onda Abajo',
clockwise: 'Círculo CW',
anticlockwise: 'Círculo CCW',
wave: 'Onda (Conmute)',
},
gestureActions: {
forward: 'Avanzar cruce',
right: 'Girar a la derecha 90\u00B0',
left: 'Girar a la izquierda 90\u00B0',
up: 'Tomar / Extraer',
down: 'Dejar / Dispensar',
clockwise: 'Inactivo',
anticlockwise: 'Inactivo',
wave: 'Cambiar modo',
},
gestureRef: {
forward: 'Empujar Adelante',
left: 'Onda Izquierda',
right: 'Onda Derecha',
up: 'Onda Arriba',
down: 'Onda Abajo',
clockwise: 'Círculo CW',
anticlockwise: 'Círculo CCW',
wave: 'Onda (Ambas)',
},
modes: {
block: 'Modo Bloques',
gesture: 'Modo Gestos',
},
toast: {
cleared: 'Cola borrada',
switched: 'Cambiado a modo {mode}',
},
theme: {
light: 'Cambiar a modo claro',
dark: 'Cambiar a modo oscuro',
},
settings: {
title: 'Configuración',
save: 'Guardar',
saved: 'Configuración guardada',
theme: {
label: 'Tema',
dark: 'Oscuro',
light: 'Claro',
},
language: {
label: 'Idioma',
},
piUrl: {
label: 'URL de Raspberry Pi',
placeholder: 'http://raspberrypi.local:8000',
},
},
}

View File

@@ -0,0 +1,38 @@
import { defineNuxtPlugin, useRuntimeConfig } from '#app'
import { createI18n } from 'vue-i18n'
import en from '~/locales/en'
import ca from '~/locales/ca'
import es from '~/locales/es'
const locales = ['en', 'ca', 'es'] as const
type LocaleType = (typeof locales)[number]
function getSavedLocale(): LocaleType {
if (typeof window !== 'undefined') {
const saved = localStorage.getItem('quibot-locale')
if (saved && locales.includes(saved as LocaleType)) {
return saved as LocaleType
}
}
return 'en'
}
export default defineNuxtPlugin((nuxtApp) => {
const locale = getSavedLocale()
const i18n = createI18n({
legacy: false,
locale,
fallbackLocale: 'en',
messages: { en, ca, es },
})
nuxtApp.vueApp.use(i18n)
// Expose setLocale for global access
nuxtApp.provide('setLocale', (l: LocaleType) => {
if (locales.includes(l)) {
i18n.global.locale.value = l
localStorage.setItem('quibot-locale', l)
}
})
})

View File

@@ -0,0 +1,269 @@
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const { locale } = useI18n()
const props = defineProps<{
show: boolean
}>()
const emit = defineEmits<{
'update:show': [value: boolean]
toast: [msg: string, type: 'success' | 'error']
}>()
const localShow = ref(props.show)
watch(() => props.show, (val) => {
localShow.value = val
})
const selectedTheme = ref(localStorage.getItem('quibot-theme') || 'dark')
const selectedLanguage = ref(localStorage.getItem('quibot-locale') || 'en')
const piUrl = ref(localStorage.getItem('quibot-pi-url') || 'http://raspberrypi.local:8000')
function applyTheme(theme: string) {
document.documentElement.setAttribute('data-theme', theme)
localStorage.setItem('quibot-theme', theme)
}
function saveSettings() {
applyTheme(selectedTheme.value)
locale.value = selectedLanguage.value as any
localStorage.setItem('quibot-locale', selectedLanguage.value)
localStorage.setItem('quibot-pi-url', piUrl.value)
document.cookie = `quibot-pi-url=${encodeURIComponent(piUrl.value)};path=/`
emit('toast', t('settings.saved'), 'success')
emit('update:show', false)
}
function close() {
localShow.value = false
emit('update:show', false)
}
function backdropClick(e: MouseEvent) {
if ((e.target as HTMLElement).classList.contains('settings-backdrop')) {
close()
}
}
</script>
<template>
<Teleport to="body">
<div v-show="localShow" class="settings-backdrop" @click="backdropClick">
<div class="settings-modal">
<div class="settings-header">
<h2>{{ $t('settings.title') }}</h2>
<button class="close-btn" @click="close">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
</button>
</div>
<!-- Theme -->
<div class="setting-row">
<span class="setting-label">{{ $t('settings.theme.label') }}</span>
<div class="theme-options">
<button
:class="['theme-option', { active: selectedTheme === 'light' }]"
@click="selectedTheme = 'light'"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1111.21 3a7 7 0 009.79 9.79z"/></svg>
<span>{{ $t('settings.theme.light') }}</span>
</button>
<button
:class="['theme-option', { active: selectedTheme === 'dark' }]"
@click="selectedTheme = 'dark'"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><path d="M12 1v2m0 18v2m11-11h-2M3 12H1m17.07-7.07l-1.41 1.41M6.34 17.66l-1.41 1.41m14.14 0l-1.41-1.41M6.34 6.34L4.93 4.93"/></svg>
<span>{{ $t('settings.theme.dark') }}</span>
</button>
</div>
</div>
<!-- Language -->
<div class="setting-row">
<span class="setting-label">{{ $t('settings.language.label') }}</span>
<select v-model="selectedLanguage" class="lang-select">
<option value="en">English</option>
<option value="ca">Català</option>
<option value="es">Español</option>
</select>
</div>
<!-- PI URL -->
<div class="setting-row">
<span class="setting-label">{{ $t('settings.piUrl.label') }}</span>
<input
v-model="piUrl"
:placeholder="$t('settings.piUrl.placeholder')"
type="text"
class="url-input"
/>
</div>
<!-- Save -->
<button class="btn-save" @click="saveSettings">{{ $t('settings.save') }}</button>
</div>
</div>
</Teleport>
</template>
<style scoped>
.settings-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
transition: opacity 0.15s ease;
}
.settings-backdrop[style*="display: block"],
.settings-backdrop[style*="display:flex"] {
display: flex !important;
opacity: 1;
}
.settings-modal {
background: var(--bg-panel);
border: 1px solid var(--border-color);
border-radius: 1rem;
padding: 1.5rem;
width: 90%;
max-width: 420px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.settings-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.settings-header h2 {
font-size: 1rem;
font-weight: 700;
color: var(--text-primary);
margin: 0;
text-transform: uppercase;
letter-spacing: 0.1em;
}
.close-btn {
display: grid;
place-content: center;
width: 32px;
height: 32px;
padding: 0;
background: var(--btn-bg);
border: 2px solid var(--border-color);
border-radius: 0.5rem;
color: var(--text-muted);
cursor: pointer;
transition: all 0.15s ease;
}
.close-btn:hover {
border-color: #ef4444;
color: #ef4444;
}
.setting-row {
margin-bottom: 1.25rem;
}
.setting-label {
display: block;
font-size: 0.75rem;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 0.5rem;
}
.theme-options {
display: flex;
gap: 0.5rem;
}
.theme-option {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
padding: 0.625rem 1rem;
font-size: 0.8rem;
font-weight: 600;
font-family: inherit;
color: var(--text-muted);
background: var(--bg-secondary);
border: 2px solid var(--border-color);
border-radius: 0.625rem;
cursor: pointer;
transition: all 0.15s ease;
}
.theme-option.active {
border-color: var(--accent);
color: var(--accent);
background: var(--active-bg);
}
.theme-option:not(:disabled):hover {
border-color: var(--border-subtle);
color: var(--text-primary);
}
.lang-select,
.url-input {
width: 100%;
padding: 0.625rem 0.75rem;
font-family: inherit;
font-size: 0.85rem;
color: var(--text-primary);
background: var(--bg-secondary);
border: 2px solid var(--border-color);
border-radius: 0.625rem;
outline: none;
transition: border-color 0.15s ease;
box-sizing: border-box;
}
.lang-select:focus,
.url-input:focus {
border-color: var(--accent);
}
.url-input::placeholder {
color: var(--text-ghost);
}
.btn-save {
width: 100%;
padding: 0.75rem 1rem;
font-size: 0.85rem;
font-weight: 700;
font-family: inherit;
color: #fff;
background: linear-gradient(135deg, var(--accent), #ea580c);
border: none;
border-radius: 0.625rem;
cursor: pointer;
transition: all 0.2s ease;
margin-top: 0.5rem;
}
.btn-save:hover {
box-shadow: 0 4px 16px var(--accent-glow);
transform: translateY(-1px);
}
</style>

View File

@@ -5,6 +5,10 @@ export default defineNuxtConfig({
runtimeConfig: {
quibotBaseUrl: process.env.QUIBOT_BASE_URL || 'http://quibot:8000',
quibotToken: process.env.QUIBOT_TOKEN || 'MY_SECRET_TOKEN',
public: {
defaultLocale: 'en',
supportedLocales: ['en', 'ca', 'es'],
}
},
vite: {
optimizeDeps: {

File diff suppressed because it is too large Load Diff

View File

@@ -11,9 +11,8 @@
},
"dependencies": {
"nuxt": "^4.4.2",
"react-native-svg": "^15.15.5",
"vue": "^3.5.32",
"vue-router": "^5.0.4"
},
"devDependencies": {
}
}

View File

@@ -1,5 +1,4 @@
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const direction = getRouterParam(event, 'direction')
if (direction !== 'forward' && direction !== 'backwards') {
@@ -9,7 +8,10 @@ export default defineEventHandler(async (event) => {
})
}
return await $fetch(`${config.quibotBaseUrl}/motor/step/${direction}`, {
const baseUrl = getPiBaseUrl(event)
const config = useRuntimeConfig()
return await $fetch(`${baseUrl}/motor/step/${direction}`, {
method: 'POST',
query: {
token: config.quibotToken,

View File

@@ -1,7 +1,8 @@
export default defineEventHandler(async () => {
export default defineEventHandler(async (event) => {
const baseUrl = getPiBaseUrl(event)
const config = useRuntimeConfig()
return await $fetch(`${config.quibotBaseUrl}/motor/stop`, {
return await $fetch(`${baseUrl}/motor/stop`, {
method: 'POST',
query: {
token: config.quibotToken,

View File

@@ -0,0 +1,10 @@
export default defineNitroPlugin((nitroApp) => {
const base = useRuntimeConfig().quibotBaseUrl
nitroApp.hooks.hook('request', (event) => {
const url = getCookie(event, 'quibot-pi-url')
if (url && url !== decodeURIComponent(base)) {
// Override the base URL for this request
(event.context as any).__piBaseUrl = url
}
})
})

View File

@@ -0,0 +1,9 @@
import { getCookie } from 'h3'
export function getPiBaseUrl(event: any): string {
const cookieUrl = getCookie(event, 'quibot-pi-url')
if (cookieUrl) return decodeURIComponent(cookieUrl)
const config = useRuntimeConfig()
return config.quibotBaseUrl
}