@electerm/electerm-react 5.3.26 → 5.5.6
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 +246 -12
- package/client/common/choose-save-folder.js +21 -2
- package/client/common/constants.js +20 -6
- package/client/common/create-title.jsx +6 -0
- package/client/common/default-setting.js +14 -0
- package/client/common/init-setting-item.js +5 -0
- package/client/common/platform.js +24 -0
- package/client/components/ai/agent-tool-call-card.jsx +10 -2
- package/client/components/ai/agent.js +4 -1
- package/client/components/ai/ai-attachments.js +87 -0
- package/client/components/ai/ai-chat-empty.jsx +43 -0
- package/client/components/ai/ai-chat-entry.jsx +13 -4
- package/client/components/ai/ai-chat-history-item.jsx +28 -4
- package/client/components/ai/ai-chat-history.jsx +2 -1
- package/client/components/ai/ai-chat.jsx +104 -13
- package/client/components/ai/ai-config-props.js +4 -130
- package/client/components/ai/ai-config.jsx +135 -15
- package/client/components/ai/ai-presets.js +173 -0
- package/client/components/ai/ai.styl +83 -0
- package/client/components/bg/css-overwrite.jsx +11 -0
- package/client/components/bookmark-form/bookmark-schema.js +4 -0
- package/client/components/bookmark-form/common/fields.jsx +3 -0
- package/client/components/bookmark-form/config/common-fields.js +12 -0
- package/client/components/bookmark-form/config/local.js +3 -2
- package/client/components/bookmark-form/config/serial.js +3 -2
- package/client/components/bookmark-form/config/ssh.js +2 -1
- package/client/components/bookmark-form/config/telnet.js +3 -2
- package/client/components/common/item-filter.jsx +76 -0
- package/client/components/common/item-filter.styl +54 -0
- package/client/components/common/lazy-boundary.jsx +34 -0
- package/client/components/common/opacity.jsx +3 -1
- package/client/components/footer/footer-entry.jsx +35 -5
- package/client/components/footer/footer.styl +8 -1
- package/client/components/layout/layout.jsx +23 -31
- package/client/components/layout/pixed.js +3 -0
- package/client/components/main/main.jsx +55 -28
- package/client/components/main/term-fullscreen.styl +9 -3
- package/client/components/quick-commands/qm.styl +3 -2
- package/client/components/rdp/file-transfer.js +4 -4
- package/client/components/remote-monitor/monitor-details.jsx +458 -0
- package/client/components/remote-monitor/monitor-details.styl +87 -0
- package/client/components/remote-monitor/monitor-model.js +764 -0
- package/client/components/remote-monitor/remote-monitor-bar-entry.jsx +18 -0
- package/client/components/remote-monitor/remote-monitor-bar.jsx +344 -0
- package/client/components/remote-monitor/remote-monitor-bar.styl +106 -0
- package/client/components/remote-monitor/session-monitor.js +361 -0
- package/client/components/remote-monitor/use-monitor-details.js +10 -0
- package/client/components/remote-monitor/visibility.js +18 -0
- package/client/components/session/session-control.jsx +29 -2
- package/client/components/session/session.jsx +40 -18
- package/client/components/session/sessions.jsx +5 -2
- package/client/components/setting-panel/setting-common.jsx +25 -56
- package/client/components/setting-panel/setting-modal.jsx +57 -42
- package/client/components/setting-panel/setting-terminal.jsx +7 -2
- package/client/components/setting-panel/setting.styl +1 -1
- package/client/components/setting-panel/tab-triggers.jsx +71 -0
- package/client/components/sftp/file-compare-modal.jsx +6 -3
- package/client/components/sftp/file-item.jsx +5 -3
- package/client/components/sftp/owner-list.js +11 -2
- package/client/components/side-panel-r/side-panel-r.jsx +2 -2
- package/client/components/sys-menu/menu-btn.jsx +30 -1
- package/client/components/tabs/index.jsx +2 -2
- package/client/components/tabs/tab-title.jsx +3 -2
- package/client/components/tabs/tab.jsx +5 -1
- package/client/components/terminal/attach-addon-custom.js +295 -49
- package/client/components/terminal/automation/index.js +64 -0
- package/client/components/terminal/automation/keys.js +49 -0
- package/client/components/terminal/automation/stream-matcher.js +82 -0
- package/client/components/terminal/automation/strip-ansi.js +19 -0
- package/client/components/terminal/automation/trigger-engine.js +233 -0
- package/client/components/terminal/automation/trigger-presets.js +110 -0
- package/client/components/terminal/command-tracker-addon.js +31 -1
- package/client/components/terminal/mixins/index.js +27 -0
- package/client/components/terminal/mixins/term-attach.js +123 -0
- package/client/components/terminal/mixins/term-config.jsx +84 -0
- package/client/components/terminal/mixins/term-content.js +88 -0
- package/client/components/terminal/mixins/term-context-menu.jsx +291 -0
- package/client/components/terminal/mixins/term-file-drop.js +149 -0
- package/client/components/terminal/mixins/term-init.js +122 -0
- package/client/components/terminal/mixins/term-log.jsx +96 -0
- package/client/components/terminal/mixins/term-resize.js +87 -0
- package/client/components/terminal/mixins/term-socket.js +433 -0
- package/client/components/terminal/mixins/term-suggestions.js +224 -0
- package/client/components/terminal/mixins/term-theme.js +190 -0
- package/client/components/terminal/mixins/term-touch.js +136 -0
- package/client/components/terminal/shell-detect.js +37 -0
- package/client/components/terminal/shell.js +0 -28
- package/client/components/terminal/shortcut-bar-entry.jsx +6 -3
- package/client/components/terminal/shortcut-bar.jsx +4 -6
- package/client/components/terminal/shortcut-bar.styl +4 -0
- package/client/components/terminal/ssh-reload-state.js +56 -0
- package/client/components/terminal/startup-queue.js +379 -0
- package/client/components/terminal/terminal-interactive.jsx +32 -0
- package/client/components/terminal/terminal.jsx +102 -2007
- package/client/components/terminal/transfer-client-base.js +5 -29
- package/client/components/terminal/trzsz-client.js +1 -3
- package/client/components/terminal/xmodem-client.js +1 -3
- package/client/components/terminal/xterm-loader.js +7 -0
- package/client/components/terminal/zmodem-client.js +1 -3
- package/client/components/terminal-info/base.jsx +18 -70
- package/client/components/terminal-info/terminal-info-entry.jsx +13 -5
- package/client/components/terminal-info/terminal-info.jsx +50 -59
- package/client/components/terminal-info/terminal-info.styl +6 -49
- package/client/components/text-editor/open-text-editor.js +30 -0
- package/client/components/text-editor/text-editor-entry.jsx +17 -6
- package/client/components/text-editor/text-editor.jsx +14 -0
- package/client/components/triggers/trigger-editor.jsx +246 -0
- package/client/components/triggers/trigger-form.jsx +200 -0
- package/client/components/triggers/trigger-session-modal.jsx +199 -0
- package/client/components/triggers/trigger-setting-list.jsx +89 -0
- package/client/components/triggers/trigger-transport.jsx +60 -0
- package/client/components/triggers/triggers-field.jsx +19 -0
- package/client/components/widgets/widgets-list.jsx +11 -1
- package/client/css/mobile.styl +9 -1
- package/client/store/common.js +12 -1
- package/client/store/init-state.js +7 -0
- package/client/store/mcp-handler.js +158 -30
- package/client/store/setting.js +9 -0
- package/client/store/store.js +2 -0
- package/client/store/sync.js +2 -0
- package/client/store/tab.js +41 -3
- package/client/store/trigger.js +118 -0
- package/client/store/watch.js +13 -5
- package/client/views/index.pug +3 -0
- package/package.json +1 -1
- package/client/components/terminal-info/activity.jsx +0 -57
- package/client/components/terminal-info/data-cols-parser.jsx +0 -54
- package/client/components/terminal-info/disk.jsx +0 -45
- package/client/components/terminal-info/format-disks.js +0 -45
- package/client/components/terminal-info/network.jsx +0 -121
- package/client/components/terminal-info/resource.jsx +0 -99
- package/client/components/terminal-info/run-cmd.jsx +0 -263
- package/client/components/terminal-info/sys-info.jsx +0 -56
- package/client/components/terminal-info/up.jsx +0 -17
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import { lazy, Suspense } from 'react'
|
|
1
|
+
import { lazy, Suspense, useState } from 'react'
|
|
2
|
+
import LazyBoundary from '../common/lazy-boundary'
|
|
2
3
|
|
|
3
4
|
const AIChat = lazy(() => import('./ai-chat'))
|
|
4
5
|
|
|
5
6
|
export default function AIChatEntry (props) {
|
|
7
|
+
const [activated, setActivated] = useState(false)
|
|
8
|
+
const active = props.rightPanelTab === 'ai'
|
|
9
|
+
if (active && !activated) setActivated(true)
|
|
10
|
+
// Keep drafts and attachments when switching to info after opening chat.
|
|
11
|
+
if (!active && !activated) return null
|
|
12
|
+
|
|
6
13
|
return (
|
|
7
|
-
<
|
|
8
|
-
<
|
|
9
|
-
|
|
14
|
+
<LazyBoundary>
|
|
15
|
+
<Suspense fallback={null}>
|
|
16
|
+
<AIChat {...props} />
|
|
17
|
+
</Suspense>
|
|
18
|
+
</LazyBoundary>
|
|
10
19
|
)
|
|
11
20
|
}
|
|
@@ -6,15 +6,18 @@ import { runAgentLoop } from './agent'
|
|
|
6
6
|
import { appendMandatoryGuardrails } from './ai-guardrails'
|
|
7
7
|
import {
|
|
8
8
|
Alert,
|
|
9
|
-
Tooltip
|
|
9
|
+
Tooltip,
|
|
10
|
+
Tag
|
|
10
11
|
} from 'antd'
|
|
11
12
|
import {
|
|
12
13
|
CopyOutlined,
|
|
13
14
|
CloseOutlined,
|
|
14
15
|
CaretDownOutlined,
|
|
15
|
-
CaretRightOutlined
|
|
16
|
+
CaretRightOutlined,
|
|
17
|
+
PaperClipOutlined
|
|
16
18
|
} from '@ant-design/icons'
|
|
17
19
|
import { copy } from '../../common/clipboard'
|
|
20
|
+
import { formatSize } from './ai-attachments'
|
|
18
21
|
|
|
19
22
|
export default function AIChatHistoryItem ({ item }) {
|
|
20
23
|
const [showOutput, setShowOutput] = useState(true)
|
|
@@ -81,7 +84,12 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
81
84
|
content: 'Understood. I will use this context as we continue.'
|
|
82
85
|
})
|
|
83
86
|
} else {
|
|
84
|
-
messages.push({
|
|
87
|
+
messages.push({
|
|
88
|
+
role: 'user',
|
|
89
|
+
// promptWithAttachments carries the inlined file blocks;
|
|
90
|
+
// falls back to the plain prompt for legacy entries
|
|
91
|
+
content: entry.promptWithAttachments || entry.prompt
|
|
92
|
+
})
|
|
85
93
|
if (entry.response && entry.id !== item.id) {
|
|
86
94
|
messages.push({ role: 'assistant', content: entry.response })
|
|
87
95
|
}
|
|
@@ -271,6 +279,21 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
271
279
|
)
|
|
272
280
|
}
|
|
273
281
|
|
|
282
|
+
function renderAttachments () {
|
|
283
|
+
if (!item.attachments || !item.attachments.length) {
|
|
284
|
+
return null
|
|
285
|
+
}
|
|
286
|
+
return (
|
|
287
|
+
<div className='ai-chat-item-attachments mg1b'>
|
|
288
|
+
{item.attachments.map(a => (
|
|
289
|
+
<Tag key={a.name + a.size} title={`${a.name} (${formatSize(a.size)})`}>
|
|
290
|
+
<PaperClipOutlined /> {a.name}
|
|
291
|
+
</Tag>
|
|
292
|
+
))}
|
|
293
|
+
</div>
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
274
297
|
function renderToolCalls () {
|
|
275
298
|
if (mode !== 'agent' || !toolCalls || !toolCalls.length) {
|
|
276
299
|
return null
|
|
@@ -278,7 +301,7 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
278
301
|
return (
|
|
279
302
|
<div className='agent-tool-calls'>
|
|
280
303
|
{toolCalls.map((tc) => (
|
|
281
|
-
<AgentToolCallCard key={tc.id} toolCall={tc} />
|
|
304
|
+
<AgentToolCallCard key={tc.id} toolCall={tc} autoCollapse={!isStreaming} />
|
|
282
305
|
))}
|
|
283
306
|
</div>
|
|
284
307
|
)
|
|
@@ -291,6 +314,7 @@ export default function AIChatHistoryItem ({ item }) {
|
|
|
291
314
|
<Alert {...alertProps} />
|
|
292
315
|
</Tooltip>
|
|
293
316
|
</div>
|
|
317
|
+
{renderAttachments()}
|
|
294
318
|
{renderToolCalls()}
|
|
295
319
|
{showOutput && <AIOutput item={item} />}
|
|
296
320
|
{renderStopButton()}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { useLayoutEffect, useRef } from 'react'
|
|
3
3
|
import { auto } from 'manate/react'
|
|
4
4
|
import AIChatHistoryItem from './ai-chat-history-item'
|
|
5
|
+
import AiChatEmpty from './ai-chat-empty'
|
|
5
6
|
|
|
6
7
|
export default auto(function AIChatHistory ({ history }) {
|
|
7
8
|
const historyRef = useRef(null)
|
|
@@ -12,7 +13,7 @@ export default auto(function AIChatHistory ({ history }) {
|
|
|
12
13
|
}
|
|
13
14
|
}, [history])
|
|
14
15
|
if (!history.length) {
|
|
15
|
-
return <
|
|
16
|
+
return <AiChatEmpty />
|
|
16
17
|
}
|
|
17
18
|
return (
|
|
18
19
|
<div ref={historyRef} className='ai-history-wrap'>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useState, useCallback, useEffect } from 'react'
|
|
2
|
-
import { Flex, Input, Segmented, Button } from 'antd'
|
|
1
|
+
import { useState, useCallback, useEffect, useRef } from 'react'
|
|
2
|
+
import { Flex, Input, Segmented, Button, Tag, message } from 'antd'
|
|
3
3
|
import TabSelect from '../footer/tab-select'
|
|
4
4
|
import AiChatHistory from './ai-chat-history'
|
|
5
5
|
import AiChatSessions from './ai-chat-sessions'
|
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
SendOutlined,
|
|
11
11
|
PlusOutlined,
|
|
12
12
|
HistoryOutlined,
|
|
13
|
-
CompressOutlined
|
|
13
|
+
CompressOutlined,
|
|
14
|
+
PaperClipOutlined
|
|
14
15
|
} from '@ant-design/icons'
|
|
15
16
|
import {
|
|
16
17
|
aiConfigWikiLink,
|
|
@@ -19,6 +20,12 @@ import {
|
|
|
19
20
|
import { getItem, setItem } from '../../common/safe-local-storage.js'
|
|
20
21
|
import HelpIcon from '../common/help-icon'
|
|
21
22
|
import { refsStatic } from '../common/ref'
|
|
23
|
+
import {
|
|
24
|
+
readTextAttachment,
|
|
25
|
+
validateAttachmentTotal,
|
|
26
|
+
buildAttachmentsBlock,
|
|
27
|
+
formatSize
|
|
28
|
+
} from './ai-attachments'
|
|
22
29
|
import './ai.styl'
|
|
23
30
|
|
|
24
31
|
const { TextArea } = Input
|
|
@@ -27,6 +34,9 @@ const MAX_HISTORY = 500
|
|
|
27
34
|
export default function AIChat (props) {
|
|
28
35
|
const [prompt, setPrompt] = useState('')
|
|
29
36
|
const [compressing, setCompressing] = useState(false)
|
|
37
|
+
const [attachments, setAttachments] = useState([])
|
|
38
|
+
const [dragOver, setDragOver] = useState(false)
|
|
39
|
+
const fileInputRef = useRef(null)
|
|
30
40
|
const [mode, setMode] = useState(() => getItem(aiChatModeLsKey) || 'ask')
|
|
31
41
|
const isAgent = mode === 'agent'
|
|
32
42
|
const submitDisabled = isAgent && props.agentRunning
|
|
@@ -53,15 +63,55 @@ export default function AIChat (props) {
|
|
|
53
63
|
setMode(m)
|
|
54
64
|
}
|
|
55
65
|
|
|
66
|
+
const e = window.translate
|
|
67
|
+
|
|
68
|
+
async function handleFiles (files) {
|
|
69
|
+
const notText = []
|
|
70
|
+
let totalTooLarge = false
|
|
71
|
+
const next = [...attachments]
|
|
72
|
+
for (const file of Array.from(files)) {
|
|
73
|
+
if (next.some(a => a.name === file.name && a.size === file.size)) {
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
if (!validateAttachmentTotal(next, file.size)) {
|
|
77
|
+
totalTooLarge = true
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
const { attachment, error } = await readTextAttachment(file)
|
|
81
|
+
if (error === 'notTextFile') {
|
|
82
|
+
notText.push(file.name)
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
if (attachment) {
|
|
86
|
+
next.push(attachment)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
setAttachments(next)
|
|
90
|
+
if (totalTooLarge) {
|
|
91
|
+
message.error('attachment too big')
|
|
92
|
+
}
|
|
93
|
+
if (notText.length) {
|
|
94
|
+
message.error(`attachment only support text files: ${notText.join(', ')}`)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
56
98
|
const handleSubmit = useCallback(function () {
|
|
57
99
|
if (window.store.aiConfigMissing()) {
|
|
58
100
|
window.store.toggleAIConfig()
|
|
59
101
|
}
|
|
60
|
-
if (!prompt.trim()) return
|
|
102
|
+
if (!prompt.trim() && !attachments.length) return
|
|
61
103
|
|
|
62
104
|
const chatId = uid()
|
|
105
|
+
const block = buildAttachmentsBlock(attachments)
|
|
106
|
+
const promptWithAttachments = block
|
|
107
|
+
? (prompt.trim() ? prompt + '\n\n' + block : block)
|
|
108
|
+
: prompt
|
|
63
109
|
const chatEntry = {
|
|
64
110
|
prompt,
|
|
111
|
+
promptWithAttachments,
|
|
112
|
+
attachments: attachments.map(({ name, size, truncated }) => ({
|
|
113
|
+
name, size, truncated
|
|
114
|
+
})),
|
|
65
115
|
response: '',
|
|
66
116
|
isStreaming: false,
|
|
67
117
|
pending: true,
|
|
@@ -86,11 +136,12 @@ export default function AIChat (props) {
|
|
|
86
136
|
|
|
87
137
|
window.store.aiChatHistory.push(chatEntry)
|
|
88
138
|
setPrompt('')
|
|
139
|
+
setAttachments([])
|
|
89
140
|
|
|
90
141
|
if (window.store.aiChatHistory.length > MAX_HISTORY) {
|
|
91
142
|
window.store.aiChatHistory.splice(MAX_HISTORY)
|
|
92
143
|
}
|
|
93
|
-
}, [prompt, mode, currentChatSessionId])
|
|
144
|
+
}, [prompt, attachments, mode, currentChatSessionId])
|
|
94
145
|
|
|
95
146
|
function renderHistory () {
|
|
96
147
|
if (props.showChatSessions) {
|
|
@@ -168,9 +219,6 @@ export default function AIChat (props) {
|
|
|
168
219
|
setPrompt,
|
|
169
220
|
handleSubmit
|
|
170
221
|
})
|
|
171
|
-
if (props.rightPanelTab === 'ai' && window.store.aiConfigMissing()) {
|
|
172
|
-
window.store.toggleAIConfig()
|
|
173
|
-
}
|
|
174
222
|
return () => {
|
|
175
223
|
refsStatic.remove('AIChat')
|
|
176
224
|
}
|
|
@@ -180,17 +228,32 @@ export default function AIChat (props) {
|
|
|
180
228
|
return null
|
|
181
229
|
}
|
|
182
230
|
|
|
183
|
-
const handleKeyPress = (
|
|
184
|
-
if (!
|
|
185
|
-
|
|
231
|
+
const handleKeyPress = (evt) => {
|
|
232
|
+
if (!evt.shiftKey) {
|
|
233
|
+
evt.preventDefault()
|
|
186
234
|
if (!submitDisabled) {
|
|
187
235
|
handleSubmit()
|
|
188
236
|
}
|
|
189
237
|
}
|
|
190
238
|
}
|
|
191
|
-
const e = window.translate
|
|
192
239
|
return (
|
|
193
|
-
<Flex
|
|
240
|
+
<Flex
|
|
241
|
+
vertical
|
|
242
|
+
className={'ai-chat-container' + (dragOver ? ' ai-attachment-dragover' : '')}
|
|
243
|
+
onDragOver={(evt) => {
|
|
244
|
+
evt.preventDefault()
|
|
245
|
+
setDragOver(true)
|
|
246
|
+
}}
|
|
247
|
+
onDragLeave={() => setDragOver(false)}
|
|
248
|
+
onDrop={(evt) => {
|
|
249
|
+
setDragOver(false)
|
|
250
|
+
if (!evt.dataTransfer.files.length) {
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
evt.preventDefault()
|
|
254
|
+
handleFiles(evt.dataTransfer.files)
|
|
255
|
+
}}
|
|
256
|
+
>
|
|
194
257
|
<Flex className='ai-chat-history' flex='auto'>
|
|
195
258
|
{renderHistory()}
|
|
196
259
|
</Flex>
|
|
@@ -225,6 +288,20 @@ export default function AIChat (props) {
|
|
|
225
288
|
{e('history')}
|
|
226
289
|
</Button>
|
|
227
290
|
</Flex>
|
|
291
|
+
{attachments.length > 0 && (
|
|
292
|
+
<Flex wrap='wrap' gap={4} className='ai-chat-attachments mg1b'>
|
|
293
|
+
{attachments.map(a => (
|
|
294
|
+
<Tag
|
|
295
|
+
key={a.id}
|
|
296
|
+
closable
|
|
297
|
+
onClose={() => setAttachments(arr => arr.filter(x => x.id !== a.id))}
|
|
298
|
+
title={`${a.name} (${formatSize(a.size)})`}
|
|
299
|
+
>
|
|
300
|
+
<PaperClipOutlined /> {a.name}
|
|
301
|
+
</Tag>
|
|
302
|
+
))}
|
|
303
|
+
</Flex>
|
|
304
|
+
)}
|
|
228
305
|
<TextArea
|
|
229
306
|
value={prompt}
|
|
230
307
|
onChange={handlePromptChange}
|
|
@@ -233,6 +310,16 @@ export default function AIChat (props) {
|
|
|
233
310
|
autoSize={{ minRows: 3, maxRows: 10 }}
|
|
234
311
|
className='ai-chat-textarea'
|
|
235
312
|
/>
|
|
313
|
+
<input
|
|
314
|
+
type='file'
|
|
315
|
+
multiple
|
|
316
|
+
ref={fileInputRef}
|
|
317
|
+
style={{ display: 'none' }}
|
|
318
|
+
onChange={(evt) => {
|
|
319
|
+
handleFiles(evt.target.files)
|
|
320
|
+
evt.target.value = ''
|
|
321
|
+
}}
|
|
322
|
+
/>
|
|
236
323
|
<Flex className='ai-chat-terminals' justify='space-between' align='center'>
|
|
237
324
|
<Flex align='center'>
|
|
238
325
|
<Segmented
|
|
@@ -242,6 +329,10 @@ export default function AIChat (props) {
|
|
|
242
329
|
size='small'
|
|
243
330
|
/>
|
|
244
331
|
{renderTabSelect()}
|
|
332
|
+
<PaperClipOutlined
|
|
333
|
+
onClick={() => fileInputRef.current?.click()}
|
|
334
|
+
className='mg1l pointer icon-hover toggle-ai-attach-icon'
|
|
335
|
+
/>
|
|
245
336
|
<SettingOutlined
|
|
246
337
|
onClick={toggleConfig}
|
|
247
338
|
className='mg1l pointer icon-hover toggle-ai-setting-icon'
|
|
@@ -10,133 +10,7 @@ export const aiConfigsArr = [
|
|
|
10
10
|
'proxyAI'
|
|
11
11
|
]
|
|
12
12
|
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
baseURLAI: 'https://api.atlascloud.ai/v1',
|
|
18
|
-
apiPathAI: '/chat/completions',
|
|
19
|
-
modelAI: 'deepseek-ai/deepseek-v4-flash',
|
|
20
|
-
authHeaderNameAI: 'Authorization: Bearer'
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
id: 'ApiSmart',
|
|
24
|
-
nameAI: 'ApiSmart',
|
|
25
|
-
baseURLAI: 'https://api.apismart.ai/v1',
|
|
26
|
-
apiPathAI: '/chat/completions',
|
|
27
|
-
modelAI: 'DEEPSEEK_V4_FLASH',
|
|
28
|
-
authHeaderNameAI: 'Authorization: Bearer'
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
id: 'apimart',
|
|
32
|
-
nameAI: 'ApiMart',
|
|
33
|
-
baseURLAI: 'https://api.apimart.ai/v1',
|
|
34
|
-
apiPathAI: '/chat/completions',
|
|
35
|
-
modelAI: 'deepseek-v4-flash',
|
|
36
|
-
authHeaderNameAI: 'Authorization: Bearer'
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
id: 'deepseek',
|
|
40
|
-
nameAI: 'DeepSeek',
|
|
41
|
-
baseURLAI: 'https://api.deepseek.com/v1',
|
|
42
|
-
apiPathAI: '/chat/completions',
|
|
43
|
-
modelAI: 'deepseek-chat',
|
|
44
|
-
authHeaderNameAI: 'Authorization: Bearer'
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
id: 'openai',
|
|
48
|
-
nameAI: 'OpenAI',
|
|
49
|
-
baseURLAI: 'https://api.openai.com/v1',
|
|
50
|
-
apiPathAI: '/chat/completions',
|
|
51
|
-
modelAI: 'gpt-4o',
|
|
52
|
-
authHeaderNameAI: 'Authorization: Bearer'
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
id: 'openrouter',
|
|
56
|
-
nameAI: 'OpenRouter',
|
|
57
|
-
baseURLAI: 'https://openrouter.ai/api/v1',
|
|
58
|
-
apiPathAI: '/chat/completions',
|
|
59
|
-
modelAI: 'openai/gpt-4o-mini',
|
|
60
|
-
authHeaderNameAI: 'Authorization: Bearer'
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
id: 'orcarouter',
|
|
64
|
-
nameAI: 'OrcaRouter',
|
|
65
|
-
baseURLAI: 'https://api.orcarouter.ai/v1',
|
|
66
|
-
apiPathAI: '/chat/completions',
|
|
67
|
-
modelAI: 'deepseek/deepseek-v4-flash',
|
|
68
|
-
authHeaderNameAI: 'Authorization: Bearer'
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
id: 'google',
|
|
72
|
-
nameAI: 'Google Gemini',
|
|
73
|
-
baseURLAI: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
|
74
|
-
apiPathAI: '/chat/completions',
|
|
75
|
-
modelAI: 'gemini-2.0-flash',
|
|
76
|
-
authHeaderNameAI: 'Authorization: Bearer'
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
id: 'groq',
|
|
80
|
-
nameAI: 'Groq',
|
|
81
|
-
baseURLAI: 'https://api.groq.com/openai/v1',
|
|
82
|
-
apiPathAI: '/chat/completions',
|
|
83
|
-
modelAI: 'llama-3.3-70b-versatile',
|
|
84
|
-
authHeaderNameAI: 'Authorization: Bearer'
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
id: 'together',
|
|
88
|
-
nameAI: 'Together AI',
|
|
89
|
-
baseURLAI: 'https://api.together.xyz/v1',
|
|
90
|
-
apiPathAI: '/chat/completions',
|
|
91
|
-
modelAI: 'meta-llama/Llama-3.3-70B-Instruct-Turbo',
|
|
92
|
-
authHeaderNameAI: 'Authorization: Bearer'
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
id: 'mistral',
|
|
96
|
-
nameAI: 'Mistral AI',
|
|
97
|
-
baseURLAI: 'https://api.mistral.ai/v1',
|
|
98
|
-
apiPathAI: '/chat/completions',
|
|
99
|
-
modelAI: 'mistral-large-latest',
|
|
100
|
-
authHeaderNameAI: 'Authorization: Bearer'
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
id: 'xai',
|
|
104
|
-
nameAI: 'xAI (Grok)',
|
|
105
|
-
baseURLAI: 'https://api.x.ai/v1',
|
|
106
|
-
apiPathAI: '/chat/completions',
|
|
107
|
-
modelAI: 'grok-3-mini',
|
|
108
|
-
authHeaderNameAI: 'Authorization: Bearer'
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
id: 'perplexity',
|
|
112
|
-
nameAI: 'Perplexity',
|
|
113
|
-
baseURLAI: 'https://api.perplexity.ai',
|
|
114
|
-
apiPathAI: '/chat/completions',
|
|
115
|
-
modelAI: 'sonar',
|
|
116
|
-
authHeaderNameAI: 'Authorization: Bearer'
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
id: 'moonshot',
|
|
120
|
-
nameAI: 'Moonshot (Kimi)',
|
|
121
|
-
baseURLAI: 'https://api.moonshot.cn/v1',
|
|
122
|
-
apiPathAI: '/chat/completions',
|
|
123
|
-
modelAI: 'moonshot-v1-8k',
|
|
124
|
-
authHeaderNameAI: 'Authorization: Bearer'
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
id: 'siliconflow',
|
|
128
|
-
nameAI: 'SiliconFlow',
|
|
129
|
-
baseURLAI: 'https://api.siliconflow.cn/v1',
|
|
130
|
-
apiPathAI: '/chat/completions',
|
|
131
|
-
modelAI: 'deepseek-ai/DeepSeek-V3',
|
|
132
|
-
authHeaderNameAI: 'Authorization: Bearer'
|
|
133
|
-
}
|
|
134
|
-
]
|
|
135
|
-
|
|
136
|
-
export function getAIPresets () {
|
|
137
|
-
const presets = [...defaultAIPresets]
|
|
138
|
-
if (window.et?.defaultAIPreset) {
|
|
139
|
-
presets.unshift(window.et.defaultAIPreset)
|
|
140
|
-
}
|
|
141
|
-
return presets
|
|
142
|
-
}
|
|
13
|
+
export {
|
|
14
|
+
defaultAIPresets,
|
|
15
|
+
getAIPresets
|
|
16
|
+
} from './ai-presets'
|
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
Space,
|
|
8
8
|
Dropdown
|
|
9
9
|
} from 'antd'
|
|
10
|
-
import { useEffect, useState } from 'react'
|
|
11
|
-
import { DownOutlined } from '@ant-design/icons'
|
|
10
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
11
|
+
import { DownOutlined, ReloadOutlined } from '@ant-design/icons'
|
|
12
12
|
import Link from '../common/external-link'
|
|
13
13
|
import AiCache from './ai-cache'
|
|
14
14
|
import {
|
|
@@ -17,11 +17,50 @@ import {
|
|
|
17
17
|
import Password from '../common/password'
|
|
18
18
|
import AiHistory, { addHistoryItem } from './ai-history'
|
|
19
19
|
import message from '../common/message'
|
|
20
|
-
import { getAIPresets } from './ai-
|
|
20
|
+
import { getAIPresets } from './ai-presets'
|
|
21
21
|
import { appendMandatoryGuardrails } from './ai-guardrails'
|
|
22
22
|
|
|
23
23
|
const STORAGE_KEY_CONFIG = 'ai_config_history'
|
|
24
24
|
const EVENT_NAME_CONFIG = 'ai-config-history-update'
|
|
25
|
+
const STORAGE_KEY_MODELS = 'ai_models_cache'
|
|
26
|
+
|
|
27
|
+
// Model lists fetched from `<baseURL>/models`, cached per API URL
|
|
28
|
+
function normalizeBaseURL (url) {
|
|
29
|
+
return (url || '').trim().replace(/\/+$/, '')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readModelsCache () {
|
|
33
|
+
try {
|
|
34
|
+
const raw = window.localStorage.getItem(STORAGE_KEY_MODELS)
|
|
35
|
+
const parsed = raw ? JSON.parse(raw) : null
|
|
36
|
+
return parsed && typeof parsed === 'object' ? parsed : {}
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return {}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getCachedModels (baseURL) {
|
|
43
|
+
const key = normalizeBaseURL(baseURL)
|
|
44
|
+
if (!key) {
|
|
45
|
+
return []
|
|
46
|
+
}
|
|
47
|
+
const models = readModelsCache()[key]
|
|
48
|
+
return Array.isArray(models) ? models : []
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function saveModelsToCache (baseURL, models) {
|
|
52
|
+
const key = normalizeBaseURL(baseURL)
|
|
53
|
+
if (!key) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const cache = readModelsCache()
|
|
58
|
+
cache[key] = models
|
|
59
|
+
window.localStorage.setItem(STORAGE_KEY_MODELS, JSON.stringify(cache))
|
|
60
|
+
} catch (e) {
|
|
61
|
+
// storage full or disabled, keep it in memory only
|
|
62
|
+
}
|
|
63
|
+
}
|
|
25
64
|
|
|
26
65
|
const e = window.translate
|
|
27
66
|
const defaultRoles = [
|
|
@@ -39,6 +78,15 @@ const proxyOptions = [
|
|
|
39
78
|
{ value: 'https://proxy.example.com:3128' }
|
|
40
79
|
]
|
|
41
80
|
|
|
81
|
+
// The wire protocol is detected from the path:
|
|
82
|
+
// /chat/completions -> OpenAI compatible, /responses -> OpenAI Responses API,
|
|
83
|
+
// /messages -> Anthropic Claude Messages API
|
|
84
|
+
const apiPathOptions = [
|
|
85
|
+
{ value: '/chat/completions', label: '/chat/completions (OpenAI)' },
|
|
86
|
+
{ value: '/responses', label: '/responses (OpenAI Responses)' },
|
|
87
|
+
{ value: '/messages', label: '/messages (Anthropic)' }
|
|
88
|
+
]
|
|
89
|
+
|
|
42
90
|
const authHeaderOptions = [
|
|
43
91
|
{ value: 'Authorization: Bearer' },
|
|
44
92
|
{ value: 'x-api-key' },
|
|
@@ -50,7 +98,11 @@ const authHeaderOptions = [
|
|
|
50
98
|
export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig }) {
|
|
51
99
|
const [form] = Form.useForm()
|
|
52
100
|
const [testing, setTesting] = useState(false)
|
|
101
|
+
const [loadingModels, setLoadingModels] = useState(false)
|
|
102
|
+
const [cachedModels, setCachedModels] = useState([])
|
|
53
103
|
const baseURLAI = Form.useWatch('baseURLAI', form)
|
|
104
|
+
const presets = useMemo(() => getAIPresets(), [])
|
|
105
|
+
const currentPreset = presets.find(p => p.baseURLAI === baseURLAI)
|
|
54
106
|
|
|
55
107
|
useEffect(() => {
|
|
56
108
|
if (initialValues) {
|
|
@@ -58,6 +110,11 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
58
110
|
}
|
|
59
111
|
}, [initialValues])
|
|
60
112
|
|
|
113
|
+
// Restore the model list previously fetched for this API URL
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
setCachedModels(getCachedModels(baseURLAI))
|
|
116
|
+
}, [baseURLAI])
|
|
117
|
+
|
|
61
118
|
function filter () {
|
|
62
119
|
return true
|
|
63
120
|
}
|
|
@@ -105,8 +162,45 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
105
162
|
}
|
|
106
163
|
}
|
|
107
164
|
|
|
165
|
+
async function handleLoadModels () {
|
|
166
|
+
const values = form.getFieldsValue()
|
|
167
|
+
const baseURL = normalizeBaseURL(values.baseURLAI)
|
|
168
|
+
if (!baseURL) {
|
|
169
|
+
message.error('Please input API URL first')
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
setLoadingModels(true)
|
|
173
|
+
try {
|
|
174
|
+
const res = await window.pre.runGlobalAsync(
|
|
175
|
+
'AIlistModels',
|
|
176
|
+
baseURL,
|
|
177
|
+
values.apiKeyAI,
|
|
178
|
+
values.authHeaderNameAI,
|
|
179
|
+
values.proxyAI
|
|
180
|
+
)
|
|
181
|
+
if (res && res.error) {
|
|
182
|
+
message.error(res.error)
|
|
183
|
+
} else if (res && res.models && res.models.length) {
|
|
184
|
+
setCachedModels(res.models)
|
|
185
|
+
saveModelsToCache(baseURL, res.models)
|
|
186
|
+
message.success(`Got ${res.models.length} models`)
|
|
187
|
+
if (!values.modelAI) {
|
|
188
|
+
form.setFieldsValue({ modelAI: res.models[0] })
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
message.error('No models found in response')
|
|
192
|
+
}
|
|
193
|
+
} catch (e) {
|
|
194
|
+
if (e.message) {
|
|
195
|
+
message.error(e.message)
|
|
196
|
+
}
|
|
197
|
+
} finally {
|
|
198
|
+
setLoadingModels(false)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
108
202
|
function handleSelectPreset (preset) {
|
|
109
|
-
const fields = ['nameAI', 'baseURLAI', 'apiPathAI', 'modelAI', 'authHeaderNameAI', '
|
|
203
|
+
const fields = ['nameAI', 'baseURLAI', 'apiPathAI', 'modelAI', 'authHeaderNameAI', 'apiKeyAI']
|
|
110
204
|
const values = {}
|
|
111
205
|
fields.forEach(f => {
|
|
112
206
|
if (preset[f] !== undefined) {
|
|
@@ -117,7 +211,6 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
117
211
|
}
|
|
118
212
|
|
|
119
213
|
function renderPresetMenu () {
|
|
120
|
-
const presets = getAIPresets()
|
|
121
214
|
const items = presets.map(p => ({
|
|
122
215
|
key: p.id,
|
|
123
216
|
label: p.nameAI,
|
|
@@ -126,7 +219,7 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
126
219
|
return (
|
|
127
220
|
<Dropdown menu={{ items }} trigger={['click']}>
|
|
128
221
|
<Button>
|
|
129
|
-
{e('presets')
|
|
222
|
+
{e('presets')} <DownOutlined />
|
|
130
223
|
</Button>
|
|
131
224
|
</Dropdown>
|
|
132
225
|
)
|
|
@@ -145,15 +238,41 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
145
238
|
}
|
|
146
239
|
|
|
147
240
|
function renderApiKeyLabel () {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return <span className='bold'>API Key (<Link to='https://ai.electerm.org?utm=electerm'>get API key from ai.electerm.org(free)</Link>)</span>
|
|
241
|
+
const siteUrl = currentPreset?.siteUrl
|
|
242
|
+
if (siteUrl) {
|
|
243
|
+
const name = currentPreset?.nameAI || baseURLAI
|
|
244
|
+
return <span className='bold'>API Key (<Link to={siteUrl}>get API key from {name}</Link>)</span>
|
|
153
245
|
}
|
|
154
246
|
return 'API Key'
|
|
155
247
|
}
|
|
156
248
|
|
|
249
|
+
function renderModelInput () {
|
|
250
|
+
const presetModels = (currentPreset?.modelAIs || [])
|
|
251
|
+
.map(o => (typeof o === 'string' ? o : o.value))
|
|
252
|
+
const values = [...presetModels, ...cachedModels].filter(Boolean)
|
|
253
|
+
const options = [...new Set(values)].map(value => ({ value }))
|
|
254
|
+
const title = 'Fetch model list from API URL'
|
|
255
|
+
return (
|
|
256
|
+
<AutoComplete
|
|
257
|
+
options={options}
|
|
258
|
+
filterOption={filter}
|
|
259
|
+
>
|
|
260
|
+
<Input
|
|
261
|
+
placeholder='Enter or select AI model'
|
|
262
|
+
suffix={
|
|
263
|
+
<ReloadOutlined
|
|
264
|
+
spin={loadingModels}
|
|
265
|
+
className='pointer ai-model-reload'
|
|
266
|
+
title={title}
|
|
267
|
+
onMouseDown={e => e.preventDefault()}
|
|
268
|
+
onClick={handleLoadModels}
|
|
269
|
+
/>
|
|
270
|
+
}
|
|
271
|
+
/>
|
|
272
|
+
</AutoComplete>
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
157
276
|
if (!showAIConfig) {
|
|
158
277
|
return null
|
|
159
278
|
}
|
|
@@ -219,8 +338,11 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
219
338
|
]}
|
|
220
339
|
noStyle
|
|
221
340
|
>
|
|
222
|
-
<
|
|
341
|
+
<AutoComplete
|
|
342
|
+
options={apiPathOptions}
|
|
343
|
+
filterOption={filter}
|
|
223
344
|
placeholder='/chat/completions'
|
|
345
|
+
popupMatchSelectWidth={false}
|
|
224
346
|
style={{ width: '25%' }}
|
|
225
347
|
/>
|
|
226
348
|
</Form.Item>
|
|
@@ -231,9 +353,7 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
|
|
|
231
353
|
name='modelAI'
|
|
232
354
|
rules={[{ required: true, message: 'Please input or select a model!' }]}
|
|
233
355
|
>
|
|
234
|
-
|
|
235
|
-
placeholder='Enter or select AI model'
|
|
236
|
-
/>
|
|
356
|
+
{renderModelInput()}
|
|
237
357
|
</Form.Item>
|
|
238
358
|
|
|
239
359
|
<Form.Item
|