Spaces:
Sleeping
Sleeping
Commit ·
2a7af07
1
Parent(s): 5187777
start
Browse files- Dockerfile +52 -8
- main.py → backend/main.py +0 -0
- requirements.txt → backend/requirements.txt +0 -0
- frontend/app/globals.css +33 -0
- frontend/app/layout.tsx +16 -0
- frontend/app/page.tsx +89 -0
- frontend/next.config.js +14 -0
- frontend/package.json +25 -0
- frontend/postcss.config.js +6 -0
- frontend/tailwind.config.js +11 -0
- frontend/tsconfig.json +27 -0
- nginx.conf +35 -0
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 |
-
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
# Открываем порт 7860 (требование HF)
|
| 13 |
EXPOSE 7860
|
| 14 |
|
| 15 |
-
|
| 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 |
+
}
|