@electerm/electerm-react 1.60.6 → 1.60.16

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.
@@ -307,6 +307,7 @@ export const batchOpHelpLink = 'https://github.com/electerm/electerm/wiki/batch-
307
307
  export const proxyHelpLink = 'https://github.com/electerm/electerm/wiki/proxy-format'
308
308
  export const regexHelpLink = 'https://github.com/electerm/electerm/wiki/Terminal-keywords-highlight-regular-expression-exmaples'
309
309
  export const connectionHoppingWikiLink = 'https://github.com/electerm/electerm/wiki/Connection-Hopping-Behavior-Change-in-electerm-since-v1.50.65'
310
+ export const aiConfigWikiLink = 'https://github.com/electerm/electerm/wiki/AI-model-config-guide'
310
311
  export const modals = {
311
312
  hide: 0,
312
313
  setting: 1,
@@ -11,6 +11,10 @@ import {
11
11
  SendOutlined,
12
12
  UnorderedListOutlined
13
13
  } from '@ant-design/icons'
14
+ import {
15
+ aiConfigWikiLink
16
+ } from '../../common/constants'
17
+ import HelpIcon from '../common/help-icon'
14
18
  import './ai.styl'
15
19
 
16
20
  const { TextArea } = Input
@@ -152,7 +156,11 @@ export default function AIChat (props) {
152
156
  />
153
157
  <UnorderedListOutlined
154
158
  onClick={clearHistory}
155
- className='mg2l pointer clear-ai-icon icon-hover'
159
+ className='mg2x pointer clear-ai-icon icon-hover'
160
+ title='Clear AI chat history'
161
+ />
162
+ <HelpIcon
163
+ link={aiConfigWikiLink}
156
164
  />
157
165
  {renderConfig()}
158
166
  </Flex>
@@ -3,9 +3,14 @@ import {
3
3
  Input,
4
4
  Button,
5
5
  AutoComplete,
6
- Modal
6
+ Modal,
7
+ Alert
7
8
  } from 'antd'
8
9
  import { useEffect, useState } from 'react'
10
+ import Link from '../common/external-link'
11
+ import {
12
+ aiConfigWikiLink
13
+ } from '../../common/constants'
9
14
 
10
15
  // Comprehensive API provider configurations
11
16
  import providers from './providers'
@@ -70,6 +75,13 @@ export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig })
70
75
  onCancel={handleCancel}
71
76
  footer={null}
72
77
  >
78
+ <Alert
79
+ message={
80
+ <Link to={aiConfigWikiLink}>WIKI: {aiConfigWikiLink}</Link>
81
+ }
82
+ type='info'
83
+ className='mg2y'
84
+ />
73
85
  <Form
74
86
  form={form}
75
87
  onFinish={handleSubmit}
@@ -1,47 +1,15 @@
1
- import React from 'react'
1
+ import { useState, useMemo } from 'react'
2
2
  import ReactMarkdown from 'react-markdown'
3
3
  import { copy } from '../../common/clipboard'
4
4
  import Link from '../common/external-link'
5
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
- }
6
+ import { CopyOutlined, PlayCircleOutlined, DownCircleOutlined } from '@ant-design/icons'
7
+ import getBrand from './get-brand'
41
8
 
42
9
  const e = window.translate
43
10
 
