Chatbot UI design (without connecting with the backend) 99/15099/2
authorJaehyung <wogud1221@khu.ac.kr>
Wed, 1 Oct 2025 09:10:17 +0000 (09:10 +0000)
committerJaehyung <wogud1221@khu.ac.kr>
Thu, 2 Oct 2025 05:06:59 +0000 (05:06 +0000)
- Message bubbles, autoscroll
- Support dark/light mode
- mounted globally in HomePage
- Toggle Chatbot
- Connection with the backend will be done soon. Commited to check if the design appears correctly.

Issue-ID: AIMLFW-270

Change-Id: If74ed76d9e2464179ddfe334795b5339fa00f788
Signed-off-by: Jaehyung Choi <wogud1221@khu.ac.kr>
src/components/chatbot/Chatbot.js [new file with mode: 0644]
src/components/chatbot/chatbot.css [new file with mode: 0644]
src/components/chatbot/index.js [new file with mode: 0644]
src/components/home/HomePage.js
src/components/index.js

diff --git a/src/components/chatbot/Chatbot.js b/src/components/chatbot/Chatbot.js
new file mode 100644 (file)
index 0000000..6014ee9
--- /dev/null
@@ -0,0 +1,117 @@
+import React, { useEffect, useRef, useState } from 'react';
+import './chatbot.css';
+
+export function ChatbotToggle() {
+  const [theme, setTheme] = useState(() => (typeof document !== 'undefined' ? (document.body.getAttribute('data-bs-theme') || 'light') : 'light'));
+  const [open, setOpen] = useState(false);
+  const [hoverSend, setHoverSend] = useState(false);
+  const [hoverFab, setHoverFab] = useState(false);
+  const [text, setText] = useState('');
+  const [messages, setMessages] = useState([
+    { id: 1, role: 'assistant', content: 'Hello! How can I assist you today?' }
+  ]);
+  const endRef = useRef(null);
+
+  useEffect(() => {
+    if (endRef.current) {
+      endRef.current.scrollIntoView({ behavior: 'smooth' });
+    }
+  }, [messages, open]);
+
+  function pushMessage(role, content) {
+    setMessages((prev) => [...prev, { id: prev.length + 1, role, content }]);
+  }
+
+  function onKeyDown(e) {
+    if (e.key === 'Enter' && !e.shiftKey) {
+      e.preventDefault();
+      onSend();
+    }
+  }
+
+  function onSend(e) {
+    if (e) e.preventDefault();
+    if (!text.trim()) return;
+    const userText = text.trim();
+    setText('');
+    pushMessage('user', userText);
+    pushMessage('assistant', 'LLM endpoint is not connected.');
+  }
+
+  useEffect(() => {
+    const body = document.body;
+    const observer = new MutationObserver((mutations) => {
+      for (const m of mutations) {
+        if (m.attributeName === 'data-bs-theme') {
+          setTheme(body.getAttribute('data-bs-theme') || 'light');
+        }
+      }
+    });
+    observer.observe(body, { attributes: true });
+    const onStorage = (e) => {
+      if (e.key === 'theme') {
+        setTheme(e.newValue || 'light');
+      }
+    };
+    window.addEventListener('storage', onStorage);
+    return () => {
+      observer.disconnect();
+      window.removeEventListener('storage', onStorage);
+    };
+  }, []);
+
+  const isDark = theme === 'dark';
+
+  const textColor = isDark ? '#f8fafc' : '#0f172a';
+
+  return (
+    <>
+      {open && (
+        <div className={`chatbot-container ${open ? 'open' : ''}`}>
+          <div className='chatbot-panel'>
+            <div className='chatbot-header'>
+              <div className='chatbot-title'>
+                <span>Assistant</span>
+              </div>
+              <button className='chatbot-icon' aria-label='Close' onClick={() => setOpen(false)}>
+                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
+              </button>
+            </div>
+
+            <div className='chatbot-messages'>
+              {messages.map((m) => (
+                <div key={m.id} className={`chatbot-row ${m.role}`}>
+                  <div className={`chatbot-bubble ${m.role}`}>{m.content}</div>
+                </div>
+              ))}
+              <div ref={endRef} />
+            </div>
+
+            <div className='chatbot-composer'>
+              <textarea
+                className='chatbot-textarea'
+                placeholder='Type your message...'
+                value={text}
+                onChange={(e) => setText(e.target.value)}
+                onKeyDown={onKeyDown}
+              />
+              <button type='button' onClick={onSend} className='chatbot-send'>
+                Send
+              </button>
+            </div>
+            <div className='chatbot-hint'>Press Enter to send, Shift+Enter for a new line</div>
+          </div>
+        </div>
+      )}
+
+      {!open && (
+        <button onClick={() => setOpen(true)} aria-label='Open Chatbot' className='chatbot-fab'>
+          <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+            <path d="M21 15a4 4 0 0 1-4 4H8l-4 4V7a4 4 0 0 1 4-4h9a4 4 0 0 1 4 4z" />
+          </svg>
+        </button>
+      )}
+    </>
+  );
+}
+
diff --git a/src/components/chatbot/chatbot.css b/src/components/chatbot/chatbot.css
new file mode 100644 (file)
index 0000000..435aa5f
--- /dev/null
@@ -0,0 +1,182 @@
+.chatbot-container {
+  position: fixed;
+  right: 24px;
+  bottom: 16px;
+  width: 520px;
+  max-width: 94vw;
+  height: min(72vh, 700px);
+  z-index: 1030;
+  transform: translateY(16px) scale(0.98);
+  opacity: 0;
+  transition: opacity 220ms ease, transform 220ms ease, bottom 220ms ease;
+}
+
+.chatbot-container.open {
+  bottom: 24px;
+  transform: translateY(0) scale(1);
+  opacity: 1;
+}
+
+.chatbot-panel {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  padding: 16px;
+  border-radius: 16px;
+  background: #ffffff;
+  border: 1px solid #e6e8eb;
+}
+
+.chatbot-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12px;
+  color: #0f172a;
+}
+
+.chatbot-icon {
+  border: none;
+  background: transparent;
+  cursor: pointer;
+  padding: 0;
+  line-height: 0;
+}
+
+.chatbot-title {
+  display: flex;
+  align-items: baseline;
+  gap: 8px;
+  font-weight: 700;
+  font-size: 16px;
+}
+
+.chatbot-messages {
+  flex: 1;
+  overflow: auto;
+  padding: 8px;
+  border-radius: 12px;
+  background: #f8fafc;
+  border: 1px solid #e2e8f0;
+}
+
+.chatbot-row {
+  display: flex;
+  margin-bottom: 8px;
+}
+
+.chatbot-row.user {
+  justify-content: flex-end;
+}
+
+.chatbot-row.assistant {
+  justify-content: flex-start;
+}
+
+.chatbot-bubble {
+  max-width: 78%;
+  padding: 10px 12px;
+  border-radius: 14px;
+  word-break: break-word;
+  white-space: pre-wrap;
+}
+
+.chatbot-bubble.user {
+  background: linear-gradient(135deg, #4f46e5 0%, #06b6d4 100%);
+  color: #ffffff;
+  border: none;
+  box-shadow: 0 10px 24px rgba(6, 182, 212, 0.25);
+  border-radius: 14px 14px 4px 14px;
+}
+
+.chatbot-bubble.assistant {
+  background: #ffffff;
+  color: #0f172a;
+  border: 1px solid #e2e8f0;
+  box-shadow: 0 8px 18px rgba(0, 0, 0, 0.06);
+  border-radius: 14px 14px 14px 4px;
+}
+
+.chatbot-composer {
+  display: flex;
+  gap: 10px;
+  align-items: flex-end;
+  margin-top: 12px;
+}
+
+.chatbot-textarea {
+  flex: 1;
+  min-height: 60px;
+  max-height: 160px;
+  resize: vertical;
+  padding: 10px 12px;
+  border-radius: 12px;
+  border: 1px solid #e5e7eb;
+  background: #ffffff;
+  color: #0f172a;
+}
+
+.chatbot-send {
+  border: none;
+  background: linear-gradient(135deg, #4f46e5 0%, #06b6d4 100%);
+  color: #fff;
+  padding: 10px 16px;
+  border-radius: 12px;
+  font-weight: 700;
+  cursor: pointer;
+}
+
+.chatbot-hint {
+  font-size: 12px;
+  margin-top: 4px;
+  color: #94a3b8;
+}
+
+.chatbot-fab {
+  position: fixed;
+  right: 24px;
+  bottom: 24px;
+  width: 76px;
+  height: 76px;
+  border-radius: 50%;
+  border: none;
+  cursor: pointer;
+  z-index: 1030;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: linear-gradient(135deg, #4f46e5 0%, #06b6d4 100%);
+  color: #fff;
+}
+
+body[data-bs-theme='dark'] .chatbot-panel {
+  background: #0f172a;
+  border: 1px solid #334155;
+}
+
+body[data-bs-theme='dark'] .chatbot-header {
+  color: #f8fafc;
+}
+
+body[data-bs-theme='dark'] .chatbot-messages {
+  background: #0b1220;
+  border-color: #475569;
+}
+
+body[data-bs-theme='dark'] .chatbot-bubble.assistant {
+  background: #1f2937;
+  color: #f1f5f9;
+  border-color: #475569;
+}
+
+body[data-bs-theme='dark'] .chatbot-textarea {
+  background: #0b1220;
+  color: #f8fafc;
+  border-color: #475569;
+}
+
+body[data-bs-theme='dark'] .chatbot-fab {
+  background: linear-gradient(135deg, #1f2937 0%, #334155 100%);
+}
+
+
diff --git a/src/components/chatbot/index.js b/src/components/chatbot/index.js
new file mode 100644 (file)
index 0000000..3dcc727
--- /dev/null
@@ -0,0 +1,3 @@
+export * from './Chatbot';
+
+
index 939632a..8c48734 100644 (file)
@@ -27,6 +27,7 @@ import ListFeatureGroup from './status/ListFeatureGroup';
 import ListModels from './status/ListModels';
 import { NavigationBar } from '../navigation';
 import { Sidebar } from '../sidebar';
+import { ChatbotToggle } from '../../components';
 import { debug_var } from '../../states';
 
 var DEBUG = debug_var === 'true';
@@ -61,6 +62,7 @@ class HomePage extends React.Component {
             </Row>
           </Container>
         </Router>
+        <ChatbotToggle />
       </>
     );
   }
index d2d7b19..6857ae7 100644 (file)
@@ -5,3 +5,4 @@ export * from './checkbox';
 export * from './popup';
 export * from './steps-state';
 export * from './training-job-info';
+export * from './chatbot';