@electerm/electerm-react 1.51.20 → 1.60.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.
Files changed (30) hide show
  1. package/client/common/constants.js +1 -0
  2. package/client/common/default-setting.js +12 -1
  3. package/client/components/ai/ai-chat-history-item.jsx +69 -0
  4. package/client/components/ai/ai-chat-history.jsx +31 -0
  5. package/client/components/ai/ai-chat.jsx +164 -0
  6. package/client/components/ai/ai-config.jsx +133 -0
  7. package/client/components/ai/ai-output.jsx +123 -0
  8. package/client/components/ai/ai.styl +69 -0
  9. package/client/components/ai/providers.js +14 -0
  10. package/client/components/footer/batch-input.jsx +13 -67
  11. package/client/components/footer/footer-entry.jsx +19 -3
  12. package/client/components/footer/footer.styl +4 -0
  13. package/client/components/footer/tab-select.jsx +9 -3
  14. package/client/components/layout/layout.jsx +5 -4
  15. package/client/components/main/main.jsx +14 -2
  16. package/client/components/shortcuts/shortcut-control.jsx +17 -2
  17. package/client/components/shortcuts/shortcut-handler.js +24 -8
  18. package/client/components/shortcuts/shortcuts-defaults.js +6 -0
  19. package/client/components/sidebar/app-running-time.jsx +35 -0
  20. package/client/components/sidebar/info-modal.jsx +2 -0
  21. package/client/components/tabs/app-drag.jsx +1 -1
  22. package/client/components/tabs/index.jsx +8 -17
  23. package/client/components/tree-list/bookmark-toolbar.jsx +14 -15
  24. package/client/store/common.js +37 -2
  25. package/client/store/index.js +2 -290
  26. package/client/store/init-state.js +7 -1
  27. package/client/store/store.js +298 -0
  28. package/client/store/tab.js +54 -1
  29. package/client/store/watch.js +9 -2
  30. package/package.json +1 -1
@@ -412,3 +412,4 @@ export const terminalTypes = [
412
412
  export const sshConfigLoadKey = 'ssh-config-loaded'
413
413
  export const sshConfigKey = 'ignore-ssh-config'
414
414
  export const connectionHoppingWarnKey = 'connectionHoppingWarnned'
415
+ export const aiChatHistoryKey = 'ai-chat-history'
@@ -58,5 +58,16 @@ export default {
58
58
  'modifyTime'
59
59
  ],
60
60
  hideIP: false,
61
- dataSyncSelected: 'all'
61
+ dataSyncSelected: 'all',
62
+ baseURLAI: 'https://api.deepseek.com',
63
+ modelAI: 'deepseek-chat',
64
+ roleAI: `You are a terminal command expert.
65
+ - Provide clear, safe, and efficient shell commands
66
+ - Always explain what each command does
67
+ - Warn about potentially dangerous operations
68
+ - Format command output with markdown code blocks
69
+ - If multiple steps are needed, number them
70
+ - Mention any prerequisites or dependencies
71
+ - Include common flags and options
72
+ - Specify which OS (Linux/Mac/Windows) the command is for`
62
73
  }
