@electerm/electerm-react 2.8.16 → 2.10.26

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 (48) hide show
  1. package/client/common/constants.js +3 -3
  2. package/client/common/pre.js +1 -120
  3. package/client/components/ai/ai-config.jsx +26 -3
  4. package/client/components/ai/ai-history-item.jsx +46 -0
  5. package/client/components/ai/ai-history.jsx +104 -0
  6. package/client/components/bookmark-form/ai-bookmark-form.jsx +338 -0
  7. package/client/components/bookmark-form/bookmark-form.styl +1 -1
  8. package/client/components/bookmark-form/bookmark-schema.js +192 -0
  9. package/client/components/bookmark-form/common/ai-category-select.jsx +32 -0
  10. package/client/components/bookmark-form/common/category-select.jsx +2 -4
  11. package/client/components/bookmark-form/common/fields.jsx +0 -10
  12. package/client/components/bookmark-form/config/ftp.js +2 -0
  13. package/client/components/bookmark-form/config/rdp.js +0 -1
  14. package/client/components/bookmark-form/config/session-config.js +3 -1
  15. package/client/components/bookmark-form/config/spice.js +43 -0
  16. package/client/components/bookmark-form/config/vnc.js +1 -2
  17. package/client/components/bookmark-form/fix-bookmark-default.js +134 -0
  18. package/client/components/bookmark-form/index.jsx +74 -14
  19. package/client/components/common/notification.jsx +34 -2
  20. package/client/components/common/notification.styl +18 -2
  21. package/client/components/main/wrapper.styl +0 -7
  22. package/client/components/rdp/rdp-session.jsx +44 -11
  23. package/client/components/session/session.jsx +13 -3
  24. package/client/components/setting-panel/deep-link-control.jsx +3 -2
  25. package/client/components/setting-panel/keywords-transport.jsx +0 -1
  26. package/client/components/shortcuts/shortcut-editor.jsx +12 -36
  27. package/client/components/shortcuts/shortcut-handler.js +11 -5
  28. package/client/components/sidebar/index.jsx +11 -1
  29. package/client/components/spice/spice-session.jsx +296 -0
  30. package/client/components/spice/spice.styl +4 -0
  31. package/client/components/tabs/add-btn-menu.jsx +9 -2
  32. package/client/components/terminal/attach-addon-custom.js +20 -76
  33. package/client/components/terminal/terminal.jsx +34 -28
  34. package/client/components/terminal/transfer-client-base.js +232 -0
  35. package/client/components/terminal/trzsz-client.js +306 -0
  36. package/client/components/terminal/xterm-loader.js +109 -0
  37. package/client/components/terminal/zmodem-client.js +13 -166
  38. package/client/components/text-editor/simple-editor.jsx +1 -2
  39. package/client/components/vnc/vnc-session.jsx +1 -1
  40. package/client/entry/electerm.jsx +0 -2
  41. package/client/store/load-data.js +1 -1
  42. package/client/store/store.js +1 -1
  43. package/client/store/system-menu.js +10 -0
  44. package/package.json +1 -1
  45. package/client/common/trzsz.js +0 -46
  46. package/client/components/bookmark-form/common/wiki-alert.jsx +0 -9
  47. package/client/components/common/notification-with-details.jsx +0 -34
  48. package/client/components/terminal/fs.js +0 -59
@@ -67,7 +67,8 @@ export const connectionMap = buildConst([
67
67
  'web',
68
68
  'rdp',
69
69
  'vnc',
70
- 'ftp'
70
+ 'ftp',
71
+ 'spice'
71
72
  ])
72
73
 
