@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.
- package/client/common/cache.js +4 -2
- package/client/common/db.js +3 -5
- package/client/common/default-setting.js +1 -1
- package/client/common/pass-enc.js +0 -21
- package/client/common/safe-local-storage.js +106 -1
- package/client/components/ai/ai-chat-history-item.jsx +132 -8
- package/client/components/ai/ai-chat.jsx +10 -159
- package/client/components/ai/ai.styl +6 -1
- package/client/components/bookmark-form/common/render-auth-ssh.jsx +1 -117
- package/client/components/bookmark-form/config/common-fields.js +8 -0
- package/client/components/bookmark-form/config/ftp.js +1 -0
- package/client/components/bookmark-form/config/local.js +10 -51
- package/client/components/session/sessions.jsx +1 -0
- package/client/components/setting-panel/setting-terminal.jsx +2 -2
- package/client/components/sftp/address-bookmark-item.jsx +1 -1
- package/client/components/sftp/address-bookmark.jsx +11 -1
- package/client/components/sftp/file-item.jsx +1 -1
- package/client/components/sftp/sftp-entry.jsx +35 -12
- package/client/components/sidebar/history.jsx +1 -1
- package/client/components/tabs/app-drag.jsx +13 -12
- package/client/components/tabs/tabs.styl +1 -1
- package/client/components/terminal/reconnect-overlay.jsx +27 -0
- package/client/components/terminal/socket-close-warning.jsx +94 -0
- package/client/components/terminal/terminal.jsx +87 -58
- package/client/components/terminal/terminal.styl +12 -0
- package/client/components/terminal/transfer-client-base.js +3 -3
- package/client/components/text-editor/edit-with-custom-editor.jsx +3 -2
- package/client/store/init-state.js +3 -3
- package/client/store/sync.js +0 -1
- package/client/store/tab.js +2 -1
- package/client/store/watch.js +3 -3
- package/package.json +1 -1
package/client/common/cache.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
33
|
+
safeSetItem(this.key, JSON.stringify(arr))
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
set (key, value) {
|
package/client/common/db.js
CHANGED
|
@@ -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 {
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
-
|
|
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> {
|
|
172
|
+
<b>Model:</b> {modelAI}
|
|
50
173
|
</p>
|
|
51
174
|
<p>
|
|
52
|
-
<b>Role:</b> {
|
|
175
|
+
<b>Role:</b> {roleAI}
|
|
53
176
|
</p>
|
|
54
177
|
<p>
|
|
55
|
-
<b>Base URL:</b> {
|
|
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
|
-
|
|
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()
|
|
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: '',
|
|
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
|
-
|
|
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
|