@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.
- package/client/common/constants.js +3 -3
- package/client/common/pre.js +1 -120
- package/client/components/ai/ai-config.jsx +26 -3
- package/client/components/ai/ai-history-item.jsx +46 -0
- package/client/components/ai/ai-history.jsx +104 -0
- package/client/components/bookmark-form/ai-bookmark-form.jsx +338 -0
- package/client/components/bookmark-form/bookmark-form.styl +1 -1
- package/client/components/bookmark-form/bookmark-schema.js +192 -0
- package/client/components/bookmark-form/common/ai-category-select.jsx +32 -0
- package/client/components/bookmark-form/common/category-select.jsx +2 -4
- package/client/components/bookmark-form/common/fields.jsx +0 -10
- package/client/components/bookmark-form/config/ftp.js +2 -0
- package/client/components/bookmark-form/config/rdp.js +0 -1
- package/client/components/bookmark-form/config/session-config.js +3 -1
- package/client/components/bookmark-form/config/spice.js +43 -0
- package/client/components/bookmark-form/config/vnc.js +1 -2
- package/client/components/bookmark-form/fix-bookmark-default.js +134 -0
- package/client/components/bookmark-form/index.jsx +74 -14
- package/client/components/common/notification.jsx +34 -2
- package/client/components/common/notification.styl +18 -2
- package/client/components/main/wrapper.styl +0 -7
- package/client/components/rdp/rdp-session.jsx +44 -11
- package/client/components/session/session.jsx +13 -3
- package/client/components/setting-panel/deep-link-control.jsx +3 -2
- package/client/components/setting-panel/keywords-transport.jsx +0 -1
- package/client/components/shortcuts/shortcut-editor.jsx +12 -36
- package/client/components/shortcuts/shortcut-handler.js +11 -5
- package/client/components/sidebar/index.jsx +11 -1
- package/client/components/spice/spice-session.jsx +296 -0
- package/client/components/spice/spice.styl +4 -0
- package/client/components/tabs/add-btn-menu.jsx +9 -2
- package/client/components/terminal/attach-addon-custom.js +20 -76
- package/client/components/terminal/terminal.jsx +34 -28
- package/client/components/terminal/transfer-client-base.js +232 -0
- package/client/components/terminal/trzsz-client.js +306 -0
- package/client/components/terminal/xterm-loader.js +109 -0
- package/client/components/terminal/zmodem-client.js +13 -166
- package/client/components/text-editor/simple-editor.jsx +1 -2
- package/client/components/vnc/vnc-session.jsx +1 -1
- package/client/entry/electerm.jsx +0 -2
- package/client/store/load-data.js +1 -1
- package/client/store/store.js +1 -1
- package/client/store/system-menu.js +10 -0
- package/package.json +1 -1
- package/client/common/trzsz.js +0 -46
- package/client/components/bookmark-form/common/wiki-alert.jsx +0 -9
- package/client/components/common/notification-with-details.jsx +0 -34
- 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,
|
package/client/common/pre.js
CHANGED
|
@@ -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
|
+
}
|