@electerm/electerm-react 2.17.16 → 3.0.18

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 (32) hide show
  1. package/client/common/cache.js +4 -2
  2. package/client/common/db.js +3 -5
  3. package/client/common/default-setting.js +1 -1
  4. package/client/common/pass-enc.js +0 -21
  5. package/client/common/safe-local-storage.js +106 -1
  6. package/client/components/ai/ai-chat-history-item.jsx +132 -8
  7. package/client/components/ai/ai-chat.jsx +10 -159
  8. package/client/components/ai/ai.styl +6 -1
  9. package/client/components/bookmark-form/common/render-auth-ssh.jsx +1 -117
  10. package/client/components/bookmark-form/config/common-fields.js +8 -0
  11. package/client/components/bookmark-form/config/ftp.js +1 -0
  12. package/client/components/bookmark-form/config/local.js +10 -51
  13. package/client/components/session/sessions.jsx +1 -0
  14. package/client/components/setting-panel/setting-terminal.jsx +2 -2
  15. package/client/components/sftp/address-bookmark-item.jsx +1 -1
  16. package/client/components/sftp/address-bookmark.jsx +11 -1
  17. package/client/components/sftp/file-item.jsx +1 -1
  18. package/client/components/sftp/sftp-entry.jsx +35 -12
  19. package/client/components/sidebar/history.jsx +1 -1
  20. package/client/components/tabs/app-drag.jsx +13 -12
  21. package/client/components/tabs/tabs.styl +1 -1
  22. package/client/components/terminal/reconnect-overlay.jsx +27 -0
  23. package/client/components/terminal/socket-close-warning.jsx +94 -0
  24. package/client/components/terminal/terminal.jsx +87 -58
  25. package/client/components/terminal/terminal.styl +12 -0
  26. package/client/components/terminal/transfer-client-base.js +3 -3
  27. package/client/components/text-editor/edit-with-custom-editor.jsx +3 -2
  28. package/client/store/init-state.js +3 -3
  29. package/client/store/sync.js +0 -1
  30. package/client/store/tab.js +2 -1
  31. package/client/store/watch.js +3 -3
  32. package/package.json +1 -1
@@ -2,6 +2,8 @@
2
2
  // keep limit items in cache
3
3
  // we persist cache to local storage, so we can keep cache after restart
4
4
 
5
+ import { safeGetItem, safeSetItem } from './safe-local-storage.js'
6
+
5
7
  class MapCache {
6
8
  constructor (limit, key) {
7
9
  this.limit = limit
@@ -11,7 +13,7 @@ class MapCache {
11
13
  }
12
14
 
13
15
  load () {
14
- const data = window.localStorage.getItem(this.key)
16
+ const data = safeGetItem(this.key)
15
17
  if (data) {
16
18
  const arr = JSON.parse(data)
17
19
  for (const item of arr) {
@@ -28,7 +30,7 @@ class MapCache {
28
30
  value
29
31
  })
30
32
  }
31
- window.localStorage.setItem(this.key, JSON.stringify(arr))
33
+ safeSetItem(this.key, JSON.stringify(arr))
32
34
  }
33
35
 
34
36
  set (key, value) {
@@ -9,7 +9,7 @@ import { without, isArray } from 'lodash-es'
9
9
  import handleError from './error-handler'
10
10
  import generate from './uid'
11
11
  import safeParse from './to-simple-obj'
12
- import { encObj, decObj } from './pass-enc'
12
+ import { decObj } from './pass-enc'
13
13
 
14
14
  /**
15
15
  * db action, never direct use it
@@ -46,7 +46,7 @@ export function insert (dbName, inst) {
46
46
  const { id, _id, ...rest } = obj
47
47
  return {
48
48
  _id: _id || id || generate(),
49
- ...encObj(rest)
49
+ ...rest
50
50
  }
51
51
  })
52
52
  return dbAction(dbName, 'insert', safeParse(arr))
@@ -76,9 +76,7 @@ export async function remove (dbName, id) {
76
76
  export function update (_id, value, db = 'data', upsert = true) {
77
77
  const updates = dbNames.includes(db)
78
78
  ? {
79
- $set: {
80
- ...encObj(value)
81
- }
79
+ $set: value
82
80
  }
83
81
  : {
84
82
  $set: {
@@ -29,7 +29,6 @@ export default {
29
29
  terminalBackgroundTextColor: '#ffffff',
30
30
  terminalBackgroundTextFontFamily: 'Maple Mono',
31
31
  rendererType: 'canvas',
32
- enableSixel: true,
33
32
  terminalType: 'xterm-256color',
34
33
  keepaliveCountMax: 10,
35
34
  keyword2FA: 'verification code,otp,one-time,two-factor,2fa,totp,authenticator,duo,yubikey,security code,mfa,passcode',
@@ -73,6 +72,7 @@ export default {
73
72
  sessionLogPath: '',
74
73
  sshSftpSplitView: false,
75
74
  showCmdSuggestions: false,
75
+ autoReconnectTerminal: false,
76
76
  startDirectoryLocal: '',
77
77
  allowMultiInstance: false,
78
78
  disableDeveloperTool: false
@@ -1,12 +1,3 @@
1
- const enc = (str) => {
2
- if (typeof str !== 'string') {
3
- return str
4
- }
5
- return str.split('').map((s, i) => {
6
- return String.fromCharCode((s.charCodeAt(0) + i + 1) % 65536)
7
- }).join('')
8
- }
9
-
10
1
  const dec = (str) => {
11
2
  if (typeof str !== 'string') {
12
3
  return str
@@ -16,18 +7,6 @@ const dec = (str) => {
16
7
  }).join('')
17
8
  }
18
9
 
19
- /**
20
- * enc password
21
- * @param {object} obj
22
- */
23
- export function encObj (obj) {
24
- if (!obj.passwordEncrypted && obj.password) {
25
- obj.password = enc(obj.password)
26
- obj.passwordEncrypted = true
27
- }
28
- return obj
29
- }
30
-
31
10
  /**
32
11
  * dec password
33
12
  * @param {object} obj
@@ -1,4 +1,60 @@
1
1
  import { termLSPrefix } from './constants'
2
+ import parseJsonSafe from './parse-json-safe'
3
+
4
+ // ─── Encryption helpers ───────────────────────────────────────────────────────
5
+
6
+ // Prefix that marks a value encrypted by this module.
7
+ // Values without this prefix are treated as legacy plaintext.
8
+ const ENC_PREFIX = 'enc1:'
9
+
10
+ let _encKey = null
11
+
12
+ function getKey () {
13
+ if (_encKey !== null) return _encKey
14
+ try {
15
+ // window.pre is set by pre.js before the store is used; runSync is synchronous IPC
16
+ _encKey = (window.pre && window.pre.runSync && window.pre.runSync('getStorageKey')) || ''
17
+ } catch (e) {
18
+ _encKey = ''
19
+ }
20
+ return _encKey
21
+ }
22
+
23
+ function encrypt (str) {
24
+ if (!str) return str
25
+ const key = getKey()
26
+ if (!key) return str
27
+ const strBytes = new TextEncoder().encode(str)
28
+ const keyBytes = new TextEncoder().encode(key)
29
+ const out = new Uint8Array(strBytes.length)
30
+ for (let i = 0; i < strBytes.length; i++) {
31
+ out[i] = strBytes[i] ^ keyBytes[i % keyBytes.length]
32
+ }
33
+ let binary = ''
34
+ for (let i = 0; i < out.length; i++) {
35
+ binary += String.fromCharCode(out[i])
36
+ }
37
+ return ENC_PREFIX + btoa(binary)
38
+ }
39
+
40
+ function decrypt (str) {
41
+ if (!str || !str.startsWith(ENC_PREFIX)) return str
42
+ const key = getKey()
43
+ if (!key) return str
44
+ try {
45
+ const binary = atob(str.slice(ENC_PREFIX.length))
46
+ const keyBytes = new TextEncoder().encode(key)
47
+ const out = new Uint8Array(binary.length)
48
+ for (let i = 0; i < binary.length; i++) {
49
+ out[i] = binary.charCodeAt(i) ^ keyBytes[i % keyBytes.length]
50
+ }
51
+ return new TextDecoder().decode(out)
52
+ } catch (e) {
53
+ return str
54
+ }
55
+ }
56
+
57
+ // ─── Internal helper ─────────────────────────────────────────────────────────
2
58
 
3
59
  function clear () {
4
60
  const keys = Object.keys(window.localStorage)
@@ -9,6 +65,8 @@ function clear () {
9
65
  }
10
66
  }
11
67
 
68
+ // ─── Original (plain) functions ──────────────────────────────────────────────
69
+
12
70
  export function setItem (id, str) {
13
71
  try {
14
72
  window.localStorage.setItem(id, str)
@@ -26,10 +84,57 @@ export function getItem (id) {
26
84
 
27
85
  export function getItemJSON (id, defaultValue) {
28
86
  const str = window.localStorage.getItem(id) || ''
29
- return str ? JSON.parse(str) : defaultValue
87
+ const r = parseJsonSafe(str)
88
+ if (typeof r === 'string') {
89
+ return defaultValue
90
+ }
91
+ return r || defaultValue
30
92
  }
31
93
 
32
94
  export function setItemJSON (id, obj) {
33
95
  const str = JSON.stringify(obj)
34
96
  return setItem(id, str)
35
97
  }
98
+
99
+ // ─── Safe (encrypted) functions ──────────────────────────────────────────────
100
+
101
+ export function safeSetItem (id, str) {
102
+ if (window.et.isWebApp) {
103
+ return setItem(id, str)
104
+ }
105
+ try {
106
+ window.localStorage.setItem(id, encrypt(str))
107
+ } catch (e) {
108
+ console.log(e)
109
+ console.log('maybe local storage full, lets reset')
110
+ clear()
111
+ window.localStorage.setItem(id, encrypt(str))
112
+ }
113
+ }
114
+
115
+ export function safeGetItem (id) {
116
+ if (window.et.isWebApp) {
117
+ return getItem(id)
118
+ }
119
+ return decrypt(window.localStorage.getItem(id) || '')
120
+ }
121
+
122
+ export function safeGetItemJSON (id, defaultValue) {
123
+ if (window.et.isWebApp) {
124
+ return getItemJSON(id, defaultValue)
125
+ }
126
+ const str = decrypt(window.localStorage.getItem(id) || '')
127
+ const r = parseJsonSafe(str)
128
+ if (typeof r === 'string') {
129
+ return defaultValue
130
+ }
131
+ return r || defaultValue
132
+ }
133
+
134
+ export function safeSetItemJSON (id, obj) {
135
+ if (window.et.isWebApp) {
136
+ return setItemJSON(id, obj)
137
+ }
138
+ const str = JSON.stringify(obj)
139
+ return safeSetItem(id, str)
140
+ }
@@ -1,5 +1,6 @@
1
- // ai-chat-history-item.jsx
1
+ import { useState, useEffect, useRef, useCallback } from 'react'
2
2
  import AIOutput from './ai-output'
3
+ import AIStopIcon from './ai-stop-icon'
3
4
  import {
4
5
  Alert,
5
6
  Tooltip
@@ -12,47 +13,169 @@ import {
12
13
  CaretRightOutlined
13
14
  } from '@ant-design/icons'
14
15
  import { copy } from '../../common/clipboard'
15
- import { useState } from 'react'
16
16
 
17
17
  export default function AIChatHistoryItem ({ item }) {
18
18
  const [showOutput, setShowOutput] = useState(true)
19
+ const startedRef = useRef(false)
19
20
  const {
20
- prompt
21
+ prompt,
22
+ isStreaming,
23
+ sessionId,
24
+ response,
25
+ modelAI,
26
+ roleAI,
27
+ baseURLAI,
28
+ apiPathAI,
29
+ apiKeyAI,
30
+ proxyAI,
31
+ languageAI
21
32
  } = item
22
33
 
23
34
  function toggleOutput () {
24
35
  setShowOutput(!showOutput)
25
36
  }
26
37
 
38
+ function buildRole () {
39
+ const lang = languageAI || window.store.getLangName()
40
+ return roleAI + `;用[${lang}]回复`
41
+ }
42
+
43
+ const pollStreamContent = useCallback(async (sid) => {
44
+ try {
45
+ const streamResponse = await window.pre.runGlobalAsync('getStreamContent', sid)
46
+
47
+ if (streamResponse && streamResponse.error) {
48
+ if (streamResponse.error === 'Session not found') {
49
+ return
50
+ }
51
+ window.store.removeAiHistory(item.id)
52
+ return window.store.onError(new Error(streamResponse.error))
53
+ }
54
+
55
+ const index = window.store.aiChatHistory.findIndex(i => i.id === item.id)
56
+ if (index !== -1) {
57
+ window.store.aiChatHistory[index].response = streamResponse.content || ''
58
+ window.store.aiChatHistory[index].isStreaming = streamResponse.hasMore
59
+ window.store.aiChatHistory = [...window.store.aiChatHistory]
60
+
61
+ if (streamResponse.hasMore) {
62
+ setTimeout(() => pollStreamContent(sid), 200)
63
+ }
64
+ }
65
+ } catch (error) {
66
+ window.store.removeAiHistory(item.id)
67
+ window.store.onError(error)
68
+ }
69
+ }, [item.id])
70
+
71
+ const startRequest = useCallback(async () => {
72
+ try {
73
+ const aiResponse = await window.pre.runGlobalAsync(
74
+ 'AIchat',
75
+ prompt,
76
+ modelAI,
77
+ buildRole(),
78
+ baseURLAI,
79
+ apiPathAI,
80
+ apiKeyAI,
81
+ proxyAI,
82
+ true
83
+ )
84
+
85
+ if (aiResponse && aiResponse.error) {
86
+ window.store.removeAiHistory(item.id)
87
+ return window.store.onError(new Error(aiResponse.error))
88
+ }
89
+
90
+ if (aiResponse && aiResponse.isStream && aiResponse.sessionId) {
91
+ const index = window.store.aiChatHistory.findIndex(i => i.id === item.id)
92
+ if (index !== -1) {
93
+ window.store.aiChatHistory[index].isStreaming = true
94
+ window.store.aiChatHistory[index].sessionId = aiResponse.sessionId
95
+ window.store.aiChatHistory[index].response = aiResponse.content || ''
96
+ }
97
+ pollStreamContent(aiResponse.sessionId)
98
+ } else if (aiResponse && aiResponse.response) {
99
+ const index = window.store.aiChatHistory.findIndex(i => i.id === item.id)
100
+ if (index !== -1) {
101
+ window.store.aiChatHistory[index].response = aiResponse.response
102
+ window.store.aiChatHistory[index].isStreaming = false
103
+ }
104
+ }
105
+ } catch (error) {
106
+ window.store.removeAiHistory(item.id)
107
+ window.store.onError(error)
108
+ }
109
+ }, [prompt, modelAI, baseURLAI, apiPathAI, apiKeyAI, proxyAI, item.id, pollStreamContent])
110
+
111
+ useEffect(() => {
112
+ if (!response && !startedRef.current) {
113
+ startedRef.current = true
114
+ startRequest()
115
+ }
116
+ }, [])
117
+
118
+ async function handleStop (e) {
119
+ e.stopPropagation()
120
+ if (!sessionId) return
121
+
122
+ try {
123
+ await window.pre.runGlobalAsync('stopStream', sessionId)
124
+ const index = window.store.aiChatHistory.findIndex(i => i.id === item.id)
125
+ if (index !== -1) {
126
+ window.store.aiChatHistory[index].isStreaming = false
127
+ window.store.aiChatHistory = [...window.store.aiChatHistory]
128
+ }
129
+ } catch (error) {
130
+ console.error('Error stopping stream:', error)
131
+ }
132
+ }
133
+
134
+ function renderStopButton () {
135
+ if (!isStreaming) {
136
+ return null
137
+ }
138
+ return (
139
+ <AIStopIcon
140
+ onClick={handleStop}
141
+ title='Stop this AI request'
142
+ />
143
+ )
144
+ }
145
+
27
146
  const alertProps = {
28
147
  title: (
29
- <>
148
+ <div className='ai-history-item-title'>
30
149
  <span className='pointer mg1r' onClick={toggleOutput}>
31
150
  {showOutput ? <CaretDownOutlined /> : <CaretRightOutlined />}
32
151
  </span>
33
152
  <UserOutlined />: {prompt}
34
- </>
153
+ {renderStopButton()}
154
+ </div>
35
155
  ),
36
156
  type: 'info'
37
157
  }
158
+
38
159
  function handleDel (e) {
39
160
  e.stopPropagation()
40
161
  window.store.removeAiHistory(item.id)
41
162
  }
163
+
42
164
  function handleCopy () {
43
165
  copy(prompt)
44
166
  }
167
+
45
168
  function renderTitle () {
46
169
  return (
47
170
  <div>
48
171
  <p>
49
- <b>Model:</b> {item.modelAI}
172
+ <b>Model:</b> {modelAI}
50
173
  </p>
51
174
  <p>
52
- <b>Role:</b> {item.roleAI}
175
+ <b>Role:</b> {roleAI}
53
176
  </p>
54
177
  <p>
55
- <b>Base URL:</b> {item.baseURLAI}
178
+ <b>Base URL:</b> {baseURLAI}
56
179
  </p>
57
180
  <p>
58
181
  <b>Time:</b> {new Date(item.timestamp).toLocaleString()}
@@ -70,6 +193,7 @@ export default function AIChatHistoryItem ({ item }) {
70
193
  </div>
71
194
  )
72
195
  }
196
+
73
197
  return (
74
198
  <div className='chat-history-item'>
75
199
  <div className='mg1y'>
@@ -14,7 +14,6 @@ import {
14
14
  } from '../../common/constants'
15
15
  import HelpIcon from '../common/help-icon'
16
16
  import { refsStatic } from '../common/ref'
17
- import AIStopIcon from './ai-stop-icon'
18
17
  import './ai.styl'
19
18
 
20
19
  const { TextArea } = Input
@@ -22,155 +21,43 @@ const MAX_HISTORY = 100
22
21
 
23
22
  export default function AIChat (props) {
24
23
  const [prompt, setPrompt] = useState('')
25
- const [isLoading, setIsLoading] = useState(false)
26
- const [currentSessionId, setCurrentSessionId] = useState(null)
27
24
 
28
25
  function handlePromptChange (e) {
29
26
  setPrompt(e.target.value)
30
27
  }
31
28
 
32
- function buildRole () {
33
- const lang = props.config.languageAI || window.store.getLangName()
34
- return props.config.roleAI + `;用[${lang}]回复`
35
- }
36
-
37
- const handleSubmit = useCallback(async function () {
29
+ const handleSubmit = useCallback(function () {
38
30
  if (window.store.aiConfigMissing()) {
39
31
  window.store.toggleAIConfig()
40
32
  }
41
- if (!prompt.trim() || isLoading) return
42
- setIsLoading(true)
33
+ if (!prompt.trim()) return
43
34
 
44
- // Create a placeholder entry for the streaming response
45
35
  const chatId = uid()
46
36
  const chatEntry = {
47
37
  prompt,
48
- response: '', // Will be updated as stream arrives
38
+ response: '',
49
39
  isStreaming: false,
50
40
  sessionId: null,
51
41
  ...pick(props.config, [
52
42
  'modelAI',
53
43
  'roleAI',
54
- 'baseURLAI'
44
+ 'baseURLAI',
45
+ 'apiPathAI',
46
+ 'apiKeyAI',
47
+ 'proxyAI',
48
+ 'languageAI'
55
49
  ]),
56
50
  timestamp: Date.now(),
57
51
  id: chatId
58
52
  }
59
53
 
60
54
  window.store.aiChatHistory.push(chatEntry)
61
-
62
- try {
63
- const aiResponse = await window.pre.runGlobalAsync(
64
- 'AIchat',
65
- prompt,
66
- props.config.modelAI,
67
- buildRole(),
68
- props.config.baseURLAI,
69
- props.config.apiPathAI,
70
- props.config.apiKeyAI,
71
- props.config.proxyAI,
72
- true // Enable streaming for chat
73
- )
74
-
75
- if (aiResponse && aiResponse.error) {
76
- // Remove the placeholder entry and show error
77
- const index = window.store.aiChatHistory.findIndex(item => item.id === chatId)
78
- if (index !== -1) {
79
- window.store.aiChatHistory.splice(index, 1)
80
- }
81
- setIsLoading(false)
82
- return window.store.onError(
83
- new Error(aiResponse.error)
84
- )
85
- }
86
-
87
- if (aiResponse && aiResponse.isStream && aiResponse.sessionId) {
88
- // Handle streaming response with polling
89
- const index = window.store.aiChatHistory.findIndex(item => item.id === chatId)
90
- if (index !== -1) {
91
- window.store.aiChatHistory[index].isStreaming = true
92
- window.store.aiChatHistory[index].sessionId = aiResponse.sessionId
93
- window.store.aiChatHistory[index].response = aiResponse.content || ''
94
- }
95
- // Store current session ID for stop functionality
96
- setCurrentSessionId(aiResponse.sessionId)
97
-
98
- // Start polling for updates
99
- pollStreamContent(aiResponse.sessionId, chatId)
100
- } else if (aiResponse && aiResponse.response) {
101
- // Handle non-streaming response (fallback)
102
- const index = window.store.aiChatHistory.findIndex(item => item.id === chatId)
103
- if (index !== -1) {
104
- window.store.aiChatHistory[index].response = aiResponse.response
105
- window.store.aiChatHistory[index].isStreaming = false
106
- }
107
- setIsLoading(false)
108
- }
109
- } catch (error) {
110
- // Remove the placeholder entry and show error
111
- const index = window.store.aiChatHistory.findIndex(item => item.id === chatId)
112
- if (index !== -1) {
113
- window.store.aiChatHistory.splice(index, 1)
114
- }
115
- setIsLoading(false)
116
- window.store.onError(error)
117
- }
55
+ setPrompt('')
118
56
 
119
57
  if (window.store.aiChatHistory.length > MAX_HISTORY) {
120
58
  window.store.aiChatHistory.splice(MAX_HISTORY)
121
59
  }
122
- setPrompt('')
123
- }, [prompt, isLoading])
124
-
125
- // Function to poll for streaming content updates
126
- const pollStreamContent = async (sessionId, chatId) => {
127
- try {
128
- const streamResponse = await window.pre.runGlobalAsync('getStreamContent', sessionId)
129
-
130
- if (streamResponse && streamResponse.error) {
131
- // Session not found or error - stop polling
132
- if (streamResponse.error === 'Session not found') {
133
- setCurrentSessionId(null)
134
- setIsLoading(false)
135
- return
136
- }
137
- // Remove the entry and show error
138
- const index = window.store.aiChatHistory.findIndex(item => item.id === chatId)
139
- if (index !== -1) {
140
- window.store.aiChatHistory.splice(index, 1)
141
- }
142
- setIsLoading(false)
143
- return window.store.onError(new Error(streamResponse.error))
144
- }
145
-
146
- // Update the chat entry with new content
147
- const index = window.store.aiChatHistory.findIndex(item => item.id === chatId)
148
- if (index !== -1) {
149
- window.store.aiChatHistory[index].response = streamResponse.content || ''
150
- window.store.aiChatHistory[index].isStreaming = streamResponse.hasMore
151
-
152
- // Force re-render by updating the array reference
153
- window.store.aiChatHistory = [...window.store.aiChatHistory]
154
-
155
- // Continue polling if there's more content
156
- if (streamResponse.hasMore) {
157
- setTimeout(() => pollStreamContent(sessionId, chatId), 200) // Poll every 200ms
158
- } else {
159
- setCurrentSessionId(null)
160
- setIsLoading(false)
161
- }
162
- }
163
- } catch (error) {
164
- // Remove the entry and show error
165
- const index = window.store.aiChatHistory.findIndex(item => item.id === chatId)
166
- if (index !== -1) {
167
- window.store.aiChatHistory.splice(index, 1)
168
- }
169
- setCurrentSessionId(null)
170
- setIsLoading(false)
171
- window.store.onError(error)
172
- }
173
- }
60
+ }, [prompt])
174
61
 
175
62
  function renderHistory () {
176
63
  return (
@@ -188,41 +75,7 @@ export default function AIChat (props) {
188
75
  window.store.aiChatHistory = []
189
76
  }
190
77
 
191
- const handleStop = useCallback(async function () {
192
- if (!currentSessionId || !isLoading) return
193
-
194
- try {
195
- // Call server to stop the stream
196
- await window.pre.runGlobalAsync('stopStream', currentSessionId)
197
-
198
- // Reset state
199
- setCurrentSessionId(null)
200
- setIsLoading(false)
201
-
202
- // Update the chat entry to mark as stopped
203
- const chatEntries = window.store.aiChatHistory
204
- for (let i = chatEntries.length - 1; i >= 0; i--) {
205
- if (chatEntries[i].isStreaming) {
206
- chatEntries[i].isStreaming = false
207
- break
208
- }
209
- }
210
- window.store.aiChatHistory = [...chatEntries]
211
- } catch (error) {
212
- console.error('Error stopping stream:', error)
213
- setCurrentSessionId(null)
214
- setIsLoading(false)
215
- }
216
- }, [currentSessionId, isLoading])
217
-
218
78
  function renderSendIcon () {
219
- if (isLoading) {
220
- return (
221
- <AIStopIcon
222
- onClick={handleStop}
223
- />
224
- )
225
- }
226
79
  return (
227
80
  <SendOutlined
228
81
  onClick={handleSubmit}
@@ -254,7 +107,6 @@ export default function AIChat (props) {
254
107
  e.preventDefault()
255
108
  handleSubmit()
256
109
  }
257
- // If Shift+Enter, allow default behavior (new line)
258
110
  }
259
111
 
260
112
  return (
@@ -270,7 +122,6 @@ export default function AIChat (props) {
270
122
  onPressEnter={handleKeyPress}
271
123
  placeholder='Enter your prompt here'
272
124
  autoSize={{ minRows: 3, maxRows: 10 }}
273
- disabled={isLoading}
274
125
  className='ai-chat-textarea'
275
126
  />
276
127
  <Flex className='ai-chat-terminals' justify='space-between' align='center'>
@@ -66,9 +66,14 @@
66
66
  .ai-stop-icon-square
67
67
  width 20px
68
68
  height 20px
69
- background var(--error)
70
69
  border-radius 2px
71
70
  display flex
72
71
  align-items center
73
72
  justify-content center
74
73
  font-size 10px
74
+
75
+ .ai-history-item-title
76
+ display flex
77
+ align-items center
78
+ .ai-stop-icon-square
79
+ margin-left auto