feat: adiciona OpenRouter, seletor de IA de imagens, e carregador de modelos de IA dinamico para Gemini, OpenAI, Claude e OpenRouter
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 2m4s
Details
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 2m4s
Details
This commit is contained in:
parent
50eafc51bd
commit
643d3f9d33
|
|
@ -26,11 +26,44 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
|
||||||
geminiKey: data.aiConfig?.geminiKey ? '********' : '',
|
geminiKey: data.aiConfig?.geminiKey ? '********' : '',
|
||||||
openaiKey: data.aiConfig?.openaiKey ? '********' : '',
|
openaiKey: data.aiConfig?.openaiKey ? '********' : '',
|
||||||
claudeKey: data.aiConfig?.claudeKey ? '********' : '',
|
claudeKey: data.aiConfig?.claudeKey ? '********' : '',
|
||||||
|
openrouterKey: data.aiConfig?.openrouterKey ? '********' : '',
|
||||||
geminiModel: data.aiConfig?.geminiModel || 'gemini-1.5-flash',
|
geminiModel: data.aiConfig?.geminiModel || 'gemini-1.5-flash',
|
||||||
openaiModel: data.aiConfig?.openaiModel || 'gpt-4o-mini',
|
openaiModel: data.aiConfig?.openaiModel || 'gpt-4o-mini',
|
||||||
claudeModel: data.aiConfig?.claudeModel || 'claude-3-5-sonnet-20241022'
|
claudeModel: data.aiConfig?.claudeModel || 'claude-3-5-sonnet-20241022',
|
||||||
|
openrouterModel: data.aiConfig?.openrouterModel || 'google/gemini-2.5-flash',
|
||||||
|
activeImageProvider: data.aiConfig?.activeImageProvider || 'gemini'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [dynamicModels, setDynamicModels] = useState<{
|
||||||
|
gemini: any[];
|
||||||
|
openai: any[];
|
||||||
|
claude: any[];
|
||||||
|
openrouter: any[];
|
||||||
|
}>({
|
||||||
|
gemini: [
|
||||||
|
{ id: 'gemini-1.5-flash', name: 'gemini-1.5-flash (Mais Rápido & Gratuito)' },
|
||||||
|
{ id: 'gemini-1.5-pro', name: 'gemini-1.5-pro (Multimodal)' },
|
||||||
|
{ id: 'gemini-2.0-flash-exp', name: 'gemini-2.0-flash-exp (Experimental)' },
|
||||||
|
{ id: 'gemini-2.5-flash', name: 'gemini-2.5-flash (Se ativo na sua conta)' }
|
||||||
|
],
|
||||||
|
openai: [
|
||||||
|
{ id: 'gpt-4o-mini', name: 'gpt-4o-mini (Recomendado)' },
|
||||||
|
{ id: 'gpt-4o', name: 'gpt-4o (Completo)' },
|
||||||
|
{ id: 'gpt-3.5-turbo', name: 'gpt-3.5-turbo (Legado)' }
|
||||||
|
],
|
||||||
|
claude: [
|
||||||
|
{ id: 'claude-3-5-sonnet-20241022', name: 'claude-3-5-sonnet-20241022' },
|
||||||
|
{ id: 'claude-3-5-haiku-20241022', name: 'claude-3-5-haiku-20241022' }
|
||||||
|
],
|
||||||
|
openrouter: [
|
||||||
|
{ id: 'google/gemini-2.5-flash', name: 'Gemini 2.5 Flash' },
|
||||||
|
{ id: 'openai/gpt-4o-mini', name: 'GPT-4o Mini' },
|
||||||
|
{ id: 'anthropic/claude-3.5-sonnet', name: 'Claude 3.5 Sonnet' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const [loadingModels, setLoadingModels] = useState<{[key: string]: boolean}>({});
|
||||||
|
|
||||||
const [isGeneratingAI, setIsGeneratingAI] = useState(false);
|
const [isGeneratingAI, setIsGeneratingAI] = useState(false);
|
||||||
const [aiTopic, setAiTopic] = useState('');
|
const [aiTopic, setAiTopic] = useState('');
|
||||||
const [aiQuantity, setAiQuantity] = useState(5);
|
const [aiQuantity, setAiQuantity] = useState(5);
|
||||||
|
|
@ -44,22 +77,63 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
|
||||||
geminiKey: data.aiConfig?.geminiKey ? '********' : '',
|
geminiKey: data.aiConfig?.geminiKey ? '********' : '',
|
||||||
openaiKey: data.aiConfig?.openaiKey ? '********' : '',
|
openaiKey: data.aiConfig?.openaiKey ? '********' : '',
|
||||||
claudeKey: data.aiConfig?.claudeKey ? '********' : '',
|
claudeKey: data.aiConfig?.claudeKey ? '********' : '',
|
||||||
|
openrouterKey: data.aiConfig?.openrouterKey ? '********' : '',
|
||||||
geminiModel: data.aiConfig?.geminiModel || 'gemini-1.5-flash',
|
geminiModel: data.aiConfig?.geminiModel || 'gemini-1.5-flash',
|
||||||
openaiModel: data.aiConfig?.openaiModel || 'gpt-4o-mini',
|
openaiModel: data.aiConfig?.openaiModel || 'gpt-4o-mini',
|
||||||
claudeModel: data.aiConfig?.claudeModel || 'claude-3-5-sonnet-20241022'
|
claudeModel: data.aiConfig?.claudeModel || 'claude-3-5-sonnet-20241022',
|
||||||
|
openrouterModel: data.aiConfig?.openrouterModel || 'google/gemini-2.5-flash',
|
||||||
|
activeImageProvider: data.aiConfig?.activeImageProvider || 'gemini'
|
||||||
});
|
});
|
||||||
setShowAIConfigModal(true);
|
setShowAIConfigModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFetchModels = async (provider: 'gemini' | 'openai' | 'claude' | 'openrouter') => {
|
||||||
|
setLoadingModels(prev => ({ ...prev, [provider]: true }));
|
||||||
|
try {
|
||||||
|
let key = '';
|
||||||
|
if (provider === 'gemini') key = aiForm.geminiKey;
|
||||||
|
else if (provider === 'openai') key = aiForm.openaiKey;
|
||||||
|
else if (provider === 'claude') key = aiForm.claudeKey;
|
||||||
|
else if (provider === 'openrouter') key = aiForm.openrouterKey;
|
||||||
|
|
||||||
|
const response = await fetch('/api/ai/list-models', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ provider, apiKey: key })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const err = await response.json();
|
||||||
|
throw new Error(err.error || 'Erro ao carregar modelos da API.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { models } = await response.json();
|
||||||
|
if (models && models.length > 0) {
|
||||||
|
setDynamicModels(prev => ({ ...prev, [provider]: models }));
|
||||||
|
showAlert('Sucesso', `Lista de modelos para ${provider.toUpperCase()} atualizada!`, 'success');
|
||||||
|
} else {
|
||||||
|
throw new Error('Nenhum modelo compatível retornado.');
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
showAlert('Erro ao listar modelos', e.message || 'Falha ao buscar modelos, verifique sua chave de API.', 'error');
|
||||||
|
} finally {
|
||||||
|
setLoadingModels(prev => ({ ...prev, [provider]: false }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSaveAIConfig = async () => {
|
const handleSaveAIConfig = async () => {
|
||||||
const newConfig = {
|
const newConfig = {
|
||||||
activeProvider: aiForm.activeProvider,
|
activeProvider: aiForm.activeProvider,
|
||||||
geminiKey: aiForm.geminiKey === '********' ? (data.aiConfig?.geminiKey || '') : aiForm.geminiKey,
|
geminiKey: aiForm.geminiKey === '********' ? (data.aiConfig?.geminiKey || '') : aiForm.geminiKey,
|
||||||
openaiKey: aiForm.openaiKey === '********' ? (data.aiConfig?.openaiKey || '') : aiForm.openaiKey,
|
openaiKey: aiForm.openaiKey === '********' ? (data.aiConfig?.openaiKey || '') : aiForm.openaiKey,
|
||||||
claudeKey: aiForm.claudeKey === '********' ? (data.aiConfig?.claudeKey || '') : aiForm.claudeKey,
|
claudeKey: aiForm.claudeKey === '********' ? (data.aiConfig?.claudeKey || '') : aiForm.claudeKey,
|
||||||
|
openrouterKey: aiForm.openrouterKey === '********' ? (data.aiConfig?.openrouterKey || '') : aiForm.openrouterKey,
|
||||||
geminiModel: aiForm.geminiModel,
|
geminiModel: aiForm.geminiModel,
|
||||||
openaiModel: aiForm.openaiModel,
|
openaiModel: aiForm.openaiModel,
|
||||||
claudeModel: aiForm.claudeModel
|
claudeModel: aiForm.claudeModel,
|
||||||
|
openrouterModel: aiForm.openrouterModel,
|
||||||
|
activeImageProvider: aiForm.activeImageProvider
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -1310,8 +1384,9 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-5 max-h-[60vh] overflow-y-auto pr-2">
|
<div className="space-y-5 max-h-[60vh] overflow-y-auto pr-2">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-bold text-slate-700 mb-2">Provedor de IA Ativo</label>
|
<label className="block text-sm font-bold text-slate-700 mb-2">IA de Questões</label>
|
||||||
<select
|
<select
|
||||||
value={aiForm.activeProvider}
|
value={aiForm.activeProvider}
|
||||||
onChange={e => setAiForm({ ...aiForm, activeProvider: e.target.value as any })}
|
onChange={e => setAiForm({ ...aiForm, activeProvider: e.target.value as any })}
|
||||||
|
|
@ -1320,8 +1395,21 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
|
||||||
<option value="gemini">Google Gemini</option>
|
<option value="gemini">Google Gemini</option>
|
||||||
<option value="openai">OpenAI (ChatGPT)</option>
|
<option value="openai">OpenAI (ChatGPT)</option>
|
||||||
<option value="claude">Anthropic Claude</option>
|
<option value="claude">Anthropic Claude</option>
|
||||||
|
<option value="openrouter">OpenRouter</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-bold text-slate-700 mb-2">IA de Imagens</label>
|
||||||
|
<select
|
||||||
|
value={aiForm.activeImageProvider}
|
||||||
|
onChange={e => setAiForm({ ...aiForm, activeImageProvider: e.target.value as any })}
|
||||||
|
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-bold text-slate-700 text-sm"
|
||||||
|
>
|
||||||
|
<option value="gemini">Google Imagen 3</option>
|
||||||
|
<option value="openai">OpenAI DALL-E</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-slate-100 pt-4">
|
<div className="border-t border-slate-100 pt-4">
|
||||||
<h4 className="text-xs font-black text-slate-400 uppercase tracking-widest mb-3">Google Gemini</h4>
|
<h4 className="text-xs font-black text-slate-400 uppercase tracking-widest mb-3">Google Gemini</h4>
|
||||||
|
|
@ -1337,16 +1425,35 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[11px] font-bold text-slate-500 mb-1">Modelo</label>
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<label className="block text-[11px] font-bold text-slate-500">Modelo</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={loadingModels['gemini']}
|
||||||
|
onClick={() => handleFetchModels('gemini')}
|
||||||
|
className="text-[10px] font-black text-indigo-600 hover:text-indigo-800 flex items-center gap-1 transition-colors"
|
||||||
|
>
|
||||||
|
{loadingModels['gemini'] ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="animate-spin" size={10} />
|
||||||
|
<span>Carregando...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Sparkles size={10} />
|
||||||
|
<span>Carregar Modelos</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<select
|
<select
|
||||||
value={aiForm.geminiModel}
|
value={aiForm.geminiModel}
|
||||||
onChange={e => setAiForm({ ...aiForm, geminiModel: e.target.value })}
|
onChange={e => setAiForm({ ...aiForm, geminiModel: e.target.value })}
|
||||||
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-bold text-slate-700 text-xs"
|
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-bold text-slate-700 text-xs"
|
||||||
>
|
>
|
||||||
<option value="gemini-1.5-flash">gemini-1.5-flash (Mais Rápido & Gratuito)</option>
|
{dynamicModels.gemini.map(m => (
|
||||||
<option value="gemini-1.5-pro">gemini-1.5-pro (Multimodal)</option>
|
<option key={m.id} value={m.id}>{m.name}</option>
|
||||||
<option value="gemini-2.0-flash-exp">gemini-2.0-flash-exp (Experimental)</option>
|
))}
|
||||||
<option value="gemini-2.5-flash">gemini-2.5-flash (Se ativo na sua conta)</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1366,15 +1473,35 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[11px] font-bold text-slate-500 mb-1">Modelo</label>
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<label className="block text-[11px] font-bold text-slate-500">Modelo</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={loadingModels['openai']}
|
||||||
|
onClick={() => handleFetchModels('openai')}
|
||||||
|
className="text-[10px] font-black text-indigo-600 hover:text-indigo-800 flex items-center gap-1 transition-colors"
|
||||||
|
>
|
||||||
|
{loadingModels['openai'] ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="animate-spin" size={10} />
|
||||||
|
<span>Carregando...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Sparkles size={10} />
|
||||||
|
<span>Carregar Modelos</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<select
|
<select
|
||||||
value={aiForm.openaiModel}
|
value={aiForm.openaiModel}
|
||||||
onChange={e => setAiForm({ ...aiForm, openaiModel: e.target.value })}
|
onChange={e => setAiForm({ ...aiForm, openaiModel: e.target.value })}
|
||||||
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-bold text-slate-700 text-xs"
|
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-bold text-slate-700 text-xs"
|
||||||
>
|
>
|
||||||
<option value="gpt-4o-mini">gpt-4o-mini (Recomendado)</option>
|
{dynamicModels.openai.map(m => (
|
||||||
<option value="gpt-4o">gpt-4o (Completo)</option>
|
<option key={m.id} value={m.id}>{m.name}</option>
|
||||||
<option value="gpt-3.5-turbo">gpt-3.5-turbo (Legado)</option>
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1394,14 +1521,83 @@ const Exams: React.FC<ExamsProps> = ({ data, updateData }) => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-[11px] font-bold text-slate-500 mb-1">Modelo</label>
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<label className="block text-[11px] font-bold text-slate-500">Modelo</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={loadingModels['claude']}
|
||||||
|
onClick={() => handleFetchModels('claude')}
|
||||||
|
className="text-[10px] font-black text-indigo-600 hover:text-indigo-800 flex items-center gap-1 transition-colors"
|
||||||
|
>
|
||||||
|
{loadingModels['claude'] ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="animate-spin" size={10} />
|
||||||
|
<span>Carregando...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Sparkles size={10} />
|
||||||
|
<span>Carregar Modelos</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<select
|
<select
|
||||||
value={aiForm.claudeModel}
|
value={aiForm.claudeModel}
|
||||||
onChange={e => setAiForm({ ...aiForm, claudeModel: e.target.value })}
|
onChange={e => setAiForm({ ...aiForm, claudeModel: e.target.value })}
|
||||||
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-bold text-slate-700 text-xs"
|
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-bold text-slate-700 text-xs"
|
||||||
>
|
>
|
||||||
<option value="claude-3-5-sonnet-20241022">claude-3-5-sonnet-20241022</option>
|
{dynamicModels.claude.map(m => (
|
||||||
<option value="claude-3-5-haiku-20241022">claude-3-5-haiku-20241022</option>
|
<option key={m.id} value={m.id}>{m.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-slate-100 pt-4">
|
||||||
|
<h4 className="text-xs font-black text-slate-400 uppercase tracking-widest mb-3">OpenRouter</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-bold text-slate-500 mb-1">Chave de API</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={aiForm.openrouterKey}
|
||||||
|
onChange={e => setAiForm({ ...aiForm, openrouterKey: e.target.value })}
|
||||||
|
placeholder="Cole a chave (sk-or-...)"
|
||||||
|
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-medium text-slate-800 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<label className="block text-[11px] font-bold text-slate-500">Modelo</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={loadingModels['openrouter']}
|
||||||
|
onClick={() => handleFetchModels('openrouter')}
|
||||||
|
className="text-[10px] font-black text-indigo-600 hover:text-indigo-800 flex items-center gap-1 transition-colors"
|
||||||
|
>
|
||||||
|
{loadingModels['openrouter'] ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="animate-spin" size={10} />
|
||||||
|
<span>Carregando...</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Sparkles size={10} />
|
||||||
|
<span>Carregar Modelos</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
value={aiForm.openrouterModel}
|
||||||
|
onChange={e => setAiForm({ ...aiForm, openrouterModel: e.target.value })}
|
||||||
|
className="w-full px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none transition-all font-bold text-slate-700 text-xs"
|
||||||
|
>
|
||||||
|
{dynamicModels.openrouter.map(m => (
|
||||||
|
<option key={m.id} value={m.id}>{m.name}</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1289,6 +1289,9 @@ app.post('/api/ai/generate-questions', async (req, res) => {
|
||||||
} else if (provider === 'claude') {
|
} else if (provider === 'claude') {
|
||||||
apiKey = aiConfig.claudeKey;
|
apiKey = aiConfig.claudeKey;
|
||||||
modelName = aiConfig.claudeModel || 'claude-3-5-sonnet-20241022';
|
modelName = aiConfig.claudeModel || 'claude-3-5-sonnet-20241022';
|
||||||
|
} else if (provider === 'openrouter') {
|
||||||
|
apiKey = aiConfig.openrouterKey;
|
||||||
|
modelName = aiConfig.openrouterModel || 'google/gemini-2.5-flash';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
|
|
@ -1356,6 +1359,64 @@ Exemplo de formato esperado:
|
||||||
const questions = parsed.questions || parsed || [];
|
const questions = parsed.questions || parsed || [];
|
||||||
return res.json({ questions });
|
return res.json({ questions });
|
||||||
|
|
||||||
|
} else if (provider === 'openrouter') {
|
||||||
|
const payload = {
|
||||||
|
model: modelName,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: `Você é um gerador de questões escolares de múltipla escolha para provas de alunos.
|
||||||
|
Retorne um objeto JSON contendo uma lista de questões na chave "questions".
|
||||||
|
Cada questão deve conter:
|
||||||
|
- "text": o enunciado da questão.
|
||||||
|
- "options": um array com exatamente 4 opções de resposta.
|
||||||
|
- "correctOptionIndex": o índice da opção correta (de 0 a 3).
|
||||||
|
|
||||||
|
Exemplo de formato esperado:
|
||||||
|
{
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"text": "Qual é a principal função do processador?",
|
||||||
|
"options": ["Processar dados", "Armazenar arquivos", "Exibir imagens", "Conectar à internet"],
|
||||||
|
"correctOptionIndex": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: userContent
|
||||||
|
}
|
||||||
|
],
|
||||||
|
response_format: { type: 'json_object' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
'HTTP-Referer': 'https://edumanager.com.br',
|
||||||
|
'X-Title': 'EduManager Pro'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`OpenRouter API retornou erro: ${response.status} - ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resJson = await response.json();
|
||||||
|
const contentText = resJson.choices?.[0]?.message?.content;
|
||||||
|
if (!contentText) {
|
||||||
|
throw new Error('Retorno vazio da API do OpenRouter');
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = JSON.parse(contentText);
|
||||||
|
const questions = parsed.questions || parsed || [];
|
||||||
|
return res.json({ questions });
|
||||||
|
|
||||||
} else if (provider === 'claude') {
|
} else if (provider === 'claude') {
|
||||||
const payload = {
|
const payload = {
|
||||||
model: modelName,
|
model: modelName,
|
||||||
|
|
@ -1534,19 +1595,21 @@ app.post('/api/ai/generate-image', async (req, res) => {
|
||||||
const appData = await getSchoolData();
|
const appData = await getSchoolData();
|
||||||
const aiConfig = appData.aiConfig || {};
|
const aiConfig = appData.aiConfig || {};
|
||||||
|
|
||||||
|
const activeImageProvider = aiConfig.activeImageProvider || (aiConfig.openaiKey ? 'openai' : 'gemini');
|
||||||
|
|
||||||
let provider = '';
|
let provider = '';
|
||||||
let apiKey = '';
|
let apiKey = '';
|
||||||
|
|
||||||
if (aiConfig.openaiKey) {
|
if (activeImageProvider === 'openai') {
|
||||||
provider = 'openai';
|
provider = 'openai';
|
||||||
apiKey = aiConfig.openaiKey;
|
apiKey = aiConfig.openaiKey;
|
||||||
} else if (aiConfig.geminiKey) {
|
} else if (activeImageProvider === 'gemini') {
|
||||||
provider = 'gemini';
|
provider = 'gemini';
|
||||||
apiKey = aiConfig.geminiKey;
|
apiKey = aiConfig.geminiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return res.status(400).json({ error: 'Nenhuma chave de API de IA (OpenAI ou Gemini) cadastrada para geração de imagens.' });
|
return res.status(400).json({ error: `Nenhuma chave de API de IA cadastrada para o provedor de imagem selecionado: ${activeImageProvider.toUpperCase()}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
let imageUrl = '';
|
let imageUrl = '';
|
||||||
|
|
@ -1661,6 +1724,109 @@ app.post('/api/ai/generate-image', async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/api/ai/list-models', async (req, res) => {
|
||||||
|
const { provider, apiKey } = req.body;
|
||||||
|
if (!provider) {
|
||||||
|
return res.status(400).json({ error: 'Provedor não informado' });
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = apiKey;
|
||||||
|
if (!key || key === '********') {
|
||||||
|
const appData = await getSchoolData();
|
||||||
|
const aiConfig = appData.aiConfig || {};
|
||||||
|
if (provider === 'gemini') key = aiConfig.geminiKey;
|
||||||
|
else if (provider === 'openai') key = aiConfig.openaiKey;
|
||||||
|
else if (provider === 'claude') key = aiConfig.claudeKey;
|
||||||
|
else if (provider === 'openrouter') key = aiConfig.openrouterKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!key && provider !== 'openrouter') {
|
||||||
|
return res.status(400).json({ error: 'Chave de API não configurada nem informada.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (provider === 'gemini') {
|
||||||
|
const url = `https://generativelanguage.googleapis.com/v1/models?key=${key}`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
const errText = await response.text();
|
||||||
|
throw new Error(`Erro Gemini: ${response.status} - ${errText}`);
|
||||||
|
}
|
||||||
|
const json = await response.json();
|
||||||
|
const models = (json.models || [])
|
||||||
|
.filter(m => m.supportedMethods && m.supportedMethods.includes('generateContent'))
|
||||||
|
.map(m => ({
|
||||||
|
id: m.name.replace('models/', ''),
|
||||||
|
name: m.displayName || m.name
|
||||||
|
}));
|
||||||
|
return res.json({ models });
|
||||||
|
|
||||||
|
} else if (provider === 'openai') {
|
||||||
|
const url = 'https://api.openai.com/v1/models';
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: { 'Authorization': `Bearer ${key}` }
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const errText = await response.text();
|
||||||
|
throw new Error(`Erro OpenAI: ${response.status} - ${errText}`);
|
||||||
|
}
|
||||||
|
const json = await response.json();
|
||||||
|
const models = (json.data || [])
|
||||||
|
.filter(m => m.id.includes('gpt') || m.id.includes('o1') || m.id.includes('o3'))
|
||||||
|
.map(m => ({
|
||||||
|
id: m.id,
|
||||||
|
name: m.id
|
||||||
|
}));
|
||||||
|
return res.json({ models });
|
||||||
|
|
||||||
|
} else if (provider === 'claude') {
|
||||||
|
const url = 'https://api.anthropic.com/v1/models';
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'x-api-key': key,
|
||||||
|
'anthropic-version': '2023-06-01'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
console.warn('Erro ao listar modelos do Claude, retornando lista padrão curada.');
|
||||||
|
const defaultModels = [
|
||||||
|
{ id: 'claude-3-5-sonnet-20241022', name: 'claude-3-5-sonnet-20241022' },
|
||||||
|
{ id: 'claude-3-5-haiku-20241022', name: 'claude-3-5-haiku-20241022' },
|
||||||
|
{ id: 'claude-3-opus-20240229', name: 'claude-3-opus-20240229' }
|
||||||
|
];
|
||||||
|
return res.json({ models: defaultModels });
|
||||||
|
}
|
||||||
|
const json = await response.json();
|
||||||
|
const models = (json.data || []).map(m => ({
|
||||||
|
id: m.id,
|
||||||
|
name: m.display_name || m.id
|
||||||
|
}));
|
||||||
|
return res.json({ models });
|
||||||
|
|
||||||
|
} else if (provider === 'openrouter') {
|
||||||
|
const url = 'https://openrouter.ai/api/v1/models';
|
||||||
|
const headers = {};
|
||||||
|
if (key) headers['Authorization'] = `Bearer ${key}`;
|
||||||
|
const response = await fetch(url, { headers });
|
||||||
|
if (!response.ok) {
|
||||||
|
const errText = await response.text();
|
||||||
|
throw new Error(`Erro OpenRouter: ${response.status} - ${errText}`);
|
||||||
|
}
|
||||||
|
const json = await response.json();
|
||||||
|
const models = (json.data || []).map(m => ({
|
||||||
|
id: m.id,
|
||||||
|
name: `${m.name || m.id} (${m.id})`
|
||||||
|
}));
|
||||||
|
return res.json({ models });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(400).json({ error: 'Provedor inválido' });
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Erro ao listar modelos para ${provider}:`, err);
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Helper para formatar datas para o JSON
|
// Helper para formatar datas para o JSON
|
||||||
const formatDateForJSON = (val) => {
|
const formatDateForJSON = (val) => {
|
||||||
if (!val) return '';
|
if (!val) return '';
|
||||||
|
|
|
||||||
|
|
@ -298,13 +298,16 @@ export interface PreMatriculaInscricao {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AIConfig {
|
export interface AIConfig {
|
||||||
activeProvider: 'gemini' | 'openai' | 'claude';
|
activeProvider: 'gemini' | 'openai' | 'claude' | 'openrouter';
|
||||||
geminiKey?: string;
|
geminiKey?: string;
|
||||||
openaiKey?: string;
|
openaiKey?: string;
|
||||||
claudeKey?: string;
|
claudeKey?: string;
|
||||||
|
openrouterKey?: string;
|
||||||
geminiModel?: string;
|
geminiModel?: string;
|
||||||
openaiModel?: string;
|
openaiModel?: string;
|
||||||
claudeModel?: string;
|
claudeModel?: string;
|
||||||
|
openrouterModel?: string;
|
||||||
|
activeImageProvider?: 'gemini' | 'openai';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SchoolData {
|
export interface SchoolData {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue