mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-11-03 20:48:07 +00:00 
			
		
		
		
	feat: Reply Editor AI Changes
This commit is contained in:
		@@ -94,6 +94,19 @@
 | 
				
			|||||||
    --gray-11: 100 100 100;
 | 
					    --gray-11: 100 100 100;
 | 
				
			||||||
    --gray-12: 32 32 32;
 | 
					    --gray-12: 32 32 32;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    --violet-1: 253 252 254;
 | 
				
			||||||
 | 
					    --violet-2: 250 248 255;
 | 
				
			||||||
 | 
					    --violet-3: 244 240 254;
 | 
				
			||||||
 | 
					    --violet-4: 235 228 255;
 | 
				
			||||||
 | 
					    --violet-5: 225 217 255;
 | 
				
			||||||
 | 
					    --violet-6: 212 202 254;
 | 
				
			||||||
 | 
					    --violet-7: 194 178 248;
 | 
				
			||||||
 | 
					    --violet-8: 169 153 236;
 | 
				
			||||||
 | 
					    --violet-9: 110 86 207;
 | 
				
			||||||
 | 
					    --violet-10: 100 84 196;
 | 
				
			||||||
 | 
					    --violet-11: 101 85 183;
 | 
				
			||||||
 | 
					    --violet-12: 47 38 95;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    --background-color: 253 253 253;
 | 
					    --background-color: 253 253 253;
 | 
				
			||||||
    --text-blue: 8 109 224;
 | 
					    --text-blue: 8 109 224;
 | 
				
			||||||
    --border-container: 236 236 236;
 | 
					    --border-container: 236 236 236;
 | 
				
			||||||