44
11
  export default function AIOutput ({ item }) {
12
+ const [showFull, setShowFull] = useState(false)
45
13
  const {
46
14
  response,
47
15
  baseURLAI
@@ -52,6 +20,18 @@ export default function AIOutput ({ item }) {
52
20
 
53
21
  const { brand, brandUrl } = getBrand(baseURLAI)
54
22
 
23
+ const truncatedResponse = useMemo(() => {
24
+ if (!response) return ''
25
+ const codeBlockRegex = /```[\s\S]*?```/
26
+ const match = response.match(codeBlockRegex)
27
+ if (match) {
28
+ const index = match.index + match[0].length
29
+ return response.slice(0, index) + '\n\n... ...'
30
+ }
31
+ // If no code block found, show first 5 lines
32
+ return response.split('\n').slice(0, 5).join('\n') + '\n\n... ...'
33
+ }, [response])
34
+
55
35
  const renderCode = (props) => {
56
36
  const { node, className = '', children, ...rest } = props
57
37
  const code = String(children).replace(/\n$/, '')
@@ -107,8 +87,22 @@ export default function AIOutput ({ item }) {
107
87
  )
108
88
  }
109
89
 
90
+ function renderShowMore () {
91
+ if (showFull) {
92
+ return null
93
+ }
94
+ return (
95
+ <span
96
+ onClick={() => setShowFull(true)}
97
+ className='mg1t pointer'
98
+ >
99
+ <DownCircleOutlined /> {e('fullContent')}
100
+ </span>
101
+ )
102
+ }
103
+
110
104
  const mdProps = {
111
- children: response,
105
+ children: showFull ? response : truncatedResponse,
112
106
  components: {
113
107
  code: renderCode
114
108
  }
@@ -118,6 +112,7 @@ export default function AIOutput ({ item }) {
118
112
  <div className='pd1'>
119
113
  {renderBrand()}
120
114
  <ReactMarkdown {...mdProps} />
115
+ {renderShowMore()}
121
116
  </div>
122
117
  )
123
118
  }
@@ -7,6 +7,7 @@
7
7
 
8
8
  .ai-chat-history
9
9
  flex-grow 1
10
+ min-height 0
10
11
  &::-webkit-scrollbar
11
12
  width 0
12
13
  .ai-history-wrap
@@ -0,0 +1,34 @@
1
+ import providers from './providers'
2
+
3
+ export default function getBrand (baseURLAI) {
4
+ // First, try to match with providers
5
+ const provider = providers.find(p => p.baseURL === baseURLAI)
6
+ if (provider) {
7
+ return {
8
+ brand: provider.label,
9
+ brandUrl: provider.homepage
10
+ }
11
+ }
12
+
13
+ // If no match, extract brand from URL
14
+ try {
15
+ const url = new URL(baseURLAI)
16
+ const hostname = url.hostname
17
+ const parts = hostname.split('.')
18
+ let brand = parts[parts.length - 2] // Usually the brand name is the second-to-last part
19
+
20
+ // Capitalize the first letter
21
+ brand = brand.charAt(0).toUpperCase() + brand.slice(1)
22
+
23
+ return {
24
+ brand,
25
+ brandUrl: `https://${parts[parts.length - 2]}.${parts[parts.length - 1]}`
26
+ }
27
+ } catch (error) {
28
+ // If URL parsing fails, return null
29
+ return {
30
+ brand: null,
31
+ brandUrl: null
32
+ }
33
+ }
34
+ }
@@ -105,7 +105,7 @@ export default function RdpFormUi (props) {
105
105
  />
106
106
  <FormItem
107
107
  {...formItemLayout}
108
- label={e('userName')}
108
+ label={e('username')}
109
109
  hasFeedback
110
110
  name='userName'
111
111
  required
@@ -99,7 +99,9 @@ export default auto(function Index (props) {
99
99
  uiThemeConfig,
100
100
  transferHistory,
101
101
  transferToConfirm,
102
- openResolutionEdit
102
+ openResolutionEdit,
103
+ rightPanelTitle,
104
+ rightPanelTab
103
105
  } = store
104
106
  const upgradeInfo = deepCopy(store.upgradeInfo)
105
107
  const cls = classnames({
@@ -191,7 +193,9 @@ export default auto(function Index (props) {
191
193
  const rightPanelProps = {
192
194
  rightPanelVisible: store.rightPanelVisible,
193
195
  rightPanelPinned: store.rightPanelPinned,
194
- rightPanelWidth: store.rightPanelWidth
196
+ rightPanelWidth: store.rightPanelWidth,
197
+ title: rightPanelTitle,
198
+ rightPanelTab
195
199
  }
196
200
  const terminalInfoProps = {
197
201
  ...deepCopy(store.terminalInfoProps),
@@ -222,7 +226,7 @@ export default auto(function Index (props) {
222
226
  activeTabId: store.activeTabId,
223
227
  showAIConfig: store.showAIConfig
224
228
  }
225
- const rightPanelContent = store.rightPanelTab === 'ai'
229
+ const rightPanelContent = rightPanelTab === 'ai'
226
230
  ? <AIChat {...aiChatProps} />
227
231
  : <TerminalInfo {...terminalInfoProps} />
228
232
  return (
@@ -3,29 +3,28 @@
3
3
  .right-side-panel
4
4
  position absolute
5
5
  right 0
6
- top 0
6
+ top 36px
7
7
  bottom 0
8
8
  z-index 200
9
9
  background main
10
10
  color text-light
11
11
  border-left 1px solid darken(main, 30%)
12
+ &.right-side-panel-pinned
13
+ top 0
12
14
 
13
15
  .drag-handle
14
16
  left 0
15
17
  right auto
16
18
  display block
17
-
19
+ .right-panel-title
20
+ border-bottom 1px solid darken(main, 30%)
18
21
  .right-side-panel-controls
19
- position absolute
20
- left 10px
21
- top 10px
22
22
  color text-dark
23
23
  font-size 16px
24
24
  &:hover
25
25
  color text
26
26
  cursor pointer
27
27
  &.right-side-panel-pin
28
- left 30px
29
28
  &.pinned
30
29
  color success
31
30
  .right-side-panel-content
@@ -33,7 +32,7 @@
33
32
  overflow-y scroll
34
33
  position absolute
35
34
  left 0
36
- top 30px
35
+ top 55px
37
36
  right 0
38
37
  bottom 0
39
38
  .animate-fast
@@ -3,20 +3,31 @@ import DragHandle from '../common/drag-handle'
3
3
  import './right-side-panel.styl'
4
4
  import {
5
5
  CloseCircleOutlined,
6
- PushpinOutlined
6
+ PushpinOutlined,
7
+ InfoCircleOutlined
7
8
  } from '@ant-design/icons'
9
+ import {
10
+ Typography,
11
+ Flex,
12
+ Tag
13
+ } from 'antd'
8
14
 
9
15
  export default memo(function RightSidePanel (
10
16
  {
11
17
  rightPanelVisible,
12
18
  rightPanelPinned,
13
19
  rightPanelWidth,
14
- children
20
+ children,
21
+ title,
22
+ rightPanelTab
15
23
  }
16
24
  ) {
17
25
  if (!rightPanelVisible) {
18
26
  return null
19
27
  }
28
+ const tag = rightPanelTab === 'ai'
29
+ ? <Tag className='mg1r'>AI</Tag>
30
+ : <InfoCircleOutlined className='mg1r' />
20
31
 
21
32
  function onDragEnd (nw) {
22
33
  window.store.setRightSidePanelWidth(nw)
@@ -36,7 +47,7 @@ export default memo(function RightSidePanel (
36
47
  }
37
48
 
38
49
  const panelProps = {
39
- className: 'right-side-panel animate-fast',
50
+ className: 'right-side-panel animate-fast' + (rightPanelPinned ? ' right-side-panel-pinned' : ''),
40
51
  id: 'right-side-panel',
41
52
  style: {
42
53
  width: `${rightPanelWidth}px`
@@ -60,13 +71,24 @@ export default memo(function RightSidePanel (
60
71
  {...panelProps}
61
72
  >
62
73
  <DragHandle {...dragProps} />
63
- <CloseCircleOutlined
64
- className='right-side-panel-close right-side-panel-controls'
65
- onClick={onClose}
66
- />
67
- <PushpinOutlined
68
- {...pinProps}
69
- />
74
+ <Flex
75
+ className='right-panel-title pd2'
76
+ justify='space-between'
77
+ align='center'
78
+ >
79
+ <Typography.Text level={4} ellipsis style={{ margin: 0, flex: 1 }}>
80
+ {tag} {title}
81
+ </Typography.Text>
82
+ <Flex>
83
+ <PushpinOutlined
84
+ {...pinProps}
85
+ />
86
+ <CloseCircleOutlined
87
+ className='right-side-panel-close right-side-panel-controls mg1l'
88
+ onClick={onClose}
89
+ />
90
+ </Flex>
91
+ </Flex>
70
92
  <div className='right-side-panel-content'>
71
93
  {children}
72
94
  </div>
@@ -1,3 +1,4 @@
1
+ import React, { useCallback, useEffect, useRef } from 'react'
1
2
  import createTitle, { createTitleWithTag } from '../../common/create-title'
2
3
  import { DeleteOutlined } from '@ant-design/icons'
3
4
 
@@ -7,9 +8,25 @@ export default function HistoryItem (props) {
7
8
  item,
8
9
  index
9
10
  } = props
10
- function handleClick () {
11
- store.onSelectHistory(item.tab)
12
- }
11
+ const timeoutRef = useRef(null)
12
+
13
+ const handleClick = useCallback(() => {
14
+ if (timeoutRef.current) {
15
+ clearTimeout(timeoutRef.current)
16
+ }
17
+ timeoutRef.current = setTimeout(() => {
18
+ store.onSelectHistory(item.tab)
19
+ }, 10)
20
+ }, [item.tab])
21
+
22
+ useEffect(() => {
23
+ return () => {
24
+ if (timeoutRef.current) {
25
+ clearTimeout(timeoutRef.current)
26
+ }
27
+ }
28
+ }, [])
29
+
13
30
  function handleDelete (e) {
14
31
  e.stopPropagation()
15
32
  store.history.splice(index, 1)
@@ -10,13 +10,16 @@ export default auto(function HistoryPanel (props) {
10
10
  const {
11
11
  history
12
12
  } = store
13
+ const arr = props.sort
14
+ ? [...history].sort((a, b) => { return b.count - a.count })
15
+ : history
13
16
  return (
14
17
  <div
15
18
  className='sidebar-panel-history'
16
19
  >
17
20
  <div className='pd2x'>
18
21
  {
19
- history.map((item, i) => {
22
+ arr.map((item, i) => {
20
23
  return (
21
24
  <HistoryItem
22
25
  key={item.id}
@@ -24,9 +24,8 @@ import {
24
24
  TwoRowsRightIcon,
25
25
  TwoColumnsBottomIcon
26
26
  } from '../icons/split-icons'
27
- import { Dropdown, Popover, Button } from 'antd'
27
+ import { Dropdown, Popover } from 'antd'
28
28
  import Tab from './tab'
29
- import LogoElem from '../common/logo-elem.jsx'
30
29
  import './tabs.styl'
31
30
  import {
32
31
  tabWidth,
@@ -41,6 +40,7 @@ import findParentBySel from '../../common/find-parent'
41
40
  import WindowControl from './window-control'
42
41
  import BookmarksList from '../sidebar/bookmark-select'
43
42
  import AppDrag from './app-drag'
43
+ import NoSession from './no-session'
44
44
  import classNames from 'classnames'
45
45
 
46
46
  const e = window.translate
@@ -548,31 +548,13 @@ export default class Tabs extends React.Component {
548
548
  }
549
549
 
550
550
  renderNoSession = () => {
551
- const props = {
552
- style: {
553
- height: this.props.height + 'px'
554
- }
555
- }
556
551
  return (
557
- <div className='no-sessions electerm-logo-bg' {...props}>
558
- <Button
559
- onClick={this.handleNewTab}
560
- size='large'
561
- className='mg1r mg1b add-new-tab-btn'
562
- >
563
- {e('newTab')}
564
- </Button>
565
- <Button
566
- onClick={this.handleNewSsh}
567
- size='large'
568
- className='mg1r mg1b'
569
- >
570
- {e('newBookmark')}
571
- </Button>
572
- <div className='pd3'>
573
- <LogoElem />
574
- </div>
575
- </div>
552
+ <NoSession
553
+ height={this.props.height}
554
+ onNewTab={this.handleNewTab}
555
+ onNewSsh={this.handleNewSsh}
556
+ batch={this.props.batch}
557
+ />
576
558
  )
577
559
  }
578
560
 
@@ -0,0 +1,40 @@
1
+ import { Button } from 'antd'
2
+ import LogoElem from '../common/logo-elem.jsx'
3
+ import HistoryPanel from '../sidebar/history'
4
+
5
+ const e = window.translate
6
+
7
+ export default function NoSessionPanel ({ height, onNewTab, onNewSsh, batch }) {
8
+ const props = {
9
+ style: {
10
+ height: height + 'px'
11
+ }
12
+ }
13
+ const handleClick = () => {
14
+ window.openTabBatch = batch
15
+ }
16
+ return (
17
+ <div className='no-sessions electerm-logo-bg' {...props}>
18
+ <Button
19
+ onClick={onNewTab}
20
+ size='large'
21
+ className='mg1r mg1b add-new-tab-btn'
22
+ >
23
+ {e('newTab')}
24
+ </Button>
25
+ <Button
26
+ onClick={onNewSsh}
27
+ size='large'
28
+ className='mg1r mg1b'
29
+ >
30
+ {e('newBookmark')}
31
+ </Button>
32
+ <div className='pd3'>
33
+ <LogoElem />
34
+ </div>
35
+ <div className='no-session-history' onClick={handleClick}>
36
+ <HistoryPanel sort />
37
+ </div>
38
+ </div>
39
+ )
40
+ }
@@ -229,3 +229,9 @@
229
229
  padding 0 4px
230
230
  height 20px
231
231
  line-height 21px
232
+ .no-session-history
233
+ position absolute
234
+ top 280px
235
+ bottom 80px
236
+ left 0
237
+ right 0
@@ -1306,10 +1306,10 @@ clear\r`
1306
1306
  }
1307
1307
 
1308
1308
  handleShowInfo = () => {
1309
- const { id, sessionId, logName, tab } = this.props
1309
+ const { sessionId, logName, tab } = this.props
1310
1310
  const infoProps = {
1311
1311
  logName,
1312
- id,
1312
+ id: tab.id,
1313
1313
  pid: tab.id,
1314
1314
  sessionId,
1315
1315
  isRemote: this.isRemote(),
@@ -29,6 +29,7 @@ import isColorDark from '../common/is-color-dark'
29
29
  import { getReverseColor } from '../common/reverse-color'
30
30
  import { uniq } from 'lodash-es'
31
31
  import deepCopy from 'json-deep-copy'
32
+ import getBrand from '../components/ai/get-brand'
32
33
  import {
33
34
  settingMap,
34
35
  paneMap,
@@ -38,6 +39,7 @@ import {
38
39
  terminalSshConfigType
39
40
  } from '../common/constants'
40
41
  import getInitItem from '../common/init-setting-item'
42
+ import createTitle from '../common/create-title'
41
43
  import {
42
44
  theme
43
45
  } from 'antd'
@@ -93,6 +95,19 @@ class Store {
93
95
  return Array.from(window.store._batchInputSelectedTabIds)
94
96
  }
95
97
 
98
+ get rightPanelTitle () {
99
+ const {
100
+ rightPanelTab,
101
+ config: {
102
+ baseURLAI
103
+ }
104
+ } = window.store
105
+ if (rightPanelTab === 'ai') {
106
+ return getBrand(baseURLAI).brand || 'Custom AI Model'
107
+ }
108
+ return createTitle(window.store.currentTab)
109
+ }
110
+
96
111
  get inActiveTerminal () {
97
112
  const { store } = window
98
113
  if (store.showModal) {
@@ -492,7 +492,10 @@ export default (Store) => {
492
492
  'language',
493
493
  'copyWhenSelect',
494
494
  'customCss',
495
- 'dataSyncSelected'
495
+ 'dataSyncSelected',
496
+ 'baseURLAI',
497
+ 'modelAI',
498
+ 'roleAI'
496
499
  ]
497
500
  return pick(store.config, configSyncKeys)
498
501
  }
@@ -459,7 +459,8 @@ export default Store => {
459
459
  'srcId',
460
460
  'status',
461
461
  'pane',
462
- 'batch'
462
+ 'batch',
463
+ 'tabCount'
463
464
  ]
464
465
  const { history } = store
465
466
  const index = history.findIndex(d => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "1.60.6",
3
+ "version": "1.60.16",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",