Skip to content
Open
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 @@ -67,7 +67,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 'en-US' | 'id-ID' | 'ar-SA') || 'id-ID',
};

// ── Model resolution from request headers ──
Expand Down
8 changes: 4 additions & 4 deletions app/api/generate/scene-outlines-stream/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export async function POST(req: NextRequest) {

// Build prompt (same logic as generateSceneOutlinesFromRequirements)
let availableImagesText =
requirements.language === 'zh-CN' ? '无可用图片' : 'No images available';
requirements.language === 'id-ID' ? 'Tidak ada gambar tersedia' : 'No images available';
let visionImages: Array<{ id: string; src: string }> | undefined;

if (pdfImages && pdfImages.length > 0) {
Expand Down Expand Up @@ -177,11 +177,11 @@ export async function POST(req: NextRequest) {
language: requirements.language,
pdfContent: pdfText
? pdfText.substring(0, MAX_PDF_CONTENT_CHARS)
: requirements.language === 'zh-CN'
? ''
: requirements.language === 'id-ID'
? 'Tidak ada'
: 'None',
availableImages: availableImagesText,
researchContext: researchContext || (requirements.language === 'zh-CN' ? '' : 'None'),
researchContext: researchContext || (requirements.language === 'id-ID' ? 'Tidak ada' : 'None'),
mediaGenerationPolicy,
teacherContext,
});
Expand Down
2 changes: 1 addition & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const inter = localFont({
});

export const metadata: Metadata = {
title: 'OpenMAIC',
title: 'Kelas Kecerdasan Artifisial',
description:
'The open-source AI interactive classroom. Upload a PDF to instantly generate an immersive, multi-agent learning experience.',
};
Expand Down
43 changes: 28 additions & 15 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,14 @@ const RECENT_OPEN_STORAGE_KEY = 'recentClassroomsOpen';
interface FormState {
pdfFile: File | null;
requirement: string;
language: 'zh-CN' | 'en-US';
language: 'en-US' | 'id-ID' | 'ar-SA';
webSearch: boolean;
}

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

Expand Down Expand Up @@ -101,10 +101,10 @@ 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 === 'en-US' || savedLanguage === 'id-ID' || savedLanguage === 'ar-SA') {
updates.language = savedLanguage;
} else {
const detected = navigator.language?.startsWith('zh') ? 'zh-CN' : 'en-US';
const detected = navigator.language?.startsWith('id') ? 'id-ID' : navigator.language?.startsWith('ar') ? 'ar-SA' : navigator.language?.startsWith('en') ? 'en-US' : 'id-ID';
updates.language = detected;
}
if (Object.keys(updates).length > 0) {
Expand Down Expand Up @@ -338,22 +338,22 @@ 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'}
{locale === 'id-ID' ? 'ID' : locale === 'ar-SA' ? 'AR' : '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]">
<button
onClick={() => {
setLocale('zh-CN');
setLocale('id-ID');
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 === 'zh-CN' &&
locale === 'id-ID' &&
'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400',
)}
>
简体中文
Bahasa Indonesia
</button>
<button
onClick={() => {
Expand All @@ -368,6 +368,19 @@ function HomePage() {
>
English
</button>
<button
onClick={() => {
setLocale('ar-SA');
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 === 'ar-SA' &&
'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400',
)}
>
العربية
</button>
</div>
)}
</div>
Expand Down Expand Up @@ -492,10 +505,8 @@ function HomePage() {
classrooms.length === 0 ? 'justify-center min-h-[calc(100dvh-8rem)]' : 'mt-[10vh]',
)}
>
{/* ── Logo ── */}
<motion.img
src="/logo-horizontal.png"
alt="OpenMAIC"
{/* ── Title ── */}
<motion.h1
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
Expand All @@ -504,8 +515,10 @@ function HomePage() {
stiffness: 200,
damping: 20,
}}
className="h-12 md:h-16 mb-2 -ml-2 md:-ml-3"
/>
className="text-2xl md:text-4xl font-bold bg-gradient-to-r from-purple-600 via-violet-600 to-indigo-600 dark:from-purple-400 dark:via-violet-400 dark:to-indigo-400 bg-clip-text text-transparent mb-2"
>
{t('home.appTitle')}
</motion.h1>

{/* ── Slogan ── */}
<motion.p
Expand Down Expand Up @@ -686,7 +699,7 @@ function HomePage() {

{/* Footer — flows with content, at the very end */}
<div className="mt-auto pt-12 pb-4 text-center text-xs text-muted-foreground/40">
OpenMAIC Open Source Project
{t('home.appTitle')}
</div>
</div>
);
Expand Down
8 changes: 4 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: 'en-US' | 'id-ID' | 'ar-SA';
onLanguageChange: (lang: 'en-US' | 'id-ID' | 'ar-SA') => void;
webSearch: boolean;
onWebSearchChange: (v: boolean) => void;
onSettingsOpen: (section?: SettingsSection) => void;
Expand Down Expand Up @@ -361,11 +361,11 @@ export function GenerationToolbar({
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => onLanguageChange(language === 'zh-CN' ? 'en-US' : 'zh-CN')}
onClick={() => onLanguageChange(language === 'id-ID' ? 'en-US' : language === 'en-US' ? 'ar-SA' : 'id-ID')}
className={pillMuted}
>
<Globe className="size-3.5" />
<span>{language === 'zh-CN' ? '中文' : 'EN'}</span>
<span>{language === 'id-ID' ? 'ID' : language === 'ar-SA' ? 'AR' : 'EN'}</span>
</button>
</TooltipTrigger>
<TooltipContent>{t('toolbar.languageHint')}</TooltipContent>
Expand Down
21 changes: 17 additions & 4 deletions components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,22 +113,22 @@ 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'}
{locale === 'id-ID' ? 'ID' : locale === 'ar-SA' ? 'AR' : '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]">
<button
onClick={() => {
setLocale('zh-CN');
setLocale('id-ID');
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 === 'zh-CN' &&
locale === 'id-ID' &&
'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400',
)}
>
简体中文
Bahasa Indonesia
</button>
<button
onClick={() => {
Expand All @@ -143,6 +143,19 @@ export function Header({ currentSceneTitle }: HeaderProps) {
>
English
</button>
<button
onClick={() => {
setLocale('ar-SA');
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 === 'ar-SA' &&
'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400',
)}
>
العربية
</button>
</div>
)}
</div>
Expand Down
2 changes: 2 additions & 0 deletions components/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,15 @@ const IMAGE_PROVIDER_NAMES: Record<ImageProviderId, string> = {
'qwen-image': 'providerQwenImage',
'nano-banana': 'providerNanoBanana',
'grok-image': 'providerGrokImage',
'openrouter-image': 'providerOpenRouterImage',
};

const IMAGE_PROVIDER_ICONS: Record<ImageProviderId, string> = {
seedream: '/logos/doubao.svg',
'qwen-image': '/logos/bailian.svg',
'nano-banana': '/logos/gemini.svg',
'grok-image': '/logos/grok.svg',
'openrouter-image': '/logos/openrouter.svg',
};

const VIDEO_PROVIDER_NAMES: Record<VideoProviderId, string> = {
Expand Down
2 changes: 1 addition & 1 deletion components/stage/scene-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export function SceneSidebar({
className="flex items-center gap-2 cursor-pointer rounded-lg px-1.5 -mx-1.5 py-1 -my-1 hover:bg-gray-100/80 dark:hover:bg-gray-800/60 active:scale-[0.97] transition-all duration-150"
title={t('generation.backToHome')}
>
<img src="/logo-horizontal.png" alt="OpenMAIC" className="h-6" />
<span className="text-sm font-bold bg-gradient-to-r from-purple-600 via-violet-600 to-indigo-600 dark:from-purple-400 dark:via-violet-400 dark:to-indigo-400 bg-clip-text text-transparent truncate">{t('home.appTitle')}</span>
</button>
<button
onClick={() => onCollapseChange(true)}
Expand Down
134 changes: 134 additions & 0 deletions lib/ai/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -945,6 +945,140 @@ export const PROVIDERS: Record<ProviderId, ProviderConfig> = {
},
],
},

openrouter: {
id: 'openrouter',
name: 'OpenRouter',
type: 'openai',
defaultBaseUrl: 'https://openrouter.ai/api/v1',
requiresApiKey: true,
icon: '/logos/openrouter.svg',
models: [
// OpenAI models via OpenRouter
{
id: 'openai/gpt-4o',
name: 'GPT-4o (OpenAI)',
contextWindow: 128000,
outputWindow: 4096,
capabilities: { streaming: true, tools: true, vision: true },
},
{
id: 'openai/gpt-4o-mini',
name: 'GPT-4o mini (OpenAI)',
contextWindow: 128000,
outputWindow: 4096,
capabilities: { streaming: true, tools: true, vision: true },
},
// Anthropic models via OpenRouter
{
id: 'anthropic/claude-sonnet-4-5',
name: 'Claude Sonnet 4.5 (Anthropic)',
contextWindow: 200000,
outputWindow: 64000,
capabilities: { streaming: true, tools: true, vision: true },
},
{
id: 'anthropic/claude-haiku-4-5',
name: 'Claude Haiku 4.5 (Anthropic)',
contextWindow: 200000,
outputWindow: 64000,
capabilities: { streaming: true, tools: true, vision: true },
},
// Google models via OpenRouter
{
id: 'google/gemini-2.5-flash',
name: 'Gemini 2.5 Flash (Google)',
contextWindow: 1048576,
outputWindow: 65536,
capabilities: { streaming: true, tools: true, vision: true },
},
{
id: 'google/gemini-2.5-pro',
name: 'Gemini 2.5 Pro (Google)',
contextWindow: 1048576,
outputWindow: 65536,
capabilities: { streaming: true, tools: true, vision: true },
},
// DeepSeek models via OpenRouter
{
id: 'deepseek/deepseek-chat-v3-0324',
name: 'DeepSeek V3 (DeepSeek)',
contextWindow: 128000,
outputWindow: 8192,
capabilities: { streaming: true, tools: true, vision: false },
},
{
id: 'deepseek/deepseek-r1-0528',
name: 'DeepSeek R1 (DeepSeek)',
contextWindow: 128000,
outputWindow: 32000,
capabilities: {
streaming: true,
tools: true,
vision: false,
thinking: {
toggleable: false,
budgetAdjustable: false,
defaultEnabled: true,
},
},
},
// Meta Llama via OpenRouter
{
id: 'meta-llama/llama-4-maverick',
name: 'Llama 4 Maverick (Meta)',
contextWindow: 1048576,
outputWindow: 16384,
capabilities: { streaming: true, tools: true, vision: true },
},
{
id: 'meta-llama/llama-4-scout',
name: 'Llama 4 Scout (Meta)',
contextWindow: 512000,
outputWindow: 16384,
capabilities: { streaming: true, tools: true, vision: true },
},
// Mistral models via OpenRouter
{
id: 'mistralai/mistral-large',
name: 'Mistral Large (Mistral)',
contextWindow: 131072,
outputWindow: 16384,
capabilities: { streaming: true, tools: true, vision: true },
},
{
id: 'mistralai/mistral-small-3.2-24b-instruct',
name: 'Mistral Small 3.2 (Mistral)',
contextWindow: 131072,
outputWindow: 16384,
capabilities: { streaming: true, tools: true, vision: true },
},
// Free models via OpenRouter
{
id: 'meta-llama/llama-3.3-70b-instruct:free',
name: 'Llama 3.3 70B (Free)',
contextWindow: 131072,
outputWindow: 8192,
capabilities: { streaming: true, tools: true, vision: false },
},
{
id: 'deepseek/deepseek-r1:free',
name: 'DeepSeek R1 (Free)',
contextWindow: 65536,
outputWindow: 8192,
capabilities: {
streaming: true,
tools: false,
vision: false,
thinking: {
toggleable: false,
budgetAdjustable: false,
defaultEnabled: true,
},
},
},
],
},
};

/**
Expand Down
Loading
Loading