@opentiny/fluent-editor 4.0.0-alpha.4 → 4.0.0-alpha.5

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.
Files changed (106) hide show
  1. package/es/modules/ai/constants.es.js +50 -0
  2. package/es/modules/ai/constants.es.js.map +1 -0
  3. package/es/modules/ai/icons.es.js +319 -0
  4. package/es/modules/ai/icons.es.js.map +1 -0
  5. package/es/modules/ai/index.es.js +533 -101
  6. package/es/modules/ai/index.es.js.map +1 -1
  7. package/es/modules/custom-image/blot-formatter.es.js +4 -0
  8. package/es/modules/custom-image/blot-formatter.es.js.map +1 -1
  9. package/es/modules/custom-image/image.es.js +8 -1
  10. package/es/modules/custom-image/image.es.js.map +1 -1
  11. package/es/modules/custom-image/options.es.js +3 -0
  12. package/es/modules/custom-image/options.es.js.map +1 -1
  13. package/es/modules/custom-uploader.es.js +1 -1
  14. package/es/modules/custom-uploader.es.js.map +1 -1
  15. package/es/modules/emoji.es.js +3 -15
  16. package/es/modules/emoji.es.js.map +1 -1
  17. package/es/modules/table-up/index.es.js +16 -10
  18. package/es/modules/table-up/index.es.js.map +1 -1
  19. package/es/modules/toolbar/better-toolbar.es.js +1 -1
  20. package/es/modules/toolbar/better-toolbar.es.js.map +1 -1
  21. package/es/themes/snow.es.js +12 -0
  22. package/es/themes/snow.es.js.map +1 -1
  23. package/es/tools/format-painter.es.js +8 -2
  24. package/es/tools/format-painter.es.js.map +1 -1
  25. package/es/ui/icons.config.es.js +43 -13
  26. package/es/ui/icons.config.es.js.map +1 -1
  27. package/lib/modules/ai/constants.cjs.js +50 -0
  28. package/lib/modules/ai/constants.cjs.js.map +1 -0
  29. package/lib/modules/ai/icons.cjs.js +319 -0
  30. package/lib/modules/ai/icons.cjs.js.map +1 -0
  31. package/lib/modules/ai/index.cjs.js +533 -101
  32. package/lib/modules/ai/index.cjs.js.map +1 -1
  33. package/lib/modules/custom-image/blot-formatter.cjs.js +4 -0
  34. package/lib/modules/custom-image/blot-formatter.cjs.js.map +1 -1
  35. package/lib/modules/custom-image/image.cjs.js +8 -1
  36. package/lib/modules/custom-image/image.cjs.js.map +1 -1
  37. package/lib/modules/custom-image/options.cjs.js +3 -0
  38. package/lib/modules/custom-image/options.cjs.js.map +1 -1
  39. package/lib/modules/custom-uploader.cjs.js +1 -1
  40. package/lib/modules/custom-uploader.cjs.js.map +1 -1
  41. package/lib/modules/emoji.cjs.js +3 -15
  42. package/lib/modules/emoji.cjs.js.map +1 -1
  43. package/lib/modules/table-up/index.cjs.js +16 -10
  44. package/lib/modules/table-up/index.cjs.js.map +1 -1
  45. package/lib/modules/toolbar/better-toolbar.cjs.js +1 -1
  46. package/lib/modules/toolbar/better-toolbar.cjs.js.map +1 -1
  47. package/lib/themes/snow.cjs.js +12 -0
  48. package/lib/themes/snow.cjs.js.map +1 -1
  49. package/lib/tools/format-painter.cjs.js +8 -2
  50. package/lib/tools/format-painter.cjs.js.map +1 -1
  51. package/lib/ui/icons.config.cjs.js +43 -13
  52. package/lib/ui/icons.config.cjs.js.map +1 -1
  53. package/package.json +3 -3
  54. package/style.css +2471 -1509
  55. package/types/config/types/editor-config.interface.d.ts +9 -6
  56. package/types/modules/ai/constants.d.ts +30 -0
  57. package/types/modules/ai/icons.d.ts +21 -0
  58. package/types/modules/ai/index.d.ts +71 -13
  59. package/types/modules/ai/types.d.ts +16 -0
  60. package/types/modules/custom-image/image.d.ts +2 -0
  61. package/types/modules/custom-image/{Options.d.ts → options.d.ts} +1 -0
  62. package/types/modules/emoji.d.ts +0 -1
  63. package/types/modules/table-up/index.d.ts +0 -31
  64. package/types/tools/format-painter.d.ts +1 -2
  65. package/types/ui/icons.config.d.ts +1 -1
  66. package/types/config/types/additional-toolbar-item.interface.d.ts +0 -8
  67. package/types/config/types/content-change.interface.d.ts +0 -13
  68. package/types/config/types/content-save.interface.d.ts +0 -6
  69. package/types/config/types/counter-option.interface.d.ts +0 -9
  70. package/types/config/types/editor-toolbar.interface.d.ts +0 -6
  71. package/types/config/types/file-operation.interface.d.ts +0 -12
  72. package/types/config/types/focus-change.interface.d.ts +0 -4
  73. package/types/config/types/fullscreen-module.interface.d.ts +0 -4
  74. package/types/config/types/help-panel-item.interface.d.ts +0 -5
  75. package/types/config/types/help-panel-option.interface.d.ts +0 -7
  76. package/types/config/types/image-module.interface.d.ts +0 -3
  77. package/types/config/types/image-upload.interface.d.ts +0 -7
  78. package/types/config/types/load-on-demand-module.interface.d.ts +0 -5
  79. package/types/config/types/mention-module.interface.d.ts +0 -8
  80. package/types/config/types/paste-change.interface.d.ts +0 -6
  81. package/types/config/types/quick-menu-module.interface.d.ts +0 -3
  82. package/types/config/types/range.interface.d.ts +0 -4
  83. package/types/config/types/registry-options.interface.d.ts +0 -5
  84. package/types/config/types/selection-change.interface.d.ts +0 -8
  85. package/types/config/types/toolbar-item.interface.d.ts +0 -13
  86. package/types/config/types/validate-error.interface.d.ts +0 -13
  87. package/types/modules/custom-image/BlotFormatter.d.ts +0 -29
  88. package/types/modules/custom-image/actions/CustomResizeAction.d.ts +0 -24
  89. package/types/modules/custom-image/actions/DeleteAction.d.ts +0 -7
  90. package/types/modules/custom-image/image-bar.d.ts +0 -17
  91. package/types/modules/custom-image/specs/BlotSpec.d.ts +0 -13
  92. package/types/modules/custom-image/specs/CustomImageSpec.d.ts +0 -21
  93. package/types/modules/custom-image/specs/ImageSpec.d.ts +0 -10
  94. package/types/modules/emoji/emoji-list/index.d.ts +0 -1
  95. package/types/modules/emoji/emoji-list/people.d.ts +0 -1
  96. package/types/modules/emoji/emoji-list.d.ts +0 -2
  97. package/types/modules/emoji/emoji-map.d.ts +0 -2
  98. package/types/modules/emoji/emoji-sprite.d.ts +0 -1
  99. package/types/modules/emoji/formats/emoji-blot.d.ts +0 -15
  100. package/types/modules/emoji/index.d.ts +0 -3
  101. package/types/modules/emoji/modules/emoji.d.ts +0 -38
  102. package/types/modules/emoji/modules/toolbar-emoji.d.ts +0 -9
  103. package/types/modules/emoji/utils.d.ts +0 -1
  104. package/types/modules/mention/MentionLink.d.ts +0 -16
  105. /package/types/modules/custom-image/actions/{Action.d.ts → action.d.ts} +0 -0
  106. /package/types/modules/mention/{Mention.d.ts → mention.d.ts} +0 -0
