@zipify/wysiwyg 3.4.1 → 3.5.0-ai-prototype

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -42,6 +42,7 @@
42
42
  :page-blocks="pageBlocks"
43
43
  :active="isActive"
44
44
  :readonly="isReadonly"
45
+ :ai-adapter="aiAdapterRef"
45
46
  @update-favorite-colors="updateFavoriteColors"
46
47
  />
47
48
  <pre class="zw-content-structure" v-html="structurePreview" />
@@ -54,6 +55,7 @@ import { Wysiwyg } from '../lib/entryLib';
54
55
  import { FONTS } from './fonts';
55
56
  import { PRESETS, renderPresetVariable } from './presets';
56
57
  import { PAGE_BLOCKS } from './pageBlocks';
58
+ import { aiAdapter } from './aiAdapter';
57
59
 
58
60
  function getInitialContent() {
59
61
  const data = sessionStorage.getItem('wswg-data');
@@ -75,6 +77,7 @@ export default {
75
77
  },
76
78
 
77
79
  setup() {
80
+ const aiAdapterRef = ref(aiAdapter);
78
81
  const wswgRef = ref(null);
79
82
  const content = ref(getInitialContent());
80
83
  const presets = ref(PRESETS);
@@ -135,7 +138,8 @@ export default {
135
138
  presets,
136
139
  isActive,
137
140
  pageBlocks,
138
- isReadonly
141
+ isReadonly,
142
+ aiAdapterRef
139
143
  };
140
144
  }
141
145
  };
@@ -0,0 +1,21 @@
1
+ export const aiAdapter = {
2
+ generateText: async ({ prompt, context, istruction }) => {
3
+ try {
4
+ const response = await fetch('https://mendelson-test.eu.ngrok.io/process-text', {
5
+ method: 'POST',
6
+ headers: {
7
+ 'Content-Type': 'application/json'
8
+ },
9
+ body: JSON.stringify({ text: prompt, context })
10
+ });
11
+
12
+ if (!response.ok) {
13
+ throw new Error(`HTTP error: ${response.status}`);
14
+ }
15
+
16
+ return await response.json();
17
+ } catch (error) {
18
+ console.error('An error occurred while sending the request:', error);
19
+ }
20
+ }
21
+ };
package/lib/Wysiwyg.vue CHANGED
@@ -6,7 +6,7 @@
6
6
  :popup-mode="popupMode"
7
7
  ref="toolbarRef"
8
8
  />
9
-
9
+ <FloatingMenuControl />
10
10
  <EditorContent :editor="editor" />
11
11
  </div>
12
12
  </template>
@@ -22,6 +22,7 @@ import { ContextWindow, FavoriteColors, Storage } from './services';
22
22
  import { Devices } from './enums';
23
23
  import { outClick } from './directives';
24
24
  import { Font } from './models';
25
+ import { FloatingMenuControl } from './components/floatingMenu';
25
26
 
26
27
  const MIN_FONT_SIZE = 5;
27
28
  const MAX_FONT_SIZE = 112;
