From 94a41e6a6e107047ee85bfe81636f9609253d5e1 Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Tue, 23 Jun 2026 16:50:35 +0000 Subject: [PATCH] feat: persist selected provider and keys globally via node backend, remove Claude, Azure, and MiniMax from UI --- App.tsx | 47 +++++++++++++++++ Dockerfile | 9 ++-- components/ApiKeySetup.tsx | 6 --- server.js | 103 +++++++++++++++++++++++++++++++++++++ types/providers.ts | 25 +-------- 5 files changed, 157 insertions(+), 33 deletions(-) create mode 100644 server.js diff --git a/App.tsx b/App.tsx index 05a51c2..c43c5a7 100644 --- a/App.tsx +++ b/App.tsx @@ -23,6 +23,7 @@ const App: React.FC = () => { const [error, setError] = useState(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') => { diff --git a/Dockerfile b/Dockerfile index a3cade2..2fb29a5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/components/ApiKeySetup.tsx b/components/ApiKeySetup.tsx index be5f325..7d4f6b8 100644 --- a/components/ApiKeySetup.tsx +++ b/components/ApiKeySetup.tsx @@ -69,11 +69,8 @@ export const ApiKeySetup: React.FC = ({ 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 = ({ 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'; } }; diff --git a/server.js b/server.js new file mode 100644 index 0000000..62f45e1 --- /dev/null +++ b/server.js @@ -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}`); +}); diff --git a/types/providers.ts b/types/providers.ts index d54a607..5c56d1d 100644 --- a/types/providers.ts +++ b/types/providers.ts @@ -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' } ]; \ No newline at end of file