Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/api/generate/scene-content/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export async function POST(req: NextRequest) {
// Ensure outline has language from stageInfo (fallback for older outlines)
const outline: SceneOutline = {
...rawOutline,
language: rawOutline.language || (stageInfo?.language as 'zh-CN' | 'en-US') || 'zh-CN',
language: rawOutline.language || (stageInfo?.language as 'zh-CN' | 'en-US' | 'ca') || 'zh-CN',
};

// ── Model resolution from request headers ──
Expand Down
24 changes: 19 additions & 5 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,14 @@ const RECENT_OPEN_STORAGE_KEY = 'recentClassroomsOpen';
interface FormState {
pdfFile: File | null;
requirement: string;
language: 'zh-CN' | 'en-US';
language: 'zh-CN' | 'en-US' | 'ca';
webSearch: boolean;
}

const initialFormState: FormState = {
pdfFile: null,
requirement: '',
language: 'zh-CN',
language: 'ca',
webSearch: false,
};

Expand Down Expand Up @@ -100,10 +100,11 @@ function HomePage() {
const savedLanguage = localStorage.getItem(LANGUAGE_STORAGE_KEY);
const updates: Partial<FormState> = {};
if (savedWebSearch === 'true') updates.webSearch = true;
if (savedLanguage === 'zh-CN' || savedLanguage === 'en-US') {
if (savedLanguage === 'zh-CN' || savedLanguage === 'en-US' || savedLanguage === 'ca') {
updates.language = savedLanguage;
} else {
const detected = navigator.language?.startsWith('zh') ? 'zh-CN' : 'en-US';
const lang = navigator.language ?? '';
const detected: 'zh-CN' | 'en-US' | 'ca' = lang.startsWith('zh') ? 'zh-CN' : lang.startsWith('ca') ? 'ca' : 'en-US';
updates.language = detected;
}
if (Object.keys(updates).length > 0) {
Expand Down Expand Up @@ -346,7 +347,7 @@ function HomePage() {
}}
className="flex items-center gap-1 px-3 py-1.5 rounded-full text-xs font-bold text-gray-500 dark:text-gray-400 hover:bg-white dark:hover:bg-gray-700 hover:text-gray-800 dark:hover:text-gray-200 hover:shadow-sm transition-all"
>
{locale === 'zh-CN' ? 'CN' : 'EN'}
{{ 'zh-CN': 'CN', 'en-US': 'EN', 'ca': 'CA' }[locale] ?? 'EN'}
</button>
{languageOpen && (
<div className="absolute top-full mt-2 right-0 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg overflow-hidden z-50 min-w-[120px]">
Expand Down Expand Up @@ -376,6 +377,19 @@ function HomePage() {
>
English
</button>
<button
onClick={() => {
setLocale('ca');
setLanguageOpen(false);
}}
className={cn(
'w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors',
locale === 'ca' &&
'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400',
)}
>
Català
</button>
</div>
)}
</div>
Expand Down
10 changes: 8 additions & 2 deletions components/agent/agent-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ function AgentVoicePill({
const courseLanguage =
(typeof localStorage !== 'undefined' && localStorage.getItem('generationLanguage')) ||
'zh-CN';
const previewText = courseLanguage === 'en-US' ? 'Welcome to AI Classroom' : '欢迎来到AI课堂';
const previewText =
courseLanguage === 'zh-CN' ? '欢迎来到AI课堂' :
courseLanguage === 'ca' ? "Benvingut a l'aula d'IA" :
'Welcome to AI Classroom';

if (providerId === 'browser-native-tts') {
const { promise, cancel } = playBrowserTTSPreview({ text: previewText, voice: voiceId });
Expand Down Expand Up @@ -308,7 +311,10 @@ function TeacherVoicePill({
const courseLanguage =
(typeof localStorage !== 'undefined' && localStorage.getItem('generationLanguage')) ||
'zh-CN';
const previewText = courseLanguage === 'en-US' ? 'Welcome to AI Classroom' : '欢迎来到AI课堂';
const previewText =
courseLanguage === 'zh-CN' ? '欢迎来到AI课堂' :
courseLanguage === 'ca' ? "Benvingut a l'aula d'IA" :
'Welcome to AI Classroom';

if (providerId === 'browser-native-tts') {
const { promise, cancel } = playBrowserTTSPreview({ text: previewText, voice: voiceId });
Expand Down
12 changes: 8 additions & 4 deletions components/generation/generation-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ const MAX_PDF_SIZE_BYTES = MAX_PDF_SIZE_MB * 1024 * 1024;

// ─── Types ───────────────────────────────────────────────────
export interface GenerationToolbarProps {
language: 'zh-CN' | 'en-US';
onLanguageChange: (lang: 'zh-CN' | 'en-US') => void;
language: 'zh-CN' | 'en-US' | 'ca';
onLanguageChange: (lang: 'zh-CN' | 'en-US' | 'ca') => void;
webSearch: boolean;
onWebSearchChange: (v: boolean) => void;
onSettingsOpen: (section?: SettingsSection) => void;
Expand Down Expand Up @@ -361,11 +361,15 @@ export function GenerationToolbar({
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => onLanguageChange(language === 'zh-CN' ? 'en-US' : 'zh-CN')}
onClick={() => {
const LANGS = ['zh-CN', 'en-US', 'ca'] as const;
const nextLang = LANGS[(LANGS.indexOf(language) + 1) % 3];
onLanguageChange(nextLang);
}}
className={pillMuted}
>
<Globe className="size-3.5" />
<span>{language === 'zh-CN' ? '中文' : 'EN'}</span>
<span>{{ 'zh-CN': '中文', 'en-US': 'EN', 'ca': 'CA' }[language]}</span>
</button>
</TooltipTrigger>
<TooltipContent>{t('toolbar.languageHint')}</TooltipContent>
Expand Down
15 changes: 14 additions & 1 deletion components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export function Header({ currentSceneTitle }: HeaderProps) {
}}
className="flex items-center gap-1 px-3 py-1.5 rounded-full text-xs font-bold text-gray-500 dark:text-gray-400 hover:bg-white dark:hover:bg-gray-700 hover:text-gray-800 dark:hover:text-gray-200 hover:shadow-sm transition-all"
>
{locale === 'zh-CN' ? 'CN' : 'EN'}
{{ 'zh-CN': 'CN', 'en-US': 'EN', 'ca': 'CA' }[locale] ?? 'EN'}
</button>
{languageOpen && (
<div className="absolute top-full mt-2 right-0 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg overflow-hidden z-50 min-w-[120px]">
Expand Down Expand Up @@ -138,6 +138,19 @@ export function Header({ currentSceneTitle }: HeaderProps) {
>
English
</button>
<button
onClick={() => {
setLocale('ca');
setLanguageOpen(false);
}}
className={cn(
'w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors',
locale === 'ca' &&
'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400',
)}
>
Català
</button>
</div>
)}
</div>
Expand Down
2 changes: 2 additions & 0 deletions lib/audio/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ export const TTS_PROVIDERS: Record<TTSProviderId, TTSProviderConfig> = {
gender: 'female',
},
{ id: 'en-US-GuyNeural', name: 'Guy', language: 'en-US', gender: 'male' },
{ id: 'ca-ES-AlbaNeural', name: 'Alba (Català)', language: 'ca-ES', gender: 'female' },
{ id: 'ca-ES-EnricNeural', name: 'Enric (Català)', language: 'ca-ES', gender: 'male' },
],
supportedFormats: ['mp3', 'wav', 'ogg'],
speedRange: { min: 0.5, max: 2.0, default: 1.0 },
Expand Down
2 changes: 1 addition & 1 deletion lib/generation/scene-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -735,7 +735,7 @@ function normalizeQuizAnswer(question: Record<string, unknown>): string[] | unde
async function generateInteractiveContent(
outline: SceneOutline,
aiCall: AICallFn,
language: 'zh-CN' | 'en-US' = 'zh-CN',
language: 'zh-CN' | 'en-US' | 'ca' = 'zh-CN',
): Promise<GeneratedInteractiveContent | null> {
const config = outline.interactiveConfig!;

Expand Down
6 changes: 3 additions & 3 deletions lib/hooks/use-i18n.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { Locale, translate, defaultLocale } from '@/lib/i18n';
import { Locale, translate, defaultLocale, VALID_LOCALES } from '@/lib/i18n';

type I18nContextType = {
locale: Locale;
Expand All @@ -10,7 +10,6 @@ type I18nContextType = {
};

const LOCALE_STORAGE_KEY = 'locale';
const VALID_LOCALES: Locale[] = ['zh-CN', 'en-US'];

const I18nContext = createContext<I18nContextType | undefined>(undefined);

Expand All @@ -26,7 +25,8 @@ export function I18nProvider({ children }: { children: ReactNode }) {
setLocaleState(stored as Locale);
return;
}
const detected = navigator.language?.startsWith('zh') ? 'zh-CN' : 'en-US';
const lang = navigator.language ?? '';
const detected: Locale = lang.startsWith('zh') ? 'zh-CN' : lang.startsWith('ca') ? 'ca' : 'en-US';
localStorage.setItem(LOCALE_STORAGE_KEY, detected);
setLocaleState(detected);
} catch {
Expand Down
74 changes: 74 additions & 0 deletions lib/i18n/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,80 @@ export const chatZhCN = {
},
} as const;

export const chatCa = {
chat: {
lecture: 'Lliçó',
noConversations: 'Cap conversa',
startConversation: 'Escriu un missatge per començar a xatejar',
noMessages: 'Encara no hi ha missatges',
ended: 'finalitzat',
unknown: 'Desconegut',
stopDiscussion: 'Aturar discussió',
endQA: 'Finalitzar P&R',
tabs: {
lecture: 'Notes',
chat: 'Xat',
},
lectureNotes: {
empty: 'Les notes apareixeran aquí després de la reproducció de la lliçó',
emptyHint: 'Premeu reproduir per iniciar la lliçó',
pageLabel: 'Pàgina {n}',
currentPage: 'Actual',
},
badge: {
qa: 'P&R',
discussion: 'DISC',
lecture: 'LLIÇ',
},
},
actions: {
names: {
spotlight: 'Focus',
laser: 'Làser',
wb_open: 'Obrir pissarra',
wb_draw_text: 'Text a la pissarra',
wb_draw_shape: 'Forma a la pissarra',
wb_draw_chart: 'Gràfic a la pissarra',
wb_draw_latex: 'Fórmula a la pissarra',
wb_draw_table: 'Taula a la pissarra',
wb_draw_line: 'Línia a la pissarra',
wb_clear: 'Netejar pissarra',
wb_delete: 'Eliminar element',
wb_close: 'Tancar pissarra',
discussion: 'Discussió',
},
status: {
inputStreaming: 'En espera',
inputAvailable: 'Executant',
outputAvailable: 'Completat',
outputError: 'Error',
outputDenied: 'Denegat',
running: 'Executant',
result: 'Completat',
error: 'Error',
},
},
agentBar: {
readyToLearn: 'Preparats per aprendre junts?',
expandedTitle: 'Configuració de rols de l\'aula',
configTooltip: 'Clic per configurar els rols de l\'aula',
voiceLabel: 'Veu',
voiceLoading: 'Carregant...',
voiceAutoAssign: 'Les veus s\'assignaran automàticament',
},
proactiveCard: {
discussion: 'Discussió',
join: 'Unir-se',
skip: 'Ometre',
pause: 'Pausar',
resume: 'Reprendre',
},
voice: {
startListening: 'Entrada de veu',
stopListening: 'Aturar gravació',
},
} as const;

export const chatEnUS = {
chat: {
lecture: 'Lecture',
Expand Down
41 changes: 41 additions & 0 deletions lib/i18n/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,47 @@ export const commonZhCN = {
},
} as const;

export const commonCa = {
common: {
you: 'Tu',
confirm: 'Confirmar',
cancel: 'Cancel·lar',
loading: 'Carregant...',
},
home: {
slogan: 'Aprenentatge Generatiu en Aula Interactiva Multi-Agent',
greeting: 'Hola, ',
},
toolbar: {
languageHint: 'El curs es generarà en aquesta llengua',
pdfParser: 'Analitzador',
pdfUpload: 'Pujar PDF',
removePdf: 'Eliminar fitxer',
webSearchOn: 'Activat',
webSearchOff: 'Clic per activar',
webSearchDesc: 'Cerca informació actualitzada a la web abans de la generació',
webSearchProvider: 'Motor de cerca',
webSearchNoProvider: 'Configura la clau API de cerca a la Configuració',
selectProvider: 'Selecciona proveïdor',
configureProvider: 'Configura model',
configureProviderHint: 'Configura almenys un proveïdor de model per generar cursos',
enterClassroom: 'Entrar a l\'aula',
advancedSettings: 'Configuració avançada',
ttsTitle: 'Síntesi de veu',
ttsHint: 'Trieu una veu per al professor IA',
ttsPreview: 'Previsualitzar',
ttsPreviewing: 'Reproduint...',
},
export: {
pptx: 'Exportar PPTX',
resourcePack: 'Exportar paquet de recursos',
resourcePackDesc: 'PPTX + pàgines interactives',
exporting: 'Exportant...',
exportSuccess: 'Exportació correcta',
exportFailed: 'Error en l\'exportació',
},
} as const;

export const commonEnUS = {
common: {
you: 'You',
Expand Down
Loading