diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d326f31 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +node_modules +dist +Dockerfile +.dockerignore +.git/ +.githooks/ +__mocks__/ +test/ +eslint.config.js +jest.config.js +README.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c608b21 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,46 @@ +# --- Building static files --- +FROM node:23-alpine AS build + +WORKDIR /app + +COPY package.json package-lock.json ./ + +RUN npm ci + +COPY . . + +RUN npm run build + + +# --- Serving --- +FROM nginx:alpine + +RUN mkdir -p /app/www + +COPY --from=build /app/dist /app/www + +COPY nginx.conf /etc/nginx/templates/default.conf.template + +RUN adduser -D -H -u 1001 -s /sbin/nologin webuser + +RUN chown -R webuser:webuser /app/www && \ + chmod -R 755 /app/www && \ + chown -R webuser:webuser /var/cache/nginx && \ + chown -R webuser:webuser /var/log/nginx && \ + chown -R webuser:webuser /etc/nginx/conf.d && \ + touch /var/run/nginx.pid && \ + chown -R webuser:webuser /var/run/nginx.pid && \ + chmod -R 777 /etc/nginx/conf.d + +ENV PORT=80 +ENV NGINX_ENVSUBST_TEMPLATE_DIR=/etc/nginx/templates +ENV NGINX_ENVSUBST_TEMPLATE_SUFFIX=.template +ENV NGINX_ENVSUBST_OUTPUT_DIR=/etc/nginx/conf.d +# Default value, potentially overwritten in compose file +ENV BACKEND_ADDRESS="http://localhost:8000" + +EXPOSE ${PORT} + +USER webuser + +CMD [ "nginx", "-g", "daemon off;" ] diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..79386d0 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,26 @@ +server { + listen ${PORT}; + server_name localhost; + root /app/www; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-XSS-Protection "1; mode=block"; + add_header X-Content-Type-Options "nosniff"; + + # Compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + location / { + try_files $uri $uri/ /index.html; + expires -1; + } + + # Cache static assets + location /assets { + expires 1y; + add_header Cache-Control "public, no-transform"; + } +} + diff --git a/src/pages/Robot/Robot.tsx b/src/pages/Robot/Robot.tsx index 0038dd9..40e56a5 100644 --- a/src/pages/Robot/Robot.tsx +++ b/src/pages/Robot/Robot.tsx @@ -4,13 +4,13 @@ export default function Robot() { const [message, setMessage] = useState(''); const [listening, setListening] = useState(false); - const [conversation, setConversation] = useState<{"role": "user" | "assistant", "content": string}[]>([]) + const [conversation, setConversation] = useState<{ "role": "user" | "assistant", "content": string }[]>([]) const conversationRef = useRef(null); const [conversationIndex, setConversationIndex] = useState(0); const sendMessage = async () => { try { - const response = await fetch("http://localhost:8000/message", { + const response = await fetch(`${process.env.BACKEND_ADDRESS}/message`, { method: "POST", headers: { "Content-Type": "application/json", @@ -25,14 +25,14 @@ export default function Robot() { }; useEffect(() => { - const eventSource = new EventSource("http://localhost:8000/sse"); + const eventSource = new EventSource(`${process.env.BACKEND_ADDRESS}/sse`); eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); if ("voice_active" in data) setListening(data.voice_active); - if ("speech" in data) setConversation(conversation => [...conversation, {"role": "user", "content": data.speech}]); - if ("llm_response" in data) setConversation(conversation => [...conversation, {"role": "assistant", "content": data.llm_response}]); + if ("speech" in data) setConversation(conversation => [...conversation, { "role": "user", "content": data.speech }]); + if ("llm_response" in data) setConversation(conversation => [...conversation, { "role": "assistant", "content": data.llm_response }]); } catch { console.log("Unparsable SSE message:", event.data); } @@ -65,7 +65,7 @@ export default function Robot() {

Conversation

Listening {listening ? "🟢" : "🔴"}

-
+
{conversation.map((item, i) => (