mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 12:08:01 +00:00
feat: Reply Editor AI Changes
This commit is contained in:
@@ -94,6 +94,19 @@
|
||||
--gray-11: 100 100 100;
|
||||
--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;
|
||||
--text-blue: 8 109 224;
|
||||
--border-container: 236 236 236;
|
||||
@@ -209,6 +222,19 @@
|
||||
--gray-11: 180 180 180;
|
||||
--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;
|
||||
--border-strong: 52 52 52;
|
||||
--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 VariableList from '../conversation/VariableList.vue';
|
||||
import TagTools from '../conversation/TagTools.vue';
|
||||
import CopilotMenuBar from './CopilotMenuBar.vue';
|
||||
|
||||
import { useEmitter } from 'dashboard/composables/emitter';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -23,11 +24,12 @@ import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import { CONVERSATION_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import {
|
||||
MESSAGE_EDITOR_MENU_OPTIONS,
|
||||
MESSAGE_EDITOR_MENU_OPTIONS_WITHOUT_COPILLOT,
|
||||
MESSAGE_EDITOR_IMAGE_RESIZES,
|
||||
} from 'dashboard/constants/editor';
|
||||
|
||||
@@ -97,6 +99,7 @@ const emit = defineEmits([
|
||||
'focus',
|
||||
'input',
|
||||
'update:modelValue',
|
||||
'executeAction',
|
||||
]);
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -153,6 +156,8 @@ const range = ref(null);
|
||||
const isImageNodeSelected = ref(false);
|
||||
const toolbarPosition = ref({ top: 0, left: 0 });
|
||||
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;
|
||||
|
||||
// element ref
|
||||
@@ -160,6 +165,21 @@ const editorRoot = useTemplateRef('editorRoot');
|
||||
const imageUpload = useTemplateRef('imageUpload');
|
||||
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 = () => {
|
||||
return MessageMarkdownSerializer.serialize(editorView.state.doc);
|
||||
};
|
||||
@@ -177,7 +197,7 @@ const shouldShowCannedResponses = computed(() => {
|
||||
const editorMenuOptions = computed(() => {
|
||||
return props.enabledMenuOptions.length
|
||||
? props.enabledMenuOptions
|
||||
: MESSAGE_EDITOR_MENU_OPTIONS;
|
||||
: MESSAGE_EDITOR_MENU_OPTIONS_WITHOUT_COPILLOT;
|
||||
});
|
||||
|
||||
function createSuggestionPlugin({
|
||||
@@ -334,13 +354,23 @@ function openFileBrowser() {
|
||||
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) {
|
||||
const unrefContent = unref(content);
|
||||
state = createState(
|
||||
unrefContent,
|
||||
props.placeholder,
|
||||
plugins.value,
|
||||
{ onImageUpload: openFileBrowser },
|
||||
{ onCopilotClick: handleCopilotClick },
|
||||
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) {
|
||||
if (!props.showImageResizeToolbar) {
|
||||
return;
|
||||
@@ -587,6 +665,7 @@ function createEditorView() {
|
||||
if (tx.docChanged) {
|
||||
emitOnChange();
|
||||
}
|
||||
checkSelection(state);
|
||||
},
|
||||
handleDOMEvents: {
|
||||
keyup: () => {
|
||||
@@ -719,6 +798,14 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
|
||||
:search-key="toolSearchKey"
|
||||
@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
|
||||
ref="imageUpload"
|
||||
type="file"
|
||||
|
||||
@@ -282,7 +282,10 @@ export default {
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<NextButton
|
||||
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
<script>
|
||||
import { ref } from 'vue';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
import { REPLY_EDITOR_MODES, CHAR_LENGTH_WARNING } from './constants';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import EditorModeToggle from './EditorModeToggle.vue';
|
||||
import CopilotMenuBar from './CopilotMenuBar.vue';
|
||||
|
||||
export default {
|
||||
name: 'ReplyTopPanel',
|
||||
components: {
|
||||
NextButton,
|
||||
EditorModeToggle,
|
||||
CopilotMenuBar,
|
||||
},
|
||||
directives: {
|
||||
OnClickOutside: vOnClickOutside,
|
||||
},
|
||||
props: {
|
||||
mode: {
|
||||
@@ -28,7 +35,7 @@ export default {
|
||||
default: () => 0,
|
||||
},
|
||||
},
|
||||
emits: ['setReplyMode', 'togglePopout'],
|
||||
emits: ['setReplyMode', 'togglePopout', 'executeAction'],
|
||||
setup(props, { emit }) {
|
||||
const setReplyMode = mode => {
|
||||
emit('setReplyMode', mode);
|
||||
@@ -47,6 +54,22 @@ export default {
|
||||
: REPLY_EDITOR_MODES.REPLY;
|
||||
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 = {
|
||||
'Alt+KeyP': {
|
||||
action: () => handleNoteClick(),
|
||||
@@ -64,6 +87,10 @@ export default {
|
||||
handleReplyClick,
|
||||
handleNoteClick,
|
||||
REPLY_EDITOR_MODES,
|
||||
handleCopilotAction,
|
||||
showCopilotMenu,
|
||||
toggleCopilotMenu,
|
||||
handleClickOutside,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -90,11 +117,12 @@ export default {
|
||||
</script>
|
||||
|
||||
<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
|
||||
:mode="mode"
|
||||
:disabled="isReplyRestricted"
|
||||
class="mt-3"
|
||||
@toggle-mode="handleModeToggle"
|
||||
/>
|
||||
<div class="flex items-center mx-4 my-0">
|
||||
@@ -104,11 +132,33 @@ export default {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<NextButton
|
||||
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]"
|
||||
icon="i-lucide-maximize-2"
|
||||
@click="$emit('togglePopout')"
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative">
|
||||
<NextButton
|
||||
ghost
|
||||
: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"
|
||||
@click="$emit('togglePopout')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</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 ReplyEmailHead from './ReplyEmailHead.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 CopilotEditorSection from './CopilotEditorSection.vue';
|
||||
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert.vue';
|
||||
import ReplyBoxBanner from './ReplyBoxBanner.vue';
|
||||
import QuotedEmailPreview from './QuotedEmailPreview.vue';
|
||||
@@ -48,7 +50,9 @@ import {
|
||||
replaceSignature,
|
||||
extractTextFromMarkdown,
|
||||
} 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 { LocalStorage } from 'shared/helpers/localStorage';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
@@ -74,6 +78,8 @@ export default {
|
||||
WhatsappTemplates,
|
||||
WootMessageEditor,
|
||||
QuotedEmailPreview,
|
||||
CopilotEditorSection,
|
||||
CopilotReplyBottomPanel,
|
||||
},
|
||||
mixins: [inboxMixin, fileUploadMixin, keyboardEventListenerMixins],
|
||||
props: {
|
||||
@@ -95,6 +101,9 @@ export default {
|
||||
|
||||
const replyEditor = useTemplateRef('replyEditor');
|
||||
|
||||
const { formatMessage } = useMessageFormatter();
|
||||
const { draftMessage, processEvent, recordAnalytics } = useAI();
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
updateUISettings,
|
||||
@@ -103,6 +112,10 @@ export default {
|
||||
setQuotedReplyFlagForInbox,
|
||||
fetchQuotedReplyFlagFromUISettings,
|
||||
replyEditor,
|
||||
draftMessage,
|
||||
processEvent,
|
||||
recordAnalytics,
|
||||
formatMessage,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
@@ -134,6 +147,11 @@ export default {
|
||||
newConversationModalActive: false,
|
||||
showArticleSearchPopover: false,
|
||||
hasRecordedAudio: false,
|
||||
editorMenuOptions: MESSAGE_EDITOR_MENU_OPTIONS,
|
||||
showCopilotEditor: false,
|
||||
isGeneratingContent: false,
|
||||
copilotEditorContent: '',
|
||||
generatedContent: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -891,6 +909,21 @@ export default {
|
||||
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() {
|
||||
this.message = '';
|
||||
if (this.sendWithSignature && !this.isPrivate) {
|
||||
@@ -1155,6 +1188,13 @@ export default {
|
||||
togglePopout() {
|
||||
this.$emit('update:popOutReplyBox', !this.popOutReplyBox);
|
||||
},
|
||||
onSubmitCopilotReply() {
|
||||
this.message = this.generatedContent;
|
||||
this.showCopilotEditor = false;
|
||||
},
|
||||
toggleCopilotEditor() {
|
||||
this.showCopilotEditor = !this.showCopilotEditor;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1170,6 +1210,8 @@ export default {
|
||||
:popout-reply-box="popOutReplyBox"
|
||||
@set-reply-mode="setReplyMode"
|
||||
@toggle-popout="togglePopout"
|
||||
@toggle-copilot="toggleCopilotEditor"
|
||||
@execute-action="executeAIAction"
|
||||
/>
|
||||
<ArticleSearchPopover
|
||||
v-if="showArticleSearchPopover && connectedPortalSlug"
|
||||
@@ -1228,11 +1270,22 @@ export default {
|
||||
@focus="onFocus"
|
||||
@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
|
||||
v-else
|
||||
v-model="message"
|
||||
:editor-id="editorStateId"
|
||||
class="input"
|
||||
class="input reply-editor"
|
||||
:is-private="isOnPrivateNote"
|
||||
:placeholder="messagePlaceHolder"
|
||||
:update-selection-with="updateEditorSelectionWith"
|
||||
@@ -1242,6 +1295,7 @@ export default {
|
||||
:signature="signatureToApply"
|
||||
allow-signature
|
||||
:channel-type="channelType"
|
||||
:enabled-menu-options="editorMenuOptions"
|
||||
@typing-off="onTypingOff"
|
||||
@typing-on="onTypingOn"
|
||||
@focus="onFocus"
|
||||
@@ -1250,6 +1304,7 @@ export default {
|
||||
@toggle-canned-menu="toggleCannedMenu"
|
||||
@toggle-variables-menu="toggleVariablesMenu"
|
||||
@clear-selection="clearEditorSelection"
|
||||
@execute-action="executeAIAction"
|
||||
/>
|
||||
<QuotedEmailPreview
|
||||
v-if="shouldShowQuotedPreview"
|
||||
@@ -1272,7 +1327,13 @@ export default {
|
||||
<MessageSignatureMissingAlert
|
||||
v-if="isSignatureEnabledForInbox && !isSignatureAvailable"
|
||||
/>
|
||||
<CopilotReplyBottomPanel
|
||||
v-if="showCopilotEditor"
|
||||
@submit="onSubmitCopilotReply"
|
||||
@cancel="toggleCopilotEditor"
|
||||
/>
|
||||
<ReplyBottomPanel
|
||||
v-else
|
||||
:conversation-id="conversationId"
|
||||
:enable-multiple-file-upload="enableMultipleFileUpload"
|
||||
:enable-whats-app-templates="showWhatsappTemplates"
|
||||
@@ -1331,7 +1392,7 @@ export default {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style lang="scss">
|
||||
.send-button {
|
||||
@apply mb-0;
|
||||
}
|
||||
@@ -1384,4 +1445,54 @@ export default {
|
||||
width: calc(100% - 2 * 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>
|
||||
|
||||
@@ -158,14 +158,15 @@ export function useAI() {
|
||||
/**
|
||||
* Processes an AI event, such as rephrasing content.
|
||||
* @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.
|
||||
*/
|
||||
const processEvent = async (type = 'rephrase') => {
|
||||
const processEvent = async (type = 'rephrase', content = '') => {
|
||||
try {
|
||||
const result = await OpenAPI.processEvent({
|
||||
hookId: hookId.value,
|
||||
type,
|
||||
content: draftMessage.value,
|
||||
content: content || draftMessage.value,
|
||||
conversationId: conversationId.value,
|
||||
});
|
||||
const {
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
export const MESSAGE_EDITOR_MENU_OPTIONS = [
|
||||
'copilot',
|
||||
'strong',
|
||||
'em',
|
||||
'link',
|
||||
'undo',
|
||||
'redo',
|
||||
'bulletList',
|
||||
'orderedList',
|
||||
'code',
|
||||
];
|
||||
|
||||
export const MESSAGE_EDITOR_MENU_OPTIONS_WITHOUT_COPILLOT = [
|
||||
'strong',
|
||||
'em',
|
||||
'link',
|
||||
|
||||
@@ -143,6 +143,22 @@
|
||||
"MAKE_FORMAL": "Use formal tone",
|
||||
"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": {
|
||||
"DRAFT_TITLE": "Draft content",
|
||||
"GENERATED_TITLE": "Generated content",
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"dependencies": {
|
||||
"@breezystack/lamejs": "^1.2.7",
|
||||
"@chatwoot/ninja-keys": "1.2.3",
|
||||
"@chatwoot/prosemirror-schema": "1.2.1",
|
||||
"@chatwoot/prosemirror-schema": "1.2.2",
|
||||
"@chatwoot/utils": "^0.0.51",
|
||||
"@formkit/core": "^1.6.7",
|
||||
"@formkit/vue": "^1.6.7",
|
||||
@@ -109,6 +109,7 @@
|
||||
"devDependencies": {
|
||||
"@egoist/tailwindcss-icons": "^1.8.1",
|
||||
"@histoire/plugin-vue": "0.17.15",
|
||||
"@iconify-json/fluent": "^1.2.32",
|
||||
"@iconify-json/logos": "^1.2.3",
|
||||
"@iconify-json/lucide": "^1.2.11",
|
||||
"@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
|
||||
version: 1.2.3
|
||||
'@chatwoot/prosemirror-schema':
|
||||
specifier: 1.2.1
|
||||
version: 1.2.1
|
||||
specifier: 1.2.2
|
||||
version: 1.2.2
|
||||
'@chatwoot/utils':
|
||||
specifier: ^0.0.51
|
||||
version: 0.0.51
|
||||
@@ -242,6 +242,9 @@ importers:
|
||||
'@histoire/plugin-vue':
|
||||
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))
|
||||
'@iconify-json/fluent':
|
||||
specifier: ^1.2.32
|
||||
version: 1.2.32
|
||||
'@iconify-json/logos':
|
||||
specifier: ^1.2.3
|
||||
version: 1.2.3
|
||||
@@ -406,8 +409,8 @@ packages:
|
||||
'@chatwoot/ninja-keys@1.2.3':
|
||||
resolution: {integrity: sha512-xM8d9P5ikDMZm2WbaCTk/TW5HFauylrU3cJ75fq5je6ixKwyhl/0kZbVN/vbbZN4+AUX/OaSIn6IJbtCgIF67g==}
|
||||
|
||||
'@chatwoot/prosemirror-schema@1.2.1':
|
||||
resolution: {integrity: sha512-UbiEvG5tgi1d0lMbkaqxgTh7vHfywEYKLQo1sxqp4Q7aLZh4QFtbLzJ2zyBtu4Nhipe+guFfEJdic7i43MP/XQ==}
|
||||
'@chatwoot/prosemirror-schema@1.2.2':
|
||||
resolution: {integrity: sha512-9knTH6OgZJ5qhJjS70Qiy0VsQwDAte6+gz+2PO1BM3RE+wUknkVS5YAcjqs1IZ4dSZyPrvNOOZh9mXoHdmtgnA==}
|
||||
|
||||
'@chatwoot/utils@0.0.51':
|
||||
resolution: {integrity: sha512-WlEmWfOTzR7YZRUWzn5Wpm15/BRudpwqoNckph8TohyDbiim1CP4UZGa+qjajxTbNGLLhtKlm0Xl+X16+5Wceg==}
|
||||
@@ -881,6 +884,9 @@ packages:
|
||||
resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
|
||||
deprecated: Use @eslint/object-schema instead
|
||||
|
||||
'@iconify-json/fluent@1.2.32':
|
||||
resolution: {integrity: sha512-YQFHsRrBhgrfvuVWJTg+EsVDA3pN4QwrFApZ2Di7J6YQIsNVNnBiEpGdT5qX4KxXFre3Yd4D7zm6EsWfKj4QBw==}
|
||||
|
||||
'@iconify-json/logos@1.2.3':
|
||||
resolution: {integrity: sha512-JLHS5hgZP1b55EONAWNeqBUuriRfRNKWXK4cqYx0PpVaJfIIMiiMxFfvoQiX/bkE9XgkLhcKmDUqL3LXPdXPwQ==}
|
||||
|
||||
@@ -4763,7 +4769,7 @@ snapshots:
|
||||
hotkeys-js: 3.8.7
|
||||
lit: 2.2.6
|
||||
|
||||
'@chatwoot/prosemirror-schema@1.2.1':
|
||||
'@chatwoot/prosemirror-schema@1.2.2':
|
||||
dependencies:
|
||||
markdown-it-sup: 2.0.0
|
||||
prosemirror-commands: 1.6.0
|
||||
@@ -5261,6 +5267,10 @@ snapshots:
|
||||
|
||||
'@humanwhocodes/object-schema@2.0.3': {}
|
||||
|
||||
'@iconify-json/fluent@1.2.32':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@iconify-json/logos@1.2.3':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
@@ -258,6 +258,7 @@ const tailwindConfig = {
|
||||
'ph',
|
||||
'material-symbols',
|
||||
'teenyicons',
|
||||
'fluent',
|
||||
]),
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -210,6 +210,21 @@ export const colors = {
|
||||
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',
|
||||
brand: '#2781F6',
|
||||
background: 'rgb(var(--background-color) / <alpha-value>)',
|
||||
|
||||
Reference in New Issue
Block a user