@@ -0,0 +1,69 @@
1
+ // ai-chat-history-item.jsx
2
+ import AIOutput from './ai-output'
3
+ import {
4
+ Alert,
5
+ Tooltip
6
+ } from 'antd'
7
+ import {
8
+ UserOutlined,
9
+ CopyOutlined,
10
+ CloseOutlined
11
+ } from '@ant-design/icons'
12
+ import { copy } from '../../common/clipboard'
13
+
14
+ export default function AIChatHistoryItem ({ item }) {
15
+ const {
16
+ prompt
17
+ } = item
18
+ const alertProps = {
19
+ message: (
20
+ <div><UserOutlined />: {prompt}</div>
21
+ ),
22
+ type: 'info'
23
+ }
24
+ function handleDel (e) {
25
+ e.stopPropagation()
26
+ window.store.removeAiHistory(item.id)
27
+ }
28
+ function handleCopy () {
29
+ copy(prompt)
30
+ }
31
+ function renderTitle () {
32
+ return (
33
+ <div>
34
+ <p>
35
+ <b>Model:</b> {item.modelAI}
36
+ </p>
37
+ <p>
38
+ <b>Role:</b> {item.roleAI}
39
+ </p>
40
+ <p>
41
+ <b>Base URL:</b> {item.baseURLAI}
42
+ </p>
43
+ <p>
44
+ <b>Time:</b> {new Date(item.timestamp).toLocaleString()}
45
+ </p>
46
+ <p>
47
+ <CopyOutlined
48
+ className='pointer'
49
+ onClick={handleCopy}
50
+ />
51
+ <CloseOutlined
52
+ className='pointer mg1l'
53
+ onClick={handleDel}
54
+ />
55
+ </p>
56
+ </div>
57
+ )
58
+ }
59
+ return (
60
+ <div className='chat-history-item'>
61
+ <div className='mg1y'>
62
+ <Tooltip title={renderTitle()}>
63
+ <Alert {...alertProps} />
64
+ </Tooltip>
65
+ </div>
66
+ <AIOutput item={item} />
67
+ </div>
68
+ )
69
+ }
@@ -0,0 +1,31 @@
1
+ // ai-chat-history.jsx
2
+ import { useLayoutEffect, useRef } from 'react'
3
+ import { auto } from 'manate/react'
4
+ import AIChatHistoryItem from './ai-chat-history-item'
5
+
6
+ export default auto(function AIChatHistory ({ history }) {
7
+ const historyRef = useRef(null)
8
+
9
+ useLayoutEffect(() => {
10
+ if (historyRef.current) {
11
+ historyRef.current.scrollTop = historyRef.current.scrollHeight
12
+ }
13
+ }, [history.length])
14
+ if (!history.length) {
15
+ return <div />
16
+ }
17
+ return (
18
+ <div ref={historyRef} className='ai-history-wrap'>
19
+ {
20
+ history.map((item) => {
21
+ return (
22
+ <AIChatHistoryItem
23
+ key={item.id}
24
+ item={item}
25
+ />
26
+ )
27
+ })
28
+ }
29
+ </div>
30
+ )
31
+ })
@@ -0,0 +1,164 @@
1
+ import { useState, useCallback, useEffect } from 'react'
2
+ import { Flex, Input, message } from 'antd'
3
+ import AIConfigForm from './ai-config'
4
+ import TabSelect from '../footer/tab-select'
5
+ import AiChatHistory from './ai-chat-history'
6
+ import uid from '../../common/uid'
7
+ import { pick } from 'lodash-es'
8
+ import {
9
+ SettingOutlined,
10
+ LoadingOutlined,
11
+ SendOutlined,
12
+ UnorderedListOutlined
13
+ } from '@ant-design/icons'
14
+ import './ai.styl'
15
+
16
+ const { TextArea } = Input
17
+ const MAX_HISTORY = 100
18
+ const aiConfigsArr = [
19
+ 'baseURLAI',
20
+ 'modelAI',
21
+ 'roleAI',
22
+ 'apiKeyAI'
23
+ ]
24
+
25
+ export default function AIChat (props) {
26
+ const [prompt, setPrompt] = useState('')
27
+ const [isLoading, setIsLoading] = useState(false)
28
+
29
+ function handlePromptChange (e) {
30
+ setPrompt(e.target.value)
31
+ }
32
+
33
+ async function handleSubmit () {
34
+ if (aiConfigMissing()) {
35
+ window.store.toggleAIConfig()
36
+ }
37
+ if (!prompt.trim() || isLoading) return
38
+ setIsLoading(true)
39
+ const aiResponse = await window.pre.runGlobalAsync(
40
+ 'AIchat',
41
+ prompt,
42
+ props.config.modelAI,
43
+ props.config.roleAI,
44
+ props.config.baseURLAI,
45
+ props.config.apiKeyAI
46
+ ).catch(
47
+ window.store.onError
48
+ )
49
+ if (aiResponse && aiResponse.error) {
50
+ return window.store.onError(
51
+ new Error(aiResponse.error)
52
+ )
53
+ }
54
+ window.store.aiChatHistory.push({
55
+ prompt,
56
+ response: aiResponse.response,
57
+ ...pick(props.config, [
58
+ 'modelAI',
59
+ 'roleAI',
60
+ 'baseURLAI'
61
+ ]),
62
+ timestamp: Date.now(),
63
+ id: uid()
64
+ })
65
+
66
+ if (window.store.aiChatHistory.length > MAX_HISTORY) {
67
+ window.store.aiChatHistory.splice(MAX_HISTORY)
68
+ }
69
+ setPrompt('')
70
+ setIsLoading(false)
71
+ }
72
+
73
+ function handleConfigSubmit (values) {
74
+ window.store.updateConfig(values)
75
+ message.success('Saved')
76
+ }
77
+
78
+ const renderConfig = useCallback(() => {
79
+ if (!props.showAIConfig) return null
80
+ const aiConfigs = pick(props.config, aiConfigsArr)
81
+ return (
82
+ <AIConfigForm
83
+ initialValues={aiConfigs}
84
+ onSubmit={handleConfigSubmit}
85
+ showAIConfig={props.showAIConfig}
86
+ />
87
+ )
88
+ }, [props.showAIConfig, props.config])
89
+
90
+ function renderHistory () {
91
+ return (
92
+ <AiChatHistory
93
+ history={props.aiChatHistory}
94
+ />
95
+ )
96
+ }
97
+
98
+ function toggleConfig () {
99
+ window.store.toggleAIConfig()
100
+ }
101
+
102
+ function clearHistory () {
103
+ window.store.aiChatHistory = []
104
+ }
105
+
106
+ function aiConfigMissing () {
107
+ return aiConfigsArr.some(k => !props.config[k])
108
+ }
109
+
110
+ function renderSendIcon () {
111
+ if (isLoading) {
112
+ return <LoadingOutlined />
113
+ }
114
+ return (
115
+ <SendOutlined
116
+ onClick={handleSubmit}
117
+ className='mg1l pointer icon-hover'
118
+ />
119
+ )
120
+ }
121
+
122
+ useEffect(() => {
123
+ if (aiConfigMissing()) {
124
+ window.store.toggleAIConfig()
125
+ }
126
+ }, [])
127
+
128
+ return (
129
+ <Flex vertical className='ai-chat-container'>
130
+ <Flex className='ai-chat-history' flex='auto'>
131
+ {renderHistory()}
132
+ </Flex>
133
+
134
+ <Flex className='ai-chat-input'>
135
+ <TextArea
136
+ value={prompt}
137
+ onChange={handlePromptChange}
138
+ placeholder='Enter your prompt here'
139
+ autoSize={{ minRows: 3, maxRows: 10 }}
140
+ disabled={isLoading}
141
+ />
142
+ <Flex className='ai-chat-terminals' justify='space-between' align='center'>
143
+ <Flex align='center'>
144
+ <TabSelect
145
+ selectedTabIds={props.selectedTabIds}
146
+ tabs={props.tabs}
147
+ activeTabId={props.activeTabId}
148
+ />
149
+ <SettingOutlined
150
+ onClick={toggleConfig}
151
+ className='mg1l pointer icon-hover toggle-ai-setting-icon'
152
+ />
153
+ <UnorderedListOutlined
154
+ onClick={clearHistory}
155
+ className='mg2l pointer clear-ai-icon icon-hover'
156
+ />
157
+ {renderConfig()}
158
+ </Flex>
159
+ {renderSendIcon()}
160
+ </Flex>
161
+ </Flex>
162
+ </Flex>
163
+ )
164
+ }
@@ -0,0 +1,133 @@
1
+ import {
2
+ Form,
3
+ Input,
4
+ Button,
5
+ AutoComplete,
6
+ Modal
7
+ } from 'antd'
8
+ import { useEffect, useState } from 'react'
9
+
10
+ // Comprehensive API provider configurations
11
+ import providers from './providers'
12
+
13
+ const e = window.translate
14
+
15
+ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig }) {
16
+ const [form] = Form.useForm()
17
+ const [modelOptions, setModelOptions] = useState([])
18
+
19
+ useEffect(() => {
20
+ if (initialValues) {
21
+ form.setFieldsValue(initialValues)
22
+ }
23
+ }, [initialValues])
24
+
25
+ function filter () {
26
+ return true
27
+ }
28
+
29
+ const getBaseURLOptions = () => {
30
+ return providers.map(provider => ({
31
+ value: provider.baseURL,
32
+ label: provider.label
33
+ }))
34
+ }
35
+
36
+ const getModelOptions = (baseURL) => {
37
+ const provider = providers.find(p => p.baseURL === baseURL)
38
+ if (!provider) return []
39
+
40
+ return provider.models.map(model => ({
41
+ value: model,
42
+ label: model
43
+ }))
44
+ }
45
+
46
+ const handleSubmit = async (values) => {
47
+ onSubmit(values)
48
+ }
49
+
50
+ function handleCancel () {
51
+ window.store.toggleAIConfig()
52
+ }
53
+
54
+ function handleChange (v) {
55
+ const options = getModelOptions(v)
56
+ setModelOptions(options)
57
+ form.setFieldsValue({
58
+ modelAI: options[0]?.value || ''
59
+ })
60
+ }
61
+
62
+ if (!showAIConfig) {
63
+ return null
64
+ }
65
+ const title = 'AI ' + e('setting')
66
+ return (
67
+ <Modal
68
+ title={title}
69
+ open
70
+ onCancel={handleCancel}
71
+ footer={null}
72
+ >
73
+ <Form
74
+ form={form}
75
+ onFinish={handleSubmit}
76
+ initialValues={initialValues}
77
+ >
78
+ <Form.Item
79
+ label='API URL'
80
+ name='baseURLAI'
81
+ rules={[
82
+ { required: true, message: 'Please input or select API provider URL!' },
83
+ { type: 'url', message: 'Please enter a valid URL!' }
84
+ ]}
85
+ >
86
+ <AutoComplete
87
+ options={getBaseURLOptions()}
88
+ placeholder='Enter or select API provider URL'
89
+ filterOption={filter}
90
+ onChange={handleChange}
91
+ allowClear
92
+ />
93
+ </Form.Item>
94
+
95
+ <Form.Item
96
+ label='Model'
97
+ name='modelAI'
98
+ rules={[{ required: true, message: 'Please input or select a model!' }]}
99
+ >
100
+ <AutoComplete
101
+ options={modelOptions}
102
+ placeholder='Enter or select AI model'
103
+ filterOption={filter}
104
+ />
105
+ </Form.Item>
106
+
107
+ <Form.Item
108
+ label='API Key'
109
+ name='apiKeyAI'
110
+ >
111
+ <Input.Password placeholder='Enter your API key' />
112
+ </Form.Item>
113
+
114
+ <Form.Item
115
+ label='System Role'
116
+ name='roleAI'
117
+ rules={[{ required: true, message: 'Please input the AI role!' }]}
118
+ >
119
+ <Input.TextArea
120
+ placeholder='Enter AI role/system prompt'
121
+ rows={4}
122
+ />
123
+ </Form.Item>
124
+
125
+ <Form.Item>
126
+ <Button type='primary' htmlType='submit'>
127
+ {e('save')}
128
+ </Button>
129
+ </Form.Item>
130
+ </Form>
131
+ </Modal>
132
+ )
133
+ }
@@ -0,0 +1,123 @@
1
+ import React from 'react'
2
+ import ReactMarkdown from 'react-markdown'
3
+ import { copy } from '../../common/clipboard'
4
+ import Link from '../common/external-link'
5
+ import { Tag } from 'antd'
6
+ import { CopyOutlined, PlayCircleOutlined } from '@ant-design/icons'
7
+ import providers from './providers'
8
+
9
+ function getBrand (baseURLAI) {
10
+ // First, try to match with providers
11
+ const provider = providers.find(p => p.baseURL === baseURLAI)
12
+ if (provider) {
13
+ return {
14
+ brand: provider.label,
15
+ brandUrl: provider.homepage
16
+ }
17
+ }
18
+
19
+ // If no match, extract brand from URL
20
+ try {
21
+ const url = new URL(baseURLAI)
22
+ const hostname = url.hostname
23
+ const parts = hostname.split('.')
24
+ let brand = parts[parts.length - 2] // Usually the brand name is the second-to-last part
25
+
26
+ // Capitalize the first letter
27
+ brand = brand.charAt(0).toUpperCase() + brand.slice(1)
28
+
29
+ return {
30
+ brand,
31
+ brandUrl: `https://${parts[parts.length - 2]}.${parts[parts.length - 1]}`
32
+ }
33
+ } catch (error) {
34
+ // If URL parsing fails, return null
35
+ return {
36
+ brand: null,
37
+ brandUrl: null
38
+ }
39
+ }
40
+ }
41
+
42
+ const e = window.translate
43
+
44
+ export default function AIOutput ({ item }) {
45
+ const {
46
+ response,
47
+ baseURLAI
48
+ } = item
49
+ if (!response) {
50
+ return null
51
+ }
52
+
53
+ const { brand, brandUrl } = getBrand(baseURLAI)
54
+
55
+ const renderCode = (props) => {
56
+ const { node, className = '', children, ...rest } = props
57
+ const code = String(children).replace(/\n$/, '')
58
+ const inline = !className.includes('language-')
59
+ if (inline) {
60
+ return (
61
+ <code className={className} {...props}>
62
+ {children}
63
+ </code>
64
+ )
65
+ }
66
+
67
+ const copyToClipboard = () => {
68
+ copy(code)
69
+ }
70
+
71
+ const runInTerminal = () => {
72
+ window.store.runCommandInTerminal(code)
73
+ }
74
+
75
+ return (
76
+ <div className='code-block'>
77
+ <pre>
78
+ <code className={className} {...rest}>
79
+ {children}
80
+ </code>
81
+ </pre>
82
+ <div className='code-block-actions'>
83
+ <CopyOutlined
84
+ className='code-action-icon pointer'
85
+ onClick={copyToClipboard}
86
+ title={e('copy')}
87
+ />
88
+ <PlayCircleOutlined
89
+ className='code-action-icon pointer mg1l'
90
+ onClick={runInTerminal}
91
+ />
92
+ </div>
93
+ </div>
94
+ )
95
+ }
96
+
97
+ function renderBrand () {
98
+ if (!brand) {
99
+ return null
100
+ }
101
+ return (
102
+ <div className='pd1y'>
103
+ <Link to={brandUrl}>
104
+ <Tag>{brand}</Tag>
105
+ </Link>
106
+ </div>
107
+ )
108
+ }
109
+
110
+ const mdProps = {
111
+ children: response,
112
+ components: {
113
+ code: renderCode
114
+ }
115
+ }
116
+
117
+ return (
118
+ <div className='pd1'>
119
+ {renderBrand()}
120
+ <ReactMarkdown {...mdProps} />
121
+ </div>
122
+ )
123
+ }
@@ -0,0 +1,69 @@
1
+ @require '../../css/includes/theme-default'
2
+ .ai-chat-container
3
+ height 100%
4
+ display flex
5
+ flex-direction column
6
+ color text
7
+
8
+ .ai-chat-history
9
+ flex-grow 1
10
+ &::-webkit-scrollbar
11
+ width 0
12
+ .ai-history-wrap
13
+ width 100%
14
+ overflow-y auto
15
+ overflow-x hidden
16
+
17
+ .ai-config-form
18
+ height 200px
19
+ overflow-y auto
20
+
21
+ .chat-history-item
22
+ .code-block
23
+ border 1px dashed text
24
+ padding 5px
25
+ border-radius 3px
26
+ pre
27
+ margin-bottom 0
28
+ .code-block-actions
29
+ display none
30
+ &:hover
31
+ .code-block-actions
32
+ display block
33
+
34
+ .ai-chat-input
35
+ position relative
36
+ margin-top 10px
37
+
38
+ .ant-input
39
+ padding-bottom 40px
40
+
41
+ .ai-chat-terminals
42
+ position absolute
43
+ bottom 5px
44
+ left 5px
45
+ right 5px
46
+ background transparent
47
+ z-index 1
48
+ font-size 16px
49
+
50
+ .code-block
51
+ width 100%
52
+ pre
53
+ white-space pre-wrap
54
+ word-wrap break-word
55
+ overflow-x auto
56
+ max-width 100%
57
+ code
58
+ white-space pre-wrap
59
+ word-break break-all
60
+
61
+ .clear-ai-icon
62
+ position relative
63
+ &::after
64
+ content '×'
65
+ position absolute
66
+ font-size 12px
67
+ top 8px
68
+ right -4px
69
+ font-weight bold
@@ -0,0 +1,14 @@
1
+ export default [
2
+ {
3
+ label: 'OpenAI',
4
+ baseURL: 'https://api.openai.com/v1',
5
+ homepage: 'https://openai.com',
6
+ models: ['gpt-4', 'gpt-3.5-turbo', 'gpt-3.5-turbo-16k']
7
+ },
8
+ {
9
+ label: 'DeepSeek',
10
+ baseURL: 'https://api.deepseek.com/v1',
11
+ homepage: 'https://deepseek.com',
12
+ models: ['deepseek-chat', 'deepseek-coder']
13
+ }
14
+ ]