@electerm/electerm-react 5.5.15 → 5.5.35
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/README.md +14 -4
- package/client/common/bookmark-schemas.js +3 -0
- package/client/common/constants.js +16 -0
- package/client/common/create-title.jsx +5 -1
- package/client/common/default-setting.js +4 -2
- package/client/common/parse-vv.js +407 -0
- package/client/common/theme-defaults.js +5 -2
- package/client/common/update-check.js +5 -4
- package/client/common/vv-to-tab.js +120 -0
- package/client/components/ai/agent-tool-call-card.jsx +19 -4
- package/client/components/ai/agent.js +33 -16
- package/client/components/ai/ai-chat-history-item.jsx +12 -48
- package/client/components/ai/ai-chat.jsx +82 -25
- package/client/components/ai/ai-config-props.js +12 -1
- package/client/components/ai/ai-config.jsx +14 -0
- package/client/components/ai/ai-context-indicator.jsx +83 -0
- package/client/components/ai/ai-context.js +272 -0
- package/client/components/ai/ai-output.jsx +4 -0
- package/client/components/ai/ai-presets.js +13 -2
- package/client/components/ai/ai.styl +105 -6
- package/client/components/ai/copy-icon.jsx +31 -0
- package/client/components/bg/css-overwrite.jsx +16 -1
- package/client/components/bookmark-form/common/fields.jsx +3 -0
- package/client/components/bookmark-form/common/vv-file-field.jsx +138 -0
- package/client/components/bookmark-form/config/spice.js +22 -0
- package/client/components/footer/cmd-history.jsx +279 -65
- package/client/components/footer/cmd-history.styl +68 -1
- package/client/components/footer/footer-entry.jsx +46 -15
- package/client/components/icons/ai-icon.styl +4 -1
- package/client/components/layout/layout.jsx +12 -13
- package/client/components/main/main.jsx +23 -3
- package/client/components/main/term-fullscreen.styl +7 -0
- package/client/components/quick-commands/qm.styl +58 -0
- package/client/components/quick-commands/quick-command-ai-editor.jsx +255 -0
- package/client/components/quick-commands/quick-command-ai-editor.styl +13 -0
- package/client/components/quick-commands/quick-commands-box.jsx +197 -82
- package/client/components/quick-commands/quick-commands-form-elem.jsx +129 -65
- package/client/components/quick-commands/quick-commands-select.jsx +11 -0
- package/client/components/setting-panel/list.jsx +2 -2
- package/client/components/setting-panel/setting-common.jsx +2 -0
- package/client/components/side-panel-r/right-side-panel.styl +11 -1
- package/client/components/side-panel-r/side-panel-r.jsx +56 -13
- package/client/components/sidebar/bookmark-select.jsx +17 -3
- package/client/components/sidebar/history-item.jsx +20 -4
- package/client/components/sidebar/history.jsx +7 -0
- package/client/components/sidebar/index.jsx +13 -4
- package/client/components/sidebar/side-panel.jsx +8 -3
- package/client/components/sidebar/sidebar-panel.jsx +20 -7
- package/client/components/terminal/shortcut-bar.styl +14 -0
- package/client/components/terminal/unix-timestamp-tooltip.jsx +27 -1
- package/client/components/terminal-info/terminal-info.styl +3 -0
- package/client/components/theme/terminal-theme-list.styl +3 -0
- package/client/components/theme/theme-ai-editor.jsx +2 -2
- package/client/components/theme/theme-form.jsx +2 -2
- package/client/components/theme/theme-list-item.jsx +59 -25
- package/client/components/tree-list/tree-list.jsx +31 -3
- package/client/css/mobile.styl +28 -0
- package/client/store/common.js +164 -32
- package/client/store/init-state.js +10 -1
- package/client/store/load-data.js +72 -0
- package/client/store/sidebar.js +5 -1
- package/client/store/store.js +15 -2
- package/client/store/watch.js +3 -1
- package/package.json +1 -1
|
@@ -37,6 +37,7 @@ export default auto(function Layout (props) {
|
|
|
37
37
|
height,
|
|
38
38
|
isMobile,
|
|
39
39
|
pinnedQuickCommandBar,
|
|
40
|
+
quickCommandsInRightPanel,
|
|
40
41
|
leftSidePanelWidth,
|
|
41
42
|
leftSideBarWidth,
|
|
42
43
|
pinned,
|
|
@@ -49,7 +50,14 @@ export default auto(function Layout (props) {
|
|
|
49
50
|
shortcutBarKbOffset
|
|
50
51
|
} = props.store
|
|
51
52
|
const monitorHeight = isRemoteMonitorBarVisible(props.store) ? remoteMonitorBarHeight : 0
|
|
52
|
-
|
|
53
|
+
// a pin left on while the panel is docked in the right side panel reserves
|
|
54
|
+
// nothing: the footer box is not rendered then (quick-commands-box.jsx)
|
|
55
|
+
const qmPinnedHeight = inActiveTerminal &&
|
|
56
|
+
pinnedQuickCommandBar &&
|
|
57
|
+
!quickCommandsInRightPanel
|
|
58
|
+
? quickCommandBoxHeight
|
|
59
|
+
: 0
|
|
60
|
+
const h = height - footerHeight - monitorHeight - qmPinnedHeight - (shortcutBarVisible ? shortcutBarHeight + shortcutBarKbOffset : 0) + resizeTrigger
|
|
53
61
|
const l = pinned && !isMobile ? leftSideBarWidth + leftSidePanelWidth : leftSideBarWidth
|
|
54
62
|
const r = rightPanelVisible && rightPanelPinned && !isMobile ? rightPanelWidth : 0
|
|
55
63
|
return {
|
|
@@ -141,18 +149,9 @@ export default auto(function Layout (props) {
|
|
|
141
149
|
const footerProps = {
|
|
142
150
|
store
|
|
143
151
|
}
|
|
144
|
-
const qmProps =
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
'openQuickCommandBar',
|
|
148
|
-
'pinnedQuickCommandBar',
|
|
149
|
-
'qmSortByFrequency',
|
|
150
|
-
'inActiveTerminal',
|
|
151
|
-
'leftSidePanelWidth',
|
|
152
|
-
'leftSideBarWidth',
|
|
153
|
-
'openedSideBar',
|
|
154
|
-
'currentQuickCommands'
|
|
155
|
-
])
|
|
152
|
+
const qmProps = {
|
|
153
|
+
store
|
|
154
|
+
}
|
|
156
155
|
const sessionsProps = {
|
|
157
156
|
styles: styles.wrapStyles,
|
|
158
157
|
sizes,
|
|
@@ -29,6 +29,8 @@ import { isAIDisabled } from '../../common/ai-feature'
|
|
|
29
29
|
import { ConfigProvider } from 'antd'
|
|
30
30
|
import { NotificationContainer } from '../common/notification'
|
|
31
31
|
import RightSidePanel from '../side-panel-r/side-panel-r'
|
|
32
|
+
import CmdHistory from '../footer/cmd-history'
|
|
33
|
+
import QuickCommandsFooterBox from '../quick-commands/quick-commands-box'
|
|
32
34
|
import ConnectionHoppingWarning from './connection-hopping-warnning'
|
|
33
35
|
import SshConfigLoadNotify from '../ssh-config/ssh-config-load-notify'
|
|
34
36
|
import LoadSshConfigs from '../ssh-config/load-ssh-configs'
|
|
@@ -94,6 +96,7 @@ export default auto(function Index (props) {
|
|
|
94
96
|
store.checkForDbUpgrade()
|
|
95
97
|
store.handleGetSerials()
|
|
96
98
|
store.checkPendingDeepLink()
|
|
99
|
+
store.checkPendingVvFile()
|
|
97
100
|
}, [])
|
|
98
101
|
|
|
99
102
|
// Track the actual input modality rather than a static capability probe:
|
|
@@ -128,6 +131,7 @@ export default auto(function Index (props) {
|
|
|
128
131
|
pinned,
|
|
129
132
|
isSecondInstance,
|
|
130
133
|
pinnedQuickCommandBar,
|
|
134
|
+
quickCommandsInRightPanel,
|
|
131
135
|
installSrc,
|
|
132
136
|
fileTransfers,
|
|
133
137
|
uiThemeConfig,
|
|
@@ -148,9 +152,16 @@ export default auto(function Index (props) {
|
|
|
148
152
|
'is-mac': isMac,
|
|
149
153
|
'not-mac': !isMac,
|
|
150
154
|
'is-win': isWin,
|
|
151
|
-
|
|
155
|
+
// The left sidebar dock is desktop-only (see sidebar/index.jsx). This
|
|
156
|
+
// class drives the docked-layout CSS — the footer's left offset and the
|
|
157
|
+
// sidebar panel's top padding — so it has to follow the same gate as the
|
|
158
|
+
// layout maths in layout.jsx, or a pin persisted from a desktop session
|
|
159
|
+
// would restyle the mobile layout.
|
|
160
|
+
pinned: pinned && !store.isMobile,
|
|
152
161
|
'not-win': !isWin,
|
|
153
|
-
|
|
162
|
+
// the pinned quick-command box only shapes the layout while it is actually
|
|
163
|
+
// in the footer — docked in the right panel it renders there instead
|
|
164
|
+
'qm-pinned': pinnedQuickCommandBar && !quickCommandsInRightPanel,
|
|
154
165
|
fullscreen,
|
|
155
166
|
// terminal fullscreen keeps the footer visible (rdp/vnc/spice fullscreen
|
|
156
167
|
// does not — the footer would be an empty bar there)
|
|
@@ -226,6 +237,7 @@ export default auto(function Index (props) {
|
|
|
226
237
|
transferHistory: copiedHistory,
|
|
227
238
|
upgradeInfo,
|
|
228
239
|
pinned,
|
|
240
|
+
isMobile: store.isMobile,
|
|
229
241
|
leftSideBarIcons: config.leftSideBarIcons,
|
|
230
242
|
widgetInstancesLength: widgetInstances.length
|
|
231
243
|
}
|
|
@@ -253,7 +265,8 @@ export default auto(function Index (props) {
|
|
|
253
265
|
rightPanelPinned: store.rightPanelPinned,
|
|
254
266
|
rightPanelWidth: store.rightPanelWidth,
|
|
255
267
|
title: rightPanelTitle,
|
|
256
|
-
rightPanelTab
|
|
268
|
+
rightPanelTab,
|
|
269
|
+
isMobile: store.isMobile
|
|
257
270
|
}
|
|
258
271
|
const sshConfigProps = {
|
|
259
272
|
...pick(store, [
|
|
@@ -275,6 +288,7 @@ export default auto(function Index (props) {
|
|
|
275
288
|
showAIConfig: store.showAIConfig,
|
|
276
289
|
rightPanelTab,
|
|
277
290
|
agentRunning: store.agentRunning,
|
|
291
|
+
aiContextInfo: store.aiContextInfo,
|
|
278
292
|
currentChatSessionId: store.currentChatSessionId,
|
|
279
293
|
showChatSessions: store.showChatSessions
|
|
280
294
|
}
|
|
@@ -351,6 +365,12 @@ export default auto(function Index (props) {
|
|
|
351
365
|
<RightSidePanel {...rightPanelProps}>
|
|
352
366
|
{!isAIDisabled() && <AIChat {...aiChatProps} />}
|
|
353
367
|
<TerminalInfo key={store.activeTabId} store={store} {...deepCopy(store.terminalInfoProps)} />
|
|
368
|
+
{/* the cmd history panel has two homes; the footer popover is the
|
|
369
|
+
other one, and both are the same component (see cmd-history.jsx) */}
|
|
370
|
+
<CmdHistory store={store} inline />
|
|
371
|
+
{/* same for the quick command panel: the footer box is the other home
|
|
372
|
+
(see quick-commands-box.jsx) */}
|
|
373
|
+
<QuickCommandsFooterBox store={store} inline />
|
|
354
374
|
</RightSidePanel>
|
|
355
375
|
<SshConfigLoadNotify {...sshConfigProps} />
|
|
356
376
|
<LoadSshConfigs
|
|
@@ -37,6 +37,13 @@
|
|
|
37
37
|
right 10px !important
|
|
38
38
|
bottom 10px !important
|
|
39
39
|
|
|
40
|
+
// RDP/VNC/Spice fullscreen has no footer (it is in the hide list above), so
|
|
41
|
+
// the right panel has nothing to clear and may use the full height. When the
|
|
42
|
+
// footer IS kept, see .fullscreen.fs-with-footer below.
|
|
43
|
+
.fullscreen:not(.fs-with-footer)
|
|
44
|
+
.right-side-panel
|
|
45
|
+
bottom 0
|
|
46
|
+
|
|
40
47
|
// Terminal fullscreen keeps the footer (and the quick-command popup above it)
|
|
41
48
|
// visible; the session shrinks to sit on top of the footer. RDP/VNC/Spice
|
|
42
49
|
// fullscreen stays footer-less — inActiveTerminal is false for those tabs, so
|
|
@@ -2,6 +2,58 @@
|
|
|
2
2
|
max-height calc(100vh - 100px)
|
|
3
3
|
overflow-y scroll
|
|
4
4
|
padding 20px 0 10px 0
|
|
5
|
+
|
|
6
|
+
// Docked in the right side panel the panel *is* the container, so the quick
|
|
7
|
+
// commands fill it instead of sizing themselves like the footer box. The parent
|
|
8
|
+
// (.right-side-panel-content) is an absolutely positioned box with top and
|
|
9
|
+
// bottom set, so 100% resolves to a real height and the list scrolls inside it
|
|
10
|
+
// rather than scrolling the panel.
|
|
11
|
+
.qm-panel-in-right-panel
|
|
12
|
+
display flex
|
|
13
|
+
flex-direction column
|
|
14
|
+
width 100%
|
|
15
|
+
height 100%
|
|
16
|
+
> .pd2
|
|
17
|
+
display flex
|
|
18
|
+
flex-direction column
|
|
19
|
+
height 100%
|
|
20
|
+
min-height 0
|
|
21
|
+
width 100%
|
|
22
|
+
.qm-flex
|
|
23
|
+
flex-shrink 0
|
|
24
|
+
// the footer box is as wide as the footer; the right panel is not, and the
|
|
25
|
+
// search + label + sort + actions row does not fit in one line there
|
|
26
|
+
flex-wrap wrap
|
|
27
|
+
// ...so give the search a line of its own and let it fill it, instead of
|
|
28
|
+
// sitting at its footer width with a gap beside it. `flex-basis: 100%` is what
|
|
29
|
+
// forces the wrap; the controls then share the line below.
|
|
30
|
+
.qm-search-input
|
|
31
|
+
flex 1 1 100%
|
|
32
|
+
max-width none
|
|
33
|
+
.qm-list-wrap
|
|
34
|
+
flex 1
|
|
35
|
+
min-height 0
|
|
36
|
+
max-height none
|
|
37
|
+
overflow-y auto
|
|
38
|
+
|
|
39
|
+
.qm-panel-actions
|
|
40
|
+
display flex
|
|
41
|
+
align-items center
|
|
42
|
+
gap 6px
|
|
43
|
+
|
|
44
|
+
.qm-move-icon
|
|
45
|
+
font-size 14px
|
|
46
|
+
padding 4px
|
|
47
|
+
cursor pointer
|
|
48
|
+
&:hover
|
|
49
|
+
color var(--primary)
|
|
50
|
+
|
|
51
|
+
// The footer Q stays lit while the panel is docked in the right panel, same
|
|
52
|
+
// contract as the info, AI and cmd history icons
|
|
53
|
+
.quick-command-trigger
|
|
54
|
+
&.active
|
|
55
|
+
color var(--success)
|
|
56
|
+
|
|
5
57
|
.qm-wrap-tooltip
|
|
6
58
|
background var(--main)
|
|
7
59
|
position absolute
|
|
@@ -80,6 +132,12 @@
|
|
|
80
132
|
width 100%
|
|
81
133
|
.qm-label-select
|
|
82
134
|
width 100%
|
|
135
|
+
// The docked panel's search claims a whole row with `flex-basis: 100%`, which
|
|
136
|
+
// is a width only while `.qm-flex` is a row. Here it is a column, so the same
|
|
137
|
+
// basis would be 100% of the *height* and would push the controls off the
|
|
138
|
+
// panel. Same specificity as the rule above it, declared later, so it wins.
|
|
139
|
+
.qm-panel-in-right-panel .qm-search-input
|
|
140
|
+
flex none
|
|
83
141
|
|
|
84
142
|
// On mobile, stack the quick-command row (name / delay / command / remove)
|
|
85
143
|
// vertically instead of side by side — the horizontal Space.Compact layout
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI-powered quick command generator.
|
|
3
|
+
*
|
|
4
|
+
* This is the "create with AI" tab of the quick command form: describe the
|
|
5
|
+
* quick command in natural language, the configured AI provider returns the
|
|
6
|
+
* quick command (name + sub commands in JSON), and the result is written back
|
|
7
|
+
* into the very same form through `onChange`, so the user reviews/edits it in
|
|
8
|
+
* the manual tab before saving.
|
|
9
|
+
*/
|
|
10
|
+
import { useState, useEffect } from 'react'
|
|
11
|
+
import { Button, Input, Space, Alert } from 'antd'
|
|
12
|
+
import {
|
|
13
|
+
RobotOutlined,
|
|
14
|
+
LoadingOutlined
|
|
15
|
+
} from '@ant-design/icons'
|
|
16
|
+
import message from '../common/message'
|
|
17
|
+
import { appendMandatoryGuardrails } from '../ai/ai-guardrails'
|
|
18
|
+
import AiHistory, { addHistoryItem } from '../ai/ai-history.jsx'
|
|
19
|
+
import HelpIcon from '../common/help-icon'
|
|
20
|
+
import { getItem, setItem } from '../../common/safe-local-storage'
|
|
21
|
+
import templates from './templates'
|
|
22
|
+
import './quick-command-ai-editor.styl'
|
|
23
|
+
|
|
24
|
+
const STORAGE_KEY_DESC = 'ai_quick_command_description'
|
|
25
|
+
const STORAGE_KEY_HISTORY = 'ai_quick_command_history'
|
|
26
|
+
const EVENT_NAME_HISTORY = 'ai-quick-command-history-update'
|
|
27
|
+
const defaultDelay = 100
|
|
28
|
+
const maxDelay = 65535
|
|
29
|
+
const maxNameLength = 60
|
|
30
|
+
const { TextArea } = Input
|
|
31
|
+
const e = window.translate
|
|
32
|
+
|
|
33
|
+
const wiki = 'https://github.com/electerm/electerm/wiki/Create-quick-command-by-AI'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* build the prompt that asks the AI for one quick command definition
|
|
37
|
+
* @param {string} description
|
|
38
|
+
* @return {string}
|
|
39
|
+
*/
|
|
40
|
+
export function buildPrompt (description) {
|
|
41
|
+
const lang = window.store.config.languageAI || window.store.getLangName()
|
|
42
|
+
const templateList = templates.map(t => `{{${t}}}`).join(', ')
|
|
43
|
+
return `You are an electerm quick command generator. Based on the user's natural language description, generate ONE quick command definition in JSON format.
|
|
44
|
+
|
|
45
|
+
The quick command JSON shape:
|
|
46
|
+
{
|
|
47
|
+
"name": "string (required) - short name of the quick command",
|
|
48
|
+
"commands": [
|
|
49
|
+
{
|
|
50
|
+
"name": "string - short name of this step",
|
|
51
|
+
"command": "string (required) - the exact shell command to send to the terminal",
|
|
52
|
+
"delay": "number - milliseconds to wait before sending this step, 1-${maxDelay}, default ${defaultDelay}"
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"inputOnly": "boolean - true means only type the command into the terminal without pressing enter, default false",
|
|
56
|
+
"labels": "array of strings - optional tags used to group quick commands"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Important rules:
|
|
60
|
+
1. Respond with ONE quick command object, never an array of quick commands
|
|
61
|
+
2. Split a multi step operation into several "commands" items, one shell command per item, and give a later step a longer "delay" when the step before it takes time
|
|
62
|
+
3. "command" must be a plain shell command, no markdown, no code fence, no trailing newline
|
|
63
|
+
4. Keep "name" short (max ${maxNameLength} chars) and write it in ${lang} language, labels included
|
|
64
|
+
5. You may use the built in templates ${templateList} inside a command when the user asks for clipboard content, current time or current date
|
|
65
|
+
6. Never invent hosts, paths, ports or credentials the user did not mention
|
|
66
|
+
7. Respond ONLY with valid JSON, no markdown formatting and no extra explanation
|
|
67
|
+
|
|
68
|
+
User description: ${description}
|
|
69
|
+
|
|
70
|
+
Generate the quick command JSON:`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function stripFence (str) {
|
|
74
|
+
let s = str.trim()
|
|
75
|
+
if (s.startsWith('```json')) {
|
|
76
|
+
s = s.slice(7)
|
|
77
|
+
} else if (s.startsWith('```')) {
|
|
78
|
+
s = s.slice(3)
|
|
79
|
+
}
|
|
80
|
+
if (s.endsWith('```')) {
|
|
81
|
+
s = s.slice(0, -3)
|
|
82
|
+
}
|
|
83
|
+
return s.trim()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Normalize whatever the model returned into a quick command definition the
|
|
88
|
+
* quick command form can consume. Tolerates markdown fences, prose around the
|
|
89
|
+
* JSON, an array payload and string commands.
|
|
90
|
+
* @param {string|object} raw
|
|
91
|
+
* @return {object|null} {name, commands, inputOnly, labels}
|
|
92
|
+
*/
|
|
93
|
+
export function normalizeQuickCommand (raw) {
|
|
94
|
+
if (!raw) {
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
let data = raw
|
|
98
|
+
if (typeof data === 'string') {
|
|
99
|
+
const txt = stripFence(data)
|
|
100
|
+
try {
|
|
101
|
+
data = JSON.parse(txt)
|
|
102
|
+
} catch (err) {
|
|
103
|
+
// the model sometimes wraps the JSON in explanation text
|
|
104
|
+
const match = txt.match(/[{[][\s\S]*[}\]]/)
|
|
105
|
+
if (!match) {
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
data = JSON.parse(match[0])
|
|
110
|
+
} catch (err2) {
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (Array.isArray(data)) {
|
|
116
|
+
data = data[0]
|
|
117
|
+
}
|
|
118
|
+
if (!data || typeof data !== 'object') {
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
const rawCommands = Array.isArray(data.commands)
|
|
122
|
+
? data.commands
|
|
123
|
+
: (data.command ? [data] : [])
|
|
124
|
+
const commands = rawCommands.map(c => {
|
|
125
|
+
if (typeof c === 'string') {
|
|
126
|
+
return {
|
|
127
|
+
name: '',
|
|
128
|
+
command: c,
|
|
129
|
+
delay: defaultDelay
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const delay = Number(c.delay)
|
|
133
|
+
return {
|
|
134
|
+
name: (c.name || '').toString(),
|
|
135
|
+
command: (c.command || c.cmd || '').toString(),
|
|
136
|
+
delay: delay >= 1 ? Math.min(Math.round(delay), maxDelay) : defaultDelay
|
|
137
|
+
}
|
|
138
|
+
}).filter(c => c.command.trim())
|
|
139
|
+
if (!commands.length) {
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
const name = (data.name || data.title || '').toString().trim()
|
|
143
|
+
return {
|
|
144
|
+
name: name || commands[0].command.slice(0, maxNameLength),
|
|
145
|
+
commands,
|
|
146
|
+
inputOnly: !!data.inputOnly,
|
|
147
|
+
labels: Array.isArray(data.labels)
|
|
148
|
+
? data.labels.map(l => l.toString()).filter(Boolean)
|
|
149
|
+
: []
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export default function QuickCommandAiEditor (props) {
|
|
154
|
+
const { onChange } = props
|
|
155
|
+
const [description, setDescription] = useState(() => getItem(STORAGE_KEY_DESC) || '')
|
|
156
|
+
const [loading, setLoading] = useState(false)
|
|
157
|
+
const [preview, setPreview] = useState('')
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
setItem(STORAGE_KEY_DESC, description)
|
|
161
|
+
}, [description])
|
|
162
|
+
|
|
163
|
+
async function handleGenerate () {
|
|
164
|
+
if (window.store.aiConfigMissing()) {
|
|
165
|
+
window.store.toggleAIConfig()
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
if (!description.trim()) {
|
|
169
|
+
return message.warning(e('description') + ' ' + e('required'))
|
|
170
|
+
}
|
|
171
|
+
setLoading(true)
|
|
172
|
+
try {
|
|
173
|
+
const config = window.store.config
|
|
174
|
+
const aiResponse = await window.pre.runGlobalAsync(
|
|
175
|
+
'AIchat',
|
|
176
|
+
buildPrompt(description),
|
|
177
|
+
config.modelAI,
|
|
178
|
+
appendMandatoryGuardrails('You are a helpful assistant that generates electerm quick commands.'),
|
|
179
|
+
config.baseURLAI,
|
|
180
|
+
config.apiPathAI,
|
|
181
|
+
config.apiKeyAI,
|
|
182
|
+
config.proxyAI,
|
|
183
|
+
false, // disable streaming, we need the whole JSON at once
|
|
184
|
+
config.authHeaderNameAI
|
|
185
|
+
)
|
|
186
|
+
if (aiResponse && aiResponse.error) {
|
|
187
|
+
throw new Error(aiResponse.error)
|
|
188
|
+
}
|
|
189
|
+
const data = normalizeQuickCommand(aiResponse && aiResponse.response)
|
|
190
|
+
if (!data) {
|
|
191
|
+
throw new Error('no quick command found in AI response')
|
|
192
|
+
}
|
|
193
|
+
setPreview(JSON.stringify(data, null, 2))
|
|
194
|
+
addHistoryItem(STORAGE_KEY_HISTORY, description, EVENT_NAME_HISTORY)
|
|
195
|
+
onChange(data)
|
|
196
|
+
message.success(e('Done'))
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.error('AI quick command generation error:', err)
|
|
199
|
+
message.error('Can not generate quick command from AI response: ' + err.message)
|
|
200
|
+
} finally {
|
|
201
|
+
setLoading(false)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const textAreaProps = {
|
|
206
|
+
value: description,
|
|
207
|
+
onChange: ev => setDescription(ev.target.value),
|
|
208
|
+
placeholder: e('description'),
|
|
209
|
+
autoSize: { minRows: 4, maxRows: 8 },
|
|
210
|
+
disabled: loading
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const generateBtnProps = {
|
|
214
|
+
type: 'primary',
|
|
215
|
+
onClick: handleGenerate,
|
|
216
|
+
disabled: !description.trim(),
|
|
217
|
+
icon: loading ? <LoadingOutlined /> : <RobotOutlined />,
|
|
218
|
+
loading
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<div className='qm-ai-editor'>
|
|
223
|
+
<div className='pd1b'>
|
|
224
|
+
<Alert
|
|
225
|
+
type='info'
|
|
226
|
+
showIcon
|
|
227
|
+
title={e('aiSecurityNotice')}
|
|
228
|
+
/>
|
|
229
|
+
</div>
|
|
230
|
+
<div className='pd1b'>
|
|
231
|
+
<TextArea {...textAreaProps} />
|
|
232
|
+
</div>
|
|
233
|
+
<AiHistory
|
|
234
|
+
storageKey={STORAGE_KEY_HISTORY}
|
|
235
|
+
eventName={EVENT_NAME_HISTORY}
|
|
236
|
+
onSelect={setDescription}
|
|
237
|
+
/>
|
|
238
|
+
<div className='pd1t'>
|
|
239
|
+
<Space>
|
|
240
|
+
<Button {...generateBtnProps}>
|
|
241
|
+
{e('submit')}
|
|
242
|
+
</Button>
|
|
243
|
+
<HelpIcon link={wiki} />
|
|
244
|
+
</Space>
|
|
245
|
+
</div>
|
|
246
|
+
{
|
|
247
|
+
!!preview && (
|
|
248
|
+
<div className='pd1t'>
|
|
249
|
+
<pre className='qm-ai-preview'>{preview}</pre>
|
|
250
|
+
</div>
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
</div>
|
|
254
|
+
)
|
|
255
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// "create with AI" tab of the quick command form
|
|
2
|
+
.qm-ai-editor
|
|
3
|
+
// the generated quick command, kept visible for reference after it has been
|
|
4
|
+
// written back into the form
|
|
5
|
+
.qm-ai-preview
|
|
6
|
+
padding 12px
|
|
7
|
+
margin 0
|
|
8
|
+
max-height 300px
|
|
9
|
+
overflow auto
|
|
10
|
+
font-size 12px
|
|
11
|
+
font-family monospace
|
|
12
|
+
white-space pre-wrap
|
|
13
|
+
word-break break-word
|