diff options
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()}'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> - ); -}; |