Skip to content

Commit 91163e2

Browse files
committed
refactor(sidepanel): extract AppHeader component
1 parent 31e1de3 commit 91163e2

File tree

4 files changed

+214
-1
lines changed

4 files changed

+214
-1
lines changed

public/_locales/en/messages.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
"message": "AI-powered translation using Chrome's built-in APIs"
77
},
88
"processingError": {
9-
"message": "There was an error processing the request"
9+
"message": "An error occurred while processing the request"
10+
},
11+
"languageDetectionError": {
12+
"message": "Failed to detect language"
1013
},
1114
"inputLabel": {
1215
"message": "Text to process"

public/_locales/es/messages.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
"processingError": {
99
"message": "Hubo un error al procesar la solicitud"
1010
},
11+
"languageDetectionError": {
12+
"message": "Error al detectar el idioma"
13+
},
1114
"inputLabel": {
1215
"message": "Texto a procesar"
1316
},

src/components/AppHeader.vue

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script setup lang="ts">
2+
defineProps<{
3+
apiAvailable: boolean;
4+
}>();
5+
</script>
6+
7+
<template>
8+
<header>
9+
<div v-if="!apiAvailable" id="api-warning-container" class="mt-2 p-2 bg-yellow-50 border border-yellow-200 rounded-lg">
10+
<p class="text-yellow-800 text-xs">
11+
{{ t('apiWarning') }}
12+
</p>
13+
</div>
14+
</header>
15+
</template>
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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

Comments
 (0)