@baishuyun/ai-design 2.0.37 → 2.1.0-alpha.0

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 (269) hide show
  1. package/.storybook/main.ts +31 -31
  2. package/.storybook/preview.ts +16 -16
  3. package/CHANGELOG.md +512 -492
  4. package/components.json +22 -22
  5. package/package.json +9 -6
  6. package/scripts/strip-layers.cjs +41 -41
  7. package/src/components/abs-fullscreen-gradient-bg.tsx +35 -35
  8. package/src/components/agent-helper/agent-category-select-menu/index.tsx +57 -57
  9. package/src/components/agent-helper/agent-entry-card/index.tsx +94 -94
  10. package/src/components/agent-helper/agent-helper-card/index.tsx +179 -179
  11. package/src/components/agent-helper/agent-helper-card-list/index.tsx +59 -59
  12. package/src/components/agent-helper/agent-helper-main-layout/index.tsx +60 -60
  13. package/src/components/agent-helper/agent-sort-select/index.tsx +89 -89
  14. package/src/components/agent-helper/agent-tag/index.tsx +29 -29
  15. package/src/components/agent-helper/app-select/index.tsx +559 -559
  16. package/src/components/agent-helper/condition-group/index.tsx +75 -75
  17. package/src/components/agent-helper/condition-input/index.tsx +233 -233
  18. package/src/components/agent-helper/condition-item/index.tsx +237 -237
  19. package/src/components/agent-helper/condition-quantifier-select/index.tsx +142 -142
  20. package/src/components/agent-helper/condition-trigger-description/index.tsx +49 -49
  21. package/src/components/agent-helper/date-picker/calendar-panel.tsx +164 -164
  22. package/src/components/agent-helper/date-picker/date-input.tsx +91 -91
  23. package/src/components/agent-helper/date-picker/date-picker.tsx +327 -327
  24. package/src/components/agent-helper/date-picker/footer-panel.tsx +44 -44
  25. package/src/components/agent-helper/date-picker/index.ts +3 -3
  26. package/src/components/agent-helper/date-picker/month-panel.tsx +82 -82
  27. package/src/components/agent-helper/date-picker/time-panel.tsx +146 -146
  28. package/src/components/agent-helper/date-picker/types.ts +37 -37
  29. package/src/components/agent-helper/date-picker/utils.ts +121 -121
  30. package/src/components/agent-helper/date-picker/year-panel.tsx +86 -86
  31. package/src/components/agent-helper/exec-log-filter/index.tsx +232 -232
  32. package/src/components/agent-helper/field-edit-preference-select/index.tsx +291 -291
  33. package/src/components/agent-helper/field-select/index.tsx +466 -466
  34. package/src/components/agent-helper/field-value-input/china-address-data.ts +4117 -4117
  35. package/src/components/agent-helper/field-value-input/display.tsx +71 -71
  36. package/src/components/agent-helper/field-value-input/icons.tsx +38 -38
  37. package/src/components/agent-helper/field-value-input/index.tsx +144 -144
  38. package/src/components/agent-helper/field-value-input/inputs.tsx +247 -247
  39. package/src/components/agent-helper/field-value-input/picker-inputs.tsx +1141 -1141
  40. package/src/components/agent-helper/flow-node-action-trigger-select/index.tsx +57 -57
  41. package/src/components/agent-helper/form-action-trigger-select/index.tsx +60 -60
  42. package/src/components/agent-helper/form-divider/index.tsx +44 -44
  43. package/src/components/agent-helper/form-select/index.tsx +732 -732
  44. package/src/components/agent-helper/monthdays-select/index.tsx +378 -378
  45. package/src/components/agent-helper/offset-preference-select/index.tsx +268 -268
  46. package/src/components/agent-helper/offset-type-select/index.tsx +66 -66
  47. package/src/components/agent-helper/offset-unit-select/index.tsx +54 -54
  48. package/src/components/agent-helper/placeholder-card/index.tsx +75 -75
  49. package/src/components/agent-helper/prompt-input/index.tsx +646 -640
  50. package/src/components/agent-helper/relational-operator-select/index.tsx +179 -179
  51. package/src/components/agent-helper/repeat-by-type-select/index.tsx +53 -53
  52. package/src/components/agent-helper/repeat-preference-select/index.tsx +162 -162
  53. package/src/components/agent-helper/repeat-preference-type-select/index.tsx +58 -58
  54. package/src/components/agent-helper/search-input/index.tsx +35 -35
  55. package/src/components/agent-helper/table/index.tsx +114 -114
  56. package/src/components/agent-helper/trigger-select/index.tsx +121 -121
  57. package/src/components/agent-helper/trigger-time-preference-select/index.tsx +189 -189
  58. package/src/components/agent-helper/trigger-time-source-type-select/index.tsx +53 -53
  59. package/src/components/agent-helper/weekdays-select/index.tsx +230 -230
  60. package/src/components/agent-scroll-selector.tsx +193 -193
  61. package/src/components/agent-select.tsx +154 -154
  62. package/src/components/attachment-part-group.tsx +35 -35
  63. package/src/components/attachment-part.tsx +296 -296
  64. package/src/components/attachments-previewer.tsx +272 -272
  65. package/src/components/base-button.tsx +49 -49
  66. package/src/components/border-animation.tsx +107 -107
  67. package/src/components/border-color-animation.tsx +66 -66
  68. package/src/components/bot-avatar-name.tsx +36 -36
  69. package/src/components/bot-icon.tsx +23 -23
  70. package/src/components/bs-entry-select/app-group-folder.tsx +61 -61
  71. package/src/components/bs-entry-select/bs-entry-select.tsx +296 -296
  72. package/src/components/bs-entry-select/build-tree.ts +265 -265
  73. package/src/components/bs-entry-select/entry-item.tsx +45 -45
  74. package/src/components/bs-entry-select/entry-popover.tsx +99 -99
  75. package/src/components/bs-entry-select/index.ts +7 -7
  76. package/src/components/bs-entry-select/other-app-section.tsx +96 -96
  77. package/src/components/bs-entry-select/scroll-slider.tsx +872 -872
  78. package/src/components/bs-entry-select/search-input.tsx +4 -4
  79. package/src/components/bs-entry-select/tree-item-node.tsx +84 -84
  80. package/src/components/bs-entry-select/types.ts +46 -46
  81. package/src/components/bs-icons.tsx +1362 -1362
  82. package/src/components/card-field.tsx +23 -23
  83. package/src/components/card.tsx +36 -36
  84. package/src/components/chat-area-header.tsx +65 -65
  85. package/src/components/check-card.tsx +77 -77
  86. package/src/components/circle-checker.tsx +66 -66
  87. package/src/components/collapsible-txt-msg.tsx +75 -75
  88. package/src/components/collapsible.tsx +9 -9
  89. package/src/components/confirm-dialog.tsx +139 -139
  90. package/src/components/date-range-picker.tsx +91 -91
  91. package/src/components/fake-bot-msg.tsx +51 -51
  92. package/src/components/field-checker-list-msg.tsx +146 -146
  93. package/src/components/field-checker.tsx +185 -185
  94. package/src/components/field-icon.tsx +13 -13
  95. package/src/components/field-value-checker.tsx +116 -116
  96. package/src/components/fields-design-info-table.tsx +160 -160
  97. package/src/components/fields-generating-indicator.tsx +95 -95
  98. package/src/components/fields-portal.tsx +18 -18
  99. package/src/components/fields-previewer.tsx +216 -216
  100. package/src/components/font-icon.tsx +65 -65
  101. package/src/components/form-info-editor.tsx +263 -263
  102. package/src/components/formula-tag.tsx +32 -32
  103. package/src/components/generate-animation.tsx +56 -56
  104. package/src/components/icon-btn.tsx +43 -43
  105. package/src/components/icon-card.tsx +67 -67
  106. package/src/components/icons/calendar-icon.tsx +5 -5
  107. package/src/components/icons/clear-all-icon.tsx +5 -5
  108. package/src/components/icons/empty-agent-icon.tsx +93 -93
  109. package/src/components/icons/empty-table-icon.tsx +104 -104
  110. package/src/components/icons/grids-icon.tsx +5 -5
  111. package/src/components/img-part.tsx +27 -27
  112. package/src/components/legacy-slot.tsx +110 -110
  113. package/src/components/line-checker.tsx +168 -168
  114. package/src/components/linear-gradient-border-btn.tsx +48 -48
  115. package/src/components/linear-gradient-color-bg-animation.tsx +103 -103
  116. package/src/components/memoized-markdown.tsx +32 -32
  117. package/src/components/message-content.tsx +21 -21
  118. package/src/components/number-part.tsx +46 -46
  119. package/src/components/office-container.tsx +289 -265
  120. package/src/components/option-item.tsx +27 -27
  121. package/src/components/output-config-editor.tsx +478 -478
  122. package/src/components/previewer-header.tsx +270 -270
  123. package/src/components/primary-confirm-btn.tsx +58 -58
  124. package/src/components/primary-entry-btn.tsx +48 -48
  125. package/src/components/radio-card.tsx +63 -63
  126. package/src/components/scroll-to-bottom-btn.tsx +28 -28
  127. package/src/components/split-line.tsx +9 -9
  128. package/src/components/square-checker.tsx +87 -87
  129. package/src/components/subform-fields-value-checker.tsx +184 -184
  130. package/src/components/tab-radio-group.tsx +83 -83
  131. package/src/components/tooltip.tsx +53 -53
  132. package/src/components/ui/button.tsx +51 -51
  133. package/src/components/ui/calendar.tsx +57 -57
  134. package/src/components/ui/dialog.tsx +104 -104
  135. package/src/components/ui/drawer.tsx +279 -279
  136. package/src/components/ui/form.tsx +202 -202
  137. package/src/components/ui/icons.tsx +1267 -1267
  138. package/src/components/ui/input.tsx +28 -28
  139. package/src/components/ui/label.tsx +30 -30
  140. package/src/components/ui/popover.tsx +85 -85
  141. package/src/components/ui/select.tsx +236 -236
  142. package/src/components/ui/spinner.tsx +17 -17
  143. package/src/components/ui/switch.tsx +33 -33
  144. package/src/components/ui/tabs.tsx +62 -62
  145. package/src/components/ui/textarea.tsx +28 -28
  146. package/src/components/ui/tooltip.tsx +53 -53
  147. package/src/components/user-txt-msg-bubble.tsx +47 -47
  148. package/src/components/warning-msg.tsx +27 -27
  149. package/src/const/ui.ts +119 -119
  150. package/src/css.d.ts +4 -4
  151. package/src/hooks/use-column-appear.ts +78 -78
  152. package/src/hooks/use-portal-container.ts +27 -27
  153. package/src/hooks/use-resizable-width.ts +129 -129
  154. package/src/hooks/use-select-popover-position.ts +74 -74
  155. package/src/hooks/use-shadow.ts +127 -127
  156. package/src/index.ts +226 -226
  157. package/src/lib/utils.ts +138 -138
  158. package/src/stories/AbsFullscreenGradientBg.stories.tsx +36 -36
  159. package/src/stories/AgentCategorySelectMenu.stories.tsx +33 -33
  160. package/src/stories/AgentEntryCard.stories.tsx +90 -90
  161. package/src/stories/AgentHelperCard.stories.tsx +105 -105
  162. package/src/stories/AgentHelperCardList.stories.tsx +120 -120
  163. package/src/stories/AgentHelperMainLayout.stories.tsx +71 -71
  164. package/src/stories/AgentScrollSelector.stories.tsx +95 -95
  165. package/src/stories/AgentSelect.stories.tsx +134 -134
  166. package/src/stories/AgentTag.stories.tsx +44 -44
  167. package/src/stories/AppSelect.stories.tsx +250 -250
  168. package/src/stories/AttachmentPart.stories.tsx +122 -122
  169. package/src/stories/AttachmentPartGroup.stories.tsx +152 -152
  170. package/src/stories/AttachmentsPreviewer.stories.tsx +306 -306
  171. package/src/stories/BaseButton.stories.tsx +33 -33
  172. package/src/stories/BorderAnimation.stories.tsx +116 -116
  173. package/src/stories/BorderColorAnimation.stories.tsx +63 -63
  174. package/src/stories/BotAvatarAndName.stories.ts +24 -24
  175. package/src/stories/BotIcon.stories.ts +22 -22
  176. package/src/stories/BsConditionInput.stories.tsx +460 -460
  177. package/src/stories/BsEntrySelect.stories.tsx +343 -343
  178. package/src/stories/BsTooltip.stories.tsx +32 -32
  179. package/src/stories/Card.stories.tsx +28 -28
  180. package/src/stories/CardField.stories.tsx +87 -87
  181. package/src/stories/ChatAreaHeader.stories.ts +26 -26
  182. package/src/stories/CheckCard.stories.tsx +114 -114
  183. package/src/stories/CircleChecker.stories.tsx +89 -89
  184. package/src/stories/CollapsibleTextMsg.stories.ts +44 -44
  185. package/src/stories/ConditionGroup.stories.tsx +216 -216
  186. package/src/stories/ConditionInput.stories.tsx +502 -502
  187. package/src/stories/ConditionItem.stories.tsx +163 -163
  188. package/src/stories/ConditonQuantifierSelect.stories.tsx +54 -54
  189. package/src/stories/ConditonTriggerDescription.stories.tsx +61 -61
  190. package/src/stories/ConfirmDialog.stories.tsx +123 -123
  191. package/src/stories/DataTable.stories.tsx +191 -191
  192. package/src/stories/DatePicker.stories.tsx +174 -174
  193. package/src/stories/ExecLogFilter.stories.tsx +145 -145
  194. package/src/stories/FakeBotMsg.stories.tsx +36 -36
  195. package/src/stories/FieldEditPreferenceSelect.stories.tsx +161 -161
  196. package/src/stories/FieldSelect.stories.tsx +228 -228
  197. package/src/stories/FieldValueInput.stories.tsx +442 -442
  198. package/src/stories/FlowNodeActionTriggerSelect.stories.tsx +80 -80
  199. package/src/stories/Form.stories.tsx +302 -302
  200. package/src/stories/FormActionTriggerSelect.stories.tsx +80 -80
  201. package/src/stories/FormDivider.stories.tsx +31 -31
  202. package/src/stories/FormInfoEditor.stories.tsx +173 -173
  203. package/src/stories/FormSelect.stories.tsx +222 -222
  204. package/src/stories/FormulaTag.stories.tsx +68 -68
  205. package/src/stories/GenerateAnimation.stories.ts +20 -20
  206. package/src/stories/IconBtn.stories.tsx +32 -32
  207. package/src/stories/IconCard.stories.tsx +35 -35
  208. package/src/stories/IconLib.tsx +45 -45
  209. package/src/stories/Icons.stories.ts +22 -22
  210. package/src/stories/ImgMsgPart.stories.tsx +42 -42
  211. package/src/stories/Input.stories.tsx +102 -102
  212. package/src/stories/Label.stories.tsx +66 -66
  213. package/src/stories/LegacySlot.stories.tsx +56 -56
  214. package/src/stories/LineChecker.stories.tsx +133 -133
  215. package/src/stories/LinearGradientBorderBtn.stories.tsx +86 -86
  216. package/src/stories/LinearGradientColorBgAnimation.stories.tsx +60 -60
  217. package/src/stories/MemoizedMarkdown.stories.tsx +102 -102
  218. package/src/stories/MessageContent.stories.tsx +36 -36
  219. package/src/stories/MonthdaysSelect.stories.tsx +78 -78
  220. package/src/stories/NumbersMsgPart.stories.tsx +42 -42
  221. package/src/stories/OfficeContainer.stories.tsx +287 -287
  222. package/src/stories/OffsetPreferenceSelect.stories.tsx +118 -118
  223. package/src/stories/OffsetTypeSelect.stories.tsx +75 -75
  224. package/src/stories/OffsetUnitSelect.stories.tsx +61 -61
  225. package/src/stories/OptionItem.stories.tsx +77 -77
  226. package/src/stories/OutputConfigEditor.stories.tsx +96 -96
  227. package/src/stories/PlaceholderCard.stories.tsx +35 -35
  228. package/src/stories/PreviewerHeader.stories.tsx +91 -91
  229. package/src/stories/PrimaryConfirmBtn.stories.tsx +60 -60
  230. package/src/stories/PrimaryEntryBtn.stories.tsx +74 -74
  231. package/src/stories/PromptInput.stories.tsx +228 -228
  232. package/src/stories/RadioCard.stories.tsx +103 -103
  233. package/src/stories/RelationalOperatorSelect.stories.tsx +61 -61
  234. package/src/stories/RepeatByTypeSelect.stories.tsx +58 -58
  235. package/src/stories/RepeatPreferenceSelect.stories.tsx +107 -107
  236. package/src/stories/RepeatPreferenceTypeSelect.stories.tsx +58 -58
  237. package/src/stories/ScrollToBottom.stories.tsx +24 -24
  238. package/src/stories/SearchInput.stories.tsx +50 -50
  239. package/src/stories/Select.stories.tsx +128 -128
  240. package/src/stories/SplitLine.stories.tsx +40 -40
  241. package/src/stories/SquareChecker.stories.tsx +96 -96
  242. package/src/stories/TabRadioGroup.stories.tsx +84 -84
  243. package/src/stories/Textarea.stories.tsx +67 -67
  244. package/src/stories/TriggerSelect.stories.tsx +48 -48
  245. package/src/stories/TriggerTimePreferenceSelect.stories.tsx +106 -106
  246. package/src/stories/TriggerTimeSourceTypeSelect.stories.tsx +68 -68
  247. package/src/stories/UserTextMsgBubble.stories.ts +24 -24
  248. package/src/stories/WarningMessage.stories.tsx +23 -23
  249. package/src/stories/WeekdaysSelect.stories.tsx +66 -66
  250. package/src/stories/assets/demo.css +539 -539
  251. package/src/stories/assets/demo_index.html +7988 -7988
  252. package/src/stories/assets/iconfont.css +1377 -1377
  253. package/src/stories/assets/iconfont.json +2382 -2382
  254. package/src/stories/assets/iconfont.svg +697 -697
  255. package/src/stories/collapsible.stories.tsx +47 -47
  256. package/src/stories/drawer.stories.tsx +196 -196
  257. package/src/stories/field-checker-list-msg.stories.tsx +95 -95
  258. package/src/stories/field-checker.stories.tsx +117 -117
  259. package/src/stories/field-icon.stories.tsx +70 -70
  260. package/src/stories/field-value-checker.stories.tsx +113 -113
  261. package/src/stories/fields-design-info-table.stories.tsx +203 -203
  262. package/src/stories/fields-generating.stories.tsx +41 -41
  263. package/src/stories/fields-previewer.stories.tsx +77 -77
  264. package/src/stories/subform-fields-value-checker.stories.tsx +98 -98
  265. package/src/style.css +226 -226
  266. package/src/types.ts +8 -8
  267. package/style.css +3652 -3652
  268. package/tsconfig.json +13 -13
  269. package/vite.config.ts +44 -44
