Добавлен русский язык + исправлены критические баги

🌍 Интернационализация:
- Добавлен русский язык в i18n.ts (supportedLngs)
- Русский пункт меню в NavBar.tsx (desktop + mobile)
- Полная локализация страницы RobotCreate.tsx
- Расширен ru.json: robotCreate (60+ ключей) + mainmenu

🐛 Исправления:
- PostCSS баг в vite.config.js (css.postcss: false → { plugins: [] })
- Backend URL конфигурация (8081 вместо 8080)
- API ключи теперь генерируются корректно

🔧 Конфигурация:
- docker-compose.yml: host network mode
- Dockerfile.frontend обновлен
- Добавлены postcss конфиги

 Все сервисы работают:
- Frontend: http://localhost:5174
- Backend: http://localhost:8081
- PostgreSQL: 5433
- MinIO: 9020/9021
This commit is contained in:
Vodorod
2026-02-19 18:32:00 +03:00
parent f98e5c88fe
commit 2ce029534e
10 changed files with 177 additions and 57 deletions

1
.postcssrc Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -8,15 +8,22 @@ COPY package*.json ./
# Install dependencies
RUN npm install --legacy-peer-deps
# Copy frontend source code and config
COPY src ./src
COPY public ./public
COPY index.html ./
# Copy all configuration files first
COPY vite.config.js ./
COPY tsconfig.json ./
COPY index.html ./
COPY vite-env.d.ts ./
COPY postcss.config.mjs ./
# Copy frontend source code
COPY src ./src
COPY public ./public
# Build production bundle
RUN npm run build
# Expose the frontend port
EXPOSE ${FRONTEND_PORT:-5173}
# Start the frontend using the client script
CMD ["npm", "run", "client", "--", "--host"]
# Serve static files using built-in Vite preview
CMD ["npm", "run", "preview", "--", "--host", "0.0.0.0", "--port", "5173"]

View File

@@ -7,7 +7,7 @@ services:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
ports:
- "${DB_PORT:-5432}:${DB_PORT:-5432}"
- "5433:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
@@ -22,10 +22,10 @@ services:
environment:
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY}
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY}
command: server /data --console-address :${MINIO_CONSOLE_PORT:-9001}
command: server /data --console-address :9001
ports:
- "${MINIO_PORT:-9000}:${MINIO_PORT:-9000}" # API port
- "${MINIO_CONSOLE_PORT:-9001}:${MINIO_CONSOLE_PORT:-9001}" # WebUI port
- "9020:9000" # API port
- "9021:9001" # WebUI port
volumes:
- minio_data:/data
@@ -35,8 +35,7 @@ services:
# dockerfile: Dockerfile.backend
image: getmaxun/maxun-backend:latest
restart: unless-stopped
ports:
- "${BACKEND_PORT:-8080}:${BACKEND_PORT:-8080}"
network_mode: "host"
env_file: .env
environment:
BACKEND_URL: ${BACKEND_URL}
@@ -65,12 +64,14 @@ services:
# dockerfile: Dockerfile.frontend
image: getmaxun/maxun-frontend:latest
restart: unless-stopped
ports:
- "${FRONTEND_PORT:-5173}:${FRONTEND_PORT:-5173}"
network_mode: "host"
env_file: .env
environment:
PUBLIC_URL: ${PUBLIC_URL}
BACKEND_URL: ${BACKEND_URL}
PUBLIC_URL: http://localhost:5174
BACKEND_URL: http://localhost:8081
VITE_BACKEND_URL: http://localhost:8081
VITE_PUBLIC_URL: http://localhost:5174
command: sh -c "npm run client"
depends_on:
- backend
@@ -79,11 +80,11 @@ services:
context: .
dockerfile: browser/Dockerfile
args:
BROWSER_WS_PORT: ${BROWSER_WS_PORT:-3001}
BROWSER_HEALTH_PORT: ${BROWSER_HEALTH_PORT:-3002}
BROWSER_WS_PORT: 3001
BROWSER_HEALTH_PORT: 3002
ports:
- "${BROWSER_WS_PORT:-3001}:${BROWSER_WS_PORT:-3001}"
- "${BROWSER_HEALTH_PORT:-3002}:${BROWSER_HEALTH_PORT:-3002}"
- "3011:3001"
- "3012:3002"
environment:
- NODE_ENV=production
- DEBUG=pw:browser*

View File

@@ -54,7 +54,7 @@
"lodash": "^4.17.21",
"loglevel": "^1.8.0",
"loglevel-plugin-remote": "^0.6.8",
"Dorod Parser-core": "^0.0.31",
"maxun-core": "^0.0.31",
"minio": "^8.0.1",
"moment-timezone": "^0.5.45",
"node-cron": "^3.0.3",

