|
| 1 | +<script setup lang="ts"> |
| 2 | +import type { AIModelStatus } from '@/entrypoints/background/model-manager/model-manager.model'; |
| 3 | +import { getAIService } from '@/entrypoints/background/ai/ai.service'; |
| 4 | +import { onMessage, sendMessage } from '@/entrypoints/background/messaging'; |
| 5 | +import { type SupportedLanguageCode, LanguageService } from '@/entrypoints/background/language/language.service'; |
| 6 | +
|
| 7 | +const AIService = getAIService(); |
| 8 | +const languageService = LanguageService.getInstance(); |
| 9 | +
|
| 10 | +const modelStatus = ref<AIModelStatus | null>(null); |
| 11 | +const text = ref(''); |
| 12 | +const translatedText = ref(''); |
| 13 | +const sourceLanguage = ref<SupportedLanguageCode | null>(null); |
| 14 | +const targetLanguage = ref<SupportedLanguageCode>('es'); |
| 15 | +const isLoading = ref(false); |
| 16 | +const error = ref<string | null>(null); |
| 17 | +const summarize = ref(false); |
| 18 | +const availableLanguages = ref<SupportedLanguageCode[]>([]); |
| 19 | +
|
| 20 | +const warning = ref<string | null>(null); |
| 21 | +
|
| 22 | +
|
| 23 | +
|
| 24 | +const canProcess = computed(() => { |
| 25 | + const hasText = text.value.trim().length > 0; |
| 26 | + const hasSourceLanguage = sourceLanguage.value !== null; |
| 27 | + const languagesAreSame = sourceLanguage.value?.toLowerCase() === targetLanguage.value.toLowerCase() && !summarize.value; |
| 28 | + const modelIsDownloading = modelStatus.value?.state === 'downloading'; |
| 29 | + return hasText && hasSourceLanguage && (!languagesAreSame || summarize.value) && !isLoading.value && !modelIsDownloading; |
| 30 | +}); |
| 31 | +
|
| 32 | +const apiAvailable = ref(true); |
| 33 | +
|
| 34 | +onMounted(async () => { |
| 35 | + apiAvailable.value = await AIService.checkAPIAvailability(); |
| 36 | + availableLanguages.value = [...languageService.getSupportedLanguages()]; |
| 37 | +
|
| 38 | + const browserLang = languageService.getBrowserLanguage(); |
| 39 | + targetLanguage.value = languageService.isLanguageSupported(browserLang) |
| 40 | + ? browserLang |
| 41 | + : availableLanguages.value[0]!; |
| 42 | +
|
| 43 | + onMessage('modelStatusUpdate', (message) => { |
| 44 | + if (message.data.state === 'downloading') { |
| 45 | + modelStatus.value = message.data; |
| 46 | + } else { |
| 47 | + modelStatus.value = null; |
| 48 | + } |
| 49 | + }); |
| 50 | +
|
| 51 | + onMessage('selectedText', async (message) => { |
| 52 | + text.value = message.data.text; |
| 53 | + summarize.value = message.data.summarize ?? false; |
| 54 | + warning.value = null; |
| 55 | + error.value = null; |
| 56 | + |
| 57 | + // Detectar idioma primero |
| 58 | + if (text.value.trim().length >= 15) { |
| 59 | + try { |
| 60 | + const lang = await AIService.detectLanguage(text.value); |
| 61 | + if (languageService.isLanguageSupported(lang)) { |
| 62 | + sourceLanguage.value = lang; |
| 63 | + await processText(); |
| 64 | + } else { |
| 65 | + sourceLanguage.value = null; |
| 66 | + error.value = t('detectedLanguageNotSupported', lang); |
| 67 | + } |
| 68 | + } catch (e: unknown) { |
| 69 | + if (e instanceof Error) { |
| 70 | + error.value = t('languageDetectionError'); |
| 71 | + } |
| 72 | + } |
| 73 | + } |
| 74 | + }); |
| 75 | +
|
| 76 | + void sendMessage('sidepanelReady'); |
| 77 | +}); |
| 78 | +
|
| 79 | +const processText = async () => { |
| 80 | + if (!sourceLanguage.value) return; |
| 81 | +
|
| 82 | + if (sourceLanguage.value === targetLanguage.value && !summarize.value) { |
| 83 | + warning.value = t('sameLanguageWarning'); |
| 84 | + return; |
| 85 | + } |
| 86 | +
|
| 87 | + isLoading.value = true; |
| 88 | + error.value = null; |
| 89 | + translatedText.value = ''; |
| 90 | + try { |
| 91 | + const response = await AIService.processText( |
| 92 | + text.value, |
| 93 | + { |
| 94 | + sourceLanguage: sourceLanguage.value, |
| 95 | + targetLanguage: targetLanguage.value, |
| 96 | + summarize: summarize.value, |
| 97 | + } |
| 98 | + ); |
| 99 | + translatedText.value = response; |
| 100 | + } catch (e: unknown) { |
| 101 | + if (e instanceof Error) { |
| 102 | + const errorMessage = e.message; |
| 103 | + error.value = `${t('processingError')}\n${errorMessage}`; |
| 104 | + } |
| 105 | + } finally { |
| 106 | + isLoading.value = false; |
| 107 | + } |
| 108 | +}; |
| 109 | +
|
| 110 | +watch(text, async (newText) => { |
| 111 | +
|
| 112 | + isLoading.value = false; // Restablecer estado de carga cuando cambia el texto |
| 113 | + modelStatus.value = null; // Restablecer estado del modelo cuando cambia el texto |
| 114 | + warning.value = null; |
| 115 | + if (newText.trim().length < 15) { |
| 116 | + sourceLanguage.value = null; |
| 117 | + error.value = null; // Limpiar error cuando el texto es demasiado corto |
| 118 | + return; |
| 119 | + } |
| 120 | + try { |
| 121 | + const lang = await AIService.detectLanguage(newText); |
| 122 | + if (languageService.isLanguageSupported(lang)) { |
| 123 | + sourceLanguage.value = lang; |
| 124 | + error.value = null; |
| 125 | + } else { |
| 126 | + sourceLanguage.value = null; |
| 127 | + error.value = t('detectedLanguageNotSupported', lang); |
| 128 | + } |
| 129 | + } catch (e: unknown) { |
| 130 | + if (e instanceof Error) { |
| 131 | + error.value = t('languageDetectionError'); |
| 132 | + } |
| 133 | + } |
| 134 | +}); |
| 135 | +
|
| 136 | +watch(targetLanguage, () => { |
| 137 | +
|
| 138 | + isLoading.value = false; // Restablecer estado de carga cuando cambia el idioma de destino |
| 139 | + modelStatus.value = null; // Restablecer estado del modelo cuando cambia el idioma de destino |
| 140 | + warning.value = null; |
| 141 | +}); |
| 142 | +
|
| 143 | +watch(summarize, () => { |
| 144 | +
|
| 145 | + isLoading.value = false; // Restablecer estado de carga cuando cambia resumir |
| 146 | + warning.value = null; |
| 147 | +}); |
| 148 | +
|
| 149 | +</script> |
| 150 | + |
| 151 | +<template> |
| 152 | + <div class="p-4 flex flex-col gap-4"> |
| 153 | + <AppHeader :api-available="apiAvailable" /> |
| 154 | + |
| 155 | + <ModelDownloadCard v-if="modelStatus" :status="modelStatus" /> |
| 156 | + |
| 157 | + <div class="flex flex-col gap-2"> |
| 158 | + <label for="input-text">Text to process:</label> |
| 159 | + <textarea id="input-text" v-model="text" class="border p-2 rounded-md" rows="5"></textarea> |
| 160 | + <div v-if="sourceLanguage"> |
| 161 | + Detected Language: {{ languageService.getLanguageKey(sourceLanguage) }} |
| 162 | + </div> |
| 163 | + </div> |
| 164 | + |
| 165 | + <div v-if="warning" id="process-warning-container" class="text-yellow-800 bg-yellow-100 p-2 rounded-md"> |
| 166 | + {{ warning }} |
| 167 | + </div> |
| 168 | + |
| 169 | + <ProcessControls |
| 170 | + v-model:targetLanguage="targetLanguage" |
| 171 | + v-model:summarize="summarize" |
| 172 | + :available-languages="availableLanguages" |
| 173 | + :is-loading="isLoading" |
| 174 | + :can-process="canProcess" |
| 175 | + @process="processText" |
| 176 | + /> |
| 177 | + |
| 178 | + <div v-if="error" class="text-red-500 bg-red-100 p-2 rounded-md"> |
| 179 | + {{ error }} |
| 180 | + </div> |
| 181 | + |
| 182 | + <div v-if="translatedText" class="flex flex-col gap-2"> |
| 183 | + <div class="flex justify-between items-center"> |
| 184 | + <label for="output-text">Result:</label> |
| 185 | + <span id="processing-source" class="text-xs bg-green-100 text-green-800 px-2 py-1 rounded-full"> |
| 186 | + {{ t('localProcessingBadge') }} |
| 187 | + </span> |
| 188 | + </div> |
| 189 | + <textarea id="output-text" :value="translatedText" class="border p-2 rounded-md" rows="5" readonly></textarea> |
| 190 | + </div> |
| 191 | + </div> |
| 192 | +</template> |
0 commit comments