@@ -1,7 +1,8 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
- import { AI_ICON } from "../../ui/icons.config.es.js";
4
+ import { REPLACE_SELECT, INSERT_TEXT, INSERT_SUB_CONTENT_TEXT, REGENERATE, CLOSE, RESULT_HEADER_TEXT, MENU_ID_MAP, MENU_TITLE_DATA, SELECT_PLACEHOLDER, INPUT_PLACEHOLDER, THINK_TEXT, STOP_ANSWER } from "./constants.es.js";
5
+ import { REPLACE_SELECT_ICON, INSERT_ICON, REBUILD_ICON, MENU_CLOSE_ICON, EDITOR_ICON, CALL_ICON, ADJUST_ICON, REFRESH_ICON, COPY_ICON, RIGHT_ARROW_ICON, SEND_BTN_ICON, CLOSE_ICON, AI_ICON, THINK_ICON, STOP_ICON } from "./icons.es.js";
5
6
  class AI {
6
7
  constructor(quill, options) {
7
8
  __publicField(this, "toolbar");
@@ -11,22 +12,56 @@ class AI {
11
12
  __publicField(this, "message");
12
13
  __publicField(this, "isBreak", false);
13
14
  // 打断标记
15
+ __publicField(this, "textNumber");
16
+ // 文本字数限制
17
+ __publicField(this, "_isSelectRangeMode", false);
18
+ // 选择/点击模式
19
+ __publicField(this, "_charCount", 0);
20
+ // 文本字数
21
+ __publicField(this, "_debounceTimer", null);
22
+ __publicField(this, "_inputPlaceholder", "");
23
+ __publicField(this, "_showOperationMenu", false);
24
+ __publicField(this, "_isThinking", false);
25
+ // 思考中
26
+ __publicField(this, "_showResultPopupEl", false);
27
+ // 结果弹窗
28
+ __publicField(this, "selectedText", "");
29
+ // 选择的文本
14
30
  __publicField(this, "inputValue", "");
15
31
  // 存储输入框的值
16
- __publicField(this, "SEND", "发送");
17
- // 发送按钮文字
18
- __publicField(this, "BREAK", "停止");
19
- // 取消按钮文字
20
- __publicField(this, "DONE", "完成");
21
- __publicField(this, "REGENERATE", "重新生成");
22
- __publicField(this, "CLOSE", "关闭");
32
+ __publicField(this, "resultMenuList", []);
33
+ __publicField(this, "operationMenuList", []);
34
+ __publicField(this, "_operationMenuItemList", []);
35
+ __publicField(this, "alertEl", null);
36
+ __publicField(this, "alertTimer", null);
37
+ __publicField(this, "selectionBubbleEl", null);
38
+ __publicField(this, "selectionRange", null);
23
39
  __publicField(this, "dialogContainerEl", null);
24
40
  __publicField(this, "wrapContainerEl", null);
25
- __publicField(this, "aiPreTextEl", null);
41
+ __publicField(this, "aiIconEl", null);
26
42
  __publicField(this, "inputContainerEl", null);
27
43
  __publicField(this, "inputEl", null);
28
- __publicField(this, "sendButtonEl", null);
44
+ __publicField(this, "menuContainerEl", null);
45
+ __publicField(this, "subMenuEl", null);
46
+ __publicField(this, "subMenuEditorEl", null);
47
+ __publicField(this, "subMenuToneEl", null);
48
+ __publicField(this, "subMenuAdjustEl", null);
49
+ __publicField(this, "inputRightEl", null);
50
+ __publicField(this, "inputSendBtnEl", null);
51
+ __publicField(this, "inputCloseBtnEl", null);
52
+ __publicField(this, "thinkContainerEl", null);
53
+ // 思考元素
54
+ __publicField(this, "thinkBtnEl", null);
29
55
  __publicField(this, "resultPopupEl", null);
56
+ __publicField(this, "resultPopupHeaderEl", null);
57
+ __publicField(this, "resultPopupContentEl", null);
58
+ __publicField(this, "resultPopupFooterEl", null);
59
+ __publicField(this, "resultPopupFooterTextEl", null);
60
+ __publicField(this, "resultRefreshBtnEl", null);
61
+ __publicField(this, "resultCopyBtnEl", null);
62
+ // 分享和朗读功能待放开
63
+ // private resultShareBtnEl: HTMLSpanElement | null = null
64
+ // private resultVoiceBtnEl: HTMLSpanElement | null = null
30
65
  __publicField(this, "actionMenuEl", null);
31
66
  this.quill = quill;
32
67
  this.options = options;
@@ -35,97 +70,434 @@ class AI {
35
70
  if (typeof this.toolbar !== "undefined") {
36
71
  this.toolbar.addHandler("ai", this.showAIInput.bind(this));
37
72
  }
38
- this.host = options.host;
73
+ this.quill.on("selection-change", this.handleSelectionChange.bind(this));
74
+ this.host = options.host || "https://api.deepseek.com/v1";
39
75
  this.apiKey = options.apiKey;
40
- this.model = options.model;
76
+ this.model = options.model || "deepseek-chat";
77
+ this.textNumber = options.contentMaxLength || 5e3;
78
+ this.resultMenuList = [
79
+ { text: REPLACE_SELECT, icon: REPLACE_SELECT_ICON },
80
+ { text: INSERT_TEXT, icon: INSERT_ICON, selectText: INSERT_SUB_CONTENT_TEXT },
81
+ { text: REGENERATE, icon: REBUILD_ICON },
82
+ { text: CLOSE, icon: MENU_CLOSE_ICON }
83
+ ];
84
+ this.operationMenuList = [
85
+ { id: "editor", text: "编辑调整内容", icon: EDITOR_ICON },
86
+ { id: "tone", text: "改写口吻", icon: CALL_ICON },
87
+ { id: "adjust", text: "整理选区内容", icon: ADJUST_ICON }
88
+ ];
41
89
  }
42
- positionElements() {
43
- if (!this.dialogContainerEl) return;
44
- const range = this.quill.getSelection();
45
- if (range) {
46
- const bounds = this.quill.getBounds(range.index);
47
- this.dialogContainerEl.style.position = "absolute";
48
- this.dialogContainerEl.style.top = `${bounds.top + bounds.height}px`;
90
+ // 工具栏启动
91
+ showAIInput() {
92
+ this.create();
93
+ this.selectionRange = this.quill.getSelection();
94
+ if (this.selectionRange.length) {
95
+ this.isSelectRangeMode = true;
96
+ } else {
97
+ this.isSelectRangeMode = false;
49
98
  }
99
+ this.positionElements();
100
+ const handleKeyDown = (e) => {
101
+ if (e.key === "Escape") {
102
+ this.closeAIPanel();
103
+ this.quill.container.removeEventListener("keydown", handleKeyDown);
104
+ }
105
+ };
106
+ this.quill.container.addEventListener("keydown", handleKeyDown);
107
+ }
108
+ // 气泡启动
109
+ selectTextEvent() {
110
+ if (!this.selectionRange) return;
111
+ this.create();
112
+ this.positionElements();
113
+ this.isSelectRangeMode = true;
114
+ }
115
+ create() {
116
+ this.createResultElement();
117
+ this.createOperationMenuElements();
118
+ this.createInputBoxElements();
119
+ this.addInputEvent();
120
+ this.addResultEvent();
121
+ this.handleActionMenuDisplay();
122
+ this.quill.container.appendChild(this.dialogContainerEl);
50
123
  }
51
- // 创建AI提示语元素
52
- createAiPreTextEl() {
53
- if (!this.aiPreTextEl) {
54
- this.aiPreTextEl = document.createElement("span");
55
- this.aiPreTextEl.className = "ql-ai-tip";
124
+ // 创建结果弹窗
125
+ createResultElement() {
126
+ if (!this.resultPopupEl) {
127
+ this.resultPopupEl = document.createElement("div");
128
+ this.resultPopupEl.className = "ql-ai-result";
129
+ this.resultPopupHeaderEl = document.createElement("div");
130
+ this.resultPopupHeaderEl.className = "ql-ai-result-header";
131
+ this.resultPopupHeaderEl.textContent = RESULT_HEADER_TEXT;
132
+ this.resultPopupContentEl = document.createElement("div");
133
+ this.resultPopupContentEl.className = "ql-ai-result-content";
134
+ this.resultPopupFooterEl = document.createElement("div");
135
+ this.resultPopupFooterEl.className = "ql-ai-result-footer";
136
+ this.resultPopupFooterTextEl = document.createElement("span");
137
+ this.resultPopupFooterTextEl.className = "ql-ai-result-footer-text";
138
+ this.resultPopupFooterTextEl.textContent = `0`;
139
+ this.resultRefreshBtnEl = document.createElement("span");
140
+ this.resultRefreshBtnEl.className = "ql-ai-result-footer-refresh";
141
+ this.resultRefreshBtnEl.innerHTML = REFRESH_ICON;
142
+ this.resultCopyBtnEl = document.createElement("span");
143
+ this.resultCopyBtnEl.className = "ql-ai-result-footer-copy";
144
+ this.resultCopyBtnEl.innerHTML = COPY_ICON;
145
+ const resultFooterRightEl = document.createElement("div");
146
+ resultFooterRightEl.className = "ql-ai-result-footer-right";
147
+ resultFooterRightEl.appendChild(this.resultRefreshBtnEl);
148
+ resultFooterRightEl.appendChild(this.resultCopyBtnEl);
149
+ this.resultPopupFooterEl.appendChild(this.resultPopupFooterTextEl);
150
+ this.resultPopupFooterEl.appendChild(resultFooterRightEl);
151
+ this.resultPopupEl.appendChild(this.resultPopupHeaderEl);
152
+ this.resultPopupEl.appendChild(this.resultPopupContentEl);
153
+ this.resultPopupEl.appendChild(this.resultPopupFooterEl);
56
154
  }
155
+ this.showResultPopupEl = false;
57
156
  }
58
- // 创建弹出框
59
- createElements() {
157
+ createOperationMenuElements() {
158
+ if (!this.menuContainerEl) {
159
+ this.menuContainerEl = document.createElement("div");
160
+ this.menuContainerEl.className = "ql-ai-menu-container";
161
+ const mainMenu = document.createElement("div");
162
+ mainMenu.className = "ql-ai-main-menu";
163
+ this.operationMenuList.forEach(({ text, icon, id }) => {
164
+ const menuItem = document.createElement("div");
165
+ menuItem.className = "ql-ai-menu-item";
166
+ menuItem.innerHTML = `${icon}<span>${text}</span>${RIGHT_ARROW_ICON}`;
167
+ menuItem.addEventListener("mouseenter", (e) => {
168
+ e.stopPropagation();
169
+ this.subMenuEl.style.display = "block";
170
+ this.subMenuEl.className = `ql-ai-sub-menu ${id}`;
171
+ this.createOperationMenuItem(id);
172
+ });
173
+ mainMenu.appendChild(menuItem);
174
+ });
175
+ if (!this.subMenuEl) {
176
+ this.subMenuEl = document.createElement("div");
177
+ this.subMenuEl.className = "ql-ai-sub-menu";
178
+ this.subMenuEl.style.display = "none";
179
+ }
180
+ this.menuContainerEl.appendChild(mainMenu);
181
+ this.menuContainerEl.appendChild(this.subMenuEl);
182
+ }
183
+ this.showOperationMenu = false;
184
+ }
185
+ createOperationMenuItem(id) {
186
+ let menuItemBox = this[MENU_ID_MAP[id]];
187
+ if (!menuItemBox) {
188
+ menuItemBox = document.createElement("div");
189
+ }
190
+ while (this.subMenuEl.firstChild) {
191
+ this.subMenuEl.removeChild(this.subMenuEl.firstChild);
192
+ }
193
+ MENU_TITLE_DATA[id].forEach(({ text, icon, id: id2 }) => {
194
+ const menuItem = document.createElement("div");
195
+ menuItem.className = "ql-ai-menu-item";
196
+ menuItem.innerHTML = `${icon || ""}<span>${text}</span>`;
197
+ menuItem.addEventListener("click", (e) => {
198
+ e.stopPropagation();
199
+ this.handleOperationMenuItemClick(text, id2);
200
+ });
201
+ menuItemBox.appendChild(menuItem);
202
+ });
203
+ this.subMenuEl.appendChild(menuItemBox);
204
+ }
205
+ createInputBoxElements() {
60
206
  if (!this.dialogContainerEl) {
61
207
  this.dialogContainerEl = document.createElement("div");
62
208
  this.dialogContainerEl.className = "ql-ai-dialog";
63
209
  this.wrapContainerEl = document.createElement("div");
64
210
  this.wrapContainerEl.className = "ql-ai-wrapper";
65
- this.wrapContainerEl.style.width = `${this.quill.container.clientWidth * 0.9}px`;
66
- this.inputContainerEl = document.createElement("div");
67
- this.inputContainerEl.className = "ql-ai-input";
68
- const aiIcon = document.createElement("div");
69
- aiIcon.className = "ql-ai-icon";
70
- aiIcon.innerHTML = AI_ICON;
71
- this.createAiPreTextEl();
211
+ this.wrapContainerEl.style.width = `${this.quill.container.clientWidth - 30}px`;
212
+ this.createAIInputIcon();
72
213
  this.inputEl = document.createElement("input");
73
214
  this.inputEl.type = "text";
74
- this.inputEl.placeholder = "请输入内容";
75
- this.sendButtonEl = document.createElement("div");
76
- this.sendButtonEl.className = "ql-ai-send";
77
- this.resultPopupEl = document.createElement("div");
78
- this.resultPopupEl.className = "ql-ai-result";
79
- this.wrapContainerEl.appendChild(this.resultPopupEl);
80
- this.inputContainerEl.appendChild(aiIcon);
81
- this.inputContainerEl.appendChild(this.aiPreTextEl);
215
+ this.inputPlaceholder = this._isSelectRangeMode ? SELECT_PLACEHOLDER : INPUT_PLACEHOLDER;
216
+ this.inputSendBtnEl = document.createElement("span");
217
+ this.inputSendBtnEl.className = "ql-ai-input-right-send";
218
+ this.inputSendBtnEl.innerHTML = SEND_BTN_ICON;
219
+ this.inputCloseBtnEl = document.createElement("span");
220
+ this.inputCloseBtnEl.className = "ql-ai-input-right-close";
221
+ this.inputCloseBtnEl.innerHTML = CLOSE_ICON;
222
+ this.inputRightEl = document.createElement("div");
223
+ this.inputRightEl.className = "ql-ai-input-right";
224
+ this.inputContainerEl = document.createElement("div");
225
+ this.inputContainerEl.className = "ql-ai-input";
226
+ this.inputContainerEl.appendChild(this.aiIconEl);
82
227
  this.inputContainerEl.appendChild(this.inputEl);
83
- this.inputContainerEl.appendChild(this.sendButtonEl);
228
+ this.inputRightEl.appendChild(this.inputSendBtnEl);
229
+ this.inputRightEl.appendChild(this.inputCloseBtnEl);
230
+ this.inputContainerEl.appendChild(this.inputRightEl);
231
+ this.wrapContainerEl.appendChild(this.resultPopupEl);
84
232
  this.wrapContainerEl.appendChild(this.inputContainerEl);
233
+ this.wrapContainerEl.appendChild(this.menuContainerEl);
85
234
  this.dialogContainerEl.appendChild(this.wrapContainerEl);
235
+ } else {
236
+ this.dialogContainerEl.style.display = "block";
86
237
  }
87
- this.aiPreTextEl.textContent = `${this.model}帮你写:`;
88
- this.sendButtonEl.textContent = this.SEND;
89
- this.sendButtonEl.style.display = "block";
90
- this.resultPopupEl.style.display = "none";
91
- this.quill.container.appendChild(this.dialogContainerEl);
238
+ this.hiddenInputSendBtnEl();
92
239
  }
93
- showAIInput() {
94
- this.createElements();
95
- this.positionElements();
240
+ hiddenInputSendBtnEl(display = "none") {
241
+ if (this.inputEl && this.inputSendBtnEl) {
242
+ this.inputSendBtnEl.style.display = display;
243
+ }
244
+ }
245
+ copyResult() {
246
+ if (!this.resultPopupContentEl) return;
247
+ try {
248
+ const textToCopy = this.resultPopupContentEl.textContent || "";
249
+ navigator.clipboard.writeText(textToCopy).then(() => {
250
+ this.showAlert("内容已复制到剪贴板");
251
+ }).catch((err) => {
252
+ this.showAlert(`复制失败:${err}`);
253
+ });
254
+ } catch (err) {
255
+ this.showAlert(`复制失败:${err}`);
256
+ const textarea = document.createElement("textarea");
257
+ textarea.value = this.resultPopupContentEl.textContent || "";
258
+ document.body.appendChild(textarea);
259
+ textarea.select();
260
+ document.execCommand("copy");
261
+ document.body.removeChild(textarea);
262
+ }
263
+ }
264
+ // 分享和朗读功能待放开
265
+ // private shareResult() {
266
+ // if (!this.resultPopupContentEl) return
267
+ // const textToShare = this.resultPopupContentEl.textContent || ''
268
+ // const title = 'AI生成内容分享'
269
+ // if (navigator.share) {
270
+ // navigator.share({
271
+ // title,
272
+ // text: textToShare,
273
+ // })
274
+ // .catch((err) => {
275
+ // this.showAlert(`分享失败:${err}`)
276
+ // })
277
+ // }
278
+ // else {
279
+ // // 兼容不支持Web Share API的浏览器
280
+ // const shareUrl = `mailto:?subject=${encodeURIComponent(title)}&body=${encodeURIComponent(textToShare)}`
281
+ // window.open(shareUrl, '_blank')
282
+ // }
283
+ // }
284
+ // private voiceResult() {
285
+ // if (!this.resultPopupContentEl) return
286
+ // const textToSpeak = this.resultPopupContentEl.textContent || ''
287
+ // if ('speechSynthesis' in window) {
288
+ // const utterance = new SpeechSynthesisUtterance(textToSpeak)
289
+ // utterance.lang = 'zh-CN' // 设置中文语音
290
+ // speechSynthesis.speak(utterance)
291
+ // }
292
+ // else {
293
+ // this.showAlert('当前浏览器不支持语音合成API')
294
+ // // 可以在这里添加不支持语音的提示
295
+ // }
296
+ // }
297
+ addResultEvent() {
298
+ if (this.resultRefreshBtnEl) {
299
+ this.resultRefreshBtnEl.addEventListener("click", () => {
300
+ this.regenerateResponse();
301
+ });
302
+ }
303
+ if (this.resultCopyBtnEl) {
304
+ this.resultCopyBtnEl.addEventListener("click", () => {
305
+ this.copyResult();
306
+ });
307
+ }
308
+ }
309
+ // 显示选中文本的气泡
310
+ showSelectionBubble() {
311
+ if (!this.selectionBubbleEl) {
312
+ this.selectionBubbleEl = document.createElement("div");
313
+ this.selectionBubbleEl.className = "ql-ai-selection-bubble";
314
+ const icon = AI_ICON.replaceAll("paint_linear_2", "paint_linear_bubble");
315
+ this.selectionBubbleEl.innerHTML = `${icon}<span>AI 智能</span>`;
316
+ this.selectionBubbleEl.addEventListener("click", () => this.selectTextEvent());
317
+ document.body.appendChild(this.selectionBubbleEl);
318
+ }
319
+ const { left, top } = this.quill.getBounds(this.selectionRange.index);
320
+ const { left: endLeft } = this.quill.getBounds(this.selectionRange.index + this.selectionRange.length);
321
+ const width = (endLeft - left) / 2;
322
+ const editorRect = this.quill.container.getBoundingClientRect();
323
+ this.selectionBubbleEl.style.display = "flex";
324
+ this.selectionBubbleEl.style.left = `${left + editorRect.left + width - 45}px`;
325
+ this.selectionBubbleEl.style.top = `${top + editorRect.top - 40}px`;
326
+ }
327
+ // 隐藏选中文本的气泡
328
+ hideSelectionBubble() {
329
+ if (this.selectionBubbleEl) {
330
+ this.selectionBubbleEl.style.display = "none";
331
+ }
332
+ }
333
+ // 处理文本选中变化
334
+ handleSelectionChange(range) {
335
+ if (range && range.length > 0) {
336
+ this.selectionRange = range;
337
+ this.showSelectionBubble();
338
+ this.selectedText = this.quill.getText(range.index, range.length);
339
+ } else {
340
+ if (range && range.index !== null) {
341
+ this.selectedText = "";
342
+ this.closeAIPanel();
343
+ } else {
344
+ this.hideSelectionBubble();
345
+ }
346
+ }
347
+ }
348
+ addInputEvent() {
349
+ if (this.inputContainerEl) {
350
+ this.inputContainerEl.addEventListener("click", () => {
351
+ });
352
+ }
353
+ if (this.inputEl) {
354
+ this.inputEl.addEventListener("input", () => {
355
+ this.hiddenInputSendBtnEl(this.inputEl.value.trim() ? "flex" : "none");
356
+ if (this.menuContainerEl && this._isSelectRangeMode) {
357
+ this.showOperationMenu = !this.inputEl.value.trim() && !this._showResultPopupEl;
358
+ }
359
+ });
360
+ }
361
+ if (this.inputSendBtnEl) {
362
+ this.inputSendBtnEl.addEventListener("click", async () => {
363
+ await this.queryAI();
364
+ });
365
+ }
96
366
  this.inputEl.addEventListener("keydown", async (e) => {
97
367
  if (e.key === "Enter") {
98
368
  await this.queryAI();
99
369
  }
100
370
  });
101
- this.sendButtonEl.addEventListener("click", async (e) => {
102
- if (e.target instanceof HTMLElement && e.target.textContent === this.BREAK) {
103
- this.isBreak = true;
104
- } else {
105
- await this.queryAI();
106
- }
107
- });
108
- const handleKeyDown = (e) => {
109
- if (e.key === "Escape") {
371
+ if (this.inputCloseBtnEl) {
372
+ this.inputCloseBtnEl.addEventListener("click", () => {
110
373
  this.closeAIPanel();
111
- this.quill.container.removeEventListener("keydown", handleKeyDown);
374
+ });
375
+ }
376
+ }
377
+ positionElements() {
378
+ if (!this.dialogContainerEl) return;
379
+ const range = this.selectionRange;
380
+ if (range) {
381
+ const bounds = this.quill.getBounds(range.index);
382
+ this.dialogContainerEl.style.position = "absolute";
383
+ this.dialogContainerEl.style.top = `${bounds.top + bounds.height + 20}px`;
384
+ }
385
+ }
386
+ // 添加创建alert元素的方法
387
+ createAlertElement() {
388
+ if (!this.alertEl) {
389
+ this.alertEl = document.createElement("div");
390
+ this.alertEl.className = "ql-ai-alert";
391
+ this.alertEl.style.display = "none";
392
+ document.body.appendChild(this.alertEl);
393
+ }
394
+ }
395
+ // 添加显示alert的方法
396
+ showAlert(message, duration = 3e3) {
397
+ this.createAlertElement();
398
+ if (!this.alertEl) return;
399
+ if (this.alertTimer) {
400
+ clearTimeout(this.alertTimer);
401
+ this.alertTimer = null;
402
+ }
403
+ this.alertEl.textContent = message;
404
+ this.alertEl.style.display = "block";
405
+ this.alertTimer = setTimeout(() => {
406
+ if (this.alertEl) {
407
+ this.alertEl.style.display = "none";
112
408
  }
113
- };
114
- this.quill.container.addEventListener("keydown", handleKeyDown);
409
+ this.alertTimer = null;
410
+ }, duration);
411
+ }
412
+ createAIInputIcon() {
413
+ if (!this.aiIconEl) {
414
+ this.aiIconEl = document.createElement("span");
415
+ this.aiIconEl.className = "ql-ai-input-pre-icon";
416
+ const icon = AI_ICON.replaceAll("paint_linear_2", "paint_linear_ai_input");
417
+ this.aiIconEl.innerHTML = icon;
418
+ }
419
+ }
420
+ // 添加处理子菜单点击的方法
421
+ handleOperationMenuItemClick(text, id = "") {
422
+ let quetion = "";
423
+ if (id.startsWith("1-") || id.startsWith("3-")) {
424
+ quetion = `将目标文字${text},目标文字为:${this.selectedText}`;
425
+ } else if (id.startsWith("2-")) {
426
+ quetion = `改写目标文字的口吻,让其变得${text},目标文字为:${this.selectedText}`;
427
+ }
428
+ this.showOperationMenu = false;
429
+ this.queryAI(quetion);
430
+ }
431
+ createActionMenu() {
432
+ if (!this.actionMenuEl) {
433
+ this.actionMenuEl = document.createElement("div");
434
+ this.actionMenuEl.className = "ql-ai-actions";
435
+ this.resultMenuList.forEach(({ text, icon }) => {
436
+ const menuItem = document.createElement("div");
437
+ menuItem.className = "ql-ai-action-item";
438
+ menuItem.innerHTML = `${icon}<span class="ql-ai-result-menu-text">${text}</span>`;
439
+ menuItem.addEventListener("click", () => this.handleAction(text));
440
+ this.actionMenuEl.appendChild(menuItem);
441
+ });
442
+ this.wrapContainerEl.appendChild(this.actionMenuEl);
443
+ }
444
+ const secondMenuItemText = this.actionMenuEl.children[1].querySelector(".ql-ai-result-menu-text");
445
+ const firstChild = this.actionMenuEl.firstChild;
446
+ if (!this._isSelectRangeMode) {
447
+ if (firstChild instanceof Element) {
448
+ firstChild.classList.add("hidden");
449
+ }
450
+ secondMenuItemText.textContent = INSERT_TEXT;
451
+ } else {
452
+ if (firstChild instanceof Element) {
453
+ firstChild.classList.remove("hidden");
454
+ }
455
+ secondMenuItemText.textContent = INSERT_SUB_CONTENT_TEXT;
456
+ }
457
+ this.isThinking = false;
458
+ }
459
+ handleActionMenuDisplay(value = "none") {
460
+ if (this.actionMenuEl) {
461
+ this.actionMenuEl.style.display = value;
462
+ }
463
+ }
464
+ switchInputEl(showInput = true) {
465
+ if (this.inputContainerEl) {
466
+ this.inputContainerEl.style.display = showInput ? "flex" : "none";
467
+ }
468
+ this.handleActionMenuDisplay(showInput ? "block" : "none");
469
+ if (this.thinkContainerEl) {
470
+ this.thinkContainerEl.style.display = showInput ? "none" : "flex";
471
+ }
472
+ }
473
+ // 创建思考元素
474
+ createThinkElements() {
475
+ if (!this.thinkContainerEl) {
476
+ this.thinkContainerEl = document.createElement("div");
477
+ this.thinkContainerEl.className = "ql-ai-input";
478
+ this.thinkContainerEl.innerHTML = `<span class="ql-ai-input-pre-icon ql-ai-think-icon">${THINK_ICON}</span><span class="ql-ai-think-text">${THINK_TEXT}</span>`;
479
+ this.thinkBtnEl = document.createElement("div");
480
+ this.thinkBtnEl.className = "ql-ai-think-btn";
481
+ this.thinkBtnEl.innerHTML = `${STOP_ICON}<span>${STOP_ANSWER}</span>`;
482
+ this.thinkContainerEl.appendChild(this.thinkBtnEl);
483
+ this.wrapContainerEl.appendChild(this.thinkContainerEl);
484
+ this.thinkBtnEl.addEventListener("click", () => {
485
+ this.isBreak = true;
486
+ this.isThinking = false;
487
+ });
488
+ }
489
+ this.isThinking = true;
115
490
  }
116
491
  // AI查询
117
492
  async queryAI(question) {
493
+ this.createThinkElements();
118
494
  this.inputValue = question || this.inputEl.value;
119
- this.inputEl.value = "";
120
495
  if (this.inputValue.trim() === "") {
121
496
  return;
122
497
  }
123
498
  this.isBreak = false;
124
- this.sendButtonEl.textContent = this.BREAK;
125
- this.sendButtonEl.style.display = "block";
126
- this.aiPreTextEl.textContent = "按ESC退出 | 正在编写...";
127
499
  try {
128
- const response = await fetch(`${this.host}/api/generate`, {
500
+ const response = await fetch(`${this.host}`, {
129
501
  method: "POST",
130
502
  headers: {
131
503
  "Content-Type": "application/json",
@@ -163,11 +535,8 @@ class AI {
163
535
  }
164
536
  }
165
537
  this.createActionMenu();
166
- if (content) {
167
- this.aiPreTextEl.textContent = "";
168
- this.sendButtonEl.textContent = this.SEND;
169
- this.sendButtonEl.style.display = "none";
170
- }
538
+ this.inputEl.value = "";
539
+ this.hiddenInputSendBtnEl();
171
540
  return content;
172
541
  } catch (error) {
173
542
  console.error("AI查询失败:", error);
@@ -176,60 +545,123 @@ class AI {
176
545
  }
177
546
  showAIResponse(response) {
178
547
  if (!this.resultPopupEl) return;
179
- this.resultPopupEl.innerHTML = response;
180
- this.resultPopupEl.style.display = "block";
181
- }
182
- createActionMenu() {
183
- if (!this.actionMenuEl) {
184
- this.actionMenuEl = document.createElement("div");
185
- this.actionMenuEl.className = "ql-ai-actions";
186
- const actions = [this.DONE, this.REGENERATE, this.CLOSE];
187
- actions.forEach((action) => {
188
- const menuItem = document.createElement("div");
189
- menuItem.className = "ql-ai-action-item";
190
- menuItem.textContent = action;
191
- menuItem.addEventListener("click", () => this.handleAction(action));
192
- this.actionMenuEl.appendChild(menuItem);
193
- });
194
- this.wrapContainerEl.appendChild(this.actionMenuEl);
548
+ if (this._charCount <= this.textNumber) {
549
+ this.resultPopupContentEl.innerHTML = response;
550
+ this.charCount = this.resultPopupContentEl.textContent.replace(/\s+/g, "").length;
551
+ } else {
552
+ this.isBreak = true;
553
+ this.charCount = 0;
195
554
  }
196
- this.actionMenuEl.style.display = "block";
555
+ this.showResultPopupEl = true;
197
556
  }
198
557
  handleAction(action) {
199
558
  switch (action) {
200
- case this.DONE:
559
+ case REPLACE_SELECT:
560
+ this.replaceSelectText();
561
+ break;
562
+ case INSERT_TEXT:
201
563
  this.insertAIResponse();
202
564
  break;
203
- case this.REGENERATE:
565
+ case REGENERATE:
204
566
  this.regenerateResponse();
205
567
  break;
206
- case this.CLOSE:
568
+ case CLOSE:
207
569
  this.closeAIPanel();
208
570
  break;
209
571
  }
210
572
  }
573
+ replaceSelectText() {
574
+ if (!this.resultPopupContentEl) return;
575
+ const range = this.quill.getSelection(true);
576
+ if (range && range.length > 0) {
577
+ this.quill.deleteText(range.index, range.length);
578
+ this.quill.clipboard.dangerouslyPasteHTML(range.index, this.resultPopupContentEl.innerHTML);
579
+ }
580
+ this.closeAIPanel();
581
+ }
211
582
  insertAIResponse() {
212
- if (!this.resultPopupEl) return;
583
+ if (!this.resultPopupContentEl) return;
213
584
  const range = this.quill.getSelection(true);
214
585
  if (range) {
215
- this.quill.clipboard.dangerouslyPasteHTML(
216
- range.index,
217
- this.resultPopupEl.innerHTML
218
- );
586
+ this.quill.clipboard.dangerouslyPasteHTML(range.index + range.length, this.resultPopupContentEl.innerHTML);
219
587
  }
220
588
  this.closeAIPanel();
221
589
  }
222
590
  async regenerateResponse() {
223
- this.actionMenuEl.style.display = "none";
224
591
  await this.queryAI(this.inputValue);
225
592
  }
226
593
  closeAIPanel() {
227
594
  this.isBreak = true;
228
595
  if (this.dialogContainerEl) {
229
- this.quill.container.removeChild(this.dialogContainerEl);
596
+ this.dialogContainerEl.style.display = "none";
230
597
  }
231
- this.dialogContainerEl = null;
232
- this.actionMenuEl = null;
598
+ if (this.actionMenuEl) {
599
+ this.actionMenuEl.style.display = "none";
600
+ }
601
+ this.showResultPopupEl = false;
602
+ if (this.inputEl && this.inputEl.value.trim() !== "") {
603
+ this.inputEl.value = "";
604
+ }
605
+ this.hideSelectionBubble();
606
+ }
607
+ set charCount(value) {
608
+ if (this._debounceTimer) {
609
+ clearTimeout(this._debounceTimer);
610
+ }
611
+ this._debounceTimer = setTimeout(() => {
612
+ this._charCount = value;
613
+ if (this.resultPopupFooterTextEl) {
614
+ this.resultPopupFooterTextEl.textContent = `${this._charCount}/${this.textNumber}`;
615
+ }
616
+ clearTimeout(this._debounceTimer);
617
+ this._debounceTimer = null;
618
+ }, 210);
619
+ }
620
+ get charCount() {
621
+ return this._charCount;
622
+ }
623
+ set inputPlaceholder(value) {
624
+ this._inputPlaceholder = value;
625
+ if (this.inputEl) {
626
+ this.inputEl.placeholder = value;
627
+ }
628
+ }
629
+ get inputPlaceholder() {
630
+ return this._inputPlaceholder;
631
+ }
632
+ set showOperationMenu(value) {
633
+ this._showOperationMenu = value;
634
+ if (this.menuContainerEl) {
635
+ this.menuContainerEl.style.display = value ? "flex" : "none";
636
+ }
637
+ }
638
+ get showOperationMenu() {
639
+ return this._showOperationMenu;
640
+ }
641
+ set isSelectRangeMode(value) {
642
+ this._isSelectRangeMode = value;
643
+ this.showOperationMenu = value;
644
+ this.inputPlaceholder = value ? SELECT_PLACEHOLDER : INPUT_PLACEHOLDER;
645
+ this.hideSelectionBubble();
646
+ }
647
+ get isSelectRangeMode() {
648
+ return this._isSelectRangeMode;
649
+ }
650
+ set isThinking(value) {
651
+ this._isThinking = value;
652
+ this.switchInputEl(!value);
653
+ }
654
+ get isThinking() {
655
+ return this._isThinking;
656
+ }
657
+ set showResultPopupEl(value) {
658
+ this._showResultPopupEl = value;
659
+ if (this.resultPopupEl) {
660
+ this.resultPopupEl.style.display = value ? "block" : "none";
661
+ }
662
+ }
663
+ get showResultPopupEl() {
664
+ return this._showResultPopupEl;
233
665
  }
234
666
  }
235
667
  export {