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

🌍 Интернационализация:
- Добавлен русский язык в 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 # Install dependencies
RUN npm install --legacy-peer-deps RUN npm install --legacy-peer-deps
# Copy frontend source code and config # Copy all configuration files first
COPY src ./src
COPY public ./public
COPY index.html ./
COPY vite.config.js ./ COPY vite.config.js ./
COPY tsconfig.json ./ 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 the frontend port
EXPOSE ${FRONTEND_PORT:-5173} EXPOSE ${FRONTEND_PORT:-5173}
# Start the frontend using the client script # Serve static files using built-in Vite preview
CMD ["npm", "run", "client", "--", "--host"] CMD ["npm", "run", "preview", "--", "--host", "0.0.0.0", "--port", "5173"]

View File

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

View File

@@ -54,7 +54,7 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"loglevel": "^1.8.0", "loglevel": "^1.8.0",
"loglevel-plugin-remote": "^0.6.8", "loglevel-plugin-remote": "^0.6.8",
"Dorod Parser-core": "^0.0.31", "maxun-core": "^0.0.31",
"minio": "^8.0.1", "minio": "^8.0.1",
"moment-timezone": "^0.5.45", "moment-timezone": "^0.5.45",
"node-cron": "^3.0.3", "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": "Не удалось удалить робота", "deleteFailed": "Не удалось удалить робота",
"search": "Поиск роботов..." "search": "Поиск роботов..."
}, },
"mainmenu": {
"recordings": "Роботы",
"runs": "Запуски",
"proxy": "Прокси",
"apikey": "API ключ",
"feedback": "Присоединиться к Maxun Cloud",
"apidocs": "Веб-сайт в API"
},
"recordingpage": { "recordingpage": {
"stopRecording": "Остановить запись", "stopRecording": "Остановить запись",
"recording": "Запись...", "recording": "Запись...",
@@ -375,5 +383,86 @@
"created": "Создано", "created": "Создано",
"sent": "Отправлено", "sent": "Отправлено",
"copied": "Скопировано" "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 Türkçe
</MenuItem> </MenuItem>
<MenuItem
onClick={() => {
changeLanguage("ru");
handleMenuClose();
}}
>
Русский
</MenuItem>
<MenuItem <MenuItem
onClick={() => { onClick={() => {
window.open('https://docs.maxun.dev/development/i18n', '_blank'); window.open('https://docs.maxun.dev/development/i18n', '_blank');
@@ -468,6 +476,14 @@ export const NavBar: React.FC<NavBarProps> = ({
> >
Türkçe Türkçe
</MenuItem> </MenuItem>
<MenuItem
onClick={() => {
changeLanguage("ru");
handleMenuClose();
}}
>
Русский
</MenuItem>
<MenuItem <MenuItem
onClick={() => { onClick={() => {
window.open('https://docs.maxun.dev/development/i18n', '_blank'); window.open('https://docs.maxun.dev/development/i18n', '_blank');

View File

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

View File

@@ -10,7 +10,7 @@ i18n
.init({ .init({
fallbackLng: 'en', fallbackLng: 'en',
debug: import.meta.env.DEV, debug: import.meta.env.DEV,
supportedLngs: ['en', 'es', 'ja', 'zh','de', 'tr'], supportedLngs: ['en', 'es', 'ja', 'zh','de', 'tr', 'ru'],
interpolation: { interpolation: {
escapeValue: false, // React already escapes 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_BACKEND_URL': JSON.stringify(process.env.VITE_BACKEND_URL),
'import.meta.env.VITE_PUBLIC_URL': JSON.stringify(publicUrl), 'import.meta.env.VITE_PUBLIC_URL': JSON.stringify(publicUrl),
}, },
css: {
postcss: {
plugins: [], // Empty plugins array - NO PostCSS processing
},
},
server: { server: {
host: new URL(publicUrl).hostname, host: '0.0.0.0', // Listen on all interfaces for Docker
port: parseInt(new URL(publicUrl).port), port: 5174,
}, },
build: { build: {
outDir: 'build', outDir: 'build',