@@ -1,640 +1,646 @@
1
- import { useEffect, useRef, useState, useCallback } from 'react';
2
- import CodeMirror from 'codemirror';
3
- import { cn } from '../../../lib/utils';
4
- import { Popover, PopoverTrigger, PopoverContent } from '../../ui/popover';
5
- import { FieldSelectPopover } from '../field-select';
6
- import type { IField, ISubformField } from '@baishuyun/types';
7
-
8
- // ── Create a DOM node for the field tag (used inside CodeMirror) ────
9
-
10
- function createFieldTagDom(label: string): HTMLSpanElement {
11
- const wrapper = document.createElement('span');
12
- wrapper.className = 'prompt-field-tag';
13
-
14
- const iconSpan = document.createElement('span');
15
- iconSpan.className = 'prompt-field-tag-icon';
16
- iconSpan.innerHTML =
17
- '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="2" y="2" width="10" height="10" rx="2" stroke="#6F829E" stroke-width="1.2"/><path d="M5 5.5H9M5 7.5H8" stroke="#6F829E" stroke-width="1.2" stroke-linecap="round"/></svg>';
18
-
19
- const textSpan = document.createElement('span');
20
- textSpan.textContent = label;
21
-
22
- wrapper.appendChild(iconSpan);
23
- wrapper.appendChild(textSpan);
24
- return wrapper;
25
- }
26
-
27
- // ── CodeMirror Helpers ──────────────────────────────────────────────
28
-
29
- function resolveFieldWidgetInfo(
30
- fields: IField[],
31
- key: string,
32
- ): { widgetName: string; label: string } | undefined {
33
- const dotIndex = key.indexOf('.');
34
- if (dotIndex > 0) {
35
- const parentName = key.substring(0, dotIndex);
36
- const childName = key.substring(dotIndex + 1);
37
- const parent = fields.find((f) => f.widget?.widgetName === parentName);
38
- if (parent?.widget?.items) {
39
- const child = parent.widget.items.find(
40
- (item): item is ISubformField =>
41
- 'widget' in item && item.widget?.widgetName === childName,
42
- );
43
- if (child) {
44
- return { widgetName: key, label: child.label };
45
- }
46
- }
47
- } else {
48
- const field = fields.find((f) => f.widget?.widgetName === key);
49
- if (field) {
50
- return { widgetName: key, label: field.label };
51
- }
52
- }
53
- return undefined;
54
- }
55
-
56
- function addFieldToCM(
57
- cm: CodeMirror.Editor,
58
- widgetName: string,
59
- displayLabel: string,
60
- ): void {
61
- const from = cm.getCursor('from');
62
-
63
- cm.replaceSelection('${' + widgetName + '}');
64
- const to = cm.getCursor();
65
-
66
- const tagNode = createFieldTagDom(displayLabel);
67
- tagNode.setAttribute('data-field-key', widgetName);
68
-
69
- cm.markText(from, to, {
70
- handleMouseEvents: true,
71
- atomic: true,
72
- replacedWith: tagNode,
73
- });
74
- }
75
-
76
- function getTemplateFromCM(cm: CodeMirror.Editor): string {
77
- const wrapper = cm.getWrapperElement();
78
- const lineElements = wrapper.querySelectorAll('.CodeMirror-line');
79
- const lines: string[] = [];
80
-
81
- lineElements.forEach((lineEl) => {
82
- let lineText = '';
83
- walkLineNode(lineEl);
84
- lines.push(lineText);
85
-
86
- function walkLineNode(node: Node): void {
87
- if (node instanceof HTMLElement) {
88
- const key = node.getAttribute('data-field-key');
89
- if (key && node.classList.contains('prompt-field-tag')) {
90
- lineText += '${' + key + '}';
91
- return;
92
- }
93
- }
94
- const children = Array.from(node.childNodes);
95
- for (let i = 0; i < children.length; i++) {
96
- const child = children[i];
97
- if (child.nodeType === Node.TEXT_NODE) {
98
- lineText += child.textContent || '';
99
- } else {
100
- walkLineNode(child);
101
- }
102
- }
103
- }
104
- });
105
-
106
- return lines.join('\n').trim();
107
- }
108
-
109
- function setTemplateToCM(
110
- cm: CodeMirror.Editor,
111
- template: string,
112
- fields: IField[],
113
- ): void {
114
- cm.setValue('');
115
- if (!template) {
116
- cm.refresh();
117
- return;
118
- }
119
-
120
- const normalized = template.trim().replace(/\s{2,}/g, ' ');
121
- const parts = normalized.split(/(\$\{[^${}]*\})/g);
122
-
123
- for (const part of parts) {
124
- const match = part.match(/^\$\{([^${}]*)\}$/);
125
- if (match) {
126
- const info = resolveFieldWidgetInfo(fields, match[1]);
127
- if (info) {
128
- addFieldToCM(cm, info.widgetName, info.label);
129
- } else {
130
- cm.replaceSelection(part);
131
- }
132
- } else {
133
- cm.replaceSelection(part);
134
- }
135
- }
136
-
137
- cm.refresh();
138
- }
139
-
140
- // ── PromptInput Component ──────────────────────────────────────────
141
-
142
- export interface PromptInputProps {
143
- fields: IField[];
144
- value?: string;
145
- defaultValue?: string;
146
- onChange?: (template: string) => void;
147
- placeholder?: string;
148
- className?: string;
149
- disabled?: boolean;
150
- }
151
-
152
- export const PromptInput = ({
153
- fields,
154
- value,
155
- defaultValue,
156
- onChange,
157
- placeholder = '示例:放配置时候的提示词内容进行引导说明',
158
- className,
159
- disabled = false,
160
- }: PromptInputProps) => {
161
- const containerRef = useRef<HTMLDivElement>(null);
162
- const cmRef = useRef<CodeMirror.Editor | null>(null);
163
- const [open, setOpen] = useState(false);
164
- const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
165
- const insertBtnRef = useRef<HTMLButtonElement | null>(null);
166
- const isInternalUpdateRef = useRef(false);
167
-
168
- // Create field key cache ref to avoid stale closure in CM callbacks
169
- const fieldsRef = useRef(fields);
170
- fieldsRef.current = fields;
171
- const onChangeRef = useRef(onChange);
172
- onChangeRef.current = onChange;
173
-
174
- // Initialize CodeMirror
175
- useEffect(() => {
176
- const container = containerRef.current;
177
- if (!container) return;
178
-
179
- const cm = CodeMirror(container, {
180
- mode: undefined,
181
- lineWrapping: true,
182
- lineNumbers: false,
183
- smartIndent: false,
184
- autofocus: false,
185
- extraKeys: {
186
- Enter: (instance: CodeMirror.Editor) => {
187
- // Allow regular newlines
188
- instance.replaceSelection('\n');
189
- },
190
- },
191
- } as CodeMirror.EditorConfiguration);
192
-
193
- cmRef.current = cm;
194
-
195
- const initialValue = value ?? defaultValue ?? '';
196
- if (initialValue) {
197
- isInternalUpdateRef.current = true;
198
- setTemplateToCM(cm, initialValue, fieldsRef.current);
199
- isInternalUpdateRef.current = false;
200
- }
201
-
202
- cm.on('changes', () => {
203
- if (isInternalUpdateRef.current) return;
204
- const template = getTemplateFromCM(cm);
205
- onChangeRef.current?.(template);
206
- });
207
-
208
- // Toggle placeholder visibility
209
- const wrapper = cm.getWrapperElement();
210
- const updatePlaceholder = () => {
211
- const text = getTemplateFromCM(cm);
212
- const isEmpty = text === '';
213
- if (isEmpty) {
214
- wrapper.setAttribute('data-placeholder', placeholder);
215
- wrapper.classList.add('cm-empty');
216
- } else {
217
- wrapper.removeAttribute('data-placeholder');
218
- wrapper.classList.remove('cm-empty');
219
- }
220
- };
221
- cm.on('change', updatePlaceholder);
222
- updatePlaceholder();
223
-
224
- return () => {
225
- // CodeMirror created from div (not textarea) doesn't have toTextArea
226
- const wrapper = cm.getWrapperElement();
227
- if (wrapper?.parentNode) {
228
- wrapper.parentNode.removeChild(wrapper);
229
- }
230
- };
231
- }, []); // eslint-disable-line react-hooks/exhaustive-deps
232
-
233
- // Handle controlled value updates
234
- const prevFieldsLenRef = useRef(0);
235
- useEffect(() => {
236
- const cm = cmRef.current;
237
- if (!cm || value === undefined) return;
238
- const currentTemplate = getTemplateFromCM(cm);
239
- const fieldsBecameAvailable = prevFieldsLenRef.current === 0 && fields.length > 0;
240
- prevFieldsLenRef.current = fields.length;
241
- if (currentTemplate !== value || fieldsBecameAvailable) {
242
- isInternalUpdateRef.current = true;
243
- setTemplateToCM(cm, value, fields);
244
- isInternalUpdateRef.current = false;
245
- }
246
- }, [value, fields]);
247
-
248
- // Update placeholder when prop changes
249
- useEffect(() => {
250
- const cm = cmRef.current;
251
- if (!cm) return;
252
- const wrapper = cm.getWrapperElement();
253
- const isEmpty = getTemplateFromCM(cm) === '';
254
- if (isEmpty) {
255
- wrapper.setAttribute('data-placeholder', placeholder);
256
- }
257
- }, [placeholder]);
258
-
259
- const handleFieldSelect = useCallback(
260
- (_field: IField, nodeKey: string) => {
261
- const cm = cmRef.current;
262
- if (!cm) return;
263
- const info = resolveFieldWidgetInfo(fieldsRef.current, nodeKey);
264
- if (!info) return;
265
- cm.focus();
266
- isInternalUpdateRef.current = true;
267
- addFieldToCM(cm, info.widgetName, info.label);
268
- isInternalUpdateRef.current = false;
269
- const template = getTemplateFromCM(cm);
270
- onChangeRef.current?.(template);
271
- setOpen(false);
272
- },
273
- [],
274
- );
275
-
276
- const setInsertBtnRef = useCallback((node: HTMLButtonElement | null) => {
277
- insertBtnRef.current = node;
278
- if (!node) {
279
- setPortalContainer(null);
280
- return;
281
- }
282
- const rootNode = node.getRootNode();
283
- if (rootNode instanceof ShadowRoot) {
284
- const existing = rootNode.querySelector<HTMLElement>(
285
- '[data-bs-prompt-input-portal]',
286
- );
287
- if (existing) {
288
- setPortalContainer(existing);
289
- return;
290
- }
291
- const container = document.createElement('div');
292
- container.setAttribute('data-bs-prompt-input-portal', 'true');
293
- rootNode.appendChild(container);
294
- setPortalContainer(container);
295
- } else {
296
- setPortalContainer(null);
297
- }
298
- }, []);
299
-
300
- return (
301
- <div className={cn('prompt-input-root relative', className)}>
302
- {/* CodeMirror editor area */}
303
- <div
304
- ref={containerRef}
305
- className="prompt-input-codemirror"
306
- />
307
-
308
- {/* Insert Field button - absolute top-right */}
309
- <Popover open={open} onOpenChange={setOpen}>
310
- <PopoverTrigger asChild>
311
- <button
312
- ref={setInsertBtnRef}
313
- type="button"
314
- disabled={disabled}
315
- className={cn(
316
- 'absolute top-[-26px] right-[10px] z-10',
317
- 'text-[14px] text-[#0265ff] leading-[normal]',
318
- 'cursor-pointer bg-transparent border-0',
319
- 'hover:opacity-80',
320
- disabled && 'opacity-50 cursor-not-allowed',
321
- )}
322
- >
323
- 插入字段
324
- </button>
325
- </PopoverTrigger>
326
- <PopoverContent
327
- align="end"
328
- sideOffset={4}
329
- portalContainer={portalContainer}
330
- className="!border-0 !p-0"
331
- >
332
- <FieldSelectPopover
333
- fields={fields}
334
- onSelect={handleFieldSelect}
335
- showTypeBadge
336
- />
337
- </PopoverContent>
338
- </Popover>
339
-
340
- {/* Inline styles — includes CodeMirror base + component overrides */}
341
- <style>{`
342
- /* ═══════════════════════════════════════════════════════════════
343
- CodeMirror base styles (inlined from codemirror/lib/codemirror.css)
344
- ═══════════════════════════════════════════════════════════════ */
345
- .CodeMirror {
346
- font-family: monospace;
347
- height: 300px;
348
- color: black;
349
- direction: ltr;
350
- position: relative;
351
- overflow: hidden;
352
- background: white;
353
- }
354
- .CodeMirror-lines { padding: 4px 0; cursor: text; min-height: 1px; }
355
- .CodeMirror pre.CodeMirror-line,
356
- .CodeMirror pre.CodeMirror-line-like {
357
- padding: 0 4px;
358
- -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
359
- border-width: 0;
360
- background: transparent;
361
- font-family: inherit;
362
- font-size: inherit;
363
- margin: 0;
364
- white-space: pre;
365
- word-wrap: normal;
366
- line-height: inherit;
367
- color: inherit;
368
- z-index: 2;
369
- position: relative;
370
- overflow: visible;
371
- -webkit-tap-highlight-color: transparent;
372
- -webkit-font-variant-ligatures: contextual;
373
- font-variant-ligatures: contextual;
374
- }
375
- .CodeMirror-wrap pre.CodeMirror-line,
376
- .CodeMirror-wrap pre.CodeMirror-line-like {
377
- word-wrap: break-word;
378
- white-space: pre-wrap;
379
- word-break: normal;
380
- }
381
- .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { background-color: white; }
382
- .CodeMirror-gutters {
383
- border-right: 1px solid #ddd;
384
- background-color: #f7f7f7;
385
- white-space: nowrap;
386
- position: absolute; left: 0; top: 0;
387
- min-height: 100%;
388
- z-index: 3;
389
- }
390
- .CodeMirror-linenumber {
391
- padding: 0 3px 0 5px;
392
- min-width: 20px;
393
- text-align: right;
394
- color: #999;
395
- white-space: nowrap;
396
- }
397
- .CodeMirror-guttermarker { color: black; }
398
- .CodeMirror-guttermarker-subtle { color: #999; }
399
- .CodeMirror-cursor {
400
- border-left: 1px solid black;
401
- border-right: none;
402
- width: 0;
403
- position: absolute;
404
- pointer-events: none;
405
- }
406
- .CodeMirror div.CodeMirror-secondarycursor { border-left: 1px solid silver; }
407
- .cm-fat-cursor .CodeMirror-cursor { width: auto; border: 0 !important; background: #7e7; }
408
- .cm-fat-cursor div.CodeMirror-cursors { z-index: 1; }
409
- .cm-fat-cursor .CodeMirror-line::selection,
410
- .cm-fat-cursor .CodeMirror-line > span::selection,
411
- .cm-fat-cursor .CodeMirror-line > span > span::selection { background: transparent; }
412
- .cm-fat-cursor .CodeMirror-line::-moz-selection,
413
- .cm-fat-cursor .CodeMirror-line > span::-moz-selection,
414
- .cm-fat-cursor .CodeMirror-line > span > span::-moz-selection { background: transparent; }
415
- .cm-fat-cursor { caret-color: transparent; }
416
- @-moz-keyframes blink { 0% {} 50% { background-color: transparent; } 100% {} }
417
- @-webkit-keyframes blink { 0% {} 50% { background-color: transparent; } 100% {} }
418
- @keyframes blink { 0% {} 50% { background-color: transparent; } 100% {} }
419
- .cm-tab { display: inline-block; text-decoration: inherit; }
420
- .CodeMirror-rulers {
421
- position: absolute;
422
- left: 0; right: 0; top: -50px; bottom: 0;
423
- overflow: hidden;
424
- }
425
- .CodeMirror-ruler { border-left: 1px solid #ccc; top: 0; bottom: 0; position: absolute; }
426
- .cm-s-default .cm-header { color: blue; }
427
- .cm-s-default .cm-quote { color: #090; }
428
- .cm-negative { color: #d44; }
429
- .cm-positive { color: #292; }
430
- .cm-header, .cm-strong { font-weight: bold; }
431
- .cm-em { font-style: italic; }
432
- .cm-link { text-decoration: underline; }
433
- .cm-strikethrough { text-decoration: line-through; }
434
- .cm-s-default .cm-keyword { color: #708; }
435
- .cm-s-default .cm-atom { color: #219; }
436
- .cm-s-default .cm-number { color: #164; }
437
- .cm-s-default .cm-def { color: #00f; }
438
- .cm-s-default .cm-variable-2 { color: #05a; }
439
- .cm-s-default .cm-variable-3, .cm-s-default .cm-type { color: #085; }
440
- .cm-s-default .cm-comment { color: #a50; }
441
- .cm-s-default .cm-string { color: #a11; }
442
- .cm-s-default .cm-string-2 { color: #f50; }
443
- .cm-s-default .cm-meta { color: #555; }
444
- .cm-s-default .cm-qualifier { color: #555; }
445
- .cm-s-default .cm-builtin { color: #30a; }
446
- .cm-s-default .cm-bracket { color: #997; }
447
- .cm-s-default .cm-tag { color: #170; }
448
- .cm-s-default .cm-attribute { color: #00c; }
449
- .cm-s-default .cm-hr { color: #999; }
450
- .cm-s-default .cm-link { color: #00c; }
451
- .cm-s-default .cm-error { color: #f00; }
452
- .cm-invalidchar { color: #f00; }
453
- .CodeMirror-composing { border-bottom: 2px solid; }
454
- div.CodeMirror span.CodeMirror-matchingbracket { color: #0b0; }
455
- div.CodeMirror span.CodeMirror-nonmatchingbracket { color: #a22; }
456
- .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
457
- .CodeMirror-activeline-background { background: #e8f2ff; }
458
- .CodeMirror-scroll {
459
- overflow: scroll !important;
460
- margin-bottom: -50px; margin-right: -50px;
461
- padding-bottom: 50px;
462
- height: 100%;
463
- outline: none;
464
- position: relative;
465
- z-index: 0;
466
- }
467
- .CodeMirror-sizer {
468
- position: relative;
469
- border-right: 50px solid transparent;
470
- }
471
- .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
472
- position: absolute;
473
- z-index: 6;
474
- display: none;
475
- outline: none;
476
- }
477
- .CodeMirror-vscrollbar { right: 0; top: 0; overflow-x: hidden; overflow-y: scroll; }
478
- .CodeMirror-hscrollbar { bottom: 0; left: 0; overflow-y: hidden; overflow-x: scroll; }
479
- .CodeMirror-scrollbar-filler { right: 0; bottom: 0; }
480
- .CodeMirror-gutter-filler { left: 0; bottom: 0; }
481
- .CodeMirror-gutter {
482
- white-space: normal;
483
- height: 100%;
484
- display: inline-block;
485
- vertical-align: top;
486
- margin-bottom: -50px;
487
- }
488
- .CodeMirror-gutter-wrapper {
489
- position: absolute;
490
- z-index: 4;
491
- background: none !important;
492
- border: none !important;
493
- }
494
- .CodeMirror-gutter-background { position: absolute; top: 0; bottom: 0; z-index: 4; }
495
- .CodeMirror-gutter-elt { position: absolute; cursor: default; z-index: 4; }
496
- .CodeMirror-gutter-wrapper ::selection { background-color: transparent; }
497
- .CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent; }
498
- .CodeMirror-linebackground {
499
- position: absolute;
500
- left: 0; right: 0; top: 0; bottom: 0;
501
- z-index: 0;
502
- }
503
- .CodeMirror-linewidget { position: relative; z-index: 2; padding: 0.1px; }
504
- .CodeMirror-rtl pre { direction: rtl; }
505
- .CodeMirror-code { outline: none; }
506
- .CodeMirror-scroll,
507
- .CodeMirror-sizer,
508
- .CodeMirror-gutter,
509
- .CodeMirror-gutters,
510
- .CodeMirror-linenumber {
511
- -moz-box-sizing: content-box;
512
- box-sizing: content-box;
513
- }
514
- .CodeMirror-measure {
515
- position: absolute;
516
- width: 100%;
517
- height: 0;
518
- overflow: hidden;
519
- visibility: hidden;
520
- }
521
- .CodeMirror-measure pre { position: static; }
522
- div.CodeMirror-cursors {
523
- visibility: hidden;
524
- position: relative;
525
- z-index: 3;
526
- }
527
- div.CodeMirror-dragcursors { visibility: visible; }
528
- .CodeMirror-focused div.CodeMirror-cursors { visibility: visible; }
529
- .CodeMirror-selected { background: #d9d9d9; }
530
- .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
531
- .CodeMirror-crosshair { cursor: crosshair; }
532
- .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
533
- .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
534
- .cm-searching { background-color: #ffa; background-color: rgba(255, 255, 0, .4); }
535
- .cm-force-border { padding-right: .1px; }
536
- @media print {
537
- .CodeMirror div.CodeMirror-cursors { visibility: hidden; }
538
- }
539
- .cm-tab-wrap-hack:after { content: ''; }
540
- span.CodeMirror-selectedtext { background: none; }
541
-
542
- /* ── Outer wrapper ── */
543
- .prompt-input-root {
544
- border: 1px solid #E0E0E0;
545
- border-radius: 4px;
546
- min-height: 120px;
547
- position: relative;
548
- }
549
-
550
- /* ── CodeMirror container ── */
551
- .prompt-input-codemirror {
552
- height: 100%;
553
- min-height: 120px;
554
- }
555
-
556
- .prompt-input-codemirror .CodeMirror {
557
- height: 100%;
558
- min-height: 120px;
559
- border: 0;
560
- border-radius: 4px;
561
- font-family: inherit;
562
- font-size: 14px;
563
- line-height: 1.6;
564
- color: #030303;
565
- background: transparent;
566
- }
567
-
568
- .prompt-input-codemirror .CodeMirror-scroll {
569
- min-height: 120px;
570
- overflow: hidden !important;
571
- }
572
-
573
- .prompt-input-codemirror .CodeMirror-lines {
574
- padding: 10px 80px 10px 10px;
575
- }
576
-
577
- .prompt-input-codemirror .CodeMirror pre.CodeMirror-line,
578
- .prompt-input-codemirror .CodeMirror pre.CodeMirror-line-like {
579
- padding: 0;
580
- }
581
-
582
- .prompt-input-codemirror .CodeMirror .CodeMirror-cursor {
583
- border-left: 1px solid #030303;
584
- }
585
-
586
- /* ── Placeholder ── */
587
- .prompt-input-codemirror .CodeMirror.cm-empty .CodeMirror-lines::before {
588
- content: attr(data-placeholder);
589
- position: absolute;
590
- left: 10px;
591
- top: 10px;
592
- color: #999999;
593
- font-size: 14px;
594
- pointer-events: none;
595
- z-index: 1;
596
- }
597
-
598
- /* ── Field tag widget ── */
599
- .prompt-field-tag {
600
- display: inline-flex;
601
- align-items: center;
602
- gap: 6px;
603
- height: 26px;
604
- padding: 0 6px;
605
- background: #EFF0F6;
606
- border-radius: 4px;
607
- font-size: 14px;
608
- margin: 3px 3px;
609
- line-height: normal;
610
- color: #000000;
611
- white-space: nowrap;
612
- user-select: none;
613
- vertical-align: middle;
614
- }
615
-
616
- // .CodeMirror-widget + .CodeMirror-widget > .prompt-field-tag {
617
- // margin-left: 0;
618
- // }
619
-
620
- .CodeMirror-line {
621
- line-height: 1.4; /* 设置一个符合你字体大小的行高,例如 20px */
622
- }
623
-
624
- .CodeMirror-cursor {
625
- height: 1.4em;
626
- border-left: 1px solid black;
627
- }
628
-
629
- .prompt-field-tag-icon {
630
- display: inline-flex;
631
- align-items: center;
632
- justify-content: center;
633
- width: 14px;
634
- height: 14px;
635
- flex-shrink: 0;
636
- }
637
- `}</style>
638
- </div>
639
- );
640
- };
1
+ import { useEffect, useRef, useState, useCallback } from 'react';
2
+ import CodeMirror from 'codemirror';
3
+ import { cn } from '../../../lib/utils';
4
+ import { Popover, PopoverTrigger, PopoverContent } from '../../ui/popover';
5
+ import { FieldSelectPopover } from '../field-select';
6
+ import type { IField, ISubformField } from '@baishuyun/types';
7
+
8
+ // ── Create a DOM node for the field tag (used inside CodeMirror) ────
9
+
10
+ function createFieldTagDom(label: string): HTMLSpanElement {
11
+ const wrapper = document.createElement('span');
12
+ wrapper.className = 'prompt-field-tag';
13
+
14
+ const iconSpan = document.createElement('span');
15
+ iconSpan.className = 'prompt-field-tag-icon';
16
+ iconSpan.innerHTML =
17
+ '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="2" y="2" width="10" height="10" rx="2" stroke="#6F829E" stroke-width="1.2"/><path d="M5 5.5H9M5 7.5H8" stroke="#6F829E" stroke-width="1.2" stroke-linecap="round"/></svg>';
18
+
19
+ const textSpan = document.createElement('span');
20
+ textSpan.textContent = label;
21
+
22
+ wrapper.appendChild(iconSpan);
23
+ wrapper.appendChild(textSpan);
24
+ return wrapper;
25
+ }
26
+
27
+ // ── CodeMirror Helpers ──────────────────────────────────────────────
28
+
29
+ function resolveFieldWidgetInfo(
30
+ fields: IField[],
31
+ fullKey: string,
32
+ ): { widgetName: string; label: string } | undefined {
33
+ const key = fullKey.replace('virtual.', '');
34
+
35
+ const dotIndex = key.indexOf('.');
36
+ if (dotIndex > 0) {
37
+ const parentName = key.substring(0, dotIndex);
38
+ const childName = key.substring(dotIndex + 1);
39
+ const parent = fields.find((f) => f.widget?.widgetName === parentName);
40
+ if (parent?.widget?.items) {
41
+ const child = parent.widget.items.find(
42
+ (item): item is ISubformField =>
43
+ 'widget' in item && item.widget?.widgetName === childName,
44
+ );
45
+ if (child) {
46
+ return { widgetName: fullKey, label: child.label };
47
+ }
48
+ }
49
+ } else {
50
+ const field = fields.find((f) => f.widget?.widgetName === key);
51
+ if (field) {
52
+ return { widgetName: fullKey, label: field.label };
53
+ }
54
+ }
55
+ return undefined;
56
+ }
57
+
58
+ function addFieldToCM(
59
+ cm: CodeMirror.Editor,
60
+ widgetName: string,
61
+ displayLabel: string,
62
+ ): void {
63
+ const from = cm.getCursor('from');
64
+
65
+ cm.replaceSelection('${' + widgetName + '}');
66
+ const to = cm.getCursor();
67
+
68
+ const isVirtual = widgetName?.includes("virtual");
69
+
70
+ const label = isVirtual ? `触发行.${displayLabel}` : displayLabel;
71
+
72
+ const tagNode = createFieldTagDom(label);
73
+ tagNode.setAttribute('data-field-key', widgetName);
74
+
75
+ cm.markText(from, to, {
76
+ handleMouseEvents: true,
77
+ atomic: true,
78
+ replacedWith: tagNode,
79
+ });
80
+ }
81
+
82
+ function getTemplateFromCM(cm: CodeMirror.Editor): string {
83
+ const wrapper = cm.getWrapperElement();
84
+ const lineElements = wrapper.querySelectorAll('.CodeMirror-line');
85
+ const lines: string[] = [];
86
+
87
+ lineElements.forEach((lineEl) => {
88
+ let lineText = '';
89
+ walkLineNode(lineEl);
90
+ lines.push(lineText);
91
+
92
+ function walkLineNode(node: Node): void {
93
+ if (node instanceof HTMLElement) {
94
+ const key = node.getAttribute('data-field-key');
95
+ if (key && node.classList.contains('prompt-field-tag')) {
96
+ lineText += '${' + key + '}';
97
+ return;
98
+ }
99
+ }
100
+ const children = Array.from(node.childNodes);
101
+ for (let i = 0; i < children.length; i++) {
102
+ const child = children[i];
103
+ if (child.nodeType === Node.TEXT_NODE) {
104
+ lineText += child.textContent || '';
105
+ } else {
106
+ walkLineNode(child);
107
+ }
108
+ }
109
+ }
110
+ });
111
+
112
+ return lines.join('\n').trim();
113
+ }
114
+
115
+ function setTemplateToCM(
116
+ cm: CodeMirror.Editor,
117
+ template: string,
118
+ fields: IField[],
119
+ ): void {
120
+ cm.setValue('');
121
+ if (!template) {
122
+ cm.refresh();
123
+ return;
124
+ }
125
+
126
+ const normalized = template.trim().replace(/\s{2,}/g, ' ');
127
+ const parts = normalized.split(/(\$\{[^${}]*\})/g);
128
+
129
+ for (const part of parts) {
130
+ const match = part.match(/^\$\{([^${}]*)\}$/);
131
+ if (match) {
132
+ const info = resolveFieldWidgetInfo(fields, match[1]);
133
+ if (info) {
134
+ addFieldToCM(cm, info.widgetName, info.label);
135
+ } else {
136
+ cm.replaceSelection(part);
137
+ }
138
+ } else {
139
+ cm.replaceSelection(part);
140
+ }
141
+ }
142
+
143
+ cm.refresh();
144
+ }
145
+
146
+ // ── PromptInput Component ──────────────────────────────────────────
147
+
148
+ export interface PromptInputProps {
149
+ fields: IField[];
150
+ value?: string;
151
+ defaultValue?: string;
152
+ onChange?: (template: string) => void;
153
+ placeholder?: string;
154
+ className?: string;
155
+ disabled?: boolean;
156
+ }
157
+
158
+ export const PromptInput = ({
159
+ fields,
160
+ value,
161
+ defaultValue,
162
+ onChange,
163
+ placeholder = '示例:放配置时候的提示词内容进行引导说明',
164
+ className,
165
+ disabled = false,
166
+ }: PromptInputProps) => {
167
+ const containerRef = useRef<HTMLDivElement>(null);
168
+ const cmRef = useRef<CodeMirror.Editor | null>(null);
169
+ const [open, setOpen] = useState(false);
170
+ const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
171
+ const insertBtnRef = useRef<HTMLButtonElement | null>(null);
172
+ const isInternalUpdateRef = useRef(false);
173
+
174
+ // Create field key cache ref to avoid stale closure in CM callbacks
175
+ const fieldsRef = useRef(fields);
176
+ fieldsRef.current = fields;
177
+ const onChangeRef = useRef(onChange);
178
+ onChangeRef.current = onChange;
179
+
180
+ // Initialize CodeMirror
181
+ useEffect(() => {
182
+ const container = containerRef.current;
183
+ if (!container) return;
184
+
185
+ const cm = CodeMirror(container, {
186
+ mode: undefined,
187
+ lineWrapping: true,
188
+ lineNumbers: false,
189
+ smartIndent: false,
190
+ autofocus: false,
191
+ extraKeys: {
192
+ Enter: (instance: CodeMirror.Editor) => {
193
+ // Allow regular newlines
194
+ instance.replaceSelection('\n');
195
+ },
196
+ },
197
+ } as CodeMirror.EditorConfiguration);
198
+
199
+ cmRef.current = cm;
200
+
201
+ const initialValue = value ?? defaultValue ?? '';
202
+ if (initialValue) {
203
+ isInternalUpdateRef.current = true;
204
+ setTemplateToCM(cm, initialValue, fieldsRef.current);
205
+ isInternalUpdateRef.current = false;
206
+ }
207
+
208
+ cm.on('changes', () => {
209
+ if (isInternalUpdateRef.current) return;
210
+ const template = getTemplateFromCM(cm);
211
+ onChangeRef.current?.(template);
212
+ });
213
+
214
+ // Toggle placeholder visibility
215
+ const wrapper = cm.getWrapperElement();
216
+ const updatePlaceholder = () => {
217
+ const text = getTemplateFromCM(cm);
218
+ const isEmpty = text === '';
219
+ if (isEmpty) {
220
+ wrapper.setAttribute('data-placeholder', placeholder);
221
+ wrapper.classList.add('cm-empty');
222
+ } else {
223
+ wrapper.removeAttribute('data-placeholder');
224
+ wrapper.classList.remove('cm-empty');
225
+ }
226
+ };
227
+ cm.on('change', updatePlaceholder);
228
+ updatePlaceholder();
229
+
230
+ return () => {
231
+ // CodeMirror created from div (not textarea) doesn't have toTextArea
232
+ const wrapper = cm.getWrapperElement();
233
+ if (wrapper?.parentNode) {
234
+ wrapper.parentNode.removeChild(wrapper);
235
+ }
236
+ };
237
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
238
+
239
+ // Handle controlled value updates
240
+ const prevFieldsLenRef = useRef(0);
241
+ useEffect(() => {
242
+ const cm = cmRef.current;
243
+ if (!cm || value === undefined) return;
244
+ const currentTemplate = getTemplateFromCM(cm);
245
+ const fieldsBecameAvailable = prevFieldsLenRef.current === 0 && fields.length > 0;
246
+ prevFieldsLenRef.current = fields.length;
247
+ if (currentTemplate !== value || fieldsBecameAvailable) {
248
+ isInternalUpdateRef.current = true;
249
+ setTemplateToCM(cm, value, fields);
250
+ isInternalUpdateRef.current = false;
251
+ }
252
+ }, [value, fields]);
253
+
254
+ // Update placeholder when prop changes
255
+ useEffect(() => {
256
+ const cm = cmRef.current;
257
+ if (!cm) return;
258
+ const wrapper = cm.getWrapperElement();
259
+ const isEmpty = getTemplateFromCM(cm) === '';
260
+ if (isEmpty) {
261
+ wrapper.setAttribute('data-placeholder', placeholder);
262
+ }
263
+ }, [placeholder]);
264
+
265
+ const handleFieldSelect = useCallback(
266
+ (_field: IField, nodeKey: string) => {
267
+ const cm = cmRef.current;
268
+ if (!cm) return;
269
+ const info = resolveFieldWidgetInfo(fieldsRef.current, nodeKey);
270
+ if (!info) return;
271
+ cm.focus();
272
+ isInternalUpdateRef.current = true;
273
+ addFieldToCM(cm, info.widgetName, info.label);
274
+ isInternalUpdateRef.current = false;
275
+ const template = getTemplateFromCM(cm);
276
+ onChangeRef.current?.(template);
277
+ setOpen(false);
278
+ },
279
+ [],
280
+ );
281
+
282
+ const setInsertBtnRef = useCallback((node: HTMLButtonElement | null) => {
283
+ insertBtnRef.current = node;
284
+ if (!node) {
285
+ setPortalContainer(null);
286
+ return;
287
+ }
288
+ const rootNode = node.getRootNode();
289
+ if (rootNode instanceof ShadowRoot) {
290
+ const existing = rootNode.querySelector<HTMLElement>(
291
+ '[data-bs-prompt-input-portal]',
292
+ );
293
+ if (existing) {
294
+ setPortalContainer(existing);
295
+ return;
296
+ }
297
+ const container = document.createElement('div');
298
+ container.setAttribute('data-bs-prompt-input-portal', 'true');
299
+ rootNode.appendChild(container);
300
+ setPortalContainer(container);
301
+ } else {
302
+ setPortalContainer(null);
303
+ }
304
+ }, []);
305
+
306
+ return (
307
+ <div className={cn('prompt-input-root relative', className)}>
308
+ {/* CodeMirror editor area */}
309
+ <div
310
+ ref={containerRef}
311
+ className="prompt-input-codemirror"
312
+ />
313
+
314
+ {/* Insert Field button - absolute top-right */}
315
+ <Popover open={open} onOpenChange={setOpen}>
316
+ <PopoverTrigger asChild>
317
+ <button
318
+ ref={setInsertBtnRef}
319
+ type="button"
320
+ disabled={disabled}
321
+ className={cn(
322
+ 'absolute top-[-26px] right-[10px] z-10',
323
+ 'text-[14px] text-[#0265ff] leading-[normal]',
324
+ 'cursor-pointer bg-transparent border-0',
325
+ 'hover:opacity-80',
326
+ disabled && 'opacity-50 cursor-not-allowed',
327
+ )}
328
+ >
329
+ 插入字段
330
+ </button>
331
+ </PopoverTrigger>
332
+ <PopoverContent
333
+ align="end"
334
+ sideOffset={4}
335
+ portalContainer={portalContainer}
336
+ className="!border-0 !p-0"
337
+ >
338
+ <FieldSelectPopover
339
+ fields={fields}
340
+ onSelect={handleFieldSelect}
341
+ showTypeBadge
342
+ />
343
+ </PopoverContent>
344
+ </Popover>
345
+
346
+ {/* Inline styles — includes CodeMirror base + component overrides */}
347
+ <style>{`
348
+ /* ═══════════════════════════════════════════════════════════════
349
+ CodeMirror base styles (inlined from codemirror/lib/codemirror.css)
350
+ ═══════════════════════════════════════════════════════════════ */
351
+ .CodeMirror {
352
+ font-family: monospace;
353
+ height: 300px;
354
+ color: black;
355
+ direction: ltr;
356
+ position: relative;
357
+ overflow: hidden;
358
+ background: white;
359
+ }
360
+ .CodeMirror-lines { padding: 4px 0; cursor: text; min-height: 1px; }
361
+ .CodeMirror pre.CodeMirror-line,
362
+ .CodeMirror pre.CodeMirror-line-like {
363
+ padding: 0 4px;
364
+ -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
365
+ border-width: 0;
366
+ background: transparent;
367
+ font-family: inherit;
368
+ font-size: inherit;
369
+ margin: 0;
370
+ white-space: pre;
371
+ word-wrap: normal;
372
+ line-height: inherit;
373
+ color: inherit;
374
+ z-index: 2;
375
+ position: relative;
376
+ overflow: visible;
377
+ -webkit-tap-highlight-color: transparent;
378
+ -webkit-font-variant-ligatures: contextual;
379
+ font-variant-ligatures: contextual;
380
+ }
381
+ .CodeMirror-wrap pre.CodeMirror-line,
382
+ .CodeMirror-wrap pre.CodeMirror-line-like {
383
+ word-wrap: break-word;
384
+ white-space: pre-wrap;
385
+ word-break: normal;
386
+ }
387
+ .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { background-color: white; }
388
+ .CodeMirror-gutters {
389
+ border-right: 1px solid #ddd;
390
+ background-color: #f7f7f7;
391
+ white-space: nowrap;
392
+ position: absolute; left: 0; top: 0;
393
+ min-height: 100%;
394
+ z-index: 3;
395
+ }
396
+ .CodeMirror-linenumber {
397
+ padding: 0 3px 0 5px;
398
+ min-width: 20px;
399
+ text-align: right;
400
+ color: #999;
401
+ white-space: nowrap;
402
+ }
403
+ .CodeMirror-guttermarker { color: black; }
404
+ .CodeMirror-guttermarker-subtle { color: #999; }
405
+ .CodeMirror-cursor {
406
+ border-left: 1px solid black;
407
+ border-right: none;
408
+ width: 0;
409
+ position: absolute;
410
+ pointer-events: none;
411
+ }
412
+ .CodeMirror div.CodeMirror-secondarycursor { border-left: 1px solid silver; }
413
+ .cm-fat-cursor .CodeMirror-cursor { width: auto; border: 0 !important; background: #7e7; }
414
+ .cm-fat-cursor div.CodeMirror-cursors { z-index: 1; }
415
+ .cm-fat-cursor .CodeMirror-line::selection,
416
+ .cm-fat-cursor .CodeMirror-line > span::selection,
417
+ .cm-fat-cursor .CodeMirror-line > span > span::selection { background: transparent; }
418
+ .cm-fat-cursor .CodeMirror-line::-moz-selection,
419
+ .cm-fat-cursor .CodeMirror-line > span::-moz-selection,
420
+ .cm-fat-cursor .CodeMirror-line > span > span::-moz-selection { background: transparent; }
421
+ .cm-fat-cursor { caret-color: transparent; }
422
+ @-moz-keyframes blink { 0% {} 50% { background-color: transparent; } 100% {} }
423
+ @-webkit-keyframes blink { 0% {} 50% { background-color: transparent; } 100% {} }
424
+ @keyframes blink { 0% {} 50% { background-color: transparent; } 100% {} }
425
+ .cm-tab { display: inline-block; text-decoration: inherit; }
426
+ .CodeMirror-rulers {
427
+ position: absolute;
428
+ left: 0; right: 0; top: -50px; bottom: 0;
429
+ overflow: hidden;
430
+ }
431
+ .CodeMirror-ruler { border-left: 1px solid #ccc; top: 0; bottom: 0; position: absolute; }
432
+ .cm-s-default .cm-header { color: blue; }
433
+ .cm-s-default .cm-quote { color: #090; }
434
+ .cm-negative { color: #d44; }
435
+ .cm-positive { color: #292; }
436
+ .cm-header, .cm-strong { font-weight: bold; }
437
+ .cm-em { font-style: italic; }
438
+ .cm-link { text-decoration: underline; }
439
+ .cm-strikethrough { text-decoration: line-through; }
440
+ .cm-s-default .cm-keyword { color: #708; }
441
+ .cm-s-default .cm-atom { color: #219; }
442
+ .cm-s-default .cm-number { color: #164; }
443
+ .cm-s-default .cm-def { color: #00f; }
444
+ .cm-s-default .cm-variable-2 { color: #05a; }
445
+ .cm-s-default .cm-variable-3, .cm-s-default .cm-type { color: #085; }
446
+ .cm-s-default .cm-comment { color: #a50; }
447
+ .cm-s-default .cm-string { color: #a11; }
448
+ .cm-s-default .cm-string-2 { color: #f50; }
449
+ .cm-s-default .cm-meta { color: #555; }
450
+ .cm-s-default .cm-qualifier { color: #555; }
451
+ .cm-s-default .cm-builtin { color: #30a; }
452
+ .cm-s-default .cm-bracket { color: #997; }
453
+ .cm-s-default .cm-tag { color: #170; }
454
+ .cm-s-default .cm-attribute { color: #00c; }
455
+ .cm-s-default .cm-hr { color: #999; }
456
+ .cm-s-default .cm-link { color: #00c; }
457
+ .cm-s-default .cm-error { color: #f00; }
458
+ .cm-invalidchar { color: #f00; }
459
+ .CodeMirror-composing { border-bottom: 2px solid; }
460
+ div.CodeMirror span.CodeMirror-matchingbracket { color: #0b0; }
461
+ div.CodeMirror span.CodeMirror-nonmatchingbracket { color: #a22; }
462
+ .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
463
+ .CodeMirror-activeline-background { background: #e8f2ff; }
464
+ .CodeMirror-scroll {
465
+ overflow: scroll !important;
466
+ margin-bottom: -50px; margin-right: -50px;
467
+ padding-bottom: 50px;
468
+ height: 100%;
469
+ outline: none;
470
+ position: relative;
471
+ z-index: 0;
472
+ }
473
+ .CodeMirror-sizer {
474
+ position: relative;
475
+ border-right: 50px solid transparent;
476
+ }
477
+ .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
478
+ position: absolute;
479
+ z-index: 6;
480
+ display: none;
481
+ outline: none;
482
+ }
483
+ .CodeMirror-vscrollbar { right: 0; top: 0; overflow-x: hidden; overflow-y: scroll; }
484
+ .CodeMirror-hscrollbar { bottom: 0; left: 0; overflow-y: hidden; overflow-x: scroll; }
485
+ .CodeMirror-scrollbar-filler { right: 0; bottom: 0; }
486
+ .CodeMirror-gutter-filler { left: 0; bottom: 0; }
487
+ .CodeMirror-gutter {
488
+ white-space: normal;
489
+ height: 100%;
490
+ display: inline-block;
491
+ vertical-align: top;
492
+ margin-bottom: -50px;
493
+ }
494
+ .CodeMirror-gutter-wrapper {
495
+ position: absolute;
496
+ z-index: 4;
497
+ background: none !important;
498
+ border: none !important;
499
+ }
500
+ .CodeMirror-gutter-background { position: absolute; top: 0; bottom: 0; z-index: 4; }
501
+ .CodeMirror-gutter-elt { position: absolute; cursor: default; z-index: 4; }
502
+ .CodeMirror-gutter-wrapper ::selection { background-color: transparent; }
503
+ .CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent; }
504
+ .CodeMirror-linebackground {
505
+ position: absolute;
506
+ left: 0; right: 0; top: 0; bottom: 0;
507
+ z-index: 0;
508
+ }
509
+ .CodeMirror-linewidget { position: relative; z-index: 2; padding: 0.1px; }
510
+ .CodeMirror-rtl pre { direction: rtl; }
511
+ .CodeMirror-code { outline: none; }
512
+ .CodeMirror-scroll,
513
+ .CodeMirror-sizer,
514
+ .CodeMirror-gutter,
515
+ .CodeMirror-gutters,
516
+ .CodeMirror-linenumber {
517
+ -moz-box-sizing: content-box;
518
+ box-sizing: content-box;
519
+ }
520
+ .CodeMirror-measure {
521
+ position: absolute;
522
+ width: 100%;
523
+ height: 0;
524
+ overflow: hidden;
525
+ visibility: hidden;
526
+ }
527
+ .CodeMirror-measure pre { position: static; }
528
+ div.CodeMirror-cursors {
529
+ visibility: hidden;
530
+ position: relative;
531
+ z-index: 3;
532
+ }
533
+ div.CodeMirror-dragcursors { visibility: visible; }
534
+ .CodeMirror-focused div.CodeMirror-cursors { visibility: visible; }
535
+ .CodeMirror-selected { background: #d9d9d9; }
536
+ .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
537
+ .CodeMirror-crosshair { cursor: crosshair; }
538
+ .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
539
+ .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
540
+ .cm-searching { background-color: #ffa; background-color: rgba(255, 255, 0, .4); }
541
+ .cm-force-border { padding-right: .1px; }
542
+ @media print {
543
+ .CodeMirror div.CodeMirror-cursors { visibility: hidden; }
544
+ }
545
+ .cm-tab-wrap-hack:after { content: ''; }
546
+ span.CodeMirror-selectedtext { background: none; }
547
+
548
+ /* ── Outer wrapper ── */
549
+ .prompt-input-root {
550
+ border: 1px solid #E0E0E0;
551
+ border-radius: 4px;
552
+ min-height: 120px;
553
+ position: relative;
554
+ }
555
+
556
+ /* ── CodeMirror container ── */
557
+ .prompt-input-codemirror {
558
+ height: 100%;
559
+ min-height: 120px;
560
+ }
561
+
562
+ .prompt-input-codemirror .CodeMirror {
563
+ height: 100%;
564
+ min-height: 120px;
565
+ border: 0;
566
+ border-radius: 4px;
567
+ font-family: inherit;
568
+ font-size: 14px;
569
+ line-height: 1.6;
570
+ color: #030303;
571
+ background: transparent;
572
+ }
573
+
574
+ .prompt-input-codemirror .CodeMirror-scroll {
575
+ min-height: 120px;
576
+ overflow: hidden !important;
577
+ }
578
+
579
+ .prompt-input-codemirror .CodeMirror-lines {
580
+ padding: 10px 80px 10px 10px;
581
+ }
582
+
583
+ .prompt-input-codemirror .CodeMirror pre.CodeMirror-line,
584
+ .prompt-input-codemirror .CodeMirror pre.CodeMirror-line-like {
585
+ padding: 0;
586
+ }
587
+
588
+ .prompt-input-codemirror .CodeMirror .CodeMirror-cursor {
589
+ border-left: 1px solid #030303;
590
+ }
591
+
592
+ /* ── Placeholder ── */
593
+ .prompt-input-codemirror .CodeMirror.cm-empty .CodeMirror-lines::before {
594
+ content: attr(data-placeholder);
595
+ position: absolute;
596
+ left: 10px;
597
+ top: 10px;
598
+ color: #999999;
599
+ font-size: 14px;
600
+ pointer-events: none;
601
+ z-index: 1;
602
+ }
603
+
604
+ /* ── Field tag widget ── */
605
+ .prompt-field-tag {
606
+ display: inline-flex;
607
+ align-items: center;
608
+ gap: 6px;
609
+ height: 26px;
610
+ padding: 0 6px;
611
+ background: #EFF0F6;
612
+ border-radius: 4px;
613
+ font-size: 14px;
614
+ margin: 3px 3px;
615
+ line-height: normal;
616
+ color: #000000;
617
+ white-space: nowrap;
618
+ user-select: none;
619
+ vertical-align: middle;
620
+ }
621
+
622
+ // .CodeMirror-widget + .CodeMirror-widget > .prompt-field-tag {
623
+ // margin-left: 0;
624
+ // }
625
+
626
+ .CodeMirror-line {
627
+ line-height: 1.4; /* 设置一个符合你字体大小的行高,例如 20px */
628
+ }
629
+
630
+ .CodeMirror-cursor {
631
+ height: 1.4em;
632
+ border-left: 1px solid black;
633
+ }
634
+
635
+ .prompt-field-tag-icon {
636
+ display: inline-flex;
637
+ align-items: center;
638
+ justify-content: center;
639
+ width: 14px;
640
+ height: 14px;
641
+ flex-shrink: 0;
642
+ }
643
+ `}</style>
644
+ </div>
645
+ );
646
+ };