73
74
  export const authTypeMap = buildConst([
@@ -143,6 +144,7 @@ export const terminalSerialType = 'serial'
143
144
  export const terminalTelnetType = 'telnet'
144
145
  export const terminalLocalType = 'local'
145
146
  export const terminalFtpType = 'ftp'
147
+ export const terminalSpiceType = 'spice'
146
148
  export const openedSidebarKey = 'opened-sidebar'
147
149
  export const sidebarPinnedKey = 'sidebar-pinned'
148
150
  export const pinnedQuickCommandBarKey = 'pinned-quick-command-bar'
@@ -242,8 +244,6 @@ export const proxyHelpLink = 'https://github.com/electerm/electerm/wiki/proxy-fo
242
244
  export const regexHelpLink = 'https://github.com/electerm/electerm/wiki/Terminal-keywords-highlight-regular-expression-exmaples'
243
245
  export const connectionHoppingWikiLink = 'https://github.com/electerm/electerm/wiki/Connection-Hopping-Behavior-Change-in-electerm-since-v1.50.65'
244
246
  export const aiConfigWikiLink = 'https://github.com/electerm/electerm/wiki/AI-model-config-guide'
245
- export const rdpWikiLink = 'https://github.com/electerm/electerm/wiki/RDP-limitation'
246
- export const vncWikiLink = 'https://github.com/electerm/electerm/wiki/VNC-session-known-issues'
247
247
  export const modals = {
248
248
  hide: 0,
249
249
  setting: 1,
@@ -12,32 +12,6 @@ const props = runSync('getConstants')
12
12
  props.env = JSON.parse(props.env)
13
13
  props.versions = JSON.parse(props.versions)
14
14
 
15
- // Encoding function
16
- function encodeUint8Array (uint8Array) {
17
- let str = ''
18
- const len = uint8Array.byteLength
19
-
20
- for (let i = 0; i < len; i++) {
21
- str += String.fromCharCode(uint8Array[i])
22
- }
23
-
24
- return btoa(str)
25
- }
26
-
27
- // Decoding function
28
- function decodeBase64String (base64String) {
29
- const str = atob(base64String)
30
- const len = str.length
31
-
32
- const uint8Array = new Uint8Array(len)
33
-
34
- for (let i = 0; i < len; i++) {
35
- uint8Array[i] = str.charCodeAt(i)
36
- }
37
-
38
- return uint8Array
39
- }
40
-
41
15
  window.pre = {
42
16
  requireAuth: runSync('shouldAuth'),
43
17
  readClipboard: () => {
@@ -71,101 +45,8 @@ const path = {
71
45
  basename: window.pre.basename
72
46
  }
73
47
 
74
- const fs = {
75
- stat: (path, cb) => {
76
- window.fs.statCustom(path)
77
- .catch(err => cb(err))
78
- .then(obj => {
79
- obj.isDirectory = () => obj.isD
80
- obj.isFile = () => obj.isF
81
- cb(undefined, obj)
82
- })
83
- },
84
- access: (...args) => {
85
- const cb = args.pop()
86
- window.fs.access(...args)
87
- .then((data) => cb(undefined, data))
88
- .catch((err) => cb(err))
89
- },
90
- open: (...args) => {
91
- const cb = args.pop()
92
- if (window.et.isWebApp) {
93
- window.fs.openCustom(...args)
94
- .then((data) => cb(undefined, data))
95
- .catch((err) => cb(err))
96
- return
97
- }
98
- runGlobalAsync('fsOpen', ...args)
99
- .then((data) => cb(undefined, data))
100
- .catch((err) => cb(err))
101
- },
102
- read: (p1, arr, ...args) => {
103
- const cb = args.pop()
104
- if (window.et.isWebApp) {
105
- window.fs.readCustom(
106
- p1,
107
- arr.length,
108
- ...args
109
- )
110
- .then((data) => {
111
- const { n, newArr } = data
112
- const newArr1 = decodeBase64String(newArr)
113
- cb(undefined, n, newArr1)
114
- })
115
- .catch(err => cb(err))
116
- return
117
- }
118
- runGlobalAsync('fsRead', p1, arr.length, ...args)
119
- .then((data) => {
120
- const { n, buffer } = data
121
- cb(undefined, n, buffer)
122
- })
123
- .catch(err => cb(err))
124
- },
125
- close: (fd, cb) => {
126
- if (window.et.isWebApp) {
127
- window.fs.closeCustom(fd)
128
- .then((data) => cb(undefined, data))
129
- .catch((err) => cb(err))
130
- return
131
- }
132
- runGlobalAsync('fsClose', fd)
133
- .then((data) => cb(undefined, data))
134
- .catch((err) => cb(err))
135
- },
136
- readdir: (p, cb) => {
137
- window.fs.readdir(p)
138
- .then((data) => cb(undefined, data))
139
- .catch((err) => cb(err))
140
- },
141
- mkdir: (...args) => {
142
- const cb = args.pop()
143
- window.fs.mkdir(...args)
144
- .then((data) => cb(undefined, data))
145
- .catch((err) => cb(err))
146
- },
147
- write: (p1, buf, cb) => {
148
- if (window.et.isWebApp) {
149
- window.fs.writeCustom(p1, encodeUint8Array(buf))
150
- .then((data) => cb(undefined, data))
151
- .catch((err) => cb(err))
152
- return
153
- }
154
- runGlobalAsync('fsWrite', p1, buf)
155
- .then((data) => cb(undefined, data))
156
- .catch((err) => cb(err))
157
- },
158
- realpath: (p, cb) => {
159
- window.fs.realpath(p)
160
- .then((data) => cb(undefined, data))
161
- .catch((err) => cb(err))
162
- },
163
- constants: runSync('getFsContants')
164
- }
165
-
166
48
  window.reqs = {
167
- path,
168
- fs
49
+ path
169
50
  }
170
51
 
171
52
  function require (name) {
@@ -13,10 +13,14 @@ import {
13
13
  aiConfigWikiLink
14
14
  } from '../../common/constants'
15
15
  import Password from '../common/password'
16
+ import AiHistory, { addHistoryItem } from './ai-history'
16
17
 
17
18
  // Comprehensive API provider configurations
18
19
  import providers from './providers'
19
20
 
21
+ const STORAGE_KEY_CONFIG = 'ai_config_history'
22
+ const EVENT_NAME_CONFIG = 'ai-config-history-update'
23
+
20
24
  const e = window.translate
21
25
  const defaultRoles = [
22
26
  {
@@ -66,14 +70,27 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
66
70
 
67
71
  const handleSubmit = async (values) => {
68
72
  onSubmit(values)
73
+ addHistoryItem(STORAGE_KEY_CONFIG, values, EVENT_NAME_CONFIG)
74
+ }
75
+
76
+ function handleSelectHistory (item) {
77
+ if (item && typeof item === 'object') {
78
+ form.setFieldsValue(item)
79
+ }
80
+ }
81
+
82
+ function renderHistoryItem (item) {
83
+ if (!item || typeof item !== 'object') return { label: 'Unknown', title: 'Unknown' }
84
+ const model = item.modelAI || 'Default Model'
85
+ const rolePrefix = item.roleAI ? item.roleAI.substring(0, 15) + '...' : ''
86
+ const label = `[${model}] ${rolePrefix}`
87
+ const title = `Model: ${item.modelAI}\nRole: ${item.roleAI}\nURL: ${item.baseURLAI}`
88
+ return { label, title }
69
89
  }
70
90
 
71
91
  function handleChange (v) {
72
92
  const options = getModelOptions(v)
73
93
  setModelOptions(options)
74
- form.setFieldsValue({
75
- modelAI: options[0]?.value || ''
76
- })
77
94
  }
78
95
 
79
96
  if (!showAIConfig) {
@@ -199,6 +216,12 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
199
216
  </Button>
200
217
  </Form.Item>
201
218
  </Form>
219
+ <AiHistory
220
+ storageKey={STORAGE_KEY_CONFIG}
221
+ eventName={EVENT_NAME_CONFIG}
222
+ onSelect={handleSelectHistory}
223
+ renderItem={renderHistoryItem}
224
+ />
202
225
  <AiCache />
203
226
  </>
204
227
  )
@@ -0,0 +1,46 @@
1
+ import { Tag, Tooltip } from 'antd'
2
+
3
+ export default function AiHistoryItem (props) {
4
+ const {
5
+ item,
6
+ onSelect,
7
+ onDelete,
8
+ renderItem
9
+ } = props
10
+
11
+ // If a custom render function is provided, use it to get the display string and title
12
+ // otherwise assume it's a string
13
+ let displayItem = ''
14
+ let fullItem = ''
15
+
16
+ if (renderItem) {
17
+ const renderedInfo = renderItem(item)
18
+ displayItem = renderedInfo.label
19
+ fullItem = renderedInfo.title || renderedInfo.label
20
+ } else {
21
+ fullItem = item
22
+ displayItem = item.length > 50 ? `${item.slice(0, 50)}...` : item
23
+ }
24
+
25
+ const isLong = fullItem.length > 50
26
+ const tagElem = (
27
+ <Tag
28
+ closable
29
+ onClose={(event) => onDelete(item, event)}
30
+ onClick={() => onSelect(item)}
31
+ className='pointer'
32
+ >
33
+ {displayItem}
34
+ </Tag>
35
+ )
36
+
37
+ return isLong
38
+ ? (
39
+ <Tooltip title={fullItem}>
40
+ {tagElem}
41
+ </Tooltip>
42
+ )
43
+ : (
44
+ tagElem
45
+ )
46
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Generic AI history component
3
+ */
4
+ import { useState, useEffect } from 'react'
5
+ import { Space } from 'antd'
6
+ import { HistoryOutlined } from '@ant-design/icons'
7
+ import { getItemJSON, setItemJSON } from '../../common/safe-local-storage'
8
+ import AiHistoryItem from './ai-history-item'
9
+
10
+ const MAX_HISTORY = 20
11
+ const e = window.translate
12
+
13
+ export function getHistory (storageKey) {
14
+ return getItemJSON(storageKey, [])
15
+ }
16
+
17
+ export function addHistoryItem (storageKey, itemData, eventName) {
18
+ if (!itemData) return
19
+
20
+ // Standardize the check: if itemData is an object, we serialize it
21
+ // But wait, the comparison should be stable. We can rely on a unique identifier
22
+ // or a stringified version to avoid duplicates.
23
+ let history = getHistory(storageKey)
24
+ const itemStr = typeof itemData === 'string' ? itemData.trim() : JSON.stringify(itemData)
25
+
26
+ if (!itemStr) return
27
+
28
+ history = history.filter(h => {
29
+ const hStr = typeof h === 'string' ? h.trim() : JSON.stringify(h)
30
+ return hStr !== itemStr
31
+ })
32
+
33
+ // use original data structure to save
34
+ const dataToSave = typeof itemData === 'string' ? itemData.trim() : itemData
35
+
36
+ history.unshift(dataToSave)
37
+ if (history.length > MAX_HISTORY) {
38
+ history = history.slice(0, MAX_HISTORY)
39
+ }
40
+ setItemJSON(storageKey, history)
41
+
42
+ // Custom event to trigger update
43
+ if (eventName) {
44
+ window.dispatchEvent(new Event(eventName))
45
+ }
46
+ }
47
+
48
+ export default function AiHistory (props) {
49
+ const { onSelect, storageKey, eventName, renderItem } = props
50
+ const [history, setHistory] = useState([])
51
+
52
+ const loadHistory = () => {
53
+ setHistory(getHistory(storageKey))
54
+ }
55
+
56
+ useEffect(() => {
57
+ loadHistory()
58
+ if (eventName) {
59
+ window.addEventListener(eventName, loadHistory)
60
+ return () => {
61
+ window.removeEventListener(eventName, loadHistory)
62
+ }
63
+ }
64
+ }, [storageKey, eventName])
65
+
66
+ const handleDelete = (item, event) => {
67
+ event.preventDefault()
68
+ event.stopPropagation()
69
+ const itemStr = typeof item === 'string' ? item : JSON.stringify(item)
70
+ const newHistory = history.filter(h => {
71
+ const hStr = typeof h === 'string' ? h : JSON.stringify(h)
72
+ return hStr !== itemStr
73
+ })
74
+ setHistory(newHistory)
75
+ setItemJSON(storageKey, newHistory)
76
+ }
77
+
78
+ if (!history.length) {
79
+ return null
80
+ }
81
+
82
+ return (
83
+ <div className='ai-bookmark-history pd1b'>
84
+ <div className='pd1b text-muted'>
85
+ <HistoryOutlined className='mg1r' />
86
+ <span className='mg1r'>{e('history') || 'History'}:</span>
87
+ </div>
88
+ <Space size={[8, 8]} wrap>
89
+ {history.map((item, index) => {
90
+ const keyStr = typeof item === 'string' ? item : JSON.stringify(item)
91
+ return (
92
+ <AiHistoryItem
93
+ key={keyStr + index}
94
+ item={item}
95
+ onSelect={onSelect}
96
+ onDelete={handleDelete}
97
+ renderItem={renderItem}
98
+ />
99
+ )
100
+ })}
101
+ </Space>
102
+ </div>
103
+ )
104
+ }
@@ -0,0 +1,338 @@
1
+ /**
2
+ * AI-powered bookmark generation form
3
+ */
4
+ import { useState, useEffect } from 'react'
5
+ import { Button, Input, message, Space, Alert } from 'antd'
6
+ import {
7
+ RobotOutlined,
8
+ LoadingOutlined,
9
+ CheckOutlined,
10
+ CloseOutlined,
11
+ EditOutlined,
12
+ CopyOutlined,
13
+ DownloadOutlined,
14
+ EyeOutlined
15
+ } from '@ant-design/icons'
16
+ import SimpleEditor from '../text-editor/simple-editor'
17
+ import { copy } from '../../common/clipboard'
18
+ import download from '../../common/download'
19
+ import AICategorySelect from './common/ai-category-select.jsx'
20
+ import HelpIcon from '../common/help-icon'
21
+ import Modal from '../common/modal.jsx'
22
+ import { buildPrompt } from './bookmark-schema.js'
23
+ import { fixBookmarkData } from './fix-bookmark-default.js'
24
+ import generate from '../../common/id-with-stamp'
25
+ import AiHistory, { addHistoryItem } from '../ai/ai-history.jsx'
26
+ import { getItem, setItem } from '../../common/safe-local-storage'
27
+
28
+ const STORAGE_KEY_DESC = 'ai_bookmark_description'
29
+ const STORAGE_KEY_HISTORY = 'ai_bookmark_history'
30
+ const EVENT_NAME_HISTORY = 'ai-bookmark-history-update'
31
+ const { TextArea } = Input
32
+ const e = window.translate
33
+
34
+ export default function AIBookmarkForm (props) {
35
+ const { onCancel } = props
36
+ const [description, setDescription] = useState(() => getItem(STORAGE_KEY_DESC) || '')
37
+ const [loading, setLoading] = useState(false)
38
+ const [showConfirm, setShowConfirm] = useState(false)
39
+ const [editMode, setEditMode] = useState(false)
40
+ const [editorText, setEditorText] = useState('')
41
+ const [selectedCategory, setSelectedCategory] = useState('default')
42
+
43
+ useEffect(() => {
44
+ setItem(STORAGE_KEY_DESC, description)
45
+ }, [description])
46
+
47
+ const handleGenerate = async () => {
48
+ if (window.store.aiConfigMissing()) {
49
+ window.store.toggleAIConfig()
50
+ return
51
+ }
52
+
53
+ if (!description.trim()) {
54
+ return message.warning(e('description') + ' required')
55
+ }
56
+
57
+ setLoading(true)
58
+
59
+ try {
60
+ const config = window.store.config
61
+ const prompt = buildPrompt(description)
62
+
63
+ const aiResponse = await window.pre.runGlobalAsync(
64
+ 'AIchat',
65
+ prompt,
66
+ config.modelAI,
67
+ 'You are a helpful assistant that generates bookmark configurations in JSON format.',
68
+ config.baseURLAI,
69
+ config.apiPathAI,
70
+ config.apiKeyAI,
71
+ config.proxyAI,
72
+ false // Disable streaming for structured response
73
+ )
74
+
75
+ if (aiResponse && aiResponse.error) {
76
+ throw new Error(aiResponse.error)
77
+ }
78
+
79
+ let bookmarkData
80
+ if (aiResponse && aiResponse.response) {
81
+ // Parse the JSON response
82
+ let jsonStr = aiResponse.response.trim()
83
+ // Remove markdown code blocks if present
84
+ if (jsonStr.startsWith('```json')) {
85
+ jsonStr = jsonStr.slice(7)
86
+ } else if (jsonStr.startsWith('```')) {
87
+ jsonStr = jsonStr.slice(3)
88
+ }
89
+ if (jsonStr.endsWith('```')) {
90
+ jsonStr = jsonStr.slice(0, -3)
91
+ }
92
+ jsonStr = jsonStr.trim()
93
+
94
+ bookmarkData = JSON.parse(jsonStr)
95
+ const pretty = JSON.stringify(bookmarkData, null, 2)
96
+ setEditorText(pretty)
97
+ // set default category when preview opens
98
+ setSelectedCategory('default')
99
+ setShowConfirm(true)
100
+ addHistoryItem(STORAGE_KEY_HISTORY, description, EVENT_NAME_HISTORY)
101
+ }
102
+ } catch (error) {
103
+ console.error('AI bookmark generation error:', error)
104
+ message.error(e('aiGenerateError') || 'AI generation failed: ' + error.message)
105
+ } finally {
106
+ setLoading(false)
107
+ }
108
+ }
109
+
110
+ function getGeneratedData () {
111
+ if (!editorText) return message.warning(e('noData') || 'No data')
112
+ let parsed = null
113
+ try {
114
+ parsed = fixBookmarkData(JSON.parse(editorText))
115
+ } catch (err) {
116
+ return message.error(e('invalidJson') || 'Invalid JSON')
117
+ }
118
+ if (!parsed) return []
119
+ return Array.isArray(parsed) ? parsed : [parsed]
120
+ }
121
+
122
+ const createBookmark = async (bm) => {
123
+ const { store } = window
124
+ const { addItem } = store
125
+ const fixedBm = fixBookmarkData(bm)
126
+ if (!fixedBm.id) {
127
+ fixedBm.id = generate()
128
+ }
129
+ if (fixedBm.connectionHoppings?.length) {
130
+ fixedBm.hasHopping = true
131
+ }
132
+
133
+ // Add bookmark
134
+ addItem(fixedBm, 'bookmarks')
135
+
136
+ // Ensure the bookmark id is registered in its group
137
+ try {
138
+ const groupId = fixedBm.category || selectedCategory || 'default'
139
+ const group = window.store.bookmarkGroups.find(g => g.id === groupId)
140
+ if (group) {
141
+ group.bookmarkIds = [
142
+ ...new Set([...(group.bookmarkIds || []), fixedBm.id])
143
+ ]
144
+ fixedBm.color = group.color
145
+ }
146
+ } catch (err) {
147
+ console.error('Failed to update bookmark group:', err)
148
+ }
149
+ }
150
+
151
+ const handleConfirm = async () => {
152
+ const parsed = getGeneratedData()
153
+ if (!parsed.length) {
154
+ return
155
+ }
156
+ for (const item of parsed) {
157
+ // set defaults like mcpAddBookmark would
158
+ await createBookmark(item)
159
+ }
160
+ setShowConfirm(false)
161
+ setDescription('') // Clear description only on successful creation
162
+ message.success(e('Done'))
163
+ }
164
+
165
+ const handleCancelConfirm = () => {
166
+ setShowConfirm(false)
167
+ }
168
+
169
+ const handleCancel = () => {
170
+ if (onCancel) {
171
+ onCancel()
172
+ }
173
+ }
174
+
175
+ const handleToggleEdit = () => {
176
+ setEditMode(!editMode)
177
+ }
178
+
179
+ const handleEditorChange = (e) => {
180
+ // SimpleEditor passes event-like or value via onChange
181
+ const val = e && e.target ? e.target.value : e
182
+ setEditorText(val)
183
+ }
184
+
185
+ const handleCopy = () => {
186
+ copy(editorText)
187
+ }
188
+
189
+ const handleSaveToFile = async () => {
190
+ const parsed = getGeneratedData()
191
+ if (!parsed.length) {
192
+ return
193
+ }
194
+ const date = new Date().toISOString().slice(0, 10)
195
+ const fileName = `bookmarks-${date}.json`
196
+ await download(fileName, editorText)
197
+ }
198
+
199
+ const renderEditor = () => {
200
+ const editorProps = {
201
+ value: editorText,
202
+ onChange: handleEditorChange
203
+ }
204
+ return (
205
+ <SimpleEditor
206
+ {...editorProps}
207
+ />
208
+ )
209
+ }
210
+
211
+ const renderPreview = () => {
212
+ if (editMode) {
213
+ return renderEditor()
214
+ }
215
+ return (
216
+ <pre className='ai-bookmark-json-preview'>
217
+ {editorText}
218
+ </pre>
219
+ )
220
+ }
221
+
222
+ const renderCategorySelect = () => {
223
+ return (
224
+ <AICategorySelect
225
+ bookmarkGroups={window.store.bookmarkGroups}
226
+ value={selectedCategory}
227
+ onChange={setSelectedCategory}
228
+ />
229
+ )
230
+ }
231
+
232
+ const textAreaProps = {
233
+ value: description,
234
+ onChange: e => setDescription(e.target.value),
235
+ placeholder: e('createBookmarkByAI'),
236
+ autoSize: { minRows: 4, maxRows: 8 },
237
+ disabled: loading
238
+ }
239
+
240
+ const generateBtnProps = {
241
+ type: 'primary',
242
+ onClick: handleGenerate,
243
+ disabled: !description.trim(),
244
+ icon: loading ? <LoadingOutlined /> : <RobotOutlined />,
245
+ loading
246
+ }
247
+
248
+ const modalProps = {
249
+ title: e('confirmBookmarkData') || 'Confirm Bookmark Data',
250
+ open: showConfirm,
251
+ onCancel: handleCancelConfirm,
252
+ footer: (
253
+ <div className='custom-modal-footer-buttons'>
254
+ <Button onClick={handleCancelConfirm}>
255
+ <CloseOutlined /> {e('cancel')}
256
+ </Button>
257
+ <Button type='primary' onClick={handleConfirm}>
258
+ <CheckOutlined /> {e('confirm')}
259
+ </Button>
260
+ </div>
261
+ ),
262
+ width: '80%'
263
+ }
264
+
265
+ const editBtnProps = {
266
+ icon: editMode ? <EyeOutlined /> : <EditOutlined />,
267
+ title: editMode ? e('preview') : e('edit'),
268
+ onClick: handleToggleEdit
269
+ }
270
+
271
+ const copyBtnProps = {
272
+ icon: <CopyOutlined />,
273
+ title: e('copy'),
274
+ onClick: handleCopy
275
+ }
276
+
277
+ const downloadBtnProps = {
278
+ icon: <DownloadOutlined />,
279
+ title: e('download'),
280
+ onClick: handleSaveToFile
281
+ }
282
+
283
+ const cancelProps = {
284
+ onClick: handleCancel,
285
+ title: e('cancel'),
286
+ icon: <CloseOutlined />,
287
+ className: 'mg1l'
288
+ }
289
+
290
+ return (
291
+ <div className='ai-bookmark-form pd2'>
292
+ <div className='pd1b ai-bookmark-header'>
293
+ <span className='ai-title'>
294
+ <RobotOutlined className='mg1r' />
295
+ {e('createBookmarkByAI')}
296
+ </span>
297
+ <HelpIcon link='https://github.com/electerm/electerm/wiki/Create-bookmark-by-AI' />
298
+ </div>
299
+ <div className='pd1b'>
300
+ <Alert
301
+ type='info'
302
+ showIcon
303
+ title={e('aiSecurityNotice')}
304
+ />
305
+ </div>
306
+ <div className='pd1b'>
307
+ <TextArea {...textAreaProps} />
308
+ </div>
309
+ <AiHistory
310
+ storageKey={STORAGE_KEY_HISTORY}
311
+ eventName={EVENT_NAME_HISTORY}
312
+ onSelect={setDescription}
313
+ />
314
+ <div className='pd1t'>
315
+ <Button {...generateBtnProps}>
316
+ {e('submit')}
317
+ </Button>
318
+ <Button {...cancelProps}>
319
+ {e('cancel')}
320
+ </Button>
321
+ </div>
322
+
323
+ <Modal {...modalProps}>
324
+ <div className='pd1y'>
325
+ <Space.Compact className='ai-action-buttons'>
326
+ <Button {...editBtnProps} />
327
+ <Button {...copyBtnProps} />
328
+ <Button {...downloadBtnProps} />
329
+ </Space.Compact>
330
+ </div>
331
+ <div className='pd1y'>
332
+ {renderCategorySelect()}
333
+ </div>
334
+ {renderPreview()}
335
+ </Modal>
336
+ </div>
337
+ )
338
+ }