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_API_BASE_URL=https://hr.aiquity.xyz:8000/api
|
||||||
NEXT_PUBLIC_LIVEKIT_URL=ws://localhost:7880
|
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) {
|
export default function InterviewSession({ resumeId, onEnd }: InterviewSessionProps) {
|
||||||
const { data: tokenData, isLoading, error } = useInterviewToken(resumeId, true)
|
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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50">
|
<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" />
|
<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 className="text-xl font-semibold text-gray-900 mb-2">
|
||||||
Подключаемся к собеседованию
|
Подготавливаем собеседование
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-600 text-center max-w-md">
|
<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">
|
<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" />
|
<AlertCircle className="h-12 w-12 text-red-600 mb-4" />
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">
|
<h2 className="text-xl font-semibold text-gray-900 mb-2">
|
||||||
Ошибка подключения
|
Ошибка получения токена
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-600 text-center max-w-md mb-6">
|
<p className="text-gray-600 text-center max-w-md mb-6">
|
||||||
Не удалось подключиться к сессии собеседования
|
Не удалось получить токен доступа к сессии собеседования
|
||||||
</p>
|
</p>
|
||||||
<button
|
<div className="flex gap-4">
|
||||||
onClick={() => window.history.back()}
|
<button
|
||||||
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"
|
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>
|
||||||
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -73,13 +150,23 @@ export default function InterviewSession({ resumeId, onEnd }: InterviewSessionPr
|
|||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
<LiveKitRoom
|
<LiveKitRoom
|
||||||
token={tokenData.token}
|
token={tokenData.token}
|
||||||
serverUrl={tokenData.serverUrl || process.env.NEXT_PUBLIC_LIVEKIT_URL!}
|
serverUrl={getServerUrl()}
|
||||||
audio={true}
|
audio={true}
|
||||||
video={false}
|
video={false}
|
||||||
onConnected={() => console.log('Connected to LiveKit')}
|
connectOptions={{
|
||||||
onDisconnected={() => console.log('Disconnected from LiveKit')}
|
// Дополнительные опции подключения
|
||||||
|
autoSubscribe: true,
|
||||||
|
maxRetries: 3,
|
||||||
|
}}
|
||||||
|
onConnected={() => {
|
||||||
|
console.log('Connected to LiveKit successfully')
|
||||||
|
setConnectionError(null)
|
||||||
|
}}
|
||||||
|
onDisconnected={(reason) => {
|
||||||
|
console.log('Disconnected from LiveKit:', reason)
|
||||||
|
}}
|
||||||
onError={(error) => {
|
onError={(error) => {
|
||||||
console.error('LiveKit error:', error)
|
handleConnectionError(error)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<InterviewRoom resumeId={resumeId} onEnd={onEnd} sessionId={tokenData.session_id} />
|
<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;
|
const score = report[key as keyof InterviewReport] as number;
|
||||||
return (
|
return (
|
||||||
<p key={key}>
|
<p key={key}>
|
||||||
<span className="font-medium">{label}:</span> {score}/10
|
<span className="font-medium">{label}:</span> {score}/100
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import ky from 'ky'
|
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 заголовка
|
// Базовый клиент без Content-Type заголовка
|
||||||
const baseKyClient = ky.create({
|
const baseKyClient = ky.create({
|
||||||
|
@ -1,4 +1,36 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @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
|
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