fixes
This commit is contained in:
parent
d7fe54d6bb
commit
206527fa0d
16
.dockerignore
Normal file
16
.dockerignore
Normal file
@ -0,0 +1,16 @@
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
.next
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
.env
|
||||
.env.local
|
||||
.env.production.local
|
||||
.env.development.local
|
||||
coverage
|
||||
.nyc_output
|
||||
.DS_Store
|
||||
*.tsbuildinfo
|
@ -1,2 +1,2 @@
|
||||
NEXT_PUBLIC_API_BASE_URL=http://localhost:8000/api
|
||||
NEXT_PUBLIC_LIVEKIT_URL=ws://localhost:7880
|
||||
NEXT_PUBLIC_API_BASE_URL=https://hr.aiquity.xyz:8000/api
|
||||
NEXT_PUBLIC_LIVEKIT_URL=wss://hackaton-eizc9zqk.livekit.cloud
|
||||
|
37
Dockerfile
Normal file
37
Dockerfile
Normal file
@ -0,0 +1,37 @@
|
||||
FROM --platform=linux/amd64 node:18-alpine AS base
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
FROM base AS deps
|
||||
RUN apk add --no-cache libc6-compat
|
||||
COPY package.json yarn.lock* ./
|
||||
RUN yarn
|
||||
|
||||
FROM base AS builder
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
RUN yarn build
|
||||
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
RUN mkdir .next
|
||||
RUN chown nextjs:nodejs .next
|
||||
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT 3000
|
||||
ENV HOSTNAME "0.0.0.0"
|
||||
|
||||
CMD ["node", "server.js"]
|
@ -34,13 +34,44 @@ interface InterviewState {
|
||||
|
||||
export default function InterviewSession({ resumeId, onEnd }: InterviewSessionProps) {
|
||||
const { data: tokenData, isLoading, error } = useInterviewToken(resumeId, true)
|
||||
const [connectionError, setConnectionError] = useState<string | null>(null)
|
||||
const [isRetrying, setIsRetrying] = useState(false)
|
||||
|
||||
const getServerUrl = () => {
|
||||
// Приоритет: данные от API -> fallback URLs
|
||||
if (tokenData?.serverUrl) {
|
||||
return tokenData.serverUrl
|
||||
}
|
||||
|
||||
// Fallback URLs для разных окружений
|
||||
const fallbackUrls = [
|
||||
'wss://hackaton-eizc9zqk.livekit.cloud',
|
||||
]
|
||||
|
||||
return fallbackUrls[0]
|
||||
}
|
||||
|
||||
const handleConnectionError = (error: Error) => {
|
||||
console.error('LiveKit connection error:', error)
|
||||
setConnectionError(`Ошибка подключения: ${error.message}`)
|
||||
}
|
||||
|
||||
const retryConnection = () => {
|
||||
setIsRetrying(true)
|
||||
setConnectionError(null)
|
||||
// Перезагрузка компонента через 1 секунду
|
||||
setTimeout(() => {
|
||||
setIsRetrying(false)
|
||||
window.location.reload()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50">
|
||||
<Loader className="h-12 w-12 text-blue-600 animate-spin mb-4" />
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
Подключаемся к собеседованию
|
||||
Подготавливаем собеседование
|
||||
</h2>
|
||||
<p className="text-gray-600 text-center max-w-md">
|
||||
Пожалуйста, подождите, мы подготавливаем для вас сессию
|
||||
@ -54,17 +85,63 @@ export default function InterviewSession({ resumeId, onEnd }: InterviewSessionPr
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50">
|
||||
<AlertCircle className="h-12 w-12 text-red-600 mb-4" />
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
Ошибка подключения
|
||||
Ошибка получения токена
|
||||
</h2>
|
||||
<p className="text-gray-600 text-center max-w-md mb-6">
|
||||
Не удалось подключиться к сессии собеседования
|
||||
Не удалось получить токен доступа к сессии собеседования
|
||||
</p>
|
||||
<button
|
||||
onClick={() => window.history.back()}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
Вернуться назад
|
||||
</button>
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
Попробовать снова
|
||||
</button>
|
||||
<button
|
||||
onClick={() => window.history.back()}
|
||||
className="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||
>
|
||||
Вернуться назад
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (connectionError) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50">
|
||||
<AlertCircle className="h-12 w-12 text-red-600 mb-4" />
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">
|
||||
Ошибка подключения к LiveKit
|
||||
</h2>
|
||||
<p className="text-gray-600 text-center max-w-md mb-4">
|
||||
{connectionError}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 text-center max-w-md mb-6">
|
||||
Сервер: {getServerUrl()}
|
||||
</p>
|
||||
{isRetrying ? (
|
||||
<div className="flex items-center">
|
||||
<Loader className="h-5 w-5 text-blue-600 animate-spin mr-2" />
|
||||
<span className="text-blue-600">Переподключение...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={retryConnection}
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
Попробовать снова
|
||||
</button>
|
||||
<button
|
||||
onClick={() => window.history.back()}
|
||||
className="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||
>
|
||||
Вернуться назад
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -73,13 +150,23 @@ export default function InterviewSession({ resumeId, onEnd }: InterviewSessionPr
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<LiveKitRoom
|
||||
token={tokenData.token}
|
||||
serverUrl={tokenData.serverUrl || process.env.NEXT_PUBLIC_LIVEKIT_URL!}
|
||||
serverUrl={getServerUrl()}
|
||||
audio={true}
|
||||
video={false}
|
||||
onConnected={() => console.log('Connected to LiveKit')}
|
||||
onDisconnected={() => console.log('Disconnected from LiveKit')}
|
||||
connectOptions={{
|
||||
// Дополнительные опции подключения
|
||||
autoSubscribe: true,
|
||||
maxRetries: 3,
|
||||
}}
|
||||
onConnected={() => {
|
||||
console.log('Connected to LiveKit successfully')
|
||||
setConnectionError(null)
|
||||
}}
|
||||
onDisconnected={(reason) => {
|
||||
console.log('Disconnected from LiveKit:', reason)
|
||||
}}
|
||||
onError={(error) => {
|
||||
console.error('LiveKit error:', error)
|
||||
handleConnectionError(error)
|
||||
}}
|
||||
>
|
||||
<InterviewRoom resumeId={resumeId} onEnd={onEnd} sessionId={tokenData.session_id} />
|
||||
|
@ -100,7 +100,7 @@ export default function VacancyReports({ reports }: VacancyReportsProps) {
|
||||
const score = report[key as keyof InterviewReport] as number;
|
||||
return (
|
||||
<p key={key}>
|
||||
<span className="font-medium">{label}:</span> {score}/10
|
||||
<span className="font-medium">{label}:</span> {score}/100
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import ky from 'ky'
|
||||
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000/api'
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'https://hr.aiquity.xyz/api'
|
||||
|
||||
// Базовый клиент без Content-Type заголовка
|
||||
const baseKyClient = ky.create({
|
||||
|
@ -1,4 +1,36 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {}
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
// Apply these headers to all routes
|
||||
source: '/(.*)',
|
||||
headers: [
|
||||
{
|
||||
key: 'X-Frame-Options',
|
||||
value: 'DENY',
|
||||
},
|
||||
{
|
||||
key: 'X-Content-Type-Options',
|
||||
value: 'nosniff',
|
||||
},
|
||||
{
|
||||
key: 'Referrer-Policy',
|
||||
value: 'origin-when-cross-origin',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
},
|
||||
// Разрешить WebSocket подключения к внешним доменам
|
||||
experimental: {
|
||||
serverComponentsExternalPackages: [],
|
||||
},
|
||||
// Настройки для работы с внешними доменами
|
||||
images: {
|
||||
domains: ['hr.aiquity.xyz'],
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
71
scripts/build-and-push.sh
Executable file
71
scripts/build-and-push.sh
Executable file
@ -0,0 +1,71 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build and push script for Yandex Cloud Container Registry
|
||||
# Usage: ./scripts/build-and-push. [tag]
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
REGISTRY_ID="${YANDEX_REGISTRY_ID:-your-registry-id}"
|
||||
IMAGE_NAME="hr-ai-frontend"
|
||||
TAG="${1:-latest}"
|
||||
FULL_IMAGE_NAME="cr.yandex/${REGISTRY_ID}/${IMAGE_NAME}:${TAG}"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${YELLOW}Building and pushing HR AI Frontend to Yandex Cloud Container Registry${NC}"
|
||||
|
||||
# Check if required environment variables are set
|
||||
if [ -z "$REGISTRY_ID" ] || [ "$REGISTRY_ID" = "your-registry-id" ]; then
|
||||
echo -e "${RED}Error: YANDEX_REGISTRY_ID environment variable is not set${NC}"
|
||||
echo "Please set it to your Yandex Cloud Container Registry ID"
|
||||
echo "Example: export YANDEX_REGISTRY_ID=crp1234567890abcdef"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if yc CLI is installed and authenticated
|
||||
if ! command -v yc &> /dev/null; then
|
||||
echo -e "${RED}Error: Yandex Cloud CLI (yc) is not installed${NC}"
|
||||
echo "Please install it from: https://cloud.yandex.ru/docs/cli/quickstart"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check authentication
|
||||
if ! yc config list | grep -q "token:"; then
|
||||
echo -e "${RED}Error: Not authenticated with Yandex Cloud${NC}"
|
||||
echo "Please run: yc init"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}Configuring Docker for Yandex Cloud Container Registry...${NC}"
|
||||
yc container registry configure-docker
|
||||
|
||||
echo -e "${YELLOW}Building Docker image: ${FULL_IMAGE_NAME}${NC}"
|
||||
docker build -t "${FULL_IMAGE_NAME}" .
|
||||
|
||||
echo -e "${YELLOW}Pushing image to registry...${NC}"
|
||||
docker push "${FULL_IMAGE_NAME}"
|
||||
|
||||
echo -e "${GREEN}✓ Successfully built and pushed: ${FULL_IMAGE_NAME}${NC}"
|
||||
|
||||
# Also tag as latest if a specific tag was provided
|
||||
if [ "$TAG" != "latest" ]; then
|
||||
LATEST_IMAGE_NAME="cr.yandex/${REGISTRY_ID}/${IMAGE_NAME}:latest"
|
||||
echo -e "${YELLOW}Tagging as latest...${NC}"
|
||||
docker tag "${FULL_IMAGE_NAME}" "${LATEST_IMAGE_NAME}"
|
||||
docker push "${LATEST_IMAGE_NAME}"
|
||||
echo -e "${GREEN}✓ Also pushed as: ${LATEST_IMAGE_NAME}${NC}"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Build and push completed successfully!${NC}"
|
||||
echo ""
|
||||
echo "Image is available at:"
|
||||
echo " ${FULL_IMAGE_NAME}"
|
||||
echo ""
|
||||
echo "To use in production, update your docker-compose.prod.yml:"
|
||||
echo " frontend:"
|
||||
echo " image: ${FULL_IMAGE_NAME}"
|
Loading…
Reference in New Issue
Block a user