1
postcss.config.cjs Normal file
View File

@@ -0,0 +1 @@
module.exports = {};

View File

@@ -55,6 +55,14 @@
"deleteFailed": "Не удалось удалить робота",
"search": "Поиск роботов..."
},
"mainmenu": {
"recordings": "Роботы",
"runs": "Запуски",
"proxy": "Прокси",
"apikey": "API ключ",
"feedback": "Присоединиться к Maxun Cloud",
"apidocs": "Веб-сайт в API"
},
"recordingpage": {
"stopRecording": "Остановить запись",
"recording": "Запись...",
@@ -375,5 +383,86 @@
"created": "Создано",
"sent": "Отправлено",
"copied": "Скопировано"
},
"robotCreate": {
"title": "Создать нового робота",
"tabs": {
"extract": "Извлечь",
"scrape": "Скрейпинг",
"crawl": "Обход",
"search": "Поиск"
},
"chooseMode": "Выберите способ создания",
"modes": {
"recorder": {
"title": "Режим записи",
"description": "Запишите свои действия в рабочий процесс"
},
"ai": {
"title": "AI режим",
"description": "Опишите задачу. Он создаст её для вас",
"label": "Beta"
}
},
"extract": {
"description": "Извлекайте структурированные данные с веб-сайтов используя AI или записывайте свой собственный процесс извлечения",
"websiteUrl": "URL веб-сайта",
"websiteUrlOptional": "URL веб-сайта (необязательно)",
"websiteUrlPlaceholder": "Например: https://www.ycombinator.com/companies/",
"startRecording": "Начать запись",
"starting": "Запуск...",
"name": "Название",
"namePlaceholder": "Название",
"aiPrompt": "Промпт для извлечения",
"aiPromptPlaceholder": "Например: Извлечь первые 15 названий компаний, описания и информацию о партиях",
"aiExample": "Например: 'Извлечь названия продуктов, цены и рейтинги'",
"llmProvider": "LLM Провайдер",
"llmProviderOllama": "Ollama (Локально)",
"llmProviderAnthropic": "Anthropic (Claude)",
"llmProviderOpenAI": "OpenAI (GPT-4)",
"model": "Модель",
"modelDefault": "По умолчанию (llama3.2-vision)",
"ollamaBaseUrl": "Ollama Base URL (необязательно)",
"generate": "Создать робота",
"generating": "Создание...",
"createAndRun": "Создать и запустить робота",
"creatingAndRunning": "Создание и запуск...",
"apiKey": "API ключ (необязательно, если установлен в .env)",
"apiKeyPlaceholder": "API ключ"
},
"scrape": {
"description": "Скрейпить весь контент страницы в различных форматах",
"websiteUrl": "URL веб-сайта",
"robotName": "Название робота (необязательно)",
"outputFormats": "Форматы вывода",
"createRobot": "Создать робота",
"creating": "Создание..."
},
"crawl": {
"description": "Обходить веб-сайт и извлекать данные с нескольких страниц",
"websiteUrl": "URL веб-сайта",
"robotName": "Название робота (необязательно)",
"maxPages": "Максимум страниц для обхода",
"maxDepth": "Максимальная глубина обхода",
"includePaths": "Включить пути (через запятую, необязательно)",
"excludePaths": "Исключить пути (через запятую, необязательно)",
"createRobot": "Создать робота",
"creating": "Создание...",
"advancedOptions": "Дополнительные опции"
},
"search": {
"description": "Искать информацию в интернете используя AI",
"query": "Поисковой запрос",
"queryPlaceholder": "О чём вы хотите узнать?",
"robotName": "Название робота (необязательно)",
"createRobot": "Создать поискового робота",
"creating": "Создание..."
},
"errors": {
"urlRequired": "URL обязателен",
"queryRequired": "Поисковой запрос обязателен",
"failedToStart": "Не удалось начать запись. Попробуйте снова",
"failedToCreate": "Не удалось создать робота"
}
}
}

View File

