aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorA.J. Shulman <Shulman.aj@gmail.com>2025-05-08 15:54:56 -0400
committerA.J. Shulman <Shulman.aj@gmail.com>2025-05-08 15:54:56 -0400
commit34ecaaffb1eebef6d509ed73db336c7bdb181e76 (patch)
treee78fb7e852f697f771a9b056e030382be6f0d937
parent256cd13bd258c18a805b1e9c6a6596d8d9e0cf4b (diff)
improve: enhance ChatBox UI with consistent styling and better accessibility
• Fixed dictation button styling to match send button dimensions and colors • Created dedicated DictationButton.scss for better button styling • Removed vertical movement animations from all buttons for a more stable UI • Fixed empty space issue below the "Show Agent Thoughts/Actions" button • Implemented consistent hover effects across all interactive elements • Ensured font size scaling works for agent thoughts/actions content • Used Dash blue colors (#487af0, #3b6cd7) for consistent branding • Improved microphone button state visualization with pulse animation
-rw-r--r--src/client/views/DictationButton.scss73
-rw-r--r--src/client/views/DictationButton.tsx25
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss806
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx106
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/MessageComponent.tsx16
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/ProgressBar.scss105
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/ProgressBar.tsx30
7 files changed, 902 insertions, 259 deletions
diff --git a/src/client/views/DictationButton.scss b/src/client/views/DictationButton.scss
new file mode 100644
index 000000000..ac8740c0f
--- /dev/null
+++ b/src/client/views/DictationButton.scss
@@ -0,0 +1,73 @@
+.dictation-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 48px;
+ height: 48px;
+ min-width: 48px;
+ border-radius: 50%;
+ border: none;
+ background-color: #487af0;
+ color: white;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ box-shadow: 0 2px 8px rgba(72, 122, 240, 0.3);
+ padding: 0;
+ margin-left: 5px;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(135deg, transparent, rgba(255, 255, 255, 0.3));
+ opacity: 0;
+ transition: opacity 0.25s ease;
+ }
+
+ &:hover {
+ background-color: #3b6cd7; /* Slightly darker blue */
+ box-shadow: 0 3px 10px rgba(72, 122, 240, 0.4);
+
+ &::before {
+ opacity: 1;
+ }
+
+ svg {
+ transform: scale(1.1);
+ }
+ }
+
+ &:active {
+ background-color: #3463cc; /* Even darker for active state */
+ box-shadow: 0 2px 6px rgba(72, 122, 240, 0.3);
+ }
+
+ &.recording {
+ background-color: #ef4444;
+ color: white;
+ animation: pulse 1.5s infinite;
+ }
+
+ svg {
+ width: 22px;
+ height: 22px;
+ transition: transform 0.2s ease;
+ }
+}
+
+@keyframes pulse {
+ 0% {
+ box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.5);
+ }
+ 70% {
+ box-shadow: 0 0 0 10px rgba(239, 68, 68, 0);
+ }
+ 100% {
+ box-shadow: 0 0 0 0 rgba(239, 68, 68, 0);
+ }
+}
diff --git a/src/client/views/DictationButton.tsx b/src/client/views/DictationButton.tsx
index 0ce586df4..fc3165f67 100644
--- a/src/client/views/DictationButton.tsx
+++ b/src/client/views/DictationButton.tsx
@@ -1,8 +1,7 @@
-import { IconButton, Type } from '@dash/components';
-import { action, makeObservable, observable } from 'mobx';
+import { makeObservable, observable, action } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { BiMicrophone } from 'react-icons/bi';
+import './DictationButton.scss';
import { DictationManager } from '../util/DictationManager';
import { SnappingManager } from '../util/SnappingManager';
@@ -10,9 +9,11 @@ export interface DictationButtonProps {
setInput: (val: string) => void;
inputRef?: HTMLInputElement | null | undefined;
}
+
@observer
export class DictationButton extends React.Component<DictationButtonProps> {
@observable private _isRecording = false;
+
constructor(props: DictationButtonProps) {
super(props);
makeObservable(this);
@@ -25,11 +26,9 @@ export class DictationButton extends React.Component<DictationButtonProps> {
render() {
return (
- <IconButton
- type={Type.TERT}
- color={this._isRecording ? '#2bcaff' : SnappingManager.userVariantColor}
- tooltip="Record"
- icon={<BiMicrophone size="16px" />}
+ <button
+ className={`dictation-button ${this._isRecording ? 'recording' : ''}`}
+ title="Record"
onClick={action(() => {
if (!this._isRecording) {
this._isRecording = true;
@@ -50,8 +49,14 @@ export class DictationButton extends React.Component<DictationButtonProps> {
} else {
this.stopDictation();
}
- })}
- />
+ })}>
+ <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+ <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path>
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
+ <line x1="12" y1="19" x2="12" y2="23"></line>
+ <line x1="8" y1="23" x2="16" y2="23"></line>
+ </svg>
+ </button>
);
}
}
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss
index 3d27fa887..e7a1c3b42 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss
@@ -1,240 +1,629 @@
@use 'sass:color';
-@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
-
-$primary-color: #3f51b5;
-$secondary-color: #f0f0f0;
-$text-color: #2e2e2e;
-$light-text-color: #6d6d6d;
-$border-color: #dcdcdc;
-$shadow-color: rgba(0, 0, 0, 0.1);
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
+
+// Dash color palette - updated to use Dash's blue colors
+$primary-color: #487af0; // Dash blue
+$primary-light: #e6f0fc;
+$secondary-color: #f7f7f9;
+$accent-color: #b5d9f3; // Light blue accent
+$bg-color: #ffffff;
+$text-color: #111827;
+$light-text-color: #6b7280;
+$border-color: #e5e7eb;
+$shadow-color: rgba(0, 0, 0, 0.06);
$transition: all 0.2s ease-in-out;
+// Font size variables
+$font-size-small: 13px;
+$font-size-normal: 14px;
+$font-size-large: 16px;
+$font-size-xlarge: 18px;
+
.chat-box {
display: flex;
flex-direction: column;
height: 100%;
- background-color: #fff;
+ width: 100%;
+ background-color: $bg-color;
font-family: 'Inter', sans-serif;
- border-radius: 8px;
+ border-radius: 12px;
overflow: hidden;
- box-shadow: 0 2px 8px $shadow-color;
+ box-shadow: 0 4px 20px $shadow-color;
position: relative;
+ transition:
+ box-shadow 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94),
+ transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
+
+ &:hover {
+ box-shadow: 0 8px 30px rgba($primary-color, 0.1);
+ }
.chat-header {
- background-color: $primary-color;
- color: #fff;
- padding: 16px;
- text-align: center;
- box-shadow: 0 1px 4px $shadow-color;
+ background: $primary-color;
+ color: white;
+ padding: 14px 20px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+ z-index: 10;
+ position: relative;
h2 {
margin: 0;
- font-size: 1.5em;
- font-weight: 500;
+ font-size: 1.25rem;
+ font-weight: 600;
+ letter-spacing: 0.01em;
+ flex: 1;
+ text-align: center;
+ }
+
+ .font-size-control {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: rgba(255, 255, 255, 0.15);
+ color: white;
+ border-radius: 6px;
+ padding: 6px;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+
+ &:hover {
+ background-color: rgba(255, 255, 255, 0.25);
+ }
+
+ svg {
+ width: 20px;
+ height: 20px;
+ }
+ }
+
+ .font-size-modal {
+ position: absolute;
+ top: 100%;
+ right: 10px;
+ background-color: white;
+ border-radius: 8px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
+ padding: 12px;
+ width: 180px;
+ z-index: 100;
+ transform-origin: top right;
+ animation: scaleIn 0.2s forwards;
+
+ .font-size-option {
+ display: flex;
+ align-items: center;
+ padding: 8px 12px;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+ color: $text-color;
+ margin-bottom: 4px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ &:hover {
+ background-color: $primary-light;
+ }
+
+ &.active {
+ background-color: $primary-light;
+ color: $primary-color;
+ font-weight: 500;
+ }
+
+ .option-label {
+ flex: 1;
+ }
+
+ .size-preview {
+ font-size: 10px;
+ opacity: 0.7;
+
+ &.small {
+ font-size: 11px;
+ }
+ &.normal {
+ font-size: 14px;
+ }
+ &.large {
+ font-size: 16px;
+ }
+ &.xlarge {
+ font-size: 18px;
+ }
+ }
+ }
}
}
.chat-messages {
flex-grow: 1;
overflow-y: auto;
- padding: 16px;
+ padding: 20px;
display: flex;
flex-direction: column;
- gap: 12px;
+ gap: 16px;
+ background-color: #f9fafb;
+ background-image: radial-gradient(#e5e7eb 1px, transparent 1px), radial-gradient(#e5e7eb 1px, transparent 1px);
+ background-size: 40px 40px;
+ background-position:
+ 0 0,
+ 20px 20px;
+ background-attachment: local;
+ scroll-behavior: smooth;
&::-webkit-scrollbar {
- width: 8px;
+ width: 6px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: transparent;
}
&::-webkit-scrollbar-thumb {
- background-color: rgba(0, 0, 0, 0.1);
- border-radius: 4px;
+ background-color: rgba($primary-color, 0.2);
+ border-radius: 10px;
+
+ &:hover {
+ background-color: rgba($primary-color, 0.3);
+ }
}
}
.chat-input {
display: flex;
- padding: 12px;
+ padding: 16px 20px;
border-top: 1px solid $border-color;
- background-color: #fff;
+ background-color: white;
+ box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.05);
+ position: relative;
+ align-items: center;
+ gap: 12px;
+ z-index: 5;
+ transition: padding 0.2s ease;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: -5px;
+ left: 0;
+ right: 0;
+ height: 5px;
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.06), transparent);
+ pointer-events: none;
+ }
- input {
+ .input-container {
+ position: relative;
flex-grow: 1;
- padding: 12px 16px;
- border: 1px solid $border-color;
+ display: flex;
+ align-items: center;
border-radius: 24px;
- font-size: 15px;
- transition: $transition;
+ background-color: #f9fafb;
+ border: 1px solid $border-color;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.03);
+ transition: all 0.25s ease;
+ overflow: hidden;
- &:focus {
- outline: none;
+ &:focus-within {
border-color: $primary-color;
- box-shadow: 0 0 0 2px color.adjust($primary-color, $alpha: -0.8);
+ box-shadow: 0 0 0 3px rgba($primary-color, 0.15);
+ background-color: white;
+ transform: translateY(-1px);
}
- &:disabled {
- background-color: $secondary-color;
- cursor: not-allowed;
+ input {
+ flex-grow: 1;
+ padding: 14px 18px;
+ border: none;
+ background: transparent;
+ font-size: 14px;
+ transition: all 0.25s ease;
+ width: 100%;
+
+ &:focus {
+ outline: none;
+ }
+
+ &:disabled {
+ background-color: #f3f4f6;
+ cursor: not-allowed;
+ }
+
+ &::placeholder {
+ color: #9ca3af;
+ }
}
}
.submit-button {
- background-color: $primary-color;
+ background: $primary-color;
color: white;
border: none;
border-radius: 50%;
width: 48px;
height: 48px;
- margin-left: 10px;
+ min-width: 48px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
- transition: $transition;
+ transition: all 0.2s ease;
+ box-shadow: 0 2px 8px rgba($primary-color, 0.3);
+ position: relative;
+ overflow: hidden;
&:hover {
- background-color: color.adjust($primary-color, $lightness: -10%);
+ background-color: #3b6cd7; /* Slightly darker blue */
+ box-shadow: 0 3px 10px rgba($primary-color, 0.4);
+ }
+
+ &:active {
+ background-color: #3463cc; /* Even darker for active state */
+ box-shadow: 0 2px 6px rgba($primary-color, 0.3);
}
&:disabled {
- background-color: color.adjust($primary-color, $lightness: 20%);
+ background: #9ca3af;
+ box-shadow: none;
cursor: not-allowed;
}
+ svg {
+ width: 20px;
+ height: 20px;
+ }
+
.spinner {
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top: 3px solid #fff;
border-radius: 50%;
- animation: spin 0.6s linear infinite;
+ animation: spin 0.8s cubic-bezier(0.34, 0.61, 0.71, 0.97) infinite;
}
}
}
.citation-popup {
position: fixed;
- bottom: 50px;
+ bottom: 80px;
left: 50%;
transform: translateX(-50%);
- background-color: rgba(0, 0, 0, 0.8);
+ background-color: rgba(72, 122, 240, 0.95);
color: white;
- padding: 10px 20px;
+ padding: 14px 20px;
border-radius: 10px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.25);
z-index: 1000;
animation: fadeIn 0.3s ease-in-out;
+ max-width: 90%;
+ backdrop-filter: blur(8px);
p {
margin: 0;
font-size: 14px;
+ line-height: 1.5;
}
@keyframes fadeIn {
from {
opacity: 0;
+ transform: translate(-50%, 10px);
}
to {
opacity: 1;
+ transform: translate(-50%, 0);
}
}
}
}
+@keyframes pulse {
+ 0% {
+ box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.5);
+ }
+ 70% {
+ box-shadow: 0 0 0 10px rgba(239, 68, 68, 0);
+ }
+ 100% {
+ box-shadow: 0 0 0 0 rgba(239, 68, 68, 0);
+ }
+}
+
+// Font size modifiers
+.font-size-small {
+ .message-content,
+ .chat-input input,
+ .follow-up-button,
+ .follow-up-questions h4,
+ .processing-info,
+ .processing-info .dropdown-item,
+ .toggle-info {
+ font-size: $font-size-small !important;
+ }
+}
+
+.font-size-normal {
+ .message-content,
+ .chat-input input,
+ .follow-up-button,
+ .follow-up-questions h4,
+ .processing-info,
+ .processing-info .dropdown-item,
+ .toggle-info {
+ font-size: $font-size-normal !important;
+ }
+}
+
+.font-size-large {
+ .message-content,
+ .chat-input input,
+ .follow-up-button,
+ .follow-up-questions h4,
+ .processing-info,
+ .processing-info .dropdown-item,
+ .toggle-info {
+ font-size: $font-size-large !important;
+ }
+}
+
+.font-size-xlarge {
+ .message-content,
+ .chat-input input,
+ .follow-up-button,
+ .follow-up-questions h4,
+ .processing-info,
+ .processing-info .dropdown-item,
+ .toggle-info {
+ font-size: $font-size-xlarge !important;
+ }
+}
+
.message {
- max-width: 75%;
- padding: 12px 16px;
- border-radius: 12px;
- font-size: 15px;
+ max-width: 80%;
+ padding: 16px;
+ border-radius: 16px;
+ font-size: 14px;
line-height: 1.6;
- box-shadow: 0 1px 3px $shadow-color;
+ box-shadow: 0 2px 8px $shadow-color;
word-wrap: break-word;
display: flex;
flex-direction: column;
+ position: relative;
+ transition:
+ transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94),
+ box-shadow 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
+
+ &:hover {
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ }
&.user {
align-self: flex-end;
- background-color: $primary-color;
- color: #fff;
+ background: $primary-color;
+ color: white;
border-bottom-right-radius: 4px;
+ transform-origin: bottom right;
+ animation: messageInUser 0.3s forwards;
+
+ strong {
+ color: rgba(255, 255, 255, 0.9);
+ }
}
&.assistant {
align-self: flex-start;
- background-color: $secondary-color;
+ background-color: white;
color: $text-color;
border-bottom-left-radius: 4px;
+ border: 1px solid $border-color;
+ transform-origin: bottom left;
+ animation: messageInAssistant 0.3s forwards;
+
+ .message-content {
+ p,
+ li,
+ a {
+ margin: 8px 0;
+
+ &:first-child {
+ margin-top: 0;
+ }
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ pre {
+ background-color: #f3f4f6;
+ padding: 12px;
+ border-radius: 8px;
+ overflow-x: auto;
+ font-size: 13px;
+ border: 1px solid $border-color;
+ }
+
+ code {
+ background-color: #f3f4f6;
+ padding: 2px 5px;
+ border-radius: 4px;
+ font-size: 13px;
+ font-family: monospace;
+ }
+ }
+ }
+
+ @keyframes messageInUser {
+ 0% {
+ opacity: 0;
+ transform: translateY(10px) scale(0.95);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+ }
+
+ @keyframes messageInAssistant {
+ 0% {
+ opacity: 0;
+ transform: translateY(10px) scale(0.95);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+ }
+
+ .processing-info {
+ margin: 0 0 12px 0;
+ padding: 12px 16px;
+ background-color: #f3f4f6;
+ border-radius: 10px;
+ font-size: 14px;
+ transform-origin: top center;
+ animation: fadeInExpand 0.3s forwards;
+
+ .dropdown-item {
+ margin-bottom: 8px;
+ padding-bottom: 8px;
+ border-bottom: 1px dashed #e5e7eb;
+
+ &:last-child {
+ margin-bottom: 0;
+ padding-bottom: 0;
+ border-bottom: none;
+ }
+
+ strong {
+ color: $primary-color;
+ font-weight: 600;
+ }
+ }
+
+ .info-content {
+ margin-top: 12px;
+ max-height: 200px;
+ overflow-y: auto;
+ padding-right: 8px;
+
+ &::-webkit-scrollbar {
+ width: 4px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background-color: rgba($primary-color, 0.1);
+ border-radius: 8px;
+ }
+ }
}
.toggle-info {
- margin-top: 10px;
- background-color: transparent;
+ background-color: rgba($primary-color, 0.05);
color: $primary-color;
- border: 1px solid $primary-color;
+ border: 1px solid rgba($primary-color, 0.3);
border-radius: 8px;
padding: 8px 12px;
- font-size: 14px;
+ font-size: 13px;
+ font-weight: 500;
cursor: pointer;
- transition: $transition;
- margin-bottom: 16px;
+ transition: all 0.2s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
&:hover {
- background-color: color.adjust($primary-color, $alpha: -0.9);
+ background-color: rgba($primary-color, 0.1);
+ border-color: rgba($primary-color, 0.4);
}
- }
- .processing-info {
- margin-bottom: 12px;
- padding: 10px 15px;
- background-color: #f9f9f9;
- border-radius: 8px;
- box-shadow: 0 1px 3px $shadow-color;
- font-size: 14px;
+ &:active {
+ background-color: rgba($primary-color, 0.15);
+ }
- .processing-item {
- margin-bottom: 5px;
- font-size: 14px;
- color: $light-text-color;
+ &:focus {
+ outline: none;
+ box-shadow: 0 0 0 2px rgba($primary-color, 0.2);
}
}
.message-content {
background-color: inherit;
- padding: 10px;
+ padding: 0;
border-radius: 8px;
- font-size: 15px;
- line-height: 1.5;
+ font-size: 14px;
+ line-height: 1.6;
+ color: inherit;
.citation-button {
display: inline-flex;
align-items: center;
justify-content: center;
- width: 22px;
- height: 22px;
+ width: 16px;
+ height: 16px;
border-radius: 50%;
- background-color: rgba(0, 0, 0, 0.1);
- color: $text-color;
- font-size: 12px;
- font-weight: bold;
- margin-left: 5px;
+ background-color: rgba($primary-color, 0.1);
+ color: $primary-color;
+ font-size: 10px;
+ font-weight: 600;
+ margin-left: 3px;
cursor: pointer;
+ transition: all 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
+ border: 1px solid rgba($primary-color, 0.2);
+ vertical-align: super;
+
+ &:hover {
+ background-color: $primary-color;
+ color: white;
+ transform: scale(1.1);
+ box-shadow: 0 2px 6px rgba($primary-color, 0.4);
+ }
+
+ &:active {
+ transform: scale(0.95);
+ }
+ }
+
+ a {
+ color: $primary-color;
+ text-decoration: none;
transition: $transition;
+ border-bottom: 1px dashed rgba($primary-color, 0.3);
&:hover {
- background-color: color.adjust($primary-color, $alpha: -0.8);
- color: #fff;
+ border-bottom: 1px solid $primary-color;
}
}
}
}
.follow-up-questions {
- margin-top: 12px;
+ margin-top: 14px;
+ background-color: rgba($primary-color, 0.05);
+ padding: 14px;
+ border-radius: 10px;
+ border: 1px solid rgba($primary-color, 0.1);
+ animation: fadeInUp 0.4s forwards;
+ transition: box-shadow 0.2s ease;
+
+ &:hover {
+ box-shadow: 0 4px 12px rgba($primary-color, 0.08);
+ }
h4 {
- font-size: 15px;
+ font-size: 13px;
font-weight: 600;
- margin-bottom: 8px;
+ margin: 0 0 10px 0;
+ color: $primary-color;
+ letter-spacing: 0.02em;
}
.questions-list {
@@ -244,20 +633,52 @@ $transition: all 0.2s ease-in-out;
}
.follow-up-button {
- background-color: #fff;
- color: $primary-color;
- border: 1px solid $primary-color;
+ background-color: white;
+ color: $text-color;
+ border: 1px solid rgba($primary-color, 0.2);
border-radius: 8px;
padding: 10px 14px;
- font-size: 14px;
+ font-size: 13px;
+ font-weight: 500;
cursor: pointer;
- transition: $transition;
+ transition: all 0.2s ease;
text-align: left;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+ position: relative;
+ overflow: hidden;
+ text-transform: none !important; /* Force no text transform */
&:hover {
- background-color: $primary-color;
- color: #fff;
+ background-color: $primary-light;
+ border-color: rgba($primary-color, 0.3);
+ box-shadow: 0 2px 4px rgba($primary-color, 0.1);
}
+
+ &:active {
+ background-color: darken($primary-light, 3%);
+ }
+ }
+}
+
+@keyframes fadeInUp {
+ 0% {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes fadeInExpand {
+ 0% {
+ opacity: 0;
+ transform: scaleY(0.9);
+ }
+ 100% {
+ opacity: 1;
+ transform: scaleY(1);
}
}
@@ -267,11 +688,55 @@ $transition: all 0.2s ease-in-out;
left: 0;
right: 0;
bottom: 0;
- background-color: rgba(255, 255, 255, 0.8);
+ background-color: rgba(255, 255, 255, 0.92);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
+ backdrop-filter: blur(4px);
+ animation: fadeIn 0.3s ease;
+
+ .progress-container {
+ width: 80%;
+ max-width: 400px;
+ background-color: white;
+ padding: 24px;
+ border-radius: 12px;
+ box-shadow: 0 10px 40px rgba($primary-color, 0.2);
+ animation: scaleIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
+
+ .progress-bar-wrapper {
+ height: 8px;
+ background-color: #f3f4f6;
+ border-radius: 4px;
+ margin-bottom: 16px;
+ overflow: hidden;
+
+ .progress-bar {
+ height: 100%;
+ background: linear-gradient(90deg, $primary-color, $accent-color);
+ border-radius: 4px;
+ transition: width 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
+ }
+ }
+
+ .progress-details {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .progress-percentage {
+ font-weight: 600;
+ color: $primary-color;
+ font-size: 16px;
+ }
+
+ .step-name {
+ color: $light-text-color;
+ font-size: 14px;
+ }
+ }
+ }
}
@keyframes spin {
@@ -283,12 +748,159 @@ $transition: all 0.2s ease-in-out;
}
}
+@keyframes scaleIn {
+ 0% {
+ transform: scale(0.9);
+ opacity: 0;
+ }
+ 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+}
+
@media (max-width: 768px) {
.chat-box {
border-radius: 0;
}
.message {
- max-width: 90%;
+ max-width: 88%;
+ padding: 14px;
+ }
+
+ .chat-input {
+ padding: 12px;
+ }
+}
+
+// Responsive scaling
+@media (max-width: 480px) {
+ .chat-box .chat-input input {
+ font-size: 13px;
+ padding: 12px 14px;
+ }
+
+ .message {
+ max-width: 95%;
+ padding: 12px;
+ font-size: 13px;
+ }
+
+ .follow-up-questions {
+ padding: 12px;
+ }
+}
+
+// Dark mode support
+.dark-mode .chat-box {
+ background-color: #1f2937;
+
+ .chat-header {
+ background: $primary-color;
+
+ .font-size-control {
+ background-color: rgba(255, 255, 255, 0.2);
+
+ &:hover {
+ background-color: rgba(255, 255, 255, 0.3);
+ }
+ }
+
+ .font-size-modal {
+ background-color: #1f2937;
+ border: 1px solid #374151;
+
+ .font-size-option {
+ color: #f9fafb;
+
+ &:hover {
+ background-color: #2d3748;
+ }
+
+ &.active {
+ background-color: rgba($primary-color, 0.2);
+ }
+ }
+ }
+ }
+
+ .chat-messages {
+ background-color: #111827;
+ background-image: radial-gradient(#374151 1px, transparent 1px), radial-gradient(#374151 1px, transparent 1px);
+ }
+
+ .chat-input {
+ background-color: #1f2937;
+ border-top-color: #374151;
+
+ .input-container {
+ background-color: #374151;
+ border-color: #4b5563;
+
+ &:focus-within {
+ background-color: #2d3748;
+ border-color: $primary-color;
+ }
+
+ input {
+ color: white;
+
+ &::placeholder {
+ color: #9ca3af;
+ }
+ }
+ }
+ }
+
+ .message {
+ &.assistant {
+ background-color: #1f2937;
+ border-color: #374151;
+ color: #f9fafb;
+
+ .message-content {
+ pre,
+ code {
+ background-color: #111827;
+ border-color: #374151;
+ }
+ }
+ }
+
+ .processing-info {
+ background-color: #111827;
+
+ .dropdown-item {
+ border-color: #374151;
+ }
+ }
+ }
+
+ .follow-up-questions {
+ background-color: rgba($primary-color, 0.1);
+ border-color: rgba($primary-color, 0.2);
+
+ .follow-up-button {
+ background-color: #1f2937;
+ color: #f9fafb;
+ border-color: #4b5563;
+
+ &:hover {
+ background-color: #2d3748;
+ }
+ }
+ }
+
+ .uploading-overlay {
+ background-color: rgba(31, 41, 55, 0.9);
+
+ .progress-container {
+ background-color: #1f2937;
+
+ .progress-bar-wrapper {
+ background-color: #111827;
+ }
+ }
}
}
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
index ba30cb42b..490739be6 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
@@ -40,7 +40,6 @@ import { ASSISTANT_ROLE, AssistantMessage, CHUNK_TYPE, Citation, ProcessingInfo,
import { Vectorstore } from '../vectorstore/Vectorstore';
import './ChatBox.scss';
import MessageComponentBox from './MessageComponent';
-import { ProgressBar } from './ProgressBar';
import { OpenWhere } from '../../OpenWhere';
import { Upload } from '../../../../../server/SharedMediaTypes';
import { DocumentMetadataTool } from '../tools/DocumentMetadataTool';
@@ -76,6 +75,8 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@observable private _linked_csv_files: { filename: string; id: string; text: string }[] = [];
@observable private _isUploadingDocs: boolean = false;
@observable private _citationPopup: { text: string; visible: boolean } = { text: '', visible: false };
+ @observable private _isFontSizeModalOpen: boolean = false;
+ @observable private _fontSize: 'small' | 'normal' | 'large' | 'xlarge' = 'normal';
// Private properties for managing OpenAI API, vector store, agent, and UI elements
private openai!: OpenAI; // Using definite assignment assertion
@@ -147,6 +148,9 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this.dataDoc.data = JSON.stringify(serializableHistory);
}
);
+
+ // Initialize font size from saved preference
+ this.initFontSize();
}
/**
@@ -1074,12 +1078,61 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
};
_dictation: DictationButton | null = null;
+
+ /**
+ * Toggles the font size modal visibility
+ */
+ @action
+ toggleFontSizeModal = () => {
+ this._isFontSizeModalOpen = !this._isFontSizeModalOpen;
+ };
+
+ /**
+ * Changes the font size and applies it to the chat interface
+ * @param size The new font size to apply
+ */
+ @action
+ changeFontSize = (size: 'small' | 'normal' | 'large' | 'xlarge') => {
+ this._fontSize = size;
+ this._isFontSizeModalOpen = false;
+
+ // Save preference to localStorage if needed
+ if (typeof window !== 'undefined') {
+ localStorage.setItem('chatbox-font-size', size);
+ }
+ };
+
+ /**
+ * Initializes font size from saved preference
+ */
+ initFontSize = () => {
+ if (typeof window !== 'undefined') {
+ const savedSize = localStorage.getItem('chatbox-font-size');
+ if (savedSize && ['small', 'normal', 'large', 'xlarge'].includes(savedSize)) {
+ this._fontSize = savedSize as 'small' | 'normal' | 'large' | 'xlarge';
+ }
+ }
+ };
+
+ /**
+ * Renders a font size icon SVG
+ */
+ renderFontSizeIcon = () => (
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+ <polyline points="4 7 4 4 20 4 20 7"></polyline>
+ <line x1="9" y1="20" x2="15" y2="20"></line>
+ <line x1="12" y1="4" x2="12" y2="20"></line>
+ </svg>
+ );
+
/**
* Renders the chat interface, including the message list, input field, and other UI elements.
*/
render() {
+ const fontSizeClass = `font-size-${this._fontSize}`;
+
return (
- <div className="chat-box">
+ <div className={`chat-box ${fontSizeClass}`}>
{this._isUploadingDocs && (
<div className="uploading-overlay">
<div className="progress-container">
@@ -1095,6 +1148,29 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
)}
<div className="chat-header">
<h2>{this.userName()}&apos;s AI Assistant</h2>
+ <div className="font-size-control" onClick={this.toggleFontSizeModal}>
+ {this.renderFontSizeIcon()}
+ </div>
+ {this._isFontSizeModalOpen && (
+ <div className="font-size-modal">
+ <div className={`font-size-option ${this._fontSize === 'small' ? 'active' : ''}`} onClick={() => this.changeFontSize('small')}>
+ <span className="option-label">Small</span>
+ <span className="size-preview small">Aa</span>
+ </div>
+ <div className={`font-size-option ${this._fontSize === 'normal' ? 'active' : ''}`} onClick={() => this.changeFontSize('normal')}>
+ <span className="option-label">Normal</span>
+ <span className="size-preview normal">Aa</span>
+ </div>
+ <div className={`font-size-option ${this._fontSize === 'large' ? 'active' : ''}`} onClick={() => this.changeFontSize('large')}>
+ <span className="option-label">Large</span>
+ <span className="size-preview large">Aa</span>
+ </div>
+ <div className={`font-size-option ${this._fontSize === 'xlarge' ? 'active' : ''}`} onClick={() => this.changeFontSize('xlarge')}>
+ <span className="option-label">Extra Large</span>
+ <span className="size-preview xlarge">Aa</span>
+ </div>
+ </div>
+ )}
</div>
<div className="chat-messages" ref={this.messagesRef}>
{this._history.map((message, index) => (
@@ -1106,18 +1182,20 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
</div>
<form onSubmit={this.askGPT} className="chat-input">
- <input
- ref={r => {
- this._textInputRef = r;
- }}
- type="text"
- name="messageInput"
- autoComplete="off"
- placeholder="Type your message here..."
- value={this._inputValue}
- onChange={action(e => (this._inputValue = e.target.value))}
- disabled={this._isLoading}
- />
+ <div className="input-container">
+ <input
+ ref={r => {
+ this._textInputRef = r;
+ }}
+ type="text"
+ name="messageInput"
+ autoComplete="off"
+ placeholder="Type your message here..."
+ value={this._inputValue}
+ onChange={action(e => (this._inputValue = e.target.value))}
+ disabled={this._isLoading}
+ />
+ </div>
<button className="submit-button" onClick={() => this._dictation?.stopDictation()} type="submit" disabled={this._isLoading || !this._inputValue.trim()}>
{this._isLoading ? (
<div className="spinner"></div>
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/MessageComponent.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/MessageComponent.tsx
index 4f1d68973..c7699b57f 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/MessageComponent.tsx
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/MessageComponent.tsx
@@ -86,7 +86,6 @@ const MessageComponentBox: React.FC<MessageComponentProps> = ({ message, onFollo
}
// Handle query type content
- // bcz: What triggers this section? Where is 'query' added to item? Why isn't it a field?
else if ('query' in item) {
return (
<span key={i} className="query-text">
@@ -99,7 +98,7 @@ const MessageComponentBox: React.FC<MessageComponentProps> = ({ message, onFollo
else {
return (
<span key={i}>
- <ReactMarkdown>{item.text /* JSON.stringify(item)*/}</ReactMarkdown>
+ <ReactMarkdown>{item.text}</ReactMarkdown>
</span>
);
}
@@ -130,6 +129,18 @@ const MessageComponentBox: React.FC<MessageComponentProps> = ({ message, onFollo
return null;
};
+ /**
+ * Formats the follow-up question text to ensure proper capitalization
+ * @param {string} question - The original question text
+ * @returns {string} The formatted question
+ */
+ const formatFollowUpQuestion = (question: string) => {
+ // Only capitalize first letter if needed and preserve the rest
+ if (!question) return '';
+ const formattedQuestion = question.charAt(0).toUpperCase() + question.slice(1).toLowerCase();
+ return formattedQuestion;
+ };
+
return (
<div className={`message ${message.role}`}>
{/* Processing Information Dropdown */}
@@ -139,7 +150,6 @@ const MessageComponentBox: React.FC<MessageComponentProps> = ({ message, onFollo
{dropdownOpen ? 'Hide Agent Thoughts/Actions' : 'Show Agent Thoughts/Actions'}
</button>
{dropdownOpen && <div className="info-content">{message.processing_info.map(renderProcessingInfo)}</div>}
- <br />
</div>
)}
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ProgressBar.scss b/src/client/views/nodes/chatbot/chatboxcomponents/ProgressBar.scss
deleted file mode 100644
index 3a8334695..000000000
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ProgressBar.scss
+++ /dev/null
@@ -1,105 +0,0 @@
-.spinner-container {
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- height: 100%;
-}
-
-.spinner {
- width: 60px;
- height: 60px;
- position: relative;
- margin-bottom: 20px; // Space between spinner and text
-}
-
-.double-bounce1,
-.double-bounce2 {
- width: 100%;
- height: 100%;
- border-radius: 50%;
- background-color: #4a90e2;
- opacity: 0.6;
- position: absolute;
- top: 0;
- left: 0;
- animation: bounce 2s infinite ease-in-out;
-}
-
-.double-bounce2 {
- animation-delay: -1s;
-}
-
-@keyframes bounce {
- 0%,
- 100% {
- transform: scale(0);
- }
- 50% {
- transform: scale(1);
- }
-}
-
-.uploading-overlay {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: rgba(255, 255, 255, 0.8);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 1000;
-}
-
-.progress-container {
- display: flex;
- flex-direction: column;
- align-items: center;
- text-align: center;
- width: 80%;
- max-width: 400px;
- background-color: white;
- padding: 20px;
- border-radius: 8px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
-}
-
-.progress-bar-wrapper {
- width: 100%;
- height: 12px;
- background-color: #e0e0e0;
- border-radius: 6px;
- overflow: hidden;
- margin-bottom: 10px;
-}
-
-.progress-bar {
- height: 100%;
- background-color: #4a90e2;
- border-radius: 6px;
- transition: width 0.5s ease;
-}
-
-.progress-details {
- display: flex;
- flex-direction: column;
- align-items: center;
- width: 100%;
-}
-
-.progress-percentage {
- font-size: 18px;
- font-weight: bold;
- color: #333;
- margin-bottom: 5px;
-}
-
-.step-name {
- font-size: 16px;
- color: #666;
- text-align: center;
- width: 100%;
- margin-top: 5px;
-}
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ProgressBar.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ProgressBar.tsx
deleted file mode 100644
index 240862f8b..000000000
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ProgressBar.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * @file ProgressBar.tsx
- * @description This file defines the ProgressBar component, which displays a loading spinner
- * to indicate progress during ongoing tasks or processing. The animation consists of two
- * bouncing elements that create a pulsating effect, providing a visual cue for active progress.
- * The component is styled using the accompanying `ProgressBar.scss` for smooth animation.
- */
-
-import React from 'react';
-import './ProgressBar.scss';
-
-/**
- * ProgressBar is a functional React component that displays a loading spinner
- * to indicate progress or ongoing processing. It uses two bouncing elements
- * to create a smooth animation that represents an active state.
- *
- * The animation consists of two divs (`double-bounce1` and `double-bounce2`),
- * each of which will bounce in and out of view, creating a pulsating effect.
- */
-export const ProgressBar: React.FC = () => {
- return (
- <div className="spinner-container">
- {/* Spinner div containing two bouncing elements */}
- <div className="spinner">
- <div className="double-bounce1"></div> {/* First bouncing element */}
- <div className="double-bounce2"></div> {/* Second bouncing element */}
- </div>
- </div>
- );
-};