@@ -31,7 +32,8 @@ export default {
31
32
 
32
33
  components: {
33
34
  Toolbar,
34
- EditorContent
35
+ EditorContent,
36
+ FloatingMenuControl
35
37
  },
36
38
 
37
39
  directives: {
@@ -132,6 +134,12 @@ export default {
132
134
  window: {
133
135
  required: false,
134
136
  default: () => window
137
+ },
138
+
139
+ aiAdapter: {
140
+ type: Object,
141
+ required: false,
142
+ default: null
135
143
  }
136
144
  },
137
145
 
@@ -180,6 +188,7 @@ export default {
180
188
  basePresetClass: props.basePresetClass,
181
189
  baseListClass: props.baseListClass,
182
190
  deviceRef: toRef(props, 'device'),
191
+ aiComponent: props.aiAdapter,
183
192
  pageBlocksRef: pageBlocks,
184
193
  wrapperRef
185
194
  })
@@ -0,0 +1,11 @@
1
+ <svg xml:space="preserve" style="width:var(--zw-icon-width);height:var(--zw-icon-height)" viewBox="0 0 100 100">
2
+ <circle cx="6" cy="50" r="6" fill="var(--zw-icon-foreground)">
3
+ <animateTransform attributeName="transform" begin=".1" dur="1s" repeatCount="indefinite" type="translate" values="0 15 ; 0 -15; 0 15"/>
4
+ </circle>
5
+ <circle cx="30" cy="50" r="6" fill="var(--zw-icon-foreground)">
6
+ <animateTransform attributeName="transform" begin=".2" dur="1s" repeatCount="indefinite" type="translate" values="0 10 ; 0 -10; 0 10"/>
7
+ </circle>
8
+ <circle cx="54" cy="50" r="6" fill="var(--zw-icon-foreground)">
9
+ <animateTransform attributeName="transform" begin=".3" dur="1s" repeatCount="indefinite" type="translate" values="0 5 ; 0 -5; 0 5"/>
10
+ </circle>
11
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" style="width:var(--zw-icon-width);height:var(--zw-icon-height)" viewBox="0 0 24 24">
2
+ <path stroke="var(--zw-icon-foreground)" stroke-width="2" d="m7 10.2.4.8c.3.5.4.7.4 1 0 .3 0 .5-.4 1l-.4.8c-1.2 2.1-1.9 3.2-1.4 3.7.5.6 1.6 0 4-1l6.2-2.7c1.8-.8 2.7-1.1 2.7-1.8s-.9-1-2.7-1.8L9.5 7.4c-2.3-1-3.4-1.5-3.9-1-.5.6.2 1.7 1.4 3.8Z"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="#9c6ade" style="width:var(--zw-icon-width);height:var(--zw-icon-height)" viewBox="0 0 20 20">
2
+ <path d="M12.643 7.61c.124-.376.186-.564.288-.599a.216.216 0 0 1 .138 0c.102.035.164.223.288.6.519 1.577.778 2.366 1.268 2.991.225.288.485.548.773.773.625.49 1.414.75 2.991 1.268.377.124.565.186.6.288a.217.217 0 0 1 0 .138c-.035.102-.223.164-.6.288-1.577.519-2.366.778-2.991 1.268a4.53 4.53 0 0 0-.773.773c-.49.625-.75 1.414-1.268 2.991-.124.377-.186.565-.288.6a.217.217 0 0 1-.138 0c-.102-.035-.164-.223-.288-.6-.519-1.577-.778-2.366-1.268-2.991a4.53 4.53 0 0 0-.773-.773c-.625-.49-1.414-.75-2.991-1.268-.377-.124-.565-.186-.6-.288a.216.216 0 0 1 0-.138c.035-.102.223-.164.6-.288 1.577-.519 2.366-.778 2.991-1.268a4.53 4.53 0 0 0 .773-.773c.49-.625.75-1.414 1.268-2.991ZM4.762 4.407c.083-.251.124-.377.192-.4.03-.01.062-.01.092 0 .068.023.11.149.192.4.346 1.052.519 1.578.845 1.994.15.192.324.365.516.516.416.326.942.5 1.994.845.251.083.377.124.4.192.01.03.01.062 0 .092-.023.068-.149.11-.4.192-1.052.346-1.578.519-1.994.845-.192.15-.365.324-.516.516-.326.416-.5.942-.845 1.994-.083.251-.124.377-.192.4a.144.144 0 0 1-.092 0c-.068-.023-.11-.149-.192-.4-.346-1.052-.519-1.578-.845-1.994a3.022 3.022 0 0 0-.516-.516c-.416-.326-.942-.5-1.994-.845-.251-.083-.377-.124-.4-.192a.144.144 0 0 1 0-.092c.023-.068.149-.11.4-.192 1.052-.346 1.578-.519 1.994-.845.192-.15.365-.324.516-.516.326-.416.5-.942.845-1.994Zm5.589-3.153c.052-.157.078-.235.12-.25a.09.09 0 0 1 .058 0c.042.015.068.093.12.25.216.658.324.986.528 1.247.094.12.202.228.322.322.26.204.59.312 1.247.528.156.052.235.078.25.12a.089.089 0 0 1 0 .058c-.015.042-.094.068-.25.12-.658.216-.987.324-1.247.528-.12.094-.228.202-.322.322-.204.26-.312.59-.528 1.247-.052.156-.078.235-.12.25a.09.09 0 0 1-.058 0c-.042-.015-.068-.094-.12-.25-.216-.658-.324-.986-.528-1.247a1.888 1.888 0 0 0-.322-.322c-.26-.204-.59-.312-1.247-.528-.156-.052-.235-.078-.25-.12a.09.09 0 0 1 0-.058c.015-.042.094-.068.25-.12.658-.216.986-.324 1.247-.528.12-.094.228-.202.322-.322.204-.26.312-.59.528-1.247Z"/>
3
+ </svg>
@@ -0,0 +1,108 @@
1
+ <template>
2
+ <div class="zw-field">
3
+ <label v-if="label" class="zw-field__label" :for="fieldId" data-test-selector="label">
4
+ {{ label }}
5
+ </label>
6
+
7
+ <textarea
8
+ class="zw-field__input"
9
+ :value="value"
10
+ :id="fieldId"
11
+ :placeholder="placeholder"
12
+ @input="onInput"
13
+ data-test-selector="input"
14
+ />
15
+
16
+ <p class="zw-field__label--error" v-if="error" data-test-selector="error">
17
+ {{ error }}
18
+ </p>
19
+ </div>
20
+ </template>
21
+
22
+ <script>
23
+ import { computed } from 'vue';
24
+
25
+ export default {
26
+ name: 'TextArea',
27
+
28
+ props: {
29
+ value: {
30
+ type: [Number, String],
31
+ required: true
32
+ },
33
+
34
+ label: {
35
+ type: String,
36
+ required: false,
37
+ default: ''
38
+ },
39
+
40
+ placeholder: {
41
+ type: String,
42
+ required: false,
43
+ default: ''
44
+ },
45
+
46
+ error: {
47
+ type: String,
48
+ required: false,
49
+ default: null
50
+ }
51
+ },
52
+
53
+ setup(props, { emit }) {
54
+ const onInput = (event) => emit('input', event.target.value);
55
+ const fieldId = computed(() => {
56
+ return props.label.toLowerCase().replace(/[^a-zA-Z0-9]+(.)/g, (m, chr) => chr.toUpperCase());
57
+ });
58
+
59
+ return { onInput, fieldId };
60
+ }
61
+ };
62
+ </script>
63
+
64
+ <style scoped>
65
+ .zw-field {
66
+ display: flex;
67
+ flex-direction: column;
68
+ }
69
+
70
+ .zw-field__input {
71
+ --border-color: rgb(var(--zw-color-n60));
72
+ --text-color: rgb(var(--zw-color-n85));
73
+
74
+ border: 1px solid var(--border-color);
75
+ background-color: transparent;
76
+ color: var(--text-color);
77
+ font-size: var(--zw-font-size-xxs);
78
+ outline: none;
79
+ padding: 6px;
80
+ line-height: var(--zw-line-height-xxs);
81
+ transition: 0.1s border ease-out, 0.1s color ease-out;
82
+ will-change: border, color;
83
+ }
84
+
85
+ .zw-field__input:hover {
86
+ --border-color: rgb(var(--zw-color-n80));
87
+ --text-color: rgb(var(--zw-color-n85));
88
+ }
89
+
90
+ .zw-field__input:focus,
91
+ .zw-field__input:focus-within {
92
+ --border-color: rgb(var(--zw-color-white));
93
+ --text-color: rgb(var(--zw-color-white));
94
+ }
95
+
96
+ .zw-field__label {
97
+ display: inline-block;
98
+ font-size: var(--zw-font-size-xxs);
99
+ padding-bottom: var(--zw-offset-xxs);
100
+ line-height: var(--zw-line-height-xxs);
101
+ }
102
+
103
+ .zw-field__label--error {
104
+ font-size: var(--zw-font-size-xxs);
105
+ margin: var(--zw-offset-xxs) 0 0;
106
+ color: rgb(var(--zw-color-red));
107
+ }
108
+ </style>
@@ -7,6 +7,7 @@ export { default as Range } from './Range';
7
7
  export { default as NumberField } from './NumberField';
8
8
  export { default as Modal } from './Modal';
9
9
  export { default as TextField } from './TextField';
10
+ export { default as TextArea } from './TextArea';
10
11
  export { default as Checkbox } from './Checkbox';
11
12
  export { useModalToggler, useElementRef } from './composables';
12
13
  export * from './dropdown';
@@ -0,0 +1,74 @@
1
+ <template>
2
+ <div class="zw-ai-component__suggestion">
3
+ <p class="zw-ai-component-suggestion__request zw-margin-bottom--sm">
4
+ {{ suggestion.request }}
5
+ </p>
6
+ <p class="zw-margin-bottom--sm" v-html="suggestion.content" />
7
+
8
+ <Button v-if="suggestion.state !== 'loading'" @click="insert" class="zw-ai-component-suggestion__button">
9
+ Insert
10
+ </Button>
11
+ </div>
12
+ </template>
13
+
14
+ <script>
15
+ import { inject } from 'vue';
16
+ import { Button } from '../base';
17
+ import { InjectionTokens } from '../../injectionTokens';
18
+
19
+ export default {
20
+ name: 'AiWidgetSuggestionItem',
21
+
22
+ components: {
23
+ Button
24
+ },
25
+
26
+ props: {
27
+ suggestion: {
28
+ type: Object,
29
+ required: true
30
+ }
31
+ },
32
+
33
+ setup(props, { emit }) {
34
+ const editor = inject(InjectionTokens.EDITOR);
35
+
36
+ const insert = () => {
37
+ editor.chain().insertContent(props.suggestion.content).run();
38
+ emit('close');
39
+ };
40
+
41
+ return {
42
+ insert
43
+ };
44
+ }
45
+ };
46
+ </script>
47
+
48
+ <style scoped>
49
+ .zw-ai-component__suggestion {
50
+ font-size: 14px;
51
+ margin-bottom: 8px;
52
+ padding: 16px 8px;
53
+ background-color: #FFF;
54
+ border: 0.5px solid #CDD1DC;
55
+ border-radius: 2px;
56
+ }
57
+
58
+ .zw-ai-component-suggestion__request {
59
+ font-size: 12px;
60
+ color: #666;
61
+ }
62
+
63
+ .zw-ai-component-suggestion__button {
64
+ font-size: 14px;
65
+ padding: 2px 4px;
66
+ background-color: #3AAA35;
67
+ color: #FFF;
68
+ border-radius: 2px;
69
+ }
70
+
71
+ .zw-ai-component-suggestion__button:hover {
72
+ opacity: 0.8;
73
+ }
74
+ </style>
@@ -0,0 +1,204 @@
1
+ <template>
2
+ <div class="zw-position--relative">
3
+ <FloatingMenu
4
+ :should-show="shouldShow"
5
+ v-if="editor"
6
+ :editor="editor"
7
+ ref="floatingMenu"
8
+
9
+ :tippy-options="menuOptions"
10
+ >
11
+ <Button icon class="zw-floating-menu__button" :active="isActive" @click="toggler.open" v-tooltip="'Write with AI'">
12
+ <Icon name="sparkles" size="24px" />
13
+ </Button>
14
+ </FloatingMenu>
15
+
16
+ <Modal class="zw-link-modal" :toggler="toggler" ref="modalRef" focus-first-control>
17
+ <form class="zw-link-modal__body" @submit.prevent="generateText">
18
+ <div class="zw-link-form__body">
19
+ Selected text:
20
+ <p class="zw-ai-component__selected-text">
21
+ {{ selectedText }}
22
+ </p>
23
+
24
+ <template v-if="suggestions.length || isLoading">
25
+ <p class="zw-margin-bottom--xs">
26
+ Suggestions:
27
+ </p>
28
+ <AiWidgetSuggestionItem
29
+ v-for="suggestion of suggestions"
30
+ :suggestion="suggestion"
31
+ :key="suggestion.result"
32
+ @close="toggler.close"
33
+ />
34
+
35
+ <AiWidgetSuggestionItem v-if="isLoading" :suggestion="{ content: 'Working on it...', state: 'loading' }" />
36
+ </template>
37
+
38
+ </div>
39
+
40
+ <div class="zw-position--relative">
41
+ <input v-model="prompt" type="text" class="zw-ai-component__input" placeholder="Tell us to ...">
42
+
43
+ <Button type="submit" class="zw-ai-component__send-button">
44
+ <Icon auto-color class="zw-ai-component__icon" :name="iconName" size="32px" />
45
+ </Button>
46
+ </div>
47
+ </form>
48
+ </Modal>
49
+ </div>
50
+ </template>
51
+
52
+ <script>
53
+ import { inject, ref, unref, computed } from 'vue';
54
+ import { FloatingMenu } from '@tiptap/vue-2';
55
+ import { Button, Icon, Modal, useModalToggler } from '../base';
56
+ import { InjectionTokens } from '../../injectionTokens';
57
+ import { tooltip } from '../../directives';
58
+ import AiWidgetSuggestionItem from './AiWidgetSuggestionItem';
59
+
60
+ export default {
61
+ name: 'FloatingMenuControl',
62
+
63
+ components: {
64
+ AiWidgetSuggestionItem,
65
+ FloatingMenu,
66
+ Icon,
67
+ Button,
68
+ Modal
69
+ },
70
+
71
+ directives: {
72
+ tooltip
73
+ },
74
+
75
+ setup() {
76
+ const editor = inject(InjectionTokens.EDITOR);
77
+ const suggestions = ref([]);
78
+ const wrapperRef = ref(null);
79
+ const floatingMenu = ref(null);
80
+ const modalRef = ref(null);
81
+ const prompt = ref('');
82
+ const isLoading = ref(false);
83
+
84
+ const selectedText = computed(() => editor.commands.getSelectedText());
85
+
86
+ const iconName = computed(() => {
87
+ return unref(isLoading) ? 'loading' : 'send';
88
+ });
89
+
90
+ const onBeforeOpened = () => {
91
+ suggestions.value = [];
92
+ };
93
+
94
+ const toggler = useModalToggler({
95
+ onBeforeOpened: () => onBeforeOpened(),
96
+ wrapperRef: floatingMenu,
97
+ modalRef
98
+ });
99
+
100
+ const isActive = computed(() => unref(toggler.isOpened));
101
+
102
+ const shouldShow = () => {
103
+ return selectedText.value.length > 1;
104
+ };
105
+
106
+ const generateText = async () => {
107
+ if(isLoading.value) return;
108
+
109
+ isLoading.value = true;
110
+ const lastSuggestions = suggestions.value[suggestions.value.length - 1];
111
+
112
+ const context = lastSuggestions ? lastSuggestions.content : selectedText.value;
113
+
114
+ const result = await editor.commands.generateText({ prompt: prompt.value, context });
115
+
116
+ isLoading.value = false;
117
+ suggestions.value.push(result);
118
+ };
119
+
120
+ const menuOptions = {
121
+ placement: 'right-end',
122
+ offset: [0, 10]
123
+ };
124
+
125
+ return {
126
+ selectedText,
127
+ editor,
128
+ floatingMenu,
129
+ shouldShow,
130
+ menuOptions,
131
+ toggler,
132
+ wrapperRef,
133
+ modalRef,
134
+ isActive,
135
+ generateText,
136
+ suggestions,
137
+ prompt,
138
+ iconName,
139
+ isLoading
140
+ };
141
+ }
142
+ };
143
+ </script>
144
+
145
+ <style scoped>
146
+ .zw-floating-menu__button {
147
+ padding: 2px;
148
+ background-color: rgba(255, 252, 252, 1);
149
+ border-radius: 50%;
150
+ box-shadow: 0 0 0 0 1px #878DA2;
151
+ }
152
+
153
+ .zw-link-modal {
154
+ width: 450px;
155
+ background-color: #FCFCFC;
156
+ border-radius: 8px;
157
+ box-shadow: 0 0 0 0.5px #878DA2, 0 0 2px 0.5px rgba(135, 141, 162, 0.5), 0 1px 8px 0.5px rgba(135, 141, 162, 0.1), 0 2px 12px 0.5px rgba(135, 141, 162, 0.1), 0 4px 20 0.5px rgba(135, 141, 162, 0.25);
158
+ }
159
+
160
+ .zw-link-modal__body {
161
+ padding: var(--zw-offset-sm);
162
+ }
163
+
164
+ .zw-link-form__body {
165
+ overscroll-behavior: contain;
166
+ overflow: auto;
167
+ max-height: max(min(calc(1051.21px - calc(calc(3.5px * 16) + 47.9844px) - calc(0.25px * 16)), calc(34.75px * 16)), calc(10px * 16));
168
+ }
169
+
170
+ .zw-ai-component__selected-text {
171
+ font-size: 14px;
172
+ margin-bottom: 8px;
173
+ padding: 16px 8px;
174
+ background-color: #FFF;
175
+ border: 0.5px solid #000;
176
+ border-radius: 2px;
177
+ }
178
+
179
+ .zw-ai-component__input {
180
+ width: 100%;
181
+ min-height: 40px;
182
+ border: 1px solid #E9E9E9;
183
+ border-radius: 5px;
184
+ padding: 8px;
185
+ font-size: 14px;
186
+ }
187
+
188
+ .zw-ai-component__send-button {
189
+ position: absolute;
190
+ top: 50%;
191
+ transform: translateY(-50%);
192
+ right: 16px;
193
+ }
194
+
195
+ .zw-ai-component__icon {
196
+ color: #CDD1DC;
197
+ }
198
+
199
+ .zw-ai-component__send-button:hover .zw-ai-component__icon {
200
+ color: #878DA2;
201
+ transition: color 0.1s ease-in-out;
202
+ }
203
+ </style>
204
+
@@ -0,0 +1 @@
1
+ export { default as FloatingMenuControl } from './FloatingMenuControl';