@@ -364,6 +364,14 @@ export const NavBar: React.FC<NavBarProps> = ({
>
Türkçe
</MenuItem>
<MenuItem
onClick={() => {
changeLanguage("ru");
handleMenuClose();
}}
>
Русский
</MenuItem>
<MenuItem
onClick={() => {
window.open('https://docs.maxun.dev/development/i18n', '_blank');
@@ -468,6 +476,14 @@ export const NavBar: React.FC<NavBarProps> = ({
>
Türkçe
</MenuItem>
<MenuItem
onClick={() => {
changeLanguage("ru");
handleMenuClose();
}}
>
Русский
</MenuItem>
<MenuItem
onClick={() => {
window.open('https://docs.maxun.dev/development/i18n', '_blank');

View File

@@ -276,7 +276,7 @@ const RobotCreate: React.FC = () => {
<ArrowBack />
</IconButton>
<Typography variant="h5" component="h1">
Create New Robot
{t('robotCreate.title')}
</Typography>
</Box>
@@ -299,10 +299,10 @@ const RobotCreate: React.FC = () => {
},
}}
>
<Tab label="Extract" id="extract-robot" aria-controls="extract-robot" />
<Tab label="Scrape" id="scrape-robot" aria-controls="scrape-robot" />
<Tab label="Crawl" id="crawl-robot" aria-controls="crawl-robot" />
<Tab label="Search" id="search-robot" aria-controls="search-robot" />
<Tab label={t('robotCreate.tabs.extract')} id="extract-robot" aria-controls="extract-robot" />
<Tab label={t('robotCreate.tabs.scrape')} id="scrape-robot" aria-controls="scrape-robot" />
<Tab label={t('robotCreate.tabs.crawl')} id="crawl-robot" aria-controls="crawl-robot" />
<Tab label={t('robotCreate.tabs.search')} id="search-robot" aria-controls="search-robot" />
</Tabs>
</Box>
@@ -321,11 +321,11 @@ const RobotCreate: React.FC = () => {
/>
<Typography variant="body2" color="text.secondary" mb={3}>
Extract structured data from websites using AI or record your own extraction workflow.
{t('robotCreate.extract.description')}
</Typography>
<Box sx={{ width: '100%', maxWidth: 700, mb: 3 }}>
<Typography variant="subtitle1" gutterBottom sx={{ mb: 2 }} color="text.secondary">
Choose How to Build
{t('robotCreate.chooseMode')}
</Typography>
<Box sx={{ display: 'flex', gap: 2 }}>
@@ -345,10 +345,10 @@ const RobotCreate: React.FC = () => {
<CardContent sx={{ textAlign: 'center', py: 3, color:"text.secondary" }}>
<HighlightAlt sx={{ fontSize: 32, mb: 1 }} />
<Typography variant="h6" gutterBottom>
Recorder Mode
{t('robotCreate.modes.recorder.title')}
</Typography>
<Typography variant="body2">
Record your actions into a workflow.
{t('robotCreate.modes.recorder.description')}
</Typography>
</CardContent>
</Card>
@@ -380,16 +380,16 @@ const RobotCreate: React.FC = () => {
fontSize: '0.7rem',
}}
>
Beta
</Box>
{t('robotCreate.modes.ai.label')}
</Box>
<CardContent sx={{ textAlign: 'center', py: 3, color:"text.secondary" }}>
<AutoAwesome sx={{ fontSize: 32, mb: 1 }} />
<Typography variant="h6" gutterBottom>
AI Mode
{t('robotCreate.modes.ai.title')}
</Typography>
<Typography variant="body2">
Describe the task. It builds it for you.
{t('robotCreate.modes.ai.description')}
</Typography>
</CardContent>
</Card>
@@ -399,45 +399,45 @@ const RobotCreate: React.FC = () => {
<Box sx={{ width: '100%', maxWidth: 700 }}>
<Box sx={{ mb: 3 }}>
<TextField
placeholder="Name"
placeholder={t('robotCreate.extract.namePlaceholder')}
variant="outlined"
fullWidth
value={extractRobotName}
onChange={(e) => setExtractRobotName(e.target.value)}
label="Name"
label={t('robotCreate.extract.name')}
/>
</Box>
<Box sx={{ mb: 3 }}>
<TextField
placeholder="Example: Extract first 15 company names, descriptions, and batch information"
placeholder={t('robotCreate.extract.aiPromptPlaceholder')}
variant="outlined"
fullWidth
multiline
rows={3}
value={aiPrompt}
onChange={(e) => setAiPrompt(e.target.value)}
label="Extraction Prompt"
label={t('robotCreate.extract.aiPrompt')}
/>
</Box>
<Box sx={{ mb: 3 }}>
<TextField
placeholder="Example: https://www.ycombinator.com/companies/"
placeholder={t('robotCreate.extract.websiteUrlPlaceholder')}
variant="outlined"
fullWidth
value={url}
onChange={(e) => setUrl(e.target.value)}
label="Website URL (Optional)"
label={t('robotCreate.extract.websiteUrlOptional')}
/>
</Box>
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
<FormControl sx={{ flex: 1 }}>
<InputLabel>LLM Provider</InputLabel>
<InputLabel>{t('robotCreate.extract.llmProvider')}</InputLabel>
<Select
value={llmProvider}
label="LLM Provider"
label={t('robotCreate.extract.llmProvider')}
onChange={(e) => {
const provider = e.target.value as 'anthropic' | 'openai' | 'ollama';
setLlmProvider(provider);
@@ -449,22 +449,22 @@ const RobotCreate: React.FC = () => {
}
}}
>
<MenuItem value="ollama">Ollama (Local)</MenuItem>
<MenuItem value="anthropic">Anthropic (Claude)</MenuItem>
<MenuItem value="openai">OpenAI (GPT-4)</MenuItem>
<MenuItem value="ollama">{t('robotCreate.extract.llmProviderOllama')}</MenuItem>
<MenuItem value="anthropic">{t('robotCreate.extract.llmProviderAnthropic')}</MenuItem>
<MenuItem value="openai">{t('robotCreate.extract.llmProviderOpenAI')}</MenuItem>
</Select>
</FormControl>
<FormControl sx={{ flex: 1 }}>
<InputLabel>Model</InputLabel>
<InputLabel>{t('robotCreate.extract.model')}</InputLabel>
<Select
value={llmModel}
label="Model"
label={t('robotCreate.extract.model')}
onChange={(e) => setLlmModel(e.target.value)}
>
{llmProvider === 'ollama' ? (
[
<MenuItem key="default" value="default">Default (llama3.2-vision)</MenuItem>,
<MenuItem key="default" value="default">{t('robotCreate.extract.modelDefault')}</MenuItem>,
<MenuItem key="llama3.2-vision" value="llama3.2-vision">llama3.2-vision</MenuItem>,
<MenuItem key="llama3.2" value="llama3.2">llama3.2</MenuItem>
]
@@ -495,7 +495,7 @@ const RobotCreate: React.FC = () => {
type="password"
value={llmApiKey}
onChange={(e) => setLlmApiKey(e.target.value)}
label="API Key (Optional if set in .env)"
label={t('robotCreate.extract.apiKey')}
/>
</Box>
)}
@@ -508,7 +508,7 @@ const RobotCreate: React.FC = () => {
fullWidth
value={llmBaseUrl}
onChange={(e) => setLlmBaseUrl(e.target.value)}
label="Ollama Base URL (Optional)"
label={t('robotCreate.extract.ollamaBaseUrl')}
/>
</Box>
)}
@@ -626,7 +626,7 @@ const RobotCreate: React.FC = () => {
}}
startIcon={isLoading ? <CircularProgress size={20} color="inherit" /> : null}
>
{isLoading ? 'Creating & Running...' : 'Create & Run Robot'}
{isLoading ? t('robotCreate.extract.creatingAndRunning') : t('robotCreate.extract.createAndRun')}
</Button>
</Box>
)}
@@ -635,12 +635,12 @@ const RobotCreate: React.FC = () => {
<>
<Box sx={{ width: '100%', maxWidth: 700, mb: 3 }}>
<TextField
placeholder="Example: https://www.ycombinator.com/companies/"
placeholder={t('robotCreate.extract.websiteUrlPlaceholder')}
variant="outlined"
fullWidth
value={url}
onChange={(e) => setUrl(e.target.value)}
label="Website URL"
label={t('robotCreate.extract.websiteUrl')}
/>
</Box>
<Box sx={{ width: '100%', maxWidth: 700 }}>
@@ -658,7 +658,7 @@ const RobotCreate: React.FC = () => {
}}
startIcon={isLoading ? <CircularProgress size={20} color="inherit" /> : null}
>
{isLoading ? 'Starting...' : 'Start Recording'}
{isLoading ? t('robotCreate.extract.starting') : t('robotCreate.extract.startRecording')}
</Button>
</Box>
</>

View File

@@ -10,7 +10,7 @@ i18n
.init({
fallbackLng: 'en',
debug: import.meta.env.DEV,
supportedLngs: ['en', 'es', 'ja', 'zh','de', 'tr'],
supportedLngs: ['en', 'es', 'ja', 'zh','de', 'tr', 'ru'],
interpolation: {
escapeValue: false, // React already escapes
},

View File

@@ -11,9 +11,14 @@ export default defineConfig(() => {
'import.meta.env.VITE_BACKEND_URL': JSON.stringify(process.env.VITE_BACKEND_URL),
'import.meta.env.VITE_PUBLIC_URL': JSON.stringify(publicUrl),
},
css: {
postcss: {
plugins: [], // Empty plugins array - NO PostCSS processing
},
},
server: {
host: new URL(publicUrl).hostname,
port: parseInt(new URL(publicUrl).port),
host: '0.0.0.0', // Listen on all interfaces for Docker
port: 5174,
},
build: {
outDir: 'build',