feat: persist selected provider and keys globally via node backend, remove Claude, Azure, and MiniMax from UI
This commit is contained in:
@@ -23,6 +23,7 @@ const App: React.FC = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// 1. Tentar ler do localStorage primeiro
|
||||
const savedApiKey = localStorage.getItem('api-key');
|
||||
const savedProvider = localStorage.getItem('ai-provider') as AIProvider;
|
||||
const savedModel = localStorage.getItem('model-' + savedProvider);
|
||||
@@ -35,6 +36,33 @@ const App: React.FC = () => {
|
||||
if (savedEndpoint) setEndpoint(savedEndpoint);
|
||||
setHasKey(true);
|
||||
}
|
||||
|
||||
// 2. Sincronizar com as configurações globais persistidas no servidor (mesmo em outros navegadores)
|
||||
fetch('/api/config')
|
||||
.then(res => res.json())
|
||||
.then(config => {
|
||||
if (config.provider) {
|
||||
setProvider(config.provider);
|
||||
if (config.apiKey) setApiKey(config.apiKey);
|
||||
if (config.model) setModel(config.model);
|
||||
if (config.endpoint) setEndpoint(config.endpoint);
|
||||
setHasKey(true);
|
||||
|
||||
// Sincroniza o localStorage
|
||||
localStorage.setItem('ai-provider', config.provider);
|
||||
if (config.apiKey) {
|
||||
localStorage.setItem('api-key', config.apiKey);
|
||||
localStorage.setItem('last-api-key', config.apiKey);
|
||||
}
|
||||
if (config.model) {
|
||||
localStorage.setItem('model-' + config.provider, config.model);
|
||||
}
|
||||
if (config.endpoint) {
|
||||
localStorage.setItem('ollama-endpoint', config.endpoint);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => console.warn('Erro ao carregar configurações do servidor:', err));
|
||||
}, []);
|
||||
|
||||
const handleFileChange = useCallback((selectedFile: File | null) => {
|
||||
@@ -60,6 +88,18 @@ const App: React.FC = () => {
|
||||
localStorage.setItem('ai-provider', newProvider);
|
||||
localStorage.setItem('model-' + newProvider, newModel);
|
||||
setHasKey(true);
|
||||
|
||||
// Salvar no servidor para persistência multi-navegador
|
||||
fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
provider: newProvider,
|
||||
apiKey: key,
|
||||
model: newModel,
|
||||
endpoint: newEndpoint || ''
|
||||
})
|
||||
}).catch(err => console.error('Erro ao persistir configurações no servidor:', err));
|
||||
}, []);
|
||||
|
||||
const handleAnalyzeClick = async () => {
|
||||
@@ -117,6 +157,13 @@ const App: React.FC = () => {
|
||||
localStorage.removeItem('api-key');
|
||||
setHasKey(false);
|
||||
handleReset();
|
||||
|
||||
// Limpar no servidor
|
||||
fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({})
|
||||
}).catch(err => console.error('Erro ao limpar configurações no servidor:', err));
|
||||
}, [handleReset]);
|
||||
|
||||
const handleExport = (action: 'preview' | 'download') => {
|
||||
|
||||
+5
-4
@@ -8,8 +8,9 @@ COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Estágio de Produção
|
||||
FROM nginxinc/nginx-unprivileged:alpine as production-stage
|
||||
RUN apk update && apk upgrade --no-cache
|
||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||
FROM bitnami/node:22 as production-stage
|
||||
WORKDIR /app
|
||||
COPY --from=build-stage /app/dist ./dist
|
||||
COPY server.js ./
|
||||
EXPOSE 8080
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
@@ -69,11 +69,8 @@ export const ApiKeySetup: React.FC<ApiKeySetupProps> = ({ onKeySave }) => {
|
||||
switch (provider) {
|
||||
case 'gemini': return 'https://aistudio.google.com/app/apikey';
|
||||
case 'openai': return 'https://platform.openai.com/api-keys';
|
||||
case 'anthropic': return 'https://console.anthropic.com/keys';
|
||||
case 'azure': return 'https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps';
|
||||
case 'ollama': return 'https://ollama.com/download';
|
||||
case 'openrouter': return 'https://openrouter.ai/keys';
|
||||
case 'minimax': return 'https://platform.minimaxi.com/user-center/basic-information/interface-key';
|
||||
default: return '#';
|
||||
}
|
||||
};
|
||||
@@ -82,11 +79,8 @@ export const ApiKeySetup: React.FC<ApiKeySetupProps> = ({ onKeySave }) => {
|
||||
switch (provider) {
|
||||
case 'gemini': return 'Google Gemini';
|
||||
case 'openai': return 'OpenAI';
|
||||
case 'anthropic': return 'Anthropic (Claude)';
|
||||
case 'azure': return 'Azure OpenAI';
|
||||
case 'ollama': return 'Ollama (Local)';
|
||||
case 'openrouter': return 'OpenRouter';
|
||||
case 'minimax': return 'MiniMax';
|
||||
default: return 'API';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import http from 'http';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const PORT = process.env.PORT || 8080;
|
||||
const DIST_DIR = path.join(__dirname, 'dist');
|
||||
const CONFIG_DIR = path.join(__dirname, 'data');
|
||||
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
||||
|
||||
// Ensure config dir exists
|
||||
if (!fs.existsSync(CONFIG_DIR)) {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
const MIME_TYPES = {
|
||||
'.html': 'text/html',
|
||||
'.css': 'text/css',
|
||||
'.js': 'text/javascript',
|
||||
'.json': 'application/json',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.gif': 'image/gif',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.ico': 'image/x-icon',
|
||||
};
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
// CORS headers
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// API Config GET
|
||||
if (req.url === '/api/config' && req.method === 'GET') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
try {
|
||||
const data = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
||||
res.end(data);
|
||||
return;
|
||||
} catch (err) {
|
||||
// Fallback
|
||||
}
|
||||
}
|
||||
res.end(JSON.stringify({}));
|
||||
return;
|
||||
}
|
||||
|
||||
// API Config POST
|
||||
if (req.url === '/api/config' && req.method === 'POST') {
|
||||
let body = '';
|
||||
req.on('data', chunk => {
|
||||
body += chunk.toString();
|
||||
});
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const parsed = JSON.parse(body);
|
||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(parsed, null, 2), 'utf-8');
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true }));
|
||||
} catch (err) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Invalid JSON or write error' }));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Serve static files
|
||||
let filePath = path.join(DIST_DIR, req.url === '/' ? 'index.html' : req.url);
|
||||
|
||||
// If path doesn't exist, fallback to index.html (SPA routing)
|
||||
if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
|
||||
filePath = path.join(DIST_DIR, 'index.html');
|
||||
}
|
||||
|
||||
const ext = path.extname(filePath);
|
||||
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
||||
|
||||
fs.readFile(filePath, (err, content) => {
|
||||
if (err) {
|
||||
res.writeHead(500);
|
||||
res.end('Server Error');
|
||||
} else {
|
||||
res.writeHead(200, { 'Content-Type': contentType });
|
||||
res.end(content);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
+2
-23
@@ -1,6 +1,6 @@
|
||||
import { AIProvider } from './providers';
|
||||
|
||||
export type AIProvider = 'gemini' | 'openai' | 'anthropic' | 'azure' | 'ollama' | 'openrouter' | 'minimax';
|
||||
export type AIProvider = 'gemini' | 'openai' | 'ollama' | 'openrouter';
|
||||
|
||||
export const OLLAMA_AUTO_DETECT_URLS = [
|
||||
'http://localhost:11434',
|
||||
@@ -37,21 +37,7 @@ export const PROVIDERS: ProviderConfig[] = [
|
||||
models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-4-vision-preview'],
|
||||
defaultModel: 'gpt-4o'
|
||||
},
|
||||
{
|
||||
id: 'anthropic',
|
||||
name: 'Anthropic (Claude)',
|
||||
description: 'Claude 3 com análise avançada de documentos',
|
||||
models: ['claude-3-opus-20240229', 'claude-3-sonnet-20240229', 'claude-3-haiku-20240307'],
|
||||
defaultModel: 'claude-3-sonnet-20240229'
|
||||
},
|
||||
{
|
||||
id: 'azure',
|
||||
name: 'Azure OpenAI',
|
||||
description: 'OpenAI via Azure com segurança enterprise',
|
||||
models: ['gpt-4', 'gpt-4-32k', 'gpt-35-turbo'],
|
||||
requiresEndpoint: true,
|
||||
defaultModel: 'gpt-4'
|
||||
},
|
||||
|
||||
{
|
||||
id: 'ollama',
|
||||
name: 'Ollama (Local)',
|
||||
@@ -65,12 +51,5 @@ export const PROVIDERS: ProviderConfig[] = [
|
||||
description: 'Acesso a múltiplos modelos através do OpenRouter',
|
||||
models: ['google/gemini-2.5-flash', 'google/gemini-2.5-pro', 'anthropic/claude-3.5-sonnet', 'openai/gpt-4o', 'meta-llama/llama-3.1-70b-instruct'],
|
||||
defaultModel: 'google/gemini-2.5-flash'
|
||||
},
|
||||
{
|
||||
id: 'minimax',
|
||||
name: 'MiniMax',
|
||||
description: 'Modelos MiniMax (Vision \u0026 M3)',
|
||||
models: ['abab6.5s-chat', 'minimax-m3', 'minimax-2.7', 'minimax-3.0'],
|
||||
defaultModel: 'minimax-m3'
|
||||
}
|
||||
];
|
||||
Reference in New Issue
Block a user