aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/chatbot/chatboxcomponents
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/chatbot/chatboxcomponents')
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss999
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx988
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/MessageComponent.tsx16
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/ProgressBar.scss69
-rw-r--r--src/client/views/nodes/chatbot/chatboxcomponents/ProgressBar.tsx30
5 files changed, 1651 insertions, 451 deletions
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss
index 4db5cec3d..0bacc70c2 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.scss
@@ -1,240 +1,652 @@
@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: 0px;
- 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: 0px;
+ }
+
+ &: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;
+ z-index: 5;
+ transition: padding 0.2s ease;
- input {
+ &::before {
+ content: '';
+ position: absolute;
+ top: -5px;
+ left: 0px;
+ right: 0px;
+ height: 5px;
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.06), transparent);
+ pointer-events: none;
+ }
+
+ .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;
+ top: 50%;
left: 50%;
- transform: translateX(-50%);
- background-color: rgba(0, 0, 0, 0.8);
- color: white;
- padding: 10px 20px;
- border-radius: 10px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+ transform: translate(-50%, -50%);
+ width: 90%;
+ max-width: 500px;
+ max-height: 300px;
+ border-radius: 8px;
+ background-color: white;
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.25);
z-index: 1000;
- animation: fadeIn 0.3s ease-in-out;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ animation: popup-fade-in 0.3s ease-out;
+ }
- p {
- margin: 0px;
- font-size: 14px;
+ .citation-popup-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 15px;
+ background-color: #f5f5f5;
+ border-bottom: 1px solid #ddd;
+ }
+
+ .citation-content {
+ padding: 15px;
+ overflow-y: auto;
+ max-height: 240px;
+ }
+
+ .citation-close-button {
+ background: none;
+ border: none;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ color: #666;
+ transition: background-color 0.2s;
+
+ &:hover {
+ background-color: #ddd;
}
- @keyframes fadeIn {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
+ svg {
+ width: 20px;
+ height: 20px;
+ stroke-width: 2.5;
}
}
}
+@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: 0px;
+ }
+
+ &:last-child {
+ margin-bottom: 0px;
+ }
+ }
+
+ 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: 0px;
+ padding-bottom: 0px;
+ 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: 0px;
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,34 +656,110 @@ $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: color.adjust($primary-light, $lightness: -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);
+ }
+}
+
.uploading-overlay {
position: absolute;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
- 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 +771,301 @@ $transition: all 0.2s ease-in-out;
}
}
+@keyframes scaleIn {
+ 0% {
+ transform: scale(0.9);
+ opacity: 0;
+ }
+ 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+}
+
+@keyframes popup-slide-up {
+ from {
+ opacity: 0;
+ transform: translate(-50%, 20px);
+ }
+ to {
+ opacity: 1;
+ transform: translate(-50%, 0);
+ }
+}
+
+@keyframes popup-fade-in {
+ from {
+ opacity: 0;
+ transform: translate(-50%, -45%);
+ }
+ to {
+ opacity: 1;
+ transform: translate(-50%, -50%);
+ }
+}
+
@media (max-width: 768px) {
.chat-box {
border-radius: 0px;
}
.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;
+ }
+ }
+ }
+}
+
+/* Tool Reload Modal Styles */
+.tool-reload-modal-overlay {
+ position: fixed;
+ top: 0px;
+ left: 0px;
+ right: 0px;
+ bottom: 0px;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 10000;
+ backdrop-filter: blur(4px);
+}
+
+.tool-reload-modal {
+ background: white;
+ border-radius: 12px;
+ padding: 0px;
+ min-width: 400px;
+ max-width: 500px;
+ box-shadow:
+ 0 20px 25px -5px rgba(0, 0, 0, 0.1),
+ 0 10px 10px -5px rgba(0, 0, 0, 0.04);
+ border: 1px solid #e2e8f0;
+ animation: modalSlideIn 0.3s ease-out;
+}
+
+@keyframes modalSlideIn {
+ from {
+ opacity: 0;
+ transform: scale(0.95) translateY(-20px);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+}
+
+.tool-reload-modal-header {
+ padding: 24px 24px 16px 24px;
+ border-bottom: 1px solid #e2e8f0;
+
+ h3 {
+ margin: 0px;
+ font-size: 18px;
+ font-weight: 600;
+ color: #1a202c;
+ display: flex;
+ align-items: center;
+
+ &::before {
+ content: '🛠️';
+ margin-right: 8px;
+ font-size: 20px;
+ }
+ }
+}
+
+.tool-reload-modal-content {
+ padding: 20px 24px;
+
+ p {
+ margin: 0 0 12px 0;
+ line-height: 1.5;
+ color: #4a5568;
+
+ &:last-child {
+ margin-bottom: 0px;
+ }
+
+ strong {
+ color: #2d3748;
+ font-weight: 600;
+ }
+ }
+}
+
+.tool-reload-modal-actions {
+ padding: 16px 24px 24px 24px;
+ display: flex;
+ gap: 12px;
+ justify-content: flex-end;
+
+ button {
+ padding: 10px 20px;
+ border-radius: 6px;
+ font-weight: 500;
+ font-size: 14px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ border: none;
+
+ &.primary {
+ background: #3182ce;
+ color: white;
+
+ &:hover {
+ background: #2c5aa0;
+ transform: translateY(-1px);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+ }
+
+ &.secondary {
+ background: #f7fafc;
+ color: #4a5568;
+ border: 1px solid #e2e8f0;
+
+ &:hover {
+ background: #edf2f7;
+ border-color: #cbd5e0;
+ }
+ }
}
}
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
index 15b148372..732c4d637 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
@@ -15,13 +15,15 @@ import * as React from 'react';
import { v4 as uuidv4 } from 'uuid';
import { ClientUtils, OmitKeys } from '../../../../../ClientUtils';
import { Doc, DocListCast, Opt } from '../../../../../fields/Doc';
-import { DocData, DocViews } from '../../../../../fields/DocSymbols';
+import { DocData, DocLayout, DocViews } from '../../../../../fields/DocSymbols';
+import { Id } from '../../../../../fields/FieldSymbols';
import { RichTextField } from '../../../../../fields/RichTextField';
import { ScriptField } from '../../../../../fields/ScriptField';
-import { CsvCast, DocCast, NumCast, PDFCast, RTFCast, StrCast } from '../../../../../fields/Types';
+import { CsvCast, DocCast, NumCast, PDFCast, RTFCast, StrCast, VideoCast, AudioCast } from '../../../../../fields/Types';
import { DocUtils } from '../../../../documents/DocUtils';
import { CollectionViewType, DocumentType } from '../../../../documents/DocumentTypes';
import { Docs, DocumentOptions } from '../../../../documents/Documents';
+import { DocServer } from '../../../../DocServer';
import { DocumentManager } from '../../../../util/DocumentManager';
import { ImageUtils } from '../../../../util/Import & Export/ImageUtils';
import { LinkManager } from '../../../../util/LinkManager';
@@ -35,18 +37,29 @@ import { PDFBox } from '../../PDFBox';
import { ScriptingBox } from '../../ScriptingBox';
import { VideoBox } from '../../VideoBox';
import { Agent } from '../agentsystem/Agent';
-import { supportedDocTypes } from '../tools/CreateDocumentTool';
+import { supportedDocTypes } from '../types/tool_types';
import { ASSISTANT_ROLE, AssistantMessage, CHUNK_TYPE, Citation, ProcessingInfo, SimplifiedChunk, TEXT_TYPE } from '../types/types';
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';
+import { AgentDocumentManager } from '../utils/AgentDocumentManager';
+import { AiOutlineSend } from 'react-icons/ai';
+import { SnappingManager } from '../../../../util/SnappingManager';
+import { Button, Size, Type } from '@dash/components';
dotenv.config();
-export type parsedDocData = { doc_type: string; data: unknown };
+export type parsedDocData = {
+ doc_type: string;
+ data: unknown;
+ _disable_resource_loading?: boolean;
+ _sandbox_iframe?: boolean;
+ _iframe_sandbox?: string;
+ data_useCors?: boolean;
+};
export type parsedDoc = DocumentOptions & parsedDocData;
/**
* ChatBox is the main class responsible for managing the interaction between the user and the assistant,
@@ -67,14 +80,18 @@ 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';
+ @observable private _toolReloadModal: { visible: boolean; toolName: string } = { visible: false, toolName: '' };
// Private properties for managing OpenAI API, vector store, agent, and UI elements
- private openai: OpenAI;
+ private openai!: OpenAI; // Using definite assignment assertion
private vectorstore_id: string;
private vectorstore: Vectorstore;
private agent: Agent;
private messagesRef: React.RefObject<HTMLDivElement>;
private _textInputRef: HTMLInputElement | undefined | null;
+ private docManager: AgentDocumentManager;
/**
* Static method that returns the layout string for the field.
@@ -95,19 +112,34 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
constructor(props: FieldViewProps) {
super(props);
- makeObservable(this); // Enable MobX observables
+ makeObservable(this);
- // Initialize OpenAI, vectorstore, and agent
- this.openai = this.initializeOpenAI();
- if (StrCast(this.dataDoc.vectorstore_id) == '') {
- this.vectorstore_id = uuidv4();
- this.dataDoc.vectorstore_id = this.vectorstore_id;
- } else {
- this.vectorstore_id = StrCast(this.dataDoc.vectorstore_id);
+ // At mount time, find the DocumentView whose .Document is the collection container.
+ const parentView = DocumentView.Selected().lastElement();
+ if (!parentView) {
+ console.warn('GPT ChatBox not inside a DocumentView – cannot sort.');
}
- this.vectorstore = new Vectorstore(this.vectorstore_id, this.retrieveDocIds);
- this.agent = new Agent(this.vectorstore, this.retrieveSummaries, this.retrieveFormattedHistory, this.retrieveCSVData, this.addLinkedUrlDoc, this.createImageInDash, this.createDocInDash, this.createCSVInDash);
- this.messagesRef = React.createRef<HTMLDivElement>();
+
+ this.messagesRef = React.createRef();
+ this.docManager = new AgentDocumentManager(this, parentView);
+
+ // Initialize OpenAI client
+ this.initializeOpenAI();
+
+ // Create a unique vectorstore ID for this ChatBox
+ this.vectorstore_id = uuidv4();
+
+ // Initialize vectorstore with the document manager
+ this.vectorstore = new Vectorstore(this.vectorstore_id, this.docManager);
+
+ // Create an agent with the vectorstore
+ this.agent = new Agent(this.vectorstore, this.retrieveFormattedHistory.bind(this), this.retrieveCSVData.bind(this), this.createImageInDash.bind(this), this.createCSVInDash.bind(this), this.docManager);
+
+ // Set up the tool created callback
+ this.agent.setToolCreatedCallback(this.handleToolCreated);
+
+ // Add event listeners
+ this.addScrollListener();
// Reaction to update dataDoc when chat history changes
reaction(
@@ -122,6 +154,28 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this.dataDoc.data = JSON.stringify(serializableHistory);
}
);
+
+ /*
+ reaction(
+ () => ({ selDoc: DocumentView.Selected().lastElement(), visible: SnappingManager.ChatVisible }),
+ ({ selDoc, visible }) => {
+ const hasChildDocs = visible && selDoc?.ComponentView?.hasChildDocs;
+ if (hasChildDocs) {
+ this._textToDocMap.clear();
+ this.setCollectionContext(selDoc.Document);
+ this.onGptResponse = (sortResult: string, questionType: GPTDocCommand) => this.processGptResponse(selDoc, this._textToDocMap, sortResult, questionType);
+ this.onQuizRandom = () => this.randomlyChooseDoc(selDoc.Document, hasChildDocs());
+ this._documentDescriptions = Promise.all(hasChildDocs().map(doc =>
+ Doc.getDescription(doc).then(text => this._textToDocMap.set(text.replace(/\n/g, ' ').trim(), doc) && `${DescriptionSeperator}${text}${DescriptionSeperator}`)
+ )).then(docDescriptions => docDescriptions.join()); // prettier-ignore
+ }
+ },
+ { fireImmediately: true }
+ );
+ }*/
+
+ // Initialize font size from saved preference
+ this.initFontSize();
}
/**
@@ -131,22 +185,53 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
@action
addDocToVectorstore = async (newLinkedDoc: Doc) => {
- this._uploadProgress = 0;
- this._currentStep = 'Initializing...';
- this._isUploadingDocs = true;
-
try {
- // Add the document to the vectorstore
+ const isAudioOrVideo = VideoCast(newLinkedDoc.data)?.url?.pathname || AudioCast(newLinkedDoc.data)?.url?.pathname;
+
+ // Set UI state to show the processing overlay
+ runInAction(() => {
+ this._isUploadingDocs = true;
+ this._uploadProgress = 0;
+ this._currentStep = isAudioOrVideo ? 'Preparing media file...' : 'Processing document...';
+ });
+
+ // Process the document first to ensure it has a valid ID
+ await this.docManager.processDocument(newLinkedDoc);
+
+ // Add the document to the vectorstore which will also register chunks
await this.vectorstore.addAIDoc(newLinkedDoc, this.updateProgress);
- } catch (error) {
- console.error('Error uploading document:', error);
- this._currentStep = 'Error during upload';
- } finally {
+
+ // Give a slight delay to show the completion message
+ if (this._uploadProgress === 100) {
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ }
+
+ // Reset UI state
+ runInAction(() => {
+ this._isUploadingDocs = false;
+ this._uploadProgress = 0;
+ this._currentStep = '';
+ });
+
+ return true;
+ } catch (err) {
+ console.error('Error adding document to vectorstore:', err);
+
+ // Show error in UI
+ runInAction(() => {
+ this._currentStep = `Error: ${err instanceof Error ? err.message : 'Failed to process document'}`;
+ });
+
+ await new Promise(resolve => setTimeout(resolve, 2000));
+
+ // Reset UI state
runInAction(() => {
this._isUploadingDocs = false;
this._uploadProgress = 0;
this._currentStep = '';
});
+
+ return false;
}
};
@@ -157,10 +242,18 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
@action
updateProgress = (progress: number, step: string) => {
- this._uploadProgress = progress;
+ // Ensure progress is within expected bounds
+ const validProgress = Math.min(Math.max(0, progress), 100);
+ this._uploadProgress = validProgress;
this._currentStep = step;
+
+ // Force UI update
+ if (process.env.NODE_ENV !== 'production') {
+ console.log(`Progress: ${validProgress}%, Step: ${step}`);
+ }
};
+ //TODO: Update for new chunk_simpl on agentDocument
/**
* Adds a CSV file for analysis by sending it to OpenAI and generating a summary.
* @param newLinkedDoc The linked document representing the CSV file.
@@ -229,7 +322,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
apiKey: process.env.OPENAI_KEY,
dangerouslyAllowBrowser: true,
};
- return new OpenAI(configuration);
+ this.openai = new OpenAI(configuration);
}
/**
@@ -276,15 +369,19 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@action
askGPT = async (event: React.FormEvent): Promise<void> => {
event.preventDefault();
+ if (!this._textInputRef) {
+ console.log('ERROR: text input ref is undefined');
+ return;
+ }
this._inputValue = '';
// Extract the user's message
- const textInput = (event.currentTarget as HTMLFormElement).elements.namedItem('messageInput') as HTMLInputElement;
- const trimmedText = textInput.value.trim();
+ const textInput = this._textInputRef?.value ?? '';
+ const trimmedText = textInput.trim();
if (trimmedText) {
+ this._textInputRef.value = ''; // Clear the input field
try {
- textInput.value = '';
// Add the user's message to the history
this._history.push({
role: ASSISTANT_ROLE.USER,
@@ -367,27 +464,6 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
};
/**
- * Adds a linked document from a URL for future reference and analysis.
- * @param url The URL of the document to add.
- * @param id The unique identifier for the document.
- */
- @action
- addLinkedUrlDoc = async (url: string, id: string) => {
- const doc = Docs.Create.WebDocument(url, { data_useCors: true });
-
- const linkDoc = Docs.Create.LinkDocument(this.Document, doc);
- LinkManager.Instance.addLink(linkDoc);
-
- const chunkToAdd = {
- chunkId: id,
- chunkType: CHUNK_TYPE.URL,
- url: url,
- };
-
- doc.chunk_simpl = JSON.stringify({ chunks: [chunkToAdd] });
- };
-
- /**
* Getter to retrieve the current user's name from the client utils.
*/
@computed
@@ -408,7 +484,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
if (doc) {
LinkManager.Instance.addLink(Docs.Create.LinkDocument(this.Document, doc));
this._props.addDocument?.(doc);
- DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {}).then(() => this.addCSVForAnalysis(doc, id));
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => this.addCSVForAnalysis(doc, id));
}
});
@@ -440,21 +516,33 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
private createCollectionWithChildren = (data: parsedDoc[], insideCol: boolean): Opt<Doc>[] => data.map(doc => this.whichDoc(doc, insideCol));
@action
- whichDoc = (doc: parsedDoc, insideCol: boolean): Opt<Doc> => {
- const options = OmitKeys(doc, ['doct_type', 'data']).omit as DocumentOptions;
+ public whichDoc = (doc: parsedDoc, insideCol: boolean): Opt<Doc> => {
+ const options = OmitKeys(doc, ['doc_type', 'data']).omit as DocumentOptions;
const data = (doc as parsedDocData).data;
const ndoc = (() => {
switch (doc.doc_type) {
default:
- case supportedDocTypes.text: return Docs.Create.TextDocument(data as string, options);
+ case supportedDocTypes.note: return Docs.Create.TextDocument(data as string, options);
case supportedDocTypes.comparison: return this.createComparison(JSON.parse(data as string) as parsedDoc[], options);
case supportedDocTypes.flashcard: return this.createFlashcard(JSON.parse(data as string) as parsedDoc[], options);
case supportedDocTypes.deck: return this.createDeck(JSON.parse(data as string) as parsedDoc[], options);
case supportedDocTypes.image: return Docs.Create.ImageDocument(data as string, options);
case supportedDocTypes.equation: return Docs.Create.EquationDocument(data as string, options);
case supportedDocTypes.notetaking: return Docs.Create.NoteTakingDocument([], options);
- case supportedDocTypes.web: return Docs.Create.WebDocument(data as string, { ...options, data_useCors: true });
- case supportedDocTypes.dataviz: return Docs.Create.DataVizDocument('/users/rz/Downloads/addresses.csv', options);
+ case supportedDocTypes.web:
+ // Create web document with enhanced safety options
+ const webOptions = {
+ ...options,
+ data_useCors: true
+ };
+
+ // If iframe_sandbox was passed from AgentDocumentManager, add it to the options
+ if ('_iframe_sandbox' in options) {
+ (webOptions as any)._iframe_sandbox = options._iframe_sandbox;
+ }
+
+ return Docs.Create.WebDocument(data as string, webOptions);
+ case supportedDocTypes.dataviz: case supportedDocTypes.table: return Docs.Create.DataVizDocument('/Users/ajshul/Dash-Web/src/server/public/files/csv/0d237e7c-98c9-44d0-aa61-5285fdbcf96c-random_sample.csv.csv', options);
case supportedDocTypes.pdf: return Docs.Create.PdfDocument(data as string, options);
case supportedDocTypes.video: return Docs.Create.VideoDocument(data as string, options);
case supportedDocTypes.diagram: return Docs.Create.DiagramDocument(undefined, { text: data as unknown as RichTextField, ...options}); // text: can take a string or RichTextField but it's typed for RichTextField.
@@ -510,28 +598,6 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
};
/**
- * Creates a document in the dashboard.
- *
- * @param {string} doc_type - The type of document to create.
- * @param {string} data - The data used to generate the document.
- * @param {DocumentOptions} options - Configuration options for the document.
- * @returns {Promise<void>} A promise that resolves once the document is created and displayed.
- */
- @action
- createDocInDash = (pdoc: parsedDoc) => {
- const linkAndShowDoc = (doc: Opt<Doc>) => {
- if (doc) {
- LinkManager.Instance.addLink(Docs.Create.LinkDocument(this.Document, doc));
- this._props.addDocument?.(doc);
- DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {});
- }
- };
- const doc = this.whichDoc(pdoc, false);
- if (doc) linkAndShowDoc(doc);
- return doc;
- };
-
- /**
* Creates a deck of flashcards.
*
* @param {any} data - The data used to generate the flashcards. Can be a string or an object.
@@ -604,83 +670,137 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
@action
handleCitationClick = async (citation: Citation) => {
- const currentLinkedDocs: Doc[] = this.linkedDocs;
- const chunkId = citation.chunk_id;
+ try {
+ // Extract values from MobX proxy object if needed
+ const chunkId = typeof citation.chunk_id === 'object' ? (citation.chunk_id as any).toString() : citation.chunk_id;
- for (const doc of currentLinkedDocs) {
- if (doc.chunk_simpl) {
- const docChunkSimpl = JSON.parse(StrCast(doc.chunk_simpl)) as { chunks: SimplifiedChunk[] };
- const foundChunk = docChunkSimpl.chunks.find(chunk => chunk.chunkId === chunkId);
+ // For debugging
+ console.log('Citation clicked:', {
+ chunkId,
+ citation: JSON.stringify(citation, null, 2),
+ });
- if (foundChunk) {
- // Handle media chunks specifically
+ // Get the simplified chunk using the document manager
+ const { foundChunk, doc, dataDoc } = this.docManager.getSimplifiedChunkById(chunkId);
+ console.log('doc: ', doc);
+ console.log('dataDoc: ', dataDoc);
+ if (!foundChunk || !doc) {
+ if (doc) {
+ console.warn(`Chunk not found in document, ${doc.id}, for chunk ID: ${chunkId}`);
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {});
+ } else {
+ console.warn(`Chunk not found for chunk ID: ${chunkId}`);
+ }
+ return;
+ }
- if (doc.ai_type == 'video' || doc.ai_type == 'audio') {
- const directMatchSegmentStart = this.getDirectMatchingSegmentStart(doc, citation.direct_text || '', foundChunk.indexes || []);
+ console.log(`Found chunk in document:`, foundChunk);
- if (directMatchSegmentStart) {
- // Navigate to the segment's start time in the media player
- await this.goToMediaTimestamp(doc, directMatchSegmentStart, doc.ai_type);
- } else {
- console.error('No direct matching segment found for the citation.');
- }
- } else {
- // Handle other chunk types as before
- this.handleOtherChunkTypes(foundChunk, citation, doc);
- }
+ // Handle different chunk types
+ if (foundChunk.chunkType === CHUNK_TYPE.AUDIO || foundChunk.chunkType === CHUNK_TYPE.VIDEO) {
+ const directMatchSegmentStart = this.getDirectMatchingSegmentStart(doc, citation.direct_text || '', foundChunk.indexes || []);
+ if (directMatchSegmentStart) {
+ await this.goToMediaTimestamp(doc, directMatchSegmentStart, foundChunk.chunkType);
+ } else {
+ console.error('No direct matching segment found for the citation.');
}
+ } else if (foundChunk.chunkType === CHUNK_TYPE.TABLE || foundChunk.chunkType === CHUNK_TYPE.IMAGE) {
+ console.log('here: ', foundChunk);
+ this.handleOtherChunkTypes(foundChunk as SimplifiedChunk, citation, doc);
+ } else {
+ if (doc.type === 'web') {
+ DocumentManager.Instance.showDocument(doc, { openLocation: OpenWhere.addRight }, () => {});
+ return;
+ }
+ this.handleOtherChunkTypes(foundChunk, citation, doc, dataDoc);
+ // Show the chunk text in citation popup
+ let chunkText = citation.direct_text || 'Text content not available';
+ this.showCitationPopup(chunkText);
+
+ // Also navigate to the document
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {});
}
+ } catch (error) {
+ console.error('Error handling citation click:', error);
}
};
+ /**
+ * Finds a matching segment in a document based on text content.
+ * @param doc The document to search in
+ * @param citationText The text to find in the document
+ * @param indexesOfSegments Optional indexes of segments to search in
+ * @returns The starting timestamp of the matching segment, or -1 if not found
+ */
getDirectMatchingSegmentStart = (doc: Doc, citationText: string, indexesOfSegments: string[]): number => {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const originalSegments = JSON.parse(StrCast(doc.original_segments!)).map((segment: any, index: number) => ({
- index: index.toString(),
- text: segment.text,
- start: segment.start,
- end: segment.end,
- }));
-
- if (!Array.isArray(originalSegments) || originalSegments.length === 0 || !Array.isArray(indexesOfSegments)) {
- return 0;
+ if (!doc || !citationText) return -1;
+
+ // Get original segments using document manager
+ const original_segments = this.docManager.getOriginalSegments(doc);
+
+ if (!original_segments || !Array.isArray(original_segments) || original_segments.length === 0) {
+ return -1;
}
- // Create itemsToSearch array based on indexesOfSegments
- const itemsToSearch = indexesOfSegments.map((indexStr: string) => {
- const index = parseInt(indexStr, 10);
- const segment = originalSegments[index];
- return { text: segment.text, start: segment.start };
- });
+ let segments = original_segments;
+
+ // If specific indexes are provided, filter segments by those indexes
+ if (indexesOfSegments && indexesOfSegments.length > 0) {
+ segments = original_segments.filter((segment: any) => indexesOfSegments.includes(segment.index));
+ }
+
+ // If no segments match the indexes, use all segments
+ if (segments.length === 0) {
+ segments = original_segments;
+ }
- console.log('Constructed itemsToSearch:', itemsToSearch);
+ // First try to find an exact match
+ const exactMatch = segments.find((segment: any) => segment.text && segment.text.includes(citationText));
- // Helper function to calculate word overlap score
+ if (exactMatch) {
+ return exactMatch.start;
+ }
+
+ // If no exact match, find segment with best word overlap
const calculateWordOverlap = (text1: string, text2: string): number => {
- const words1 = new Set(text1.toLowerCase().split(/\W+/));
- const words2 = new Set(text2.toLowerCase().split(/\W+/));
- const intersection = new Set([...words1].filter(word => words2.has(word)));
- return intersection.size / Math.max(words1.size, words2.size); // Jaccard similarity
+ if (!text1 || !text2) return 0;
+
+ const words1 = text1.toLowerCase().split(/\s+/);
+ const words2 = text2.toLowerCase().split(/\s+/);
+ const wordSet1 = new Set(words1);
+
+ let overlap = 0;
+ for (const word of words2) {
+ if (wordSet1.has(word)) {
+ overlap++;
+ }
+ }
+
+ // Return percentage of overlap relative to the shorter text
+ return overlap / Math.min(words1.length, words2.length);
};
- // Search for the best matching segment
- let bestMatchStart = 0;
- let bestScore = 0;
-
- console.log(`Searching for best match for query: "${citationText}"`);
- itemsToSearch.forEach(item => {
- const score = calculateWordOverlap(citationText, item.text);
- console.log(`Comparing query to segment: "${item.text}" | Score: ${score}`);
- if (score > bestScore) {
- bestScore = score;
- bestMatchStart = item.start;
+ // Find segment with highest word overlap
+ let bestMatch = null;
+ let highestOverlap = 0;
+
+ for (const segment of segments) {
+ if (!segment.text) continue;
+
+ const overlap = calculateWordOverlap(segment.text, citationText);
+ if (overlap > highestOverlap) {
+ highestOverlap = overlap;
+ bestMatch = segment;
}
- });
+ }
- console.log('Best match found with score:', bestScore, '| Start time:', bestMatchStart);
+ // Only return matches with significant overlap (more than 30%)
+ if (bestMatch && highestOverlap > 0.3) {
+ return bestMatch.start;
+ }
- // Return the start time of the best match
- return bestMatchStart;
+ // If no good match found, return the start of the first segment as fallback
+ return segments.length > 0 ? segments[0].start : -1;
};
/**
@@ -714,7 +834,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
* @param citation The citation object.
* @param doc The document containing the chunk.
*/
- handleOtherChunkTypes = (foundChunk: SimplifiedChunk, citation: Citation, doc: Doc) => {
+ handleOtherChunkTypes = (foundChunk: SimplifiedChunk, citation: Citation, doc: Doc, dataDoc?: Doc) => {
switch (foundChunk.chunkType) {
case CHUNK_TYPE.IMAGE:
case CHUNK_TYPE.TABLE:
@@ -729,6 +849,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {});
return;
}
+
const x1 = parseFloat(values[0]) * Doc.NativeWidth(doc);
const y1 = parseFloat(values[1]) * Doc.NativeHeight(doc) + foundChunk.startPage * Doc.NativeHeight(doc);
const x2 = parseFloat(values[2]) * Doc.NativeWidth(doc);
@@ -736,31 +857,180 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const annotationKey = '$' + Doc.LayoutDataKey(doc) + '_annotations';
- const existingDoc = DocListCast(doc[annotationKey]).find(d => d.citation_id === citation.citation_id);
+ const existingDoc = DocListCast(doc[DocData][annotationKey]).find(d => d.citation_id === citation.citation_id);
+ if (existingDoc) {
+ existingDoc.x = x1;
+ existingDoc.y = y1;
+ existingDoc._width = x2 - x1;
+ existingDoc._height = y2 - y1;
+ }
const highlightDoc = existingDoc ?? this.createImageCitationHighlight(x1, y1, x2, y2, citation, annotationKey, doc);
+ //doc.layout_scroll = y1;
+ doc._layout_curPage = foundChunk.startPage + 1;
DocumentManager.Instance.showDocument(highlightDoc, { willZoomCentered: true }, () => {});
}
break;
case CHUNK_TYPE.TEXT:
this._citationPopup = { text: citation.direct_text ?? 'No text available', visible: true };
- setTimeout(() => (this._citationPopup.visible = false), 3000);
+ this.startCitationPopupTimer();
- DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {
- const firstView = Array.from(doc[DocViews])[0] as DocumentView;
- (firstView.ComponentView as PDFBox)?.gotoPage?.(foundChunk.startPage ?? 0);
- (firstView.ComponentView as PDFBox)?.search?.(citation.direct_text ?? '');
- });
+ // Check if the document is a PDF (has a PDF viewer component)
+ const isPDF = PDFCast(dataDoc!.data) !== null || dataDoc!.type === DocumentType.PDF;
+
+ // First ensure document is fully visible before trying to access its views
+ this.ensureDocumentIsVisible(dataDoc!, isPDF, citation, foundChunk, doc);
break;
case CHUNK_TYPE.CSV:
case CHUNK_TYPE.URL:
- DocumentManager.Instance.showDocument(doc, { willZoomCentered: true });
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {
+ console.log(`Showing web document in viewer with URL: ${foundChunk.url}`);
+ });
break;
default:
console.error('Unhandled chunk type:', foundChunk.chunkType);
break;
}
};
+
+ /**
+ * Ensures a document is fully visible and rendered before performing actions on it
+ * @param doc The document to ensure is visible
+ * @param isPDF Whether this is a PDF document
+ * @param citation The citation information
+ * @param foundChunk The chunk information
+ * @param doc The document to ensure is visible
+ */
+ ensureDocumentIsVisible = (doc: Doc, isPDF: boolean, citation: Citation, foundChunk: SimplifiedChunk, layoutDoc?: Doc) => {
+ try {
+ // First, check if the document already has views and is rendered
+ const hasViews = doc[DocViews] && doc[DocViews].size > 0;
+
+ console.log(`Document ${doc.id}: Current state - hasViews: ${hasViews}, isPDF: ${isPDF}`);
+
+ if (hasViews) {
+ // Document is already rendered, proceed with accessing its view
+ this.processPDFDocumentView(doc, isPDF, citation, foundChunk);
+ return;
+ } else if (layoutDoc) {
+ this.processPDFDocumentView(layoutDoc, isPDF, citation, foundChunk);
+ return;
+ }
+
+ // If document is not rendered yet, show it and wait for it to be ready
+ console.log(`Document ${doc.id} needs to be shown first`);
+
+ // Force document to be rendered by using willZoomCentered: true
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, () => {
+ // Wait a bit for the document to be fully rendered (longer than our previous attempts)
+ setTimeout(() => {
+ // Now manually check if document view exists and is valid
+ this.verifyAndProcessDocumentView(doc, isPDF, citation, foundChunk, 1);
+ }, 800); // Increased initial delay
+ });
+ } catch (error) {
+ console.error('Error ensuring document visibility:', error);
+ // Show the document anyway as a fallback
+ DocumentManager.Instance.showDocument(doc, { willZoomCentered: true });
+ }
+ };
+
+ /**
+ * Verifies document view exists and processes it, with retries if needed
+ */
+ verifyAndProcessDocumentView = (doc: Doc, isPDF: boolean, citation: Citation, foundChunk: SimplifiedChunk, attempt: number) => {
+ // Diagnostic info
+ console.log(`Verify attempt ${attempt}: Document views for ${doc.id}:`, doc[DocViews] ? `Found ${doc[DocViews].size} views` : 'No views');
+
+ // Double-check document exists in current document system
+ const docExists = DocServer.GetCachedRefField(doc[Id]) !== undefined;
+ if (!docExists) {
+ console.warn(`Document ${doc.id} no longer exists in document system`);
+ return;
+ }
+
+ try {
+ if (!doc[DocViews] || doc[DocViews].size === 0) {
+ if (attempt >= 5) {
+ console.error(`Maximum verification attempts (${attempt}) reached for document ${doc.id}`);
+
+ // Last resort: force re-creation of the document view
+ if (isPDF) {
+ console.log('Forcing document recreation as last resort');
+ DocumentManager.Instance.showDocument(doc, {
+ willZoomCentered: true,
+ });
+ }
+ return;
+ }
+
+ // Let's try explicitly requesting the document be shown again
+ if (attempt > 2) {
+ console.log(`Attempt ${attempt}: Re-requesting document be shown`);
+ DocumentManager.Instance.showDocument(doc, {
+ willZoomCentered: true,
+ openLocation: attempt % 2 === 0 ? OpenWhere.addRight : undefined,
+ });
+ }
+
+ // Use exponential backoff for retries
+ const nextDelay = Math.min(2000, 500 * Math.pow(1.5, attempt));
+ console.log(`Scheduling retry ${attempt + 1} in ${nextDelay}ms`);
+
+ setTimeout(() => {
+ this.verifyAndProcessDocumentView(doc, isPDF, citation, foundChunk, attempt + 1);
+ }, nextDelay);
+ return;
+ }
+
+ this.processPDFDocumentView(doc, isPDF, citation, foundChunk);
+ } catch (error) {
+ console.error(`Error on verification attempt ${attempt}:`, error);
+ if (attempt < 5) {
+ setTimeout(
+ () => {
+ this.verifyAndProcessDocumentView(doc, isPDF, citation, foundChunk, attempt + 1);
+ },
+ 500 * Math.pow(1.5, attempt)
+ );
+ }
+ }
+ };
+
+ /**
+ * Processes a PDF document view once we're sure it exists
+ */
+ processPDFDocumentView = (doc: Doc, isPDF: boolean, citation: Citation, foundChunk: SimplifiedChunk) => {
+ try {
+ const views = Array.from(doc[DocViews] || []);
+ if (!views.length) {
+ console.warn('No document views found in document that should have views');
+ return;
+ }
+
+ const firstView = views[0] as DocumentView;
+ if (!firstView) {
+ console.warn('First view is invalid');
+ return;
+ }
+
+ console.log(`Successfully found document view for ${doc.id}:`, firstView.ComponentView ? `Component: ${firstView.ComponentView.constructor.name}` : 'No component view');
+
+ if (!firstView.ComponentView) {
+ console.warn('Component view not available');
+ return;
+ }
+
+ // For PDF documents, perform fuzzy search
+ if (isPDF && firstView.ComponentView && citation.direct_text) {
+ const pdfComponent = firstView.ComponentView as PDFBox;
+ this.ensureFuzzySearchAndExecute(pdfComponent, citation.direct_text.trim(), foundChunk.startPage);
+ }
+ } catch (error) {
+ console.error('Error processing PDF document view:', error);
+ }
+ };
+
/**
* Creates an annotation highlight on a PDF document for image citations.
* @param x1 X-coordinate of the top-left corner of the highlight.
@@ -780,7 +1050,8 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
_height: y2 - y1,
backgroundColor: 'rgba(255, 255, 0, 0.5)',
});
- highlight_doc.$citation_id = citation.citation_id;
+ highlight_doc[DocData].citation_id = citation.citation_id;
+ highlight_doc.freeform_scale = 1;
Doc.AddDocToList(pdfDoc[DocData], annotationKey, highlight_doc);
highlight_doc.annotationOn = pdfDoc;
Doc.SetContainer(highlight_doc, pdfDoc);
@@ -860,6 +1131,16 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
});
this.addScrollListener();
+
+ // Initialize the document manager by finding existing documents
+ this.docManager.initializeFindDocsFreeform();
+
+ // If there are stored doc IDs in our list of docs to add, process them
+ if (this._linked_docs_to_add.size > 0) {
+ this._linked_docs_to_add.forEach(async doc => {
+ await this.docManager.processDocument(doc);
+ });
+ }
}
/**
@@ -871,58 +1152,6 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
/**
- * Getter that retrieves all linked documents for the current document.
- */
- @computed
- get linkedDocs() {
- return LinkManager.Instance.getAllRelatedLinks(this.Document)
- .map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document)))
- .map(d => DocCast(d?.annotationOn, d))
- .filter(d => d)
- .map(d => d!);
- }
-
- /**
- * Getter that retrieves document IDs of linked documents that have AI-related content.
- */
- @computed
- get docIds() {
- return LinkManager.Instance.getAllRelatedLinks(this.Document)
- .map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document)))
- .map(d => DocCast(d?.annotationOn, d))
- .filter(d => d)
- .map(d => d!)
- .filter(d => {
- console.log(d.ai_doc_id);
- return d.ai_doc_id;
- })
- .map(d => StrCast(d.ai_doc_id));
- }
-
- /**
- * Getter that retrieves summaries of all linked documents.
- */
- @computed
- get summaries(): string {
- return (
- LinkManager.Instance.getAllRelatedLinks(this.Document)
- .map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document)))
- .map(d => DocCast(d?.annotationOn, d))
- .filter(d => d?.summary)
- .map((doc, index) => {
- if (PDFCast(doc?.data)) {
- return `<summary file_name="${PDFCast(doc!.data)!.url.pathname}" applicable_tools=["rag"]>${doc!.summary}</summary>`;
- } else if (CsvCast(doc?.data)) {
- return `<summary file_name="${CsvCast(doc!.data)!.url.pathname}" applicable_tools=["dataAnalysis"]>${doc!.summary}</summary>`;
- } else {
- return `${index + 1}) ${doc?.summary}`;
- }
- })
- .join('\n') + '\n'
- );
- }
-
- /**
* Getter that retrieves all linked CSV files for analysis.
*/
@computed get linkedCSVs(): { filename: string; id: string; text: string }[] {
@@ -947,22 +1176,14 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
// Other helper methods for retrieving document data and processing
- retrieveSummaries = () => {
- return this.summaries;
- };
-
retrieveCSVData = () => {
return this.linkedCSVs;
};
- retrieveFormattedHistory = () => {
+ retrieveFormattedHistory = (): string => {
return this.formattedHistory;
};
- retrieveDocIds = () => {
- return this.docIds;
- };
-
/**
* Handles follow-up questions when the user clicks on them.
* Automatically sets the input value to the clicked follow-up question.
@@ -973,25 +1194,273 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this._inputValue = question;
};
+ /**
+ * Handles tool creation notification and shows the reload modal
+ * @param toolName The name of the tool that was created
+ */
+ @action
+ handleToolCreated = (toolName: string) => {
+ this._toolReloadModal = {
+ visible: true,
+ toolName: toolName,
+ };
+ };
+
+ /**
+ * Closes the tool reload modal
+ */
+ @action
+ closeToolReloadModal = () => {
+ this._toolReloadModal = {
+ visible: false,
+ toolName: '',
+ };
+ };
+
+ /**
+ * Handles the reload confirmation and triggers page reload
+ */
+ @action
+ handleReloadConfirmation = async () => {
+ // Close the modal first
+ this.closeToolReloadModal();
+
+ try {
+ // Perform the deferred tool save operation
+ const saveSuccess = await this.agent.performDeferredToolSave();
+
+ if (saveSuccess) {
+ console.log('Tool saved successfully, proceeding with reload...');
+ } else {
+ console.warn('Tool save failed, but proceeding with reload anyway...');
+ }
+ } catch (error) {
+ console.error('Error during deferred tool save:', error);
+ }
+
+ // Trigger page reload to rebuild webpack and load the new tool
+ setTimeout(() => {
+ window.location.reload();
+ }, 100);
+ };
+
_dictation: DictationButton | null = null;
- setInputRef = (r: HTMLInputElement) => (this._textInputRef = r);
- setDictationRef = (r: DictationButton) => (this._dictation = r);
+
+ /**
+ * 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>
+ );
+
+ /**
+ * Shows the citation popup with the given text.
+ * @param text The text to display in the popup.
+ */
+ @action
+ showCitationPopup = (text: string) => {
+ this._citationPopup = {
+ text: text || 'No text available',
+ visible: true,
+ };
+ this.startCitationPopupTimer();
+ };
+
+ /**
+ * Closes the citation popup.
+ */
+ @action
+ closeCitationPopup = () => {
+ this._citationPopup.visible = false;
+ };
+
+ /**
+ * Starts the auto-close timer for the citation popup.
+ */
+ startCitationPopupTimer = () => {
+ // Auto-close the popup after 5 seconds
+ setTimeout(() => this.closeCitationPopup(), 5000);
+ };
+
+ /**
+ * Retry PDF search with exponential backoff
+ */
+ retryPdfSearch = (doc: Doc, citation: Citation, foundChunk: SimplifiedChunk, isPDF: boolean, attempt: number) => {
+ if (attempt > 5) {
+ console.error('Maximum retry attempts reached for PDF search');
+ return;
+ }
+
+ const delay = Math.min(2000, 500 * Math.pow(1.5, attempt)); // Exponential backoff with max delay of 2 seconds
+
+ setTimeout(() => {
+ try {
+ if (!doc[DocViews] || doc[DocViews].size === 0) {
+ this.retryPdfSearch(doc, citation, foundChunk, isPDF, attempt + 1);
+ return;
+ }
+
+ const views = Array.from(doc[DocViews]);
+ if (!views.length) {
+ this.retryPdfSearch(doc, citation, foundChunk, isPDF, attempt + 1);
+ return;
+ }
+
+ const firstView = views[0] as DocumentView;
+ if (!firstView || !firstView.ComponentView) {
+ this.retryPdfSearch(doc, citation, foundChunk, isPDF, attempt + 1);
+ return;
+ }
+
+ const pdfComponent = firstView.ComponentView as PDFBox;
+ if (isPDF && pdfComponent && citation.direct_text) {
+ console.log(`PDF component found on attempt ${attempt}, executing search...`);
+ this.ensureFuzzySearchAndExecute(pdfComponent, citation.direct_text.trim(), foundChunk.startPage);
+ }
+ } catch (error) {
+ console.error(`Error on retry attempt ${attempt}:`, error);
+ this.retryPdfSearch(doc, citation, foundChunk, isPDF, attempt + 1);
+ }
+ }, delay);
+ };
+
+ /**
+ * Ensures fuzzy search is enabled in PDFBox and performs a search
+ * @param pdfComponent The PDFBox component
+ * @param searchText The text to search for
+ * @param startPage Optional page to navigate to before searching
+ */
+ private ensureFuzzySearchAndExecute = (pdfComponent: PDFBox, searchText: string, startPage?: number) => {
+ if (!pdfComponent) {
+ console.warn('PDF component is undefined, cannot perform search');
+ return;
+ }
+
+ if (!searchText?.trim()) {
+ console.warn('Search text is empty, skipping search');
+ return;
+ }
+
+ try {
+ // Check if the component has required methods
+ if (typeof pdfComponent.gotoPage !== 'function' || typeof pdfComponent.toggleFuzzySearch !== 'function' || typeof pdfComponent.search !== 'function') {
+ console.warn('PDF component missing required methods');
+ return;
+ }
+
+ // Navigate to the page if specified
+ if (typeof startPage === 'number') {
+ pdfComponent.gotoPage(startPage + 1);
+ }
+
+ // Always try to enable fuzzy search
+ try {
+ // PDFBox.tsx toggles fuzzy search state internally
+ // We'll call it once to make sure it's enabled
+ pdfComponent.toggleFuzzySearch();
+ } catch (toggleError) {
+ console.warn('Error toggling fuzzy search:', toggleError);
+ }
+
+ // Add a sufficient delay to ensure PDF is fully loaded before searching
+ setTimeout(() => {
+ try {
+ console.log('Performing fuzzy search for text:', searchText);
+ pdfComponent.search(searchText);
+ } catch (searchError) {
+ console.error('Error performing search:', searchError);
+ }
+ }, 1000); // Increased delay for better reliability
+ } catch (error) {
+ console.error('Error in fuzzy search setup:', error);
+ }
+ };
+
/**
- * Renders the chat interface, including the message list, input field, and other UI elements.
+ * Main render method for the ChatBox
*/
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">
- <ProgressBar />
- <div className="step-name">{this._currentStep}</div>
+ <div className="progress-bar-wrapper">
+ <div className="progress-bar" style={{ width: `${this._uploadProgress}%` }} />
+ </div>
+ <div className="progress-details">
+ <div className="progress-percentage">{Math.round(this._uploadProgress)}%</div>
+ <div className="step-name">{this._currentStep}</div>
+ </div>
</div>
</div>
)}
<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) => (
@@ -1003,34 +1472,77 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
</div>
<form onSubmit={this.askGPT} className="chat-input">
- <input
- ref={this.setInputRef}
- 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={e => this.setChatInput(e.target.value)}
+ disabled={this._isLoading}
+ />
+ </div>
+ <Button
+ // className="submit-button"
+ onClick={this.askGPT}
+ type={Type.PRIM}
+ tooltip="Send to AI"
+ color={SnappingManager.userVariantColor}
+ inactive={this._isLoading || !this._inputValue.trim()}
+ icon={<AiOutlineSend />}
+ size={Size.LARGE}
+ />
+ <DictationButton
+ ref={r => {
+ this._dictation = r;
+ }}
+ setInput={this.setChatInput}
+ inputRef={this._textInputRef}
/>
- <button className="submit-button" onClick={() => this._dictation?.stopDictation()} type="submit" disabled={this._isLoading || !this._inputValue.trim()}>
- {this._isLoading ? (
- <div className="spinner"></div>
- ) : (
- <svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round">
- <line x1="22" y1="2" x2="11" y2="13"></line>
- <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
- </svg>
- )}
- </button>
- <DictationButton ref={this.setDictationRef} setInput={this.setChatInput} inputRef={this._textInputRef} />
</form>
{/* Popup for citation */}
{this._citationPopup.visible && (
<div className="citation-popup">
- <p>
- <strong>Text from your document: </strong> {this._citationPopup.text}
- </p>
+ <div className="citation-popup-header">
+ <strong>Text from your document</strong>
+ <button className="citation-close-button" onClick={this.closeCitationPopup}>
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
+ <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="citation-content">{this._citationPopup.text}</div>
+ </div>
+ )}
+
+ {/* Tool Reload Modal */}
+ {this._toolReloadModal.visible && (
+ <div className="tool-reload-modal-overlay">
+ <div className="tool-reload-modal">
+ <div className="tool-reload-modal-header">
+ <h3>Tool Created Successfully!</h3>
+ </div>
+ <div className="tool-reload-modal-content">
+ <p>
+ The tool <strong>{this._toolReloadModal.toolName}</strong> has been created and saved successfully.
+ </p>
+ <p>To make the tool available for future use, the page needs to be reloaded to rebuild the application bundle.</p>
+ <p>Click &quot;Reload Page&quot; to complete the tool installation.</p>
+ </div>
+ <div className="tool-reload-modal-actions">
+ <button className="reload-button primary" onClick={this.handleReloadConfirmation}>
+ Reload Page
+ </button>
+ <button className="close-button secondary" onClick={this.closeToolReloadModal}>
+ Later
+ </button>
+ </div>
+ </div>
</div>
)}
</div>
@@ -1043,5 +1555,5 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
*/
Docs.Prototypes.TemplateMap.set(DocumentType.CHAT, {
layout: { view: ChatBox, dataField: 'data' },
- options: { acl: '', _layout_nativeDimEditable: true, _layout_fitWidth: true, chat: '', chat_history: '', chat_thread_id: '', chat_assistant_id: '', chat_vector_store_id: '' },
+ options: { acl: '', _layout_nativeDimEditable: true, _layout_fitWidth: true, chat: '', chat_history: '', chat_thread_id: '', chat_assistant_id: '', chat_vector_store_id: '', _forceActive: true },
});
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 77d452830..000000000
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ProgressBar.scss
+++ /dev/null
@@ -1,69 +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: 0px;
- left: 0px;
- 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: 0px;
- left: 0px;
- right: 0px;
- bottom: 0px;
- 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;
-}
-
-.step-name {
- font-size: 18px;
- color: #333;
- text-align: center;
- width: 100%;
- margin-top: -10px; // Adjust to move the text closer to the spinner
-}
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>
- );
-};