Levin-Aleksey commited on
Commit
2a7af07
·
1 Parent(s): 5187777
Dockerfile CHANGED
@@ -1,16 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
  FROM python:3.10-slim
2
 
 
 
 
3
  WORKDIR /app
4
 
5
- # Установка зависимостей
6
- COPY requirements.txt .
7
- RUN pip install --no-cache-dir -r requirements.txt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
- # Копируем код
10
- COPY . .
 
 
 
11
 
12
- # Открываем порт 7860 (требование HF)
13
  EXPOSE 7860
14
 
15
- # Запуск через uvicorn
16
- CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
 
1
+ # Stage 1: Build frontend
2
+ FROM node:20-alpine AS frontend-builder
3
+
4
+ WORKDIR /app/frontend
5
+
6
+ COPY frontend/package.json ./
7
+ RUN npm install
8
+
9
+ COPY frontend/ ./
10
+ RUN npm run build
11
+
12
+ # Stage 2: Final image
13
  FROM python:3.10-slim
14
 
15
+ # Install nginx and supervisor
16
+ RUN apt-get update && apt-get install -y nginx supervisor && rm -rf /var/lib/apt/lists/*
17
+
18
  WORKDIR /app
19
 
20
+ # Backend
21
+ COPY backend/requirements.txt ./backend/
22
+ RUN pip install --no-cache-dir -r backend/requirements.txt
23
+
24
+ COPY backend/ ./backend/
25
+
26
+ # Frontend from builder
27
+ COPY --from=frontend-builder /app/frontend/.next/standalone ./frontend/
28
+ COPY --from=frontend-builder /app/frontend/.next/static ./frontend/.next/static
29
+ COPY --from=frontend-builder /app/frontend/public ./frontend/public
30
+
31
+ # Nginx config
32
+ COPY nginx.conf /etc/nginx/nginx.conf
33
+
34
+ # Supervisor config
35
+ RUN cat > /etc/supervisor/conf.d/app.conf << 'EOF'
36
+ [supervisord]
37
+ nodaemon=true
38
+
39
+ [program:backend]
40
+ command=uvicorn backend.main:app --host 127.0.0.1 --port 8000
41
+ directory=/app
42
+ autostart=true
43
+ autorestart=true
44
+
45
+ [program:frontend]
46
+ command=node frontend/server.js
47
+ directory=/app
48
+ environment=PORT=3000
49
+ autostart=true
50
+ autorestart=true
51
 
52
+ [program:nginx]
53
+ command=nginx -g "daemon off;"
54
+ autostart=true
55
+ autorestart=true
56
+ EOF
57
 
 
58
  EXPOSE 7860
59
 
60
+ CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"]
 
main.py → backend/main.py RENAMED
File without changes
requirements.txt → backend/requirements.txt RENAMED
File without changes
frontend/app/globals.css ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ * {
6
+ box-sizing: border-box;
7
+ margin: 0;
8
+ padding: 0;
9
+ }
10
+
11
+ body {
12
+ background-color: #1a1a1a;
13
+ color: #e5e5e5;
14
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
15
+ }
16
+
17
+ /* Кастомный скроллбар */
18
+ ::-webkit-scrollbar {
19
+ width: 8px;
20
+ }
21
+
22
+ ::-webkit-scrollbar-track {
23
+ background: #2a2a2a;
24
+ }
25
+
26
+ ::-webkit-scrollbar-thumb {
27
+ background: #4a4a4a;
28
+ border-radius: 4px;
29
+ }
30
+
31
+ ::-webkit-scrollbar-thumb:hover {
32
+ background: #5a5a5a;
33
+ }
frontend/app/layout.tsx ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import './globals.css'
2
+
3
+ export default function RootLayout({
4
+ children
5
+ }: {
6
+ children: React.ReactNode
7
+ }) {
8
+ return (
9
+ <html lang="ru">
10
+ <head>
11
+ <title>Equipment Assistant</title>
12
+ </head>
13
+ <body>{children}</body>
14
+ </html>
15
+ )
16
+ }
frontend/app/page.tsx ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useChat } from 'ai/react'
4
+ import { useEffect, useRef } from 'react'
5
+
6
+ export default function Chat() {
7
+ const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
8
+ api: '/api/chat'
9
+ })
10
+
11
+ const messagesEndRef = useRef<HTMLDivElement>(null)
12
+
13
+ useEffect(() => {
14
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
15
+ }, [messages])
16
+
17
+ return (
18
+ <div className="flex flex-col h-screen bg-zinc-900">
19
+ <header className="flex items-center justify-center py-4 border-b border-zinc-800">
20
+ <h1 className="text-lg font-medium text-white">Equipment Assistant</h1>
21
+ </header>
22
+
23
+ <main className="flex-1 overflow-y-auto px-4 py-6">
24
+ <div className="max-w-3xl mx-auto space-y-6">
25
+ {messages.length === 0 && (
26
+ <div className="flex flex-col items-center justify-center text-center py-20">
27
+ <div className="w-16 h-16 mb-6 rounded-full bg-zinc-800 flex items-center justify-center">
28
+ <svg className="w-8 h-8 text-zinc-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
29
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
30
+ </svg>
31
+ </div>
32
+ <h2 className="text-xl font-medium text-white mb-2">Чем могу помочь?</h2>
33
+ <p className="text-zinc-500 max-w-md">Задайте вопрос о научном оборудовании</p>
34
+ </div>
35
+ )}
36
+
37
+ {messages.map((message) => (
38
+ <div key={message.id} className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
39
+ <div className={`max-w-[80%] px-4 py-3 rounded-2xl ${message.role === 'user' ? 'bg-zinc-700 text-white' : 'bg-zinc-800 text-zinc-100'}`}>
40
+ <p className="whitespace-pre-wrap leading-relaxed">{message.content}</p>
41
+ </div>
42
+ </div>
43
+ ))}
44
+
45
+ {isLoading && (
46
+ <div className="flex justify-start">
47
+ <div className="bg-zinc-800 px-4 py-3 rounded-2xl flex space-x-2">
48
+ <div className="w-2 h-2 bg-zinc-600 rounded-full animate-bounce"></div>
49
+ <div className="w-2 h-2 bg-zinc-600 rounded-full animate-bounce" style={{animationDelay: '150ms'}}></div>
50
+ <div className="w-2 h-2 bg-zinc-600 rounded-full animate-bounce" style={{animationDelay: '300ms'}}></div>
51
+ </div>
52
+ </div>
53
+ )}
54
+
55
+ <div ref={messagesEndRef} />
56
+ </div>
57
+ </main>
58
+
59
+ <footer className="border-t border-zinc-800 px-4 py-4">
60
+ <form onSubmit={handleSubmit} className="max-w-3xl mx-auto">
61
+ <div className="flex items-end gap-3 bg-zinc-800 rounded-2xl px-4 py-3">
62
+ <textarea
63
+ value={input}
64
+ onChange={handleInputChange}
65
+ onKeyDown={(e) => {
66
+ if (e.key === 'Enter' && !e.shiftKey) {
67
+ e.preventDefault()
68
+ handleSubmit(e)
69
+ }
70
+ }}
71
+ placeholder="Введите сообщение..."
72
+ rows={1}
73
+ className="flex-1 bg-transparent text-white placeholder-zinc-500 resize-none outline-none max-h-32"
74
+ />
75
+ <button
76
+ type="submit"
77
+ disabled={isLoading || !input.trim()}
78
+ className="p-2 rounded-lg bg-white text-black disabled:opacity-40 hover:bg-zinc-200 transition-colors"
79
+ >
80
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
81
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M12 5l7 7-7 7" />
82
+ </svg>
83
+ </button>
84
+ </div>
85
+ </form>
86
+ </footer>
87
+ </div>
88
+ )
89
+ }
frontend/next.config.js ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ output: 'standalone',
4
+ async rewrites() {
5
+ return [
6
+ {
7
+ source: '/api/:path*',
8
+ destination: 'http://localhost:8000/api/:path*'
9
+ }
10
+ ]
11
+ }
12
+ }
13
+
14
+ module.exports = nextConfig
frontend/package.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "chat-frontend",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start"
9
+ },
10
+ "dependencies": {
11
+ "next": "14.2.0",
12
+ "react": "18.3.0",
13
+ "react-dom": "18.3.0",
14
+ "ai": "3.4.0",
15
+ "react-markdown": "9.0.1"
16
+ },
17
+ "devDependencies": {
18
+ "typescript": "5.4.0",
19
+ "@types/node": "20.12.0",
20
+ "@types/react": "18.3.0",
21
+ "tailwindcss": "3.4.0",
22
+ "postcss": "8.4.38",
23
+ "autoprefixer": "10.4.19"
24
+ }
25
+ }
frontend/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {}
5
+ }
6
+ }
frontend/tailwind.config.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ content: [
4
+ './app/**/*.{js,ts,jsx,tsx}',
5
+ './components/**/*.{js,ts,jsx,tsx}'
6
+ ],
7
+ theme: {
8
+ extend: {}
9
+ },
10
+ plugins: []
11
+ }
frontend/tsconfig.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./*"]
23
+ }
24
+ },
25
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26
+ "exclude": ["node_modules"]
27
+ }
nginx.conf ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ events {
2
+ worker_connections 1024;
3
+ }
4
+
5
+ http {
6
+ include /etc/nginx/mime.types;
7
+ default_type application/octet-stream;
8
+
9
+ server {
10
+ listen 7860;
11
+ server_name localhost;
12
+
13
+ # Backend API
14
+ location /api/ {
15
+ proxy_pass http://127.0.0.1:8000;
16
+ proxy_http_version 1.1;
17
+ proxy_set_header Upgrade $http_upgrade;
18
+ proxy_set_header Connection 'upgrade';
19
+ proxy_set_header Host $host;
20
+ proxy_cache_bypass $http_upgrade;
21
+ proxy_buffering off;
22
+ proxy_read_timeout 120s;
23
+ }
24
+
25
+ # Frontend (Next.js standalone)
26
+ location / {
27
+ proxy_pass http://127.0.0.1:3000;
28
+ proxy_http_version 1.1;
29
+ proxy_set_header Upgrade $http_upgrade;
30
+ proxy_set_header Connection 'upgrade';
31
+ proxy_set_header Host $host;
32
+ proxy_cache_bypass $http_upgrade;
33
+ }
34
+ }
35
+ }