@@ -209,6 +222,19 @@
 | 
				
			|||||||
    --gray-11: 180 180 180;
 | 
					    --gray-11: 180 180 180;
 | 
				
			||||||
    --gray-12: 238 238 238;
 | 
					    --gray-12: 238 238 238;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    --violet-1: 20 17 31;
 | 
				
			||||||
 | 
					    --violet-2: 27 21 37;
 | 
				
			||||||
 | 
					    --violet-3: 41 31 67;
 | 
				
			||||||
 | 
					    --violet-4: 50 37 85;
 | 
				
			||||||
 | 
					    --violet-5: 60 46 105;
 | 
				
			||||||
 | 
					    --violet-6: 71 56 135;
 | 
				
			||||||
 | 
					    --violet-7: 86 70 151;
 | 
				
			||||||
 | 
					    --violet-8: 110 86 171;
 | 
				
			||||||
 | 
					    --violet-9: 110 86 207;
 | 
				
			||||||
 | 
					    --violet-10: 125 109 217;
 | 
				
			||||||
 | 
					    --violet-11: 169 153 236;
 | 
				
			||||||
 | 
					    --violet-12: 226 221 254;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    --background-color: 18 18 19;
 | 
					    --background-color: 18 18 19;
 | 
				
			||||||
    --border-strong: 52 52 52;
 | 
					    --border-strong: 52 52 52;
 | 
				
			||||||
    --border-weak: 38 38 42;
 | 
					    --border-weak: 38 38 42;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,235 @@
 | 
				
			|||||||
 | 
					<script setup>
 | 
				
			||||||
 | 
					import { ref, computed, watch, onMounted, useTemplateRef } from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  messageSchema,
 | 
				
			||||||
 | 
					  buildEditor,
 | 
				
			||||||
 | 
					  EditorView,
 | 
				
			||||||
 | 
					  MessageMarkdownTransformer,
 | 
				
			||||||
 | 
					  MessageMarkdownSerializer,
 | 
				
			||||||
 | 
					  EditorState,
 | 
				
			||||||
 | 
					  Selection,
 | 
				
			||||||
 | 
					} from '@chatwoot/prosemirror-schema';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import NextButton from 'dashboard/components-next/button/Button.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps({
 | 
				
			||||||
 | 
					  modelValue: { type: String, default: '' },
 | 
				
			||||||
 | 
					  editorId: { type: String, default: '' },
 | 
				
			||||||
 | 
					  placeholder: {
 | 
				
			||||||
 | 
					    type: String,
 | 
				
			||||||
 | 
					    default: 'Give copilot additional prompts, or ask anything else...',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  enabledMenuOptions: { type: Array, default: () => [] },
 | 
				
			||||||
 | 
					  generatedContent: { type: String, default: '' },
 | 
				
			||||||
 | 
					  autofocus: {
 | 
				
			||||||
 | 
					    type: Boolean,
 | 
				
			||||||
 | 
					    default: true,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emit = defineEmits([
 | 
				
			||||||
 | 
					  'blur',
 | 
				
			||||||
 | 
					  'input',
 | 
				
			||||||
 | 
					  'update:modelValue',
 | 
				
			||||||
 | 
					  'keyup',
 | 
				
			||||||
 | 
					  'focus',
 | 
				
			||||||
 | 
					  'keydown',
 | 
				
			||||||
 | 
					  'send',
 | 
				
			||||||
 | 
					]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { formatMessage } = useMessageFormatter();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const createState = (
 | 
				
			||||||
 | 
					  content,
 | 
				
			||||||
 | 
					  placeholder,
 | 
				
			||||||
 | 
					  plugins = [],
 | 
				
			||||||
 | 
					  methods = {},
 | 
				
			||||||
 | 
					  enabledMenuOptions = []
 | 
				
			||||||
 | 
					) => {
 | 
				
			||||||
 | 
					  return EditorState.create({
 | 
				
			||||||
 | 
					    doc: new MessageMarkdownTransformer(messageSchema).parse(content),
 | 
				
			||||||
 | 
					    plugins: buildEditor({
 | 
				
			||||||
 | 
					      schema: messageSchema,
 | 
				
			||||||
 | 
					      placeholder,
 | 
				
			||||||
 | 
					      methods,
 | 
				
			||||||
 | 
					      plugins,
 | 
				
			||||||
 | 
					      enabledMenuOptions,
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// we don't need them to be reactive
 | 
				
			||||||
 | 
					// It cases weird issues where the objects are proxied
 | 
				
			||||||
 | 
					// and then the editor doesn't work as expected
 | 
				
			||||||
 | 
					let editorView = null;
 | 
				
			||||||
 | 
					let state = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// reactive data
 | 
				
			||||||
 | 
					const isTextSelected = ref(false); // Tracks text selection and prevents unnecessary re-renders on mouse selection
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// element refs
 | 
				
			||||||
 | 
					const editor = useTemplateRef('editor');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function contentFromEditor() {
 | 
				
			||||||
 | 
					  if (editorView) {
 | 
				
			||||||
 | 
					    return MessageMarkdownSerializer.serialize(editorView.state.doc);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return '';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function focusEditorInputField() {
 | 
				
			||||||
 | 
					  const { tr } = editorView.state;
 | 
				
			||||||
 | 
					  const selection = Selection.atEnd(tr.doc);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  editorView.dispatch(tr.setSelection(selection));
 | 
				
			||||||
 | 
					  editorView.focus();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function emitOnChange() {
 | 
				
			||||||
 | 
					  emit('update:modelValue', contentFromEditor());
 | 
				
			||||||
 | 
					  emit('input', contentFromEditor());
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function onKeyup() {
 | 
				
			||||||
 | 
					  emit('keyup');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function onKeydown() {
 | 
				
			||||||
 | 
					  emit('keydown');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function onBlur() {
 | 
				
			||||||
 | 
					  emit('blur');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function onFocus() {
 | 
				
			||||||
 | 
					  emit('focus');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function checkSelection(editorState) {
 | 
				
			||||||
 | 
					  const hasSelection = editorState.selection.from !== editorState.selection.to;
 | 
				
			||||||
 | 
					  if (hasSelection === isTextSelected.value) return;
 | 
				
			||||||
 | 
					  isTextSelected.value = hasSelection;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// computed properties
 | 
				
			||||||
 | 
					const plugins = computed(() => {
 | 
				
			||||||
 | 
					  return [];
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function reloadState() {
 | 
				
			||||||
 | 
					  state = createState(
 | 
				
			||||||
 | 
					    props.modelValue,
 | 
				
			||||||
 | 
					    props.placeholder,
 | 
				
			||||||
 | 
					    plugins.value,
 | 
				
			||||||
 | 
					    props.enabledMenuOptions
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  editorView.updateState(state);
 | 
				
			||||||
 | 
					  focusEditorInputField();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function createEditorView() {
 | 
				
			||||||
 | 
					  editorView = new EditorView(editor.value, {
 | 
				
			||||||
 | 
					    state: state,
 | 
				
			||||||
 | 
					    dispatchTransaction: tx => {
 | 
				
			||||||
 | 
					      state = state.apply(tx);
 | 
				
			||||||
 | 
					      editorView.updateState(state);
 | 
				
			||||||
 | 
					      if (tx.docChanged) {
 | 
				
			||||||
 | 
					        emitOnChange();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      checkSelection(state);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    handleDOMEvents: {
 | 
				
			||||||
 | 
					      keyup: onKeyup,
 | 
				
			||||||
 | 
					      focus: onFocus,
 | 
				
			||||||
 | 
					      blur: onBlur,
 | 
				
			||||||
 | 
					      keydown: onKeydown,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function handleSubmit() {
 | 
				
			||||||
 | 
					  emit('send');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// watchers
 | 
				
			||||||
 | 
					watch(
 | 
				
			||||||
 | 
					  computed(() => props.modelValue),
 | 
				
			||||||
 | 
					  (newValue = '') => {
 | 
				
			||||||
 | 
					    if (newValue !== contentFromEditor()) {
 | 
				
			||||||
 | 
					      reloadState();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watch(
 | 
				
			||||||
 | 
					  computed(() => props.editorId),
 | 
				
			||||||
 | 
					  () => {
 | 
				
			||||||
 | 
					    reloadState();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// lifecycle
 | 
				
			||||||
 | 
					onMounted(() => {
 | 
				
			||||||
 | 
					  state = createState(
 | 
				
			||||||
 | 
					    props.modelValue,
 | 
				
			||||||
 | 
					    props.placeholder,
 | 
				
			||||||
 | 
					    plugins.value,
 | 
				
			||||||
 | 
					    props.enabledMenuOptions
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  createEditorView();
 | 
				
			||||||
 | 
					  editorView.updateState(state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (props.autofocus) {
 | 
				
			||||||
 | 
					    focusEditorInputField();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="space-y-2">
 | 
				
			||||||
 | 
					    <div class="editor-root relative editor--copilot space-x-2">
 | 
				
			||||||
 | 
					      <div ref="editor" />
 | 
				
			||||||
 | 
					      <div class="flex items-center justify-end absolute right-2 bottom-2">
 | 
				
			||||||
 | 
					        <NextButton
 | 
				
			||||||
 | 
					          class="bg-n-iris-9 text-white !rounded-full"
 | 
				
			||||||
 | 
					          icon="i-lucide-arrow-up"
 | 
				
			||||||
 | 
					          solid
 | 
				
			||||||
 | 
					          sm
 | 
				
			||||||
 | 
					          @click="handleSubmit"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="max-h-72 overflow-y-auto">
 | 
				
			||||||
 | 
					      <p
 | 
				
			||||||
 | 
					        v-dompurify-html="formatMessage(generatedContent, false)"
 | 
				
			||||||
 | 
					        class="text-n-iris-12 text-sm prose-sm font-normal !mb-4 underline decoration-n-iris-8 underline-offset-auto decoration-solid decoration-[10%]"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss">
 | 
				
			||||||
 | 
					@import '@chatwoot/prosemirror-schema/src/styles/base.scss';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.editor--copilot {
 | 
				
			||||||
 | 
					  @apply bg-n-iris-5 rounded;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .ProseMirror-woot-style {
 | 
				
			||||||
 | 
					    min-height: 5rem;
 | 
				
			||||||
 | 
					    max-height: 7.5rem;
 | 
				
			||||||
 | 
					    overflow: auto;
 | 
				
			||||||
 | 
					    @apply px-2 !important;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .empty-node {
 | 
				
			||||||
 | 
					      &::before {
 | 
				
			||||||
 | 
					        @apply text-n-iris-9;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
@@ -0,0 +1,233 @@
 | 
				
			|||||||
 | 
					<script setup>
 | 
				
			||||||
 | 
					import { computed, ref, useTemplateRef } from 'vue';
 | 
				
			||||||
 | 
					import { useI18n } from 'vue-i18n';
 | 
				
			||||||
 | 
					import { useElementSize } from '@vueuse/core';
 | 
				
			||||||
 | 
					import { useMapGetter } from 'dashboard/composables/store';
 | 
				
			||||||
 | 
					import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
 | 
				
			||||||
 | 
					import { useAI } from 'dashboard/composables/useAI';
 | 
				
			||||||
 | 
					import Button from 'dashboard/components-next/button/Button.vue';
 | 
				
			||||||
 | 
					import DropdownBody from 'next/dropdown-menu/base/DropdownBody.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Icon from 'next/icon/Icon.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps({
 | 
				
			||||||
 | 
					  hasSelection: {
 | 
				
			||||||
 | 
					    type: Boolean,
 | 
				
			||||||
 | 
					    default: false,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emit = defineEmits(['executeAction']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { t } = useI18n();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { draftMessage } = useAI();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const replyMode = useMapGetter('draftMessages/getReplyEditorMode');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Selection-based menu items (when text is selected)
 | 
				
			||||||
 | 
					const menuItems = computed(() => {
 | 
				
			||||||
 | 
					  const items = [];
 | 
				
			||||||
 | 
					  if (props.hasSelection) {
 | 
				
			||||||
 | 
					    items.push({
 | 
				
			||||||
 | 
					      label: t(
 | 
				
			||||||
 | 
					        'INTEGRATION_SETTINGS.OPEN_AI.REPLY_OPTIONS.IMPROVE_REPLY_SELECTION'
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      key: 'rephrase_selection',
 | 
				
			||||||
 | 
					      icon: 'i-fluent-pen-sparkle-24-regular',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  } else if (
 | 
				
			||||||
 | 
					    replyMode.value === REPLY_EDITOR_MODES.REPLY &&
 | 
				
			||||||
 | 
					    draftMessage.value
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    items.push({
 | 
				
			||||||
 | 
					      label: t('INTEGRATION_SETTINGS.OPEN_AI.REPLY_OPTIONS.IMPROVE_REPLY'),
 | 
				
			||||||
 | 
					      key: 'rephrase',
 | 
				
			||||||
 | 
					      icon: 'i-fluent-pen-sparkle-24-regular',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (draftMessage.value) {
 | 
				
			||||||
 | 
					    items.push(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        label: t(
 | 
				
			||||||
 | 
					          'INTEGRATION_SETTINGS.OPEN_AI.REPLY_OPTIONS.CHANGE_TONE.TITLE'
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        key: 'change_tone',
 | 
				
			||||||
 | 
					        icon: 'i-fluent-sound-wave-circle-sparkle-24-regular',
 | 
				
			||||||
 | 
					        subMenuItems: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            label: t(
 | 
				
			||||||
 | 
					              'INTEGRATION_SETTINGS.OPEN_AI.REPLY_OPTIONS.CHANGE_TONE.OPTIONS.FRIENDLY'
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            key: 'make_friendly',
 | 
				
			||||||
 | 
					            icon: 'i-fluent-person-voice-16-regular',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            label: t(
 | 
				
			||||||
 | 
					              'INTEGRATION_SETTINGS.OPEN_AI.REPLY_OPTIONS.CHANGE_TONE.OPTIONS.FORMAL'
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            key: 'make_formal',
 | 
				
			||||||
 | 
					            icon: 'i-fluent-person-voice-16-regular',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            label: t(
 | 
				
			||||||
 | 
					              'INTEGRATION_SETTINGS.OPEN_AI.REPLY_OPTIONS.CHANGE_TONE.OPTIONS.SIMPLIFY'
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            key: 'simplify',
 | 
				
			||||||
 | 
					            icon: 'i-fluent-person-voice-16-regular',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        label: t('INTEGRATION_SETTINGS.OPEN_AI.REPLY_OPTIONS.GRAMMAR'),
 | 
				
			||||||
 | 
					        key: 'fix_spelling_grammar',
 | 
				
			||||||
 | 
					        icon: 'i-fluent-flow-sparkle-24-regular',
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return items;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const generalMenuItems = computed(() => {
 | 
				
			||||||
 | 
					  const items = [];
 | 
				
			||||||
 | 
					  if (replyMode.value === REPLY_EDITOR_MODES.REPLY) {
 | 
				
			||||||
 | 
					    items.push({
 | 
				
			||||||
 | 
					      label: t('INTEGRATION_SETTINGS.OPEN_AI.REPLY_OPTIONS.SUGGESTION'),
 | 
				
			||||||
 | 
					      key: 'reply_suggestion',
 | 
				
			||||||
 | 
					      icon: 'i-fluent-chat-sparkle-16-regular',
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  items.push(
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      label: t('INTEGRATION_SETTINGS.OPEN_AI.REPLY_OPTIONS.SUMMARIZE'),
 | 
				
			||||||
 | 
					      key: 'summarize',
 | 
				
			||||||
 | 
					      icon: 'i-fluent-text-bullet-list-square-sparkle-32-regular',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      label: t('INTEGRATION_SETTINGS.OPEN_AI.REPLY_OPTIONS.ASK_COPILOT'),
 | 
				
			||||||
 | 
					      key: 'ask_copilot',
 | 
				
			||||||
 | 
					      icon: 'i-fluent-circle-sparkle-24-regular',
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return items;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const menuRef = useTemplateRef('menuRef');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Track expanded submenu items
 | 
				
			||||||
 | 
					const expandedItems = ref(new Set());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { height: menuHeight } = useElementSize(menuRef);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Computed style for selection menu positioning
 | 
				
			||||||
 | 
					const selectionMenuStyle = computed(() => {
 | 
				
			||||||
 | 
					  // Use the same CSS custom properties as the editor menubar
 | 
				
			||||||
 | 
					  // Dynamically calculate offset based on actual menu height + 10px gap
 | 
				
			||||||
 | 
					  const dynamicOffset = menuHeight.value > 0 ? menuHeight.value + 10 : 60;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    left: 'var(--selection-left)',
 | 
				
			||||||
 | 
					    top: `calc(var(--selection-top) - ${dynamicOffset}px)`,
 | 
				
			||||||
 | 
					    transform: 'translateX(-62%)',
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handleMenuItemClick = item => {
 | 
				
			||||||
 | 
					  if (item.subMenuItems) {
 | 
				
			||||||
 | 
					    // Toggle submenu expansion
 | 
				
			||||||
 | 
					    if (expandedItems.value.has(item.key)) {
 | 
				
			||||||
 | 
					      expandedItems.value.delete(item.key);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      expandedItems.value.add(item.key);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    emit('executeAction', item.key);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handleSubMenuItemClick = (parentItem, subItem) => {
 | 
				
			||||||
 | 
					  emit('executeAction', subItem.key, {
 | 
				
			||||||
 | 
					    parentKey: parentItem.key,
 | 
				
			||||||
 | 
					    tone: subItem.label.toLowerCase(),
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <DropdownBody
 | 
				
			||||||
 | 
					    ref="menuRef"
 | 
				
			||||||
 | 
					    class="min-w-56 [&>ul]:gap-3 z-50 [&>ul]:px-4 [&>ul]:py-3.5"
 | 
				
			||||||
 | 
					    :style="hasSelection ? selectionMenuStyle : {}"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <div v-if="menuItems.length > 0" class="flex flex-col items-start gap-2.5">
 | 
				
			||||||
 | 
					      <div v-for="item in menuItems" :key="item.key" class="w-full">
 | 
				
			||||||
 | 
					        <Button
 | 
				
			||||||
 | 
					          :label="item.label"
 | 
				
			||||||
 | 
					          :icon="item.icon"
 | 
				
			||||||
 | 
					          slate
 | 
				
			||||||
 | 
					          link
 | 
				
			||||||
 | 
					          sm
 | 
				
			||||||
 | 
					          class="hover:!no-underline text-n-slate-12 font-normal text-xs w-full !justify-start"
 | 
				
			||||||
 | 
					          @click="handleMenuItemClick(item)"
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <template v-if="item.subMenuItems" #default>
 | 
				
			||||||
 | 
					            <div class="flex items-center gap-1 justify-between w-full">
 | 
				
			||||||
 | 
					              <span class="min-w-0 truncate">{{ item.label }}</span>
 | 
				
			||||||
 | 
					              <Icon
 | 
				
			||||||
 | 
					                :icon="
 | 
				
			||||||
 | 
					                  expandedItems.has(item.key)
 | 
				
			||||||
 | 
					                    ? 'i-lucide-chevron-up'
 | 
				
			||||||
 | 
					                    : 'i-lucide-chevron-down'
 | 
				
			||||||
 | 
					                "
 | 
				
			||||||
 | 
					                class="text-n-slate-10 size-3 transition-all duration-300 ease-in-out"
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </template>
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <!-- Sliding Submenu -->
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          v-if="item.subMenuItems"
 | 
				
			||||||
 | 
					          class="overflow-hidden transition-all duration-300 ease-in-out"
 | 
				
			||||||
 | 
					          :class="
 | 
				
			||||||
 | 
					            expandedItems.has(item.key)
 | 
				
			||||||
 | 
					              ? 'max-h-96 opacity-100'
 | 
				
			||||||
 | 
					              : 'max-h-0 opacity-0'
 | 
				
			||||||
 | 
					          "
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div class="ltr:pl-5 rtl:pr-5 pt-2 flex flex-col items-start gap-2">
 | 
				
			||||||
 | 
					            <Button
 | 
				
			||||||
 | 
					              v-for="subItem in item.subMenuItems"
 | 
				
			||||||
 | 
					              :key="subItem.key + subItem.label"
 | 
				
			||||||
 | 
					              :label="subItem.label"
 | 
				
			||||||
 | 
					              slate
 | 
				
			||||||
 | 
					              link
 | 
				
			||||||
 | 
					              sm
 | 
				
			||||||
 | 
					              class="hover:!no-underline text-n-slate-12 font-normal text-xs"
 | 
				
			||||||
 | 
					              @click="handleSubMenuItemClick(item, subItem)"
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div v-if="menuItems.length > 0" class="h-px w-full bg-n-strong" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="flex flex-col items-start gap-3">
 | 
				
			||||||
 | 
					      <Button
 | 
				
			||||||
 | 
					        v-for="(item, index) in generalMenuItems"
 | 
				
			||||||
 | 
					        :key="index"
 | 
				
			||||||
 | 
					        :label="item.label"
 | 
				
			||||||
 | 
					        :icon="item.icon"
 | 
				
			||||||
 | 
					        slate
 | 
				
			||||||
 | 
					        link
 | 
				
			||||||
 | 
					        sm
 | 
				
			||||||
 | 
					        class="hover:!no-underline text-n-slate-12 font-normal text-xs"
 | 
				
			||||||
 | 
					        @click="handleMenuItemClick(item)"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </DropdownBody>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
@@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					<script setup>
 | 
				
			||||||
 | 
					import NextButton from 'dashboard/components-next/button/Button.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emit = defineEmits(['submit', 'cancel']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handleCancel = () => {
 | 
				
			||||||
 | 
					  emit('cancel');
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handleSubmit = () => {
 | 
				
			||||||
 | 
					  emit('submit');
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="flex justify-between items-center p-3 border-t border-n-weak">
 | 
				
			||||||
 | 
					    <NextButton
 | 
				
			||||||
 | 
					      label="Discard"
 | 
				
			||||||
 | 
					      slate
 | 
				
			||||||
 | 
					      link
 | 
				
			||||||
 | 
					      class="!px-1 hover:!no-underline"
 | 
				
			||||||
 | 
					      sm
 | 
				
			||||||
 | 
					      @click="handleCancel"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <NextButton
 | 
				
			||||||
 | 
					      label="Accept"
 | 
				
			||||||
 | 
					      class="bg-n-iris-9 text-white"
 | 
				
			||||||
 | 
					      solid
 | 
				
			||||||
 | 
					      sm
 | 
				
			||||||
 | 
					      @click="handleSubmit"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
@@ -16,6 +16,7 @@ import KeyboardEmojiSelector from './keyboardEmojiSelector.vue';
 | 
				
			|||||||
import TagAgents from '../conversation/TagAgents.vue';
 | 
					import TagAgents from '../conversation/TagAgents.vue';
 | 
				
			||||||
import VariableList from '../conversation/VariableList.vue';
 | 
					import VariableList from '../conversation/VariableList.vue';
 | 
				
			||||||
import TagTools from '../conversation/TagTools.vue';
 | 
					import TagTools from '../conversation/TagTools.vue';
 | 
				
			||||||
 | 
					import CopilotMenuBar from './CopilotMenuBar.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { useEmitter } from 'dashboard/composables/emitter';
 | 
					import { useEmitter } from 'dashboard/composables/emitter';
 | 
				
			||||||
import { useI18n } from 'vue-i18n';
 | 
					import { useI18n } from 'vue-i18n';
 | 
				
			||||||
@@ -23,11 +24,12 @@ import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
 | 
				
			|||||||
import { useTrack } from 'dashboard/composables';
 | 
					import { useTrack } from 'dashboard/composables';
 | 
				
			||||||
import { useUISettings } from 'dashboard/composables/useUISettings';
 | 
					import { useUISettings } from 'dashboard/composables/useUISettings';
 | 
				
			||||||
import { useAlert } from 'dashboard/composables';
 | 
					import { useAlert } from 'dashboard/composables';
 | 
				
			||||||
 | 
					import { vOnClickOutside } from '@vueuse/components';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
 | 
					import { BUS_EVENTS } from 'shared/constants/busEvents';
 | 
				
			||||||
import { CONVERSATION_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
 | 
					import { CONVERSATION_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  MESSAGE_EDITOR_MENU_OPTIONS,
 | 
					  MESSAGE_EDITOR_MENU_OPTIONS_WITHOUT_COPILLOT,
 | 
				
			||||||
  MESSAGE_EDITOR_IMAGE_RESIZES,
 | 
					  MESSAGE_EDITOR_IMAGE_RESIZES,
 | 
				
			||||||
} from 'dashboard/constants/editor';
 | 
					} from 'dashboard/constants/editor';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -97,6 +99,7 @@ const emit = defineEmits([
 | 
				
			|||||||
  'focus',
 | 
					  'focus',
 | 
				
			||||||
  'input',
 | 
					  'input',
 | 
				
			||||||
  'update:modelValue',
 | 
					  'update:modelValue',
 | 
				
			||||||
 | 
					  'executeAction',
 | 
				
			||||||
]);
 | 
					]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const { t } = useI18n();
 | 
					const { t } = useI18n();
 | 
				
			||||||
@@ -153,6 +156,8 @@ const range = ref(null);
 | 
				
			|||||||
const isImageNodeSelected = ref(false);
 | 
					const isImageNodeSelected = ref(false);
 | 
				
			||||||
const toolbarPosition = ref({ top: 0, left: 0 });
 | 
					const toolbarPosition = ref({ top: 0, left: 0 });
 | 
				
			||||||
const selectedImageNode = ref(null);
 | 
					const selectedImageNode = ref(null);
 | 
				
			||||||
 | 
					const isTextSelected = ref(false); // Tracks text selection and prevents unnecessary re-renders on mouse selection
 | 
				
			||||||
 | 
					const showSelectionMenu = ref(false);
 | 
				
			||||||
const sizes = MESSAGE_EDITOR_IMAGE_RESIZES;
 | 
					const sizes = MESSAGE_EDITOR_IMAGE_RESIZES;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// element ref
 | 
					// element ref
 | 
				
			||||||
@@ -160,6 +165,21 @@ const editorRoot = useTemplateRef('editorRoot');
 | 
				
			|||||||
const imageUpload = useTemplateRef('imageUpload');
 | 
					const imageUpload = useTemplateRef('imageUpload');
 | 
				
			||||||
const editor = useTemplateRef('editor');
 | 
					const editor = useTemplateRef('editor');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const handleCopilotAction = actionKey => {
 | 
				
			||||||
 | 
					  if (actionKey === 'rephrase_selection' && editorView?.state) {
 | 
				
			||||||
 | 
					    const { from, to } = editorView.state.selection;
 | 
				
			||||||
 | 
					    const selectedText = editorView.state.doc.textBetween(from, to).trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (from !== to && selectedText) {
 | 
				
			||||||
 | 
					      emit('executeAction', 'rephrase', selectedText);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    emit('executeAction', actionKey);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  showSelectionMenu.value = false;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const contentFromEditor = () => {
 | 
					const contentFromEditor = () => {
 | 
				
			||||||
  return MessageMarkdownSerializer.serialize(editorView.state.doc);
 | 
					  return MessageMarkdownSerializer.serialize(editorView.state.doc);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@@ -177,7 +197,7 @@ const shouldShowCannedResponses = computed(() => {
 | 
				
			|||||||
const editorMenuOptions = computed(() => {
 | 
					const editorMenuOptions = computed(() => {
 | 
				
			||||||
  return props.enabledMenuOptions.length
 | 
					  return props.enabledMenuOptions.length
 | 
				
			||||||
    ? props.enabledMenuOptions
 | 
					    ? props.enabledMenuOptions
 | 
				
			||||||
    : MESSAGE_EDITOR_MENU_OPTIONS;
 | 
					    : MESSAGE_EDITOR_MENU_OPTIONS_WITHOUT_COPILLOT;
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function createSuggestionPlugin({
 | 
					function createSuggestionPlugin({
 | 
				
			||||||
@@ -334,13 +354,23 @@ function openFileBrowser() {
 | 
				
			|||||||
  imageUpload.value.click();
 | 
					  imageUpload.value.click();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function handleCopilotClick() {
 | 
				
			||||||
 | 
					  showSelectionMenu.value = !showSelectionMenu.value;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function handleClickOutside(event) {
 | 
				
			||||||
 | 
					  // Check if the clicked element or its parents have the ignored class
 | 
				
			||||||
 | 
					  if (event.target.closest('.ProseMirror-copilot')) return;
 | 
				
			||||||
 | 
					  showSelectionMenu.value = false;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function reloadState(content = props.modelValue) {
 | 
					function reloadState(content = props.modelValue) {
 | 
				
			||||||
  const unrefContent = unref(content);
 | 
					  const unrefContent = unref(content);
 | 
				
			||||||
  state = createState(
 | 
					  state = createState(
 | 
				
			||||||
    unrefContent,
 | 
					    unrefContent,
 | 
				
			||||||
    props.placeholder,
 | 
					    props.placeholder,
 | 
				
			||||||
    plugins.value,
 | 
					    plugins.value,
 | 
				
			||||||
    { onImageUpload: openFileBrowser },
 | 
					    { onCopilotClick: handleCopilotClick },
 | 
				
			||||||
    editorMenuOptions.value
 | 
					    editorMenuOptions.value
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -391,6 +421,54 @@ function setToolbarPosition() {
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function setMenubarPosition(editorState) {
 | 
				
			||||||
 | 
					  if (!editorState?.selection) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { from, to } = editorState.selection;
 | 
				
			||||||
 | 
					  const wrapper = editorRoot.value;
 | 
				
			||||||
 | 
					  if (!wrapper) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const {
 | 
				
			||||||
 | 
					    left: editorLeft,
 | 
				
			||||||
 | 
					    top: editorTop,
 | 
				
			||||||
 | 
					    width: editorWidth,
 | 
				
			||||||
 | 
					  } = wrapper.getBoundingClientRect();
 | 
				
			||||||
 | 
					  const start = editorView.coordsAtPos(from);
 | 
				
			||||||
 | 
					  const end = editorView.coordsAtPos(to);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Calculate selection center and top
 | 
				
			||||||
 | 
					  const selCenterX =
 | 
				
			||||||
 | 
					    (Math.min(start.left, end.left) + Math.max(start.right, end.right)) / 2;
 | 
				
			||||||
 | 
					  const selTop = Math.min(start.top, end.top);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Clamp center position to keep menubar within editor bounds (with translateX(-50%))
 | 
				
			||||||
 | 
					  const menubarWidth = 560;
 | 
				
			||||||
 | 
					  const clampedCenterX = Math.max(
 | 
				
			||||||
 | 
					    editorLeft + menubarWidth / 4,
 | 
				
			||||||
 | 
					    Math.min(selCenterX, editorLeft + editorWidth - menubarWidth / 4)
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Set CSS custom properties for editor menubar
 | 
				
			||||||
 | 
					  wrapper.style.setProperty(
 | 
				
			||||||
 | 
					    '--selection-left',
 | 
				
			||||||
 | 
					    `${clampedCenterX - editorLeft}px`
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  wrapper.style.setProperty('--selection-top', `${selTop - editorTop - 50}px`);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function checkSelection(editorState) {
 | 
				
			||||||
 | 
					  showSelectionMenu.value = false;
 | 
				
			||||||
 | 
					  const hasSelection = editorState.selection.from !== editorState.selection.to;
 | 
				
			||||||
 | 
					  if (hasSelection === isTextSelected.value) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  isTextSelected.value = hasSelection;
 | 
				
			||||||
 | 
					  const wrapper = editorRoot.value;
 | 
				
			||||||
 | 
					  if (!wrapper) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  wrapper.classList.toggle('has-selection', hasSelection);
 | 
				
			||||||
 | 
					  if (hasSelection) setMenubarPosition(editorState);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function setURLWithQueryAndImageSize(size) {
 | 
					function setURLWithQueryAndImageSize(size) {
 | 
				
			||||||
  if (!props.showImageResizeToolbar) {
 | 
					  if (!props.showImageResizeToolbar) {
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
@@ -587,6 +665,7 @@ function createEditorView() {
 | 
				
			|||||||
      if (tx.docChanged) {
 | 
					      if (tx.docChanged) {
 | 
				
			||||||
        emitOnChange();
 | 
					        emitOnChange();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      checkSelection(state);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    handleDOMEvents: {
 | 
					    handleDOMEvents: {
 | 
				
			||||||
      keyup: () => {
 | 
					      keyup: () => {
 | 
				
			||||||
@@ -719,6 +798,14 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
 | 
				
			|||||||
      :search-key="toolSearchKey"
 | 
					      :search-key="toolSearchKey"
 | 
				
			||||||
      @select-tool="content => insertSpecialContent('tool', content)"
 | 
					      @select-tool="content => insertSpecialContent('tool', content)"
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
 | 
					    <CopilotMenuBar
 | 
				
			||||||
 | 
					      v-if="showSelectionMenu"
 | 
				
			||||||
 | 
					      v-on-click-outside="handleClickOutside"
 | 
				
			||||||
 | 
					      :has-selection="isTextSelected"
 | 
				
			||||||
 | 
					      :show-selection-menu="showSelectionMenu"
 | 
				
			||||||
 | 
					      :show-general-menu="false"
 | 
				
			||||||
 | 
					      @execute-action="handleCopilotAction"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
    <input
 | 
					    <input
 | 
				
			||||||
      ref="imageUpload"
 | 
					      ref="imageUpload"
 | 
				
			||||||
      type="file"
 | 
					      type="file"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -282,7 +282,10 @@ export default {
 | 
				
			|||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="flex justify-between p-3" :class="wrapClass">
 | 
					  <div
 | 
				
			||||||
 | 
					    class="flex justify-between items-center p-3 border-t border-n-weak"
 | 
				
			||||||
 | 
					    :class="wrapClass"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
    <div class="left-wrap">
 | 
					    <div class="left-wrap">
 | 
				
			||||||
      <NextButton
 | 
					      <NextButton
 | 
				
			||||||
        v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
 | 
					        v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,21 @@
 | 
				
			|||||||
<script>
 | 
					<script>
 | 
				
			||||||
 | 
					import { ref } from 'vue';
 | 
				
			||||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
 | 
					import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
 | 
				
			||||||
 | 
					import { vOnClickOutside } from '@vueuse/components';
 | 
				
			||||||
import { REPLY_EDITOR_MODES, CHAR_LENGTH_WARNING } from './constants';
 | 
					import { REPLY_EDITOR_MODES, CHAR_LENGTH_WARNING } from './constants';
 | 
				
			||||||
import NextButton from 'dashboard/components-next/button/Button.vue';
 | 
					import NextButton from 'dashboard/components-next/button/Button.vue';
 | 
				
			||||||
import EditorModeToggle from './EditorModeToggle.vue';
 | 
					import EditorModeToggle from './EditorModeToggle.vue';
 | 
				
			||||||
 | 
					import CopilotMenuBar from './CopilotMenuBar.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: 'ReplyTopPanel',
 | 
					  name: 'ReplyTopPanel',
 | 
				
			||||||
  components: {
 | 
					  components: {
 | 
				
			||||||
    NextButton,
 | 
					    NextButton,
 | 
				
			||||||
    EditorModeToggle,
 | 
					    EditorModeToggle,
 | 
				
			||||||
 | 
					    CopilotMenuBar,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  directives: {
 | 
				
			||||||
 | 
					    OnClickOutside: vOnClickOutside,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
    mode: {
 | 
					    mode: {
 | 
				
			||||||
@@ -28,7 +35,7 @@ export default {
 | 
				
			|||||||
      default: () => 0,
 | 
					      default: () => 0,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  emits: ['setReplyMode', 'togglePopout'],
 | 
					  emits: ['setReplyMode', 'togglePopout', 'executeAction'],
 | 
				
			||||||
  setup(props, { emit }) {
 | 
					  setup(props, { emit }) {
 | 
				
			||||||
    const setReplyMode = mode => {
 | 
					    const setReplyMode = mode => {
 | 
				
			||||||
      emit('setReplyMode', mode);
 | 
					      emit('setReplyMode', mode);
 | 
				
			||||||
@@ -47,6 +54,22 @@ export default {
 | 
				
			|||||||
          : REPLY_EDITOR_MODES.REPLY;
 | 
					          : REPLY_EDITOR_MODES.REPLY;
 | 
				
			||||||
      setReplyMode(newMode);
 | 
					      setReplyMode(newMode);
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const showCopilotMenu = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleCopilotAction = actionKey => {
 | 
				
			||||||
 | 
					      emit('executeAction', actionKey);
 | 
				
			||||||
 | 
					      showCopilotMenu.value = false;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const toggleCopilotMenu = () => {
 | 
				
			||||||
 | 
					      showCopilotMenu.value = !showCopilotMenu.value;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleClickOutside = () => {
 | 
				
			||||||
 | 
					      showCopilotMenu.value = false;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const keyboardEvents = {
 | 
					    const keyboardEvents = {
 | 
				
			||||||
      'Alt+KeyP': {
 | 
					      'Alt+KeyP': {
 | 
				
			||||||
        action: () => handleNoteClick(),
 | 
					        action: () => handleNoteClick(),
 | 
				
			||||||
@@ -64,6 +87,10 @@ export default {
 | 
				
			|||||||
      handleReplyClick,
 | 
					      handleReplyClick,
 | 
				
			||||||
      handleNoteClick,
 | 
					      handleNoteClick,
 | 
				
			||||||
      REPLY_EDITOR_MODES,
 | 
					      REPLY_EDITOR_MODES,
 | 
				
			||||||
 | 
					      handleCopilotAction,
 | 
				
			||||||
 | 
					      showCopilotMenu,
 | 
				
			||||||
 | 
					      toggleCopilotMenu,
 | 
				
			||||||
 | 
					      handleClickOutside,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
@@ -90,11 +117,12 @@ export default {
 | 
				
			|||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="flex justify-between h-[3.25rem] gap-2 ltr:pl-3 rtl:pr-3">
 | 
					  <div
 | 
				
			||||||
 | 
					    class="flex justify-between gap-2 h-[3.25rem] items-center ltr:pl-3 ltr:pr-2 rtl:pr-3 rtl:pl-2"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
    <EditorModeToggle
 | 
					    <EditorModeToggle
 | 
				
			||||||
      :mode="mode"
 | 
					      :mode="mode"
 | 
				
			||||||
      :disabled="isReplyRestricted"
 | 
					      :disabled="isReplyRestricted"
 | 
				
			||||||
      class="mt-3"
 | 
					 | 
				
			||||||
      @toggle-mode="handleModeToggle"
 | 
					      @toggle-mode="handleModeToggle"
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
    <div class="flex items-center mx-4 my-0">
 | 
					    <div class="flex items-center mx-4 my-0">
 | 
				
			||||||
@@ -104,11 +132,33 @@ export default {
 | 
				
			|||||||
        </span>
 | 
					        </span>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="flex items-center gap-2">
 | 
				
			||||||
 | 
					      <div class="relative">
 | 
				
			||||||
        <NextButton
 | 
					        <NextButton
 | 
				
			||||||
          ghost
 | 
					          ghost
 | 
				
			||||||
      class="ltr:rounded-bl-md rtl:rounded-br-md ltr:rounded-br-none rtl:rounded-bl-none ltr:rounded-tl-none rtl:rounded-tr-none text-n-slate-11 ltr:rounded-tr-[11px] rtl:rounded-tl-[11px]"
 | 
					          :class="{
 | 
				
			||||||
 | 
					            'text-n-violet-9 hover:!bg-n-violet-3': !showCopilotMenu,
 | 
				
			||||||
 | 
					            'text-n-violet-9 bg-n-violet-3': showCopilotMenu,
 | 
				
			||||||
 | 
					          }"
 | 
				
			||||||
 | 
					          sm
 | 
				
			||||||
 | 
					          icon="i-ph-sparkle-fill"
 | 
				
			||||||
 | 
					          @click="toggleCopilotMenu"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					        <CopilotMenuBar
 | 
				
			||||||
 | 
					          v-if="showCopilotMenu"
 | 
				
			||||||
 | 
					          v-on-click-outside="handleClickOutside"
 | 
				
			||||||
 | 
					          :has-selection="false"
 | 
				
			||||||
 | 
					          class="ltr:right-0 rtl:left-0 bottom-full mb-2"
 | 
				
			||||||
 | 
					          @execute-action="handleCopilotAction"
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <NextButton
 | 
				
			||||||
 | 
					        ghost
 | 
				
			||||||
 | 
					        class="text-n-slate-11"
 | 
				
			||||||
 | 
					        sm
 | 
				
			||||||
        icon="i-lucide-maximize-2"
 | 
					        icon="i-lucide-maximize-2"
 | 
				
			||||||
        @click="$emit('togglePopout')"
 | 
					        @click="$emit('togglePopout')"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,88 @@
 | 
				
			|||||||
 | 
					<script setup>
 | 
				
			||||||
 | 
					import { computed } from 'vue';
 | 
				
			||||||
 | 
					import CopilotEditor from 'dashboard/components/widgets/WootWriter/CopilotEditor.vue';
 | 
				
			||||||
 | 
					import Icon from 'next/icon/Icon.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const props = defineProps({
 | 
				
			||||||
 | 
					  showCopilotEditor: {
 | 
				
			||||||
 | 
					    type: Boolean,
 | 
				
			||||||
 | 
					    default: false,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  isGeneratingContent: {
 | 
				
			||||||
 | 
					    type: Boolean,
 | 
				
			||||||
 | 
					    default: false,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  copilotEditorContent: {
 | 
				
			||||||
 | 
					    type: String,
 | 
				
			||||||
 | 
					    default: '',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  generatedContent: {
 | 
				
			||||||
 | 
					    type: String,
 | 
				
			||||||
 | 
					    default: '',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  updateEditorSelectionWith: {
 | 
				
			||||||
 | 
					    type: String,
 | 
				
			||||||
 | 
					    default: '',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emit = defineEmits([
 | 
				
			||||||
 | 
					  'update:copilotEditorContent',
 | 
				
			||||||
 | 
					  'focus',
 | 
				
			||||||
 | 
					  'blur',
 | 
				
			||||||
 | 
					  'clearSelection',
 | 
				
			||||||
 | 
					]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const copilotContent = computed({
 | 
				
			||||||
 | 
					  get: () => props.copilotEditorContent,
 | 
				
			||||||
 | 
					  set: value => emit('update:copilotEditorContent', value),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const onFocus = () => {
 | 
				
			||||||
 | 
					  emit('focus');
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const onBlur = () => {
 | 
				
			||||||
 | 
					  emit('blur');
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const clearEditorSelection = () => {
 | 
				
			||||||
 | 
					  emit('clearSelection');
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <CopilotEditor
 | 
				
			||||||
 | 
					    v-if="showCopilotEditor && !isGeneratingContent"
 | 
				
			||||||
 | 
					    v-model="copilotContent"
 | 
				
			||||||
 | 
					    class="copilot-editor"
 | 
				
			||||||
 | 
					    :generated-content="generatedContent"
 | 
				
			||||||
 | 
					    :update-selection-with="updateEditorSelectionWith"
 | 
				
			||||||
 | 
					    :min-height="4"
 | 
				
			||||||
 | 
					    :enabled-menu-options="[]"
 | 
				
			||||||
 | 
					    @focus="onFocus"
 | 
				
			||||||
 | 
					    @blur="onBlur"
 | 
				
			||||||
 | 
					    @clear-selection="clearEditorSelection"
 | 
				
			||||||
 | 
					  />
 | 
				
			||||||
 | 
					  <div
 | 
				
			||||||
 | 
					    v-else-if="isGeneratingContent"
 | 
				
			||||||
 | 
					    class="bg-n-iris-5 rounded min-h-28 w-full mb-4 p-4 flex items-start"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <div class="flex items-center gap-2">
 | 
				
			||||||
 | 
					      <Icon
 | 
				
			||||||
 | 
					        icon="i-fluent-bubble-multiple-20-filled"
 | 
				
			||||||
 | 
					        class="text-n-iris-10 size-4 animate-spin"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <!-- eslint-disable-next-line vue/no-bare-strings-in-template -->
 | 
				
			||||||
 | 
					      <span class="text-sm text-n-iris-9"> Copilot is thinking </span>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss">
 | 
				
			||||||
 | 
					.copilot-editor {
 | 
				
			||||||
 | 
					  .ProseMirror-menubar {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
@@ -14,7 +14,9 @@ import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview.v
 | 
				
			|||||||
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue';
 | 
					import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue';
 | 
				
			||||||
import ReplyEmailHead from './ReplyEmailHead.vue';
 | 
					import ReplyEmailHead from './ReplyEmailHead.vue';
 | 
				
			||||||
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
 | 
					import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
 | 
				
			||||||
 | 
					import CopilotReplyBottomPanel from 'dashboard/components/widgets/WootWriter/CopilotReplyBottomPanel.vue';
 | 
				
			||||||
import ArticleSearchPopover from 'dashboard/routes/dashboard/helpcenter/components/ArticleSearch/SearchPopover.vue';
 | 
					import ArticleSearchPopover from 'dashboard/routes/dashboard/helpcenter/components/ArticleSearch/SearchPopover.vue';
 | 
				
			||||||
 | 
					import CopilotEditorSection from './CopilotEditorSection.vue';
 | 
				
			||||||
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert.vue';
 | 
					import MessageSignatureMissingAlert from './MessageSignatureMissingAlert.vue';
 | 
				
			||||||
import ReplyBoxBanner from './ReplyBoxBanner.vue';
 | 
					import ReplyBoxBanner from './ReplyBoxBanner.vue';
 | 
				
			||||||
import QuotedEmailPreview from './QuotedEmailPreview.vue';
 | 
					import QuotedEmailPreview from './QuotedEmailPreview.vue';
 | 
				
			||||||
@@ -48,7 +50,9 @@ import {
 | 
				
			|||||||
  replaceSignature,
 | 
					  replaceSignature,
 | 
				
			||||||
  extractTextFromMarkdown,
 | 
					  extractTextFromMarkdown,
 | 
				
			||||||
} from 'dashboard/helper/editorHelper';
 | 
					} from 'dashboard/helper/editorHelper';
 | 
				
			||||||
 | 
					import { MESSAGE_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
 | 
				
			||||||
 | 
					import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
 | 
				
			||||||
 | 
					import { useAI } from 'dashboard/composables/useAI';
 | 
				
			||||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
 | 
					import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
 | 
				
			||||||
import { LocalStorage } from 'shared/helpers/localStorage';
 | 
					import { LocalStorage } from 'shared/helpers/localStorage';
 | 
				
			||||||
import { emitter } from 'shared/helpers/mitt';
 | 
					import { emitter } from 'shared/helpers/mitt';
 | 
				
			||||||
@@ -74,6 +78,8 @@ export default {
 | 
				
			|||||||
    WhatsappTemplates,
 | 
					    WhatsappTemplates,
 | 
				
			||||||
    WootMessageEditor,
 | 
					    WootMessageEditor,
 | 
				
			||||||
    QuotedEmailPreview,
 | 
					    QuotedEmailPreview,
 | 
				
			||||||
 | 
					    CopilotEditorSection,
 | 
				
			||||||
 | 
					    CopilotReplyBottomPanel,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
 | 
					  mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
@@ -95,6 +101,9 @@ export default {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const replyEditor = useTemplateRef('replyEditor');
 | 
					    const replyEditor = useTemplateRef('replyEditor');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { formatMessage } = useMessageFormatter();
 | 
				
			||||||
 | 
					    const { draftMessage, processEvent, recordAnalytics } = useAI();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      uiSettings,
 | 
					      uiSettings,
 | 
				
			||||||
      updateUISettings,
 | 
					      updateUISettings,
 | 
				
			||||||
@@ -103,6 +112,10 @@ export default {
 | 
				
			|||||||
      setQuotedReplyFlagForInbox,
 | 
					      setQuotedReplyFlagForInbox,
 | 
				
			||||||
      fetchQuotedReplyFlagFromUISettings,
 | 
					      fetchQuotedReplyFlagFromUISettings,
 | 
				
			||||||
      replyEditor,
 | 
					      replyEditor,
 | 
				
			||||||
 | 
					      draftMessage,
 | 
				
			||||||
 | 
					      processEvent,
 | 
				
			||||||
 | 
					      recordAnalytics,
 | 
				
			||||||
 | 
					      formatMessage,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
@@ -134,6 +147,11 @@ export default {
 | 
				
			|||||||
      newConversationModalActive: false,
 | 
					      newConversationModalActive: false,
 | 
				
			||||||
      showArticleSearchPopover: false,
 | 
					      showArticleSearchPopover: false,
 | 
				
			||||||
      hasRecordedAudio: false,
 | 
					      hasRecordedAudio: false,
 | 
				
			||||||
 | 
					      editorMenuOptions: MESSAGE_EDITOR_MENU_OPTIONS,
 | 
				
			||||||
 | 
					      showCopilotEditor: false,
 | 
				
			||||||
 | 
					      isGeneratingContent: false,
 | 
				
			||||||
 | 
					      copilotEditorContent: '',
 | 
				
			||||||
 | 
					      generatedContent: '',
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
@@ -891,6 +909,21 @@ export default {
 | 
				
			|||||||
        this.insertIntoTextEditor(content, selectionStart, selectionEnd);
 | 
					        this.insertIntoTextEditor(content, selectionStart, selectionEnd);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    async executeAIAction(action, data) {
 | 
				
			||||||
 | 
					      if (action === 'ask_copilot') {
 | 
				
			||||||
 | 
					        this.updateUISettings({
 | 
				
			||||||
 | 
					          is_contact_sidebar_open: false,
 | 
				
			||||||
 | 
					          is_copilot_panel_open: true,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        this.isGeneratingContent = true;
 | 
				
			||||||
 | 
					        // For full message operations
 | 
				
			||||||
 | 
					        const content = await this.processEvent(action, data);
 | 
				
			||||||
 | 
					        this.generatedContent = content;
 | 
				
			||||||
 | 
					        if (content) this.toggleCopilotEditor();
 | 
				
			||||||
 | 
					        this.isGeneratingContent = false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    clearMessage() {
 | 
					    clearMessage() {
 | 
				
			||||||
      this.message = '';
 | 
					      this.message = '';
 | 
				
			||||||
      if (this.sendWithSignature && !this.isPrivate) {
 | 
					      if (this.sendWithSignature && !this.isPrivate) {
 | 
				
			||||||
@@ -1155,6 +1188,13 @@ export default {
 | 
				
			|||||||
    togglePopout() {
 | 
					    togglePopout() {
 | 
				
			||||||
      this.$emit('update:popOutReplyBox', !this.popOutReplyBox);
 | 
					      this.$emit('update:popOutReplyBox', !this.popOutReplyBox);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    onSubmitCopilotReply() {
 | 
				
			||||||
 | 
					      this.message = this.generatedContent;
 | 
				
			||||||
 | 
					      this.showCopilotEditor = false;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    toggleCopilotEditor() {
 | 
				
			||||||
 | 
					      this.showCopilotEditor = !this.showCopilotEditor;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
@@ -1170,6 +1210,8 @@ export default {
 | 
				
			|||||||
      :popout-reply-box="popOutReplyBox"
 | 
					      :popout-reply-box="popOutReplyBox"
 | 
				
			||||||
      @set-reply-mode="setReplyMode"
 | 
					      @set-reply-mode="setReplyMode"
 | 
				
			||||||
      @toggle-popout="togglePopout"
 | 
					      @toggle-popout="togglePopout"
 | 
				
			||||||
 | 
					      @toggle-copilot="toggleCopilotEditor"
 | 
				
			||||||
 | 
					      @execute-action="executeAIAction"
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
    <ArticleSearchPopover
 | 
					    <ArticleSearchPopover
 | 
				
			||||||
      v-if="showArticleSearchPopover && connectedPortalSlug"
 | 
					      v-if="showArticleSearchPopover && connectedPortalSlug"
 | 
				
			||||||
@@ -1228,11 +1270,22 @@ export default {
 | 
				
			|||||||
        @focus="onFocus"
 | 
					        @focus="onFocus"
 | 
				
			||||||
        @blur="onBlur"
 | 
					        @blur="onBlur"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
 | 
					      <CopilotEditorSection
 | 
				
			||||||
 | 
					        v-else-if="showCopilotEditor || isGeneratingContent"
 | 
				
			||||||
 | 
					        v-model:copilot-editor-content="copilotEditorContent"
 | 
				
			||||||
 | 
					        :show-copilot-editor="showCopilotEditor"
 | 
				
			||||||
 | 
					        :is-generating-content="isGeneratingContent"
 | 
				
			||||||
 | 
					        :generated-content="generatedContent"
 | 
				
			||||||
 | 
					        :update-editor-selection-with="updateEditorSelectionWith"
 | 
				
			||||||
 | 
					        @focus="onFocus"
 | 
				
			||||||
 | 
					        @blur="onBlur"
 | 
				
			||||||
 | 
					        @clear-selection="clearEditorSelection"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
      <WootMessageEditor
 | 
					      <WootMessageEditor
 | 
				
			||||||
        v-else
 | 
					        v-else
 | 
				
			||||||
        v-model="message"
 | 
					        v-model="message"
 | 
				
			||||||
        :editor-id="editorStateId"
 | 
					        :editor-id="editorStateId"
 | 
				
			||||||
        class="input"
 | 
					        class="input reply-editor"
 | 
				
			||||||
        :is-private="isOnPrivateNote"
 | 
					        :is-private="isOnPrivateNote"
 | 
				
			||||||
        :placeholder="messagePlaceHolder"
 | 
					        :placeholder="messagePlaceHolder"
 | 
				
			||||||
        :update-selection-with="updateEditorSelectionWith"
 | 
					        :update-selection-with="updateEditorSelectionWith"
 | 
				
			||||||
@@ -1242,6 +1295,7 @@ export default {
 | 
				
			|||||||
        :signature="signatureToApply"
 | 
					        :signature="signatureToApply"
 | 
				
			||||||
        allow-signature
 | 
					        allow-signature
 | 
				
			||||||
        :channel-type="channelType"
 | 
					        :channel-type="channelType"
 | 
				
			||||||
 | 
					        :enabled-menu-options="editorMenuOptions"
 | 
				
			||||||
        @typing-off="onTypingOff"
 | 
					        @typing-off="onTypingOff"
 | 
				
			||||||
        @typing-on="onTypingOn"
 | 
					        @typing-on="onTypingOn"
 | 
				
			||||||
        @focus="onFocus"
 | 
					        @focus="onFocus"
 | 
				
			||||||
@@ -1250,6 +1304,7 @@ export default {
 | 
				
			|||||||
        @toggle-canned-menu="toggleCannedMenu"
 | 
					        @toggle-canned-menu="toggleCannedMenu"
 | 
				
			||||||
        @toggle-variables-menu="toggleVariablesMenu"
 | 
					        @toggle-variables-menu="toggleVariablesMenu"
 | 
				
			||||||
        @clear-selection="clearEditorSelection"
 | 
					        @clear-selection="clearEditorSelection"
 | 
				
			||||||
 | 
					        @execute-action="executeAIAction"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      <QuotedEmailPreview
 | 
					      <QuotedEmailPreview
 | 
				
			||||||
        v-if="shouldShowQuotedPreview"
 | 
					        v-if="shouldShowQuotedPreview"
 | 
				
			||||||
@@ -1272,7 +1327,13 @@ export default {
 | 
				
			|||||||
    <MessageSignatureMissingAlert
 | 
					    <MessageSignatureMissingAlert
 | 
				
			||||||
      v-if="isSignatureEnabledForInbox && !isSignatureAvailable"
 | 
					      v-if="isSignatureEnabledForInbox && !isSignatureAvailable"
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
 | 
					    <CopilotReplyBottomPanel
 | 
				
			||||||
 | 
					      v-if="showCopilotEditor"
 | 
				
			||||||
 | 
					      @submit="onSubmitCopilotReply"
 | 
				
			||||||
 | 
					      @cancel="toggleCopilotEditor"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
    <ReplyBottomPanel
 | 
					    <ReplyBottomPanel
 | 
				
			||||||
 | 
					      v-else
 | 
				
			||||||
      :conversation-id="conversationId"
 | 
					      :conversation-id="conversationId"
 | 
				
			||||||
      :enable-multiple-file-upload="enableMultipleFileUpload"
 | 
					      :enable-multiple-file-upload="enableMultipleFileUpload"
 | 
				
			||||||
      :enable-whats-app-templates="showWhatsappTemplates"
 | 
					      :enable-whats-app-templates="showWhatsappTemplates"
 | 
				
			||||||
@@ -1331,7 +1392,7 @@ export default {
 | 
				
			|||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="scss" scoped>
 | 
					<style lang="scss">
 | 
				
			||||||
.send-button {
 | 
					.send-button {
 | 
				
			||||||
  @apply mb-0;
 | 
					  @apply mb-0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1384,4 +1445,54 @@ export default {
 | 
				
			|||||||
  width: calc(100% - 2 * 1rem);
 | 
					  width: calc(100% - 2 * 1rem);
 | 
				
			||||||
  left: 1rem;
 | 
					  left: 1rem;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.reply-editor {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .ProseMirror p:first-child {
 | 
				
			||||||
 | 
					    margin-top: 0 !important;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .ProseMirror p:last-child {
 | 
				
			||||||
 | 
					    margin-bottom: 10px !important;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Editor Menu
 | 
				
			||||||
 | 
					  .ProseMirror-menubar {
 | 
				
			||||||
 | 
					    display: none; // Hide by default
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &.has-selection {
 | 
				
			||||||
 | 
					    .ProseMirror-menubar {
 | 
				
			||||||
 | 
					      @apply rounded-lg !px-3 !py-2 z-50 bg-n-background items-center gap-4 ml-0 mb-0 shadow-md outline outline-1 outline-n-weak;
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      left: var(--selection-left);
 | 
				
			||||||
 | 
					      top: var(--selection-top);
 | 
				
			||||||
 | 
					      transform: translateX(-50%);
 | 
				
			||||||
 | 
					      width: fit-content !important;
 | 
				
			||||||
 | 
					      position: absolute !important;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .ProseMirror-menuitem {
 | 
				
			||||||
 | 
					        @apply mr-0 size-3.5 flex items-center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .ProseMirror-icon {
 | 
				
			||||||
 | 
					          @apply p-0 flex-shrink-0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          svg {
 | 
				
			||||||
 | 
					            width: 14px !important;
 | 
				
			||||||
 | 
					            height: 14px !important;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .ProseMirror-copilot svg {
 | 
				
			||||||
 | 
					          fill: #6e56cf;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .ProseMirror-menu-active {
 | 
				
			||||||
 | 
					        @apply bg-n-slate-3;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -158,14 +158,15 @@ export function useAI() {
 | 
				
			|||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Processes an AI event, such as rephrasing content.
 | 
					   * Processes an AI event, such as rephrasing content.
 | 
				
			||||||
   * @param {string} [type='rephrase'] - The type of AI event to process.
 | 
					   * @param {string} [type='rephrase'] - The type of AI event to process.
 | 
				
			||||||
 | 
					   * @param {string} [content=''] - The content to process (for full message) or selected text (for selection-based).
 | 
				
			||||||
   * @returns {Promise<string>} The generated message or an empty string if an error occurs.
 | 
					   * @returns {Promise<string>} The generated message or an empty string if an error occurs.
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  const processEvent = async (type = 'rephrase') => {
 | 
					  const processEvent = async (type = 'rephrase', content = '') => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const result = await OpenAPI.processEvent({
 | 
					      const result = await OpenAPI.processEvent({
 | 
				
			||||||
        hookId: hookId.value,
 | 
					        hookId: hookId.value,
 | 
				
			||||||
        type,
 | 
					        type,
 | 
				
			||||||
        content: draftMessage.value,
 | 
					        content: content || draftMessage.value,
 | 
				
			||||||
        conversationId: conversationId.value,
 | 
					        conversationId: conversationId.value,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      const {
 | 
					      const {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,16 @@
 | 
				
			|||||||
export const MESSAGE_EDITOR_MENU_OPTIONS = [
 | 
					export const MESSAGE_EDITOR_MENU_OPTIONS = [
 | 
				
			||||||
 | 
					  'copilot',
 | 
				
			||||||
 | 
					  'strong',
 | 
				
			||||||
 | 
					  'em',
 | 
				
			||||||
 | 
					  'link',
 | 
				
			||||||
 | 
					  'undo',
 | 
				
			||||||
 | 
					  'redo',
 | 
				
			||||||
 | 
					  'bulletList',
 | 
				
			||||||
 | 
					  'orderedList',
 | 
				
			||||||
 | 
					  'code',
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const MESSAGE_EDITOR_MENU_OPTIONS_WITHOUT_COPILLOT = [
 | 
				
			||||||
  'strong',
 | 
					  'strong',
 | 
				
			||||||
  'em',
 | 
					  'em',
 | 
				
			||||||
  'link',
 | 
					  'link',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -143,6 +143,22 @@
 | 
				
			|||||||
        "MAKE_FORMAL": "Use formal tone",
 | 
					        "MAKE_FORMAL": "Use formal tone",
 | 
				
			||||||
        "SIMPLIFY": "Simplify"
 | 
					        "SIMPLIFY": "Simplify"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      "REPLY_OPTIONS": {
 | 
				
			||||||
 | 
					        "IMPROVE_REPLY": "Improve reply",
 | 
				
			||||||
 | 
					        "IMPROVE_REPLY_SELECTION": "Improve the selection",
 | 
				
			||||||
 | 
					        "CHANGE_TONE": {
 | 
				
			||||||
 | 
					          "TITLE": "Change tone",
 | 
				
			||||||
 | 
					          "OPTIONS": {
 | 
				
			||||||
 | 
					            "FRIENDLY": "Friendly",
 | 
				
			||||||
 | 
					            "FORMAL": "Formal",
 | 
				
			||||||
 | 
					            "SIMPLIFY": "Simplify"
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "GRAMMAR": "Fix grammar & spelling",
 | 
				
			||||||
 | 
					        "SUGGESTION": "Suggest a reply",
 | 
				
			||||||
 | 
					        "SUMMARIZE": "Summarize the conversation",
 | 
				
			||||||
 | 
					        "ASK_COPILOT": "Ask Copilot"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      "ASSISTANCE_MODAL": {
 | 
					      "ASSISTANCE_MODAL": {
 | 
				
			||||||
        "DRAFT_TITLE": "Draft content",
 | 
					        "DRAFT_TITLE": "Draft content",
 | 
				
			||||||
        "GENERATED_TITLE": "Generated content",
 | 
					        "GENERATED_TITLE": "Generated content",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -33,7 +33,7 @@
 | 
				
			|||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@breezystack/lamejs": "^1.2.7",
 | 
					    "@breezystack/lamejs": "^1.2.7",
 | 
				
			||||||
    "@chatwoot/ninja-keys": "1.2.3",
 | 
					    "@chatwoot/ninja-keys": "1.2.3",
 | 
				
			||||||
    "@chatwoot/prosemirror-schema": "1.2.1",
 | 
					    "@chatwoot/prosemirror-schema": "1.2.2",
 | 
				
			||||||
    "@chatwoot/utils": "^0.0.51",
 | 
					    "@chatwoot/utils": "^0.0.51",
 | 
				
			||||||
    "@formkit/core": "^1.6.7",
 | 
					    "@formkit/core": "^1.6.7",
 | 
				
			||||||
    "@formkit/vue": "^1.6.7",
 | 
					    "@formkit/vue": "^1.6.7",
 | 
				
			||||||
@@ -109,6 +109,7 @@
 | 
				
			|||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@egoist/tailwindcss-icons": "^1.8.1",
 | 
					    "@egoist/tailwindcss-icons": "^1.8.1",
 | 
				
			||||||
    "@histoire/plugin-vue": "0.17.15",
 | 
					    "@histoire/plugin-vue": "0.17.15",
 | 
				
			||||||
 | 
					    "@iconify-json/fluent": "^1.2.32",
 | 
				
			||||||
    "@iconify-json/logos": "^1.2.3",
 | 
					    "@iconify-json/logos": "^1.2.3",
 | 
				
			||||||
    "@iconify-json/lucide": "^1.2.11",
 | 
					    "@iconify-json/lucide": "^1.2.11",
 | 
				
			||||||
    "@iconify-json/ph": "^1.2.1",
 | 
					    "@iconify-json/ph": "^1.2.1",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										20
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										20
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@@ -20,8 +20,8 @@ importers:
 | 
				
			|||||||
        specifier: 1.2.3
 | 
					        specifier: 1.2.3
 | 
				
			||||||
        version: 1.2.3
 | 
					        version: 1.2.3
 | 
				
			||||||
      '@chatwoot/prosemirror-schema':
 | 
					      '@chatwoot/prosemirror-schema':
 | 
				
			||||||
        specifier: 1.2.1
 | 
					        specifier: 1.2.2
 | 
				
			||||||
        version: 1.2.1
 | 
					        version: 1.2.2
 | 
				
			||||||
      '@chatwoot/utils':
 | 
					      '@chatwoot/utils':
 | 
				
			||||||
        specifier: ^0.0.51
 | 
					        specifier: ^0.0.51
 | 
				
			||||||
        version: 0.0.51
 | 
					        version: 0.0.51
 | 
				
			||||||
@@ -242,6 +242,9 @@ importers:
 | 
				
			|||||||
      '@histoire/plugin-vue':
 | 
					      '@histoire/plugin-vue':
 | 
				
			||||||
        specifier: 0.17.15
 | 
					        specifier: 0.17.15
 | 
				
			||||||
        version: 0.17.15(histoire@0.17.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)(vite@5.4.20(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)))(vite@5.4.20(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))(vue@3.5.12(typescript@5.6.2))
 | 
					        version: 0.17.15(histoire@0.17.15(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)(vite@5.4.20(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0)))(vite@5.4.20(@types/node@22.7.0)(sass@1.79.3)(terser@5.33.0))(vue@3.5.12(typescript@5.6.2))
 | 
				
			||||||
 | 
					      '@iconify-json/fluent':
 | 
				
			||||||
 | 
					        specifier: ^1.2.32
 | 
				
			||||||
 | 
					        version: 1.2.32
 | 
				
			||||||
      '@iconify-json/logos':
 | 
					      '@iconify-json/logos':
 | 
				
			||||||
        specifier: ^1.2.3
 | 
					        specifier: ^1.2.3
 | 
				
			||||||
        version: 1.2.3
 | 
					        version: 1.2.3
 | 
				
			||||||
@@ -406,8 +409,8 @@ packages:
 | 
				
			|||||||
  '@chatwoot/ninja-keys@1.2.3':
 | 
					  '@chatwoot/ninja-keys@1.2.3':
 | 
				
			||||||
    resolution: {integrity: sha512-xM8d9P5ikDMZm2WbaCTk/TW5HFauylrU3cJ75fq5je6ixKwyhl/0kZbVN/vbbZN4+AUX/OaSIn6IJbtCgIF67g==}
 | 
					    resolution: {integrity: sha512-xM8d9P5ikDMZm2WbaCTk/TW5HFauylrU3cJ75fq5je6ixKwyhl/0kZbVN/vbbZN4+AUX/OaSIn6IJbtCgIF67g==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@chatwoot/prosemirror-schema@1.2.1':
 | 
					  '@chatwoot/prosemirror-schema@1.2.2':
 | 
				
			||||||
    resolution: {integrity: sha512-UbiEvG5tgi1d0lMbkaqxgTh7vHfywEYKLQo1sxqp4Q7aLZh4QFtbLzJ2zyBtu4Nhipe+guFfEJdic7i43MP/XQ==}
 | 
					    resolution: {integrity: sha512-9knTH6OgZJ5qhJjS70Qiy0VsQwDAte6+gz+2PO1BM3RE+wUknkVS5YAcjqs1IZ4dSZyPrvNOOZh9mXoHdmtgnA==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@chatwoot/utils@0.0.51':
 | 
					  '@chatwoot/utils@0.0.51':
 | 
				
			||||||
    resolution: {integrity: sha512-WlEmWfOTzR7YZRUWzn5Wpm15/BRudpwqoNckph8TohyDbiim1CP4UZGa+qjajxTbNGLLhtKlm0Xl+X16+5Wceg==}
 | 
					    resolution: {integrity: sha512-WlEmWfOTzR7YZRUWzn5Wpm15/BRudpwqoNckph8TohyDbiim1CP4UZGa+qjajxTbNGLLhtKlm0Xl+X16+5Wceg==}
 | 
				
			||||||
@@ -881,6 +884,9 @@ packages:
 | 
				
			|||||||
    resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
 | 
					    resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
 | 
				
			||||||
    deprecated: Use @eslint/object-schema instead
 | 
					    deprecated: Use @eslint/object-schema instead
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  '@iconify-json/fluent@1.2.32':
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-YQFHsRrBhgrfvuVWJTg+EsVDA3pN4QwrFApZ2Di7J6YQIsNVNnBiEpGdT5qX4KxXFre3Yd4D7zm6EsWfKj4QBw==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@iconify-json/logos@1.2.3':
 | 
					  '@iconify-json/logos@1.2.3':
 | 
				
			||||||
    resolution: {integrity: sha512-JLHS5hgZP1b55EONAWNeqBUuriRfRNKWXK4cqYx0PpVaJfIIMiiMxFfvoQiX/bkE9XgkLhcKmDUqL3LXPdXPwQ==}
 | 
					    resolution: {integrity: sha512-JLHS5hgZP1b55EONAWNeqBUuriRfRNKWXK4cqYx0PpVaJfIIMiiMxFfvoQiX/bkE9XgkLhcKmDUqL3LXPdXPwQ==}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -4763,7 +4769,7 @@ snapshots:
 | 
				
			|||||||
      hotkeys-js: 3.8.7
 | 
					      hotkeys-js: 3.8.7
 | 
				
			||||||
      lit: 2.2.6
 | 
					      lit: 2.2.6
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@chatwoot/prosemirror-schema@1.2.1':
 | 
					  '@chatwoot/prosemirror-schema@1.2.2':
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      markdown-it-sup: 2.0.0
 | 
					      markdown-it-sup: 2.0.0
 | 
				
			||||||
      prosemirror-commands: 1.6.0
 | 
					      prosemirror-commands: 1.6.0
 | 
				
			||||||
@@ -5261,6 +5267,10 @@ snapshots:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  '@humanwhocodes/object-schema@2.0.3': {}
 | 
					  '@humanwhocodes/object-schema@2.0.3': {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  '@iconify-json/fluent@1.2.32':
 | 
				
			||||||
 | 
					    dependencies:
 | 
				
			||||||
 | 
					      '@iconify/types': 2.0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  '@iconify-json/logos@1.2.3':
 | 
					  '@iconify-json/logos@1.2.3':
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      '@iconify/types': 2.0.0
 | 
					      '@iconify/types': 2.0.0
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -258,6 +258,7 @@ const tailwindConfig = {
 | 
				
			|||||||
          'ph',
 | 
					          'ph',
 | 
				
			||||||
          'material-symbols',
 | 
					          'material-symbols',
 | 
				
			||||||
          'teenyicons',
 | 
					          'teenyicons',
 | 
				
			||||||
 | 
					          'fluent',
 | 
				
			||||||
        ]),
 | 
					        ]),
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -210,6 +210,21 @@ export const colors = {
 | 
				
			|||||||
      12: 'rgb(var(--gray-12) / <alpha-value>)',
 | 
					      12: 'rgb(var(--gray-12) / <alpha-value>)',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    violet: {
 | 
				
			||||||
 | 
					      1: 'rgb(var(--violet-1) / <alpha-value>)',
 | 
				
			||||||
 | 
					      2: 'rgb(var(--violet-2) / <alpha-value>)',
 | 
				
			||||||
 | 
					      3: 'rgb(var(--violet-3) / <alpha-value>)',
 | 
				
			||||||
 | 
					      4: 'rgb(var(--violet-4) / <alpha-value>)',
 | 
				
			||||||
 | 
					      5: 'rgb(var(--violet-5) / <alpha-value>)',
 | 
				
			||||||
 | 
					      6: 'rgb(var(--violet-6) / <alpha-value>)',
 | 
				
			||||||
 | 
					      7: 'rgb(var(--violet-7) / <alpha-value>)',
 | 
				
			||||||
 | 
					      8: 'rgb(var(--violet-8) / <alpha-value>)',
 | 
				
			||||||
 | 
					      9: 'rgb(var(--violet-9) / <alpha-value>)',
 | 
				
			||||||
 | 
					      10: 'rgb(var(--violet-10) / <alpha-value>)',
 | 
				
			||||||
 | 
					      11: 'rgb(var(--violet-11) / <alpha-value>)',
 | 
				
			||||||
 | 
					      12: 'rgb(var(--violet-12) / <alpha-value>)',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    black: '#000000',
 | 
					    black: '#000000',
 | 
				
			||||||
    brand: '#2781F6',
 | 
					    brand: '#2781F6',
 | 
				
			||||||
    background: 'rgb(var(--background-color) / <alpha-value>)',
 | 
					    background: 'rgb(var(--background-color) / <alpha-value>)',
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user