@electerm/electerm-react 1.51.21 → 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.
- package/client/common/constants.js +2 -0
- package/client/common/default-setting.js +12 -1
- package/client/components/ai/ai-chat-history-item.jsx +69 -0
- package/client/components/ai/ai-chat-history.jsx +31 -0
- package/client/components/ai/ai-chat.jsx +172 -0
- package/client/components/ai/ai-config.jsx +145 -0
- package/client/components/ai/ai-output.jsx +118 -0
- package/client/components/ai/ai.styl +70 -0
- package/client/components/ai/get-brand.js +34 -0
- package/client/components/ai/providers.js +14 -0
- package/client/components/bookmark-form/rdp-form-ui.jsx +1 -1
- package/client/components/footer/batch-input.jsx +13 -67
- package/client/components/footer/footer-entry.jsx +19 -3
- package/client/components/footer/footer.styl +4 -0
- package/client/components/footer/tab-select.jsx +9 -3
- package/client/components/layout/layout.jsx +5 -4
- package/client/components/main/main.jsx +20 -4
- package/client/components/shortcuts/shortcut-control.jsx +17 -2
- package/client/components/shortcuts/shortcut-handler.js +24 -8
- package/client/components/shortcuts/shortcuts-defaults.js +6 -0
- package/client/components/side-panel-r/right-side-panel.styl +6 -7
- package/client/components/side-panel-r/side-panel-r.jsx +32 -10
- package/client/components/sidebar/app-running-time.jsx +35 -0
- package/client/components/sidebar/history-item.jsx +20 -3
- package/client/components/sidebar/history.jsx +4 -1
- package/client/components/sidebar/info-modal.jsx +2 -0
- package/client/components/tabs/app-drag.jsx +1 -1
- package/client/components/tabs/index.jsx +16 -43
- package/client/components/tabs/no-session.jsx +40 -0
- package/client/components/tabs/tabs.styl +6 -0
- package/client/components/terminal/index.jsx +2 -2
- package/client/store/common.js +37 -2
- package/client/store/index.js +2 -290
- package/client/store/init-state.js +7 -1
- package/client/store/store.js +313 -0
- package/client/store/sync.js +4 -1
- package/client/store/tab.js +56 -2
- package/client/store/watch.js +9 -2
- package/package.json +1 -1
|
@@ -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,
|
|
@@ -412,3 +413,4 @@ export const terminalTypes = [
|
|
|
412
413
|
export const sshConfigLoadKey = 'ssh-config-loaded'
|
|
413
414
|
export const sshConfigKey = 'ignore-ssh-config'
|
|
414
415
|
export const connectionHoppingWarnKey = 'connectionHoppingWarnned'
|
|
416
|
+
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,172 @@
|
|
|
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 {
|
|
15
|
+
aiConfigWikiLink
|
|
16
|
+
} from '../../common/constants'
|
|
17
|
+
import HelpIcon from '../common/help-icon'
|
|
18
|
+
import './ai.styl'
|
|
19
|
+
|
|
20
|
+
const { TextArea } = Input
|
|
21
|
+
const MAX_HISTORY = 100
|
|
22
|
+
const aiConfigsArr = [
|
|
23
|
+
'baseURLAI',
|
|
24
|
+
'modelAI',
|
|
25
|
+
'roleAI',
|
|
26
|
+
'apiKeyAI'
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
export default function AIChat (props) {
|
|
30
|
+
const [prompt, setPrompt] = useState('')
|
|
31
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
32
|
+
|
|
33
|
+
function handlePromptChange (e) {
|
|
34
|
+
setPrompt(e.target.value)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function handleSubmit () {
|
|
38
|
+
if (aiConfigMissing()) {
|
|
39
|
+
window.store.toggleAIConfig()
|
|
40
|
+
}
|
|
41
|
+
if (!prompt.trim() || isLoading) return
|
|
42
|
+
setIsLoading(true)
|
|
43
|
+
const aiResponse = await window.pre.runGlobalAsync(
|
|
44
|
+
'AIchat',
|
|
45
|
+
prompt,
|
|
46
|
+
props.config.modelAI,
|
|
47
|
+
props.config.roleAI,
|
|
48
|
+
props.config.baseURLAI,
|
|
49
|
+
props.config.apiKeyAI
|
|
50
|
+
).catch(
|
|
51
|
+
window.store.onError
|
|
52
|
+
)
|
|
53
|
+
if (aiResponse && aiResponse.error) {
|
|
54
|
+
return window.store.onError(
|
|
55
|
+
new Error(aiResponse.error)
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
window.store.aiChatHistory.push({
|
|
59
|
+
prompt,
|
|
60
|
+
response: aiResponse.response,
|
|
61
|
+
...pick(props.config, [
|
|
62
|
+
'modelAI',
|
|
63
|
+
'roleAI',
|
|
64
|
+
'baseURLAI'
|
|
65
|
+
]),
|
|
66
|
+
timestamp: Date.now(),
|
|
67
|
+
id: uid()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
if (window.store.aiChatHistory.length > MAX_HISTORY) {
|
|
71
|
+
window.store.aiChatHistory.splice(MAX_HISTORY)
|
|
72
|
+
}
|
|
73
|
+
setPrompt('')
|
|
74
|
+
setIsLoading(false)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function handleConfigSubmit (values) {
|
|
78
|
+
window.store.updateConfig(values)
|
|
79
|
+
message.success('Saved')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const renderConfig = useCallback(() => {
|
|
83
|
+
if (!props.showAIConfig) return null
|
|
84
|
+
const aiConfigs = pick(props.config, aiConfigsArr)
|
|
85
|
+
return (
|
|
86
|
+
<AIConfigForm
|
|
87
|
+
initialValues={aiConfigs}
|
|
88
|
+
onSubmit={handleConfigSubmit}
|
|
89
|
+
showAIConfig={props.showAIConfig}
|
|
90
|
+
/>
|
|
91
|
+
)
|
|
92
|
+
}, [props.showAIConfig, props.config])
|
|
93
|
+
|
|
94
|
+
function renderHistory () {
|
|
95
|
+
return (
|
|
96
|
+
<AiChatHistory
|
|
97
|
+
history={props.aiChatHistory}
|
|
98
|
+
/>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function toggleConfig () {
|
|
103
|
+
window.store.toggleAIConfig()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function clearHistory () {
|
|
107
|
+
window.store.aiChatHistory = []
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function aiConfigMissing () {
|
|
111
|
+
return aiConfigsArr.some(k => !props.config[k])
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function renderSendIcon () {
|
|
115
|
+
if (isLoading) {
|
|
116
|
+
return <LoadingOutlined />
|
|
117
|
+
}
|
|
118
|
+
return (
|
|
119
|
+
<SendOutlined
|
|
120
|
+
onClick={handleSubmit}
|
|
121
|
+
className='mg1l pointer icon-hover'
|
|
122
|
+
/>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (aiConfigMissing()) {
|
|
128
|
+
window.store.toggleAIConfig()
|
|
129
|
+
}
|
|
130
|
+
}, [])
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<Flex vertical className='ai-chat-container'>
|
|
134
|
+
<Flex className='ai-chat-history' flex='auto'>
|
|
135
|
+
{renderHistory()}
|
|
136
|
+
</Flex>
|
|
137
|
+
|
|
138
|
+
<Flex className='ai-chat-input'>
|
|
139
|
+
<TextArea
|
|
140
|
+
value={prompt}
|
|
141
|
+
onChange={handlePromptChange}
|
|
142
|
+
placeholder='Enter your prompt here'
|
|
143
|
+
autoSize={{ minRows: 3, maxRows: 10 }}
|
|
144
|
+
disabled={isLoading}
|
|
145
|
+
/>
|
|
146
|
+
<Flex className='ai-chat-terminals' justify='space-between' align='center'>
|
|
147
|
+
<Flex align='center'>
|
|
148
|
+
<TabSelect
|
|
149
|
+
selectedTabIds={props.selectedTabIds}
|
|
150
|
+
tabs={props.tabs}
|
|
151
|
+
activeTabId={props.activeTabId}
|
|
152
|
+
/>
|
|
153
|
+
<SettingOutlined
|
|
154
|
+
onClick={toggleConfig}
|
|
155
|
+
className='mg1l pointer icon-hover toggle-ai-setting-icon'
|
|
156
|
+
/>
|
|
157
|
+
<UnorderedListOutlined
|
|
158
|
+
onClick={clearHistory}
|
|
159
|
+
className='mg2x pointer clear-ai-icon icon-hover'
|
|
160
|
+
title='Clear AI chat history'
|
|
161
|
+
/>
|
|
162
|
+
<HelpIcon
|
|
163
|
+
link={aiConfigWikiLink}
|
|
164
|
+
/>
|
|
165
|
+
{renderConfig()}
|
|
166
|
+
</Flex>
|
|
167
|
+
{renderSendIcon()}
|
|
168
|
+
</Flex>
|
|
169
|
+
</Flex>
|
|
170
|
+
</Flex>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Form,
|
|
3
|
+
Input,
|
|
4
|
+
Button,
|
|
5
|
+
AutoComplete,
|
|
6
|
+
Modal,
|
|
7
|
+
Alert
|
|
8
|
+
} from 'antd'
|
|
9
|
+
import { useEffect, useState } from 'react'
|
|
10
|
+
import Link from '../common/external-link'
|
|
11
|
+
import {
|
|
12
|
+
aiConfigWikiLink
|
|
13
|
+
} from '../../common/constants'
|
|
14
|
+
|
|
15
|
+
// Comprehensive API provider configurations
|
|
16
|
+
import providers from './providers'
|
|
17
|
+
|
|
18
|
+
const e = window.translate
|
|
19
|
+
|
|
20
|
+
export default function AIConfigForm ({ initialValues, onSubmit, showAIConfig }) {
|
|
21
|
+
const [form] = Form.useForm()
|
|
22
|
+
const [modelOptions, setModelOptions] = useState([])
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (initialValues) {
|
|
26
|
+
form.setFieldsValue(initialValues)
|
|
27
|
+
}
|
|
28
|
+
}, [initialValues])
|
|
29
|
+
|
|
30
|
+
function filter () {
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const getBaseURLOptions = () => {
|
|
35
|
+
return providers.map(provider => ({
|
|
36
|
+
value: provider.baseURL,
|
|
37
|
+
label: provider.label
|
|
38
|
+
}))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const getModelOptions = (baseURL) => {
|
|
42
|
+
const provider = providers.find(p => p.baseURL === baseURL)
|
|
43
|
+
if (!provider) return []
|
|
44
|
+
|
|
45
|
+
return provider.models.map(model => ({
|
|
46
|
+
value: model,
|
|
47
|
+
label: model
|
|
48
|
+
}))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const handleSubmit = async (values) => {
|
|
52
|
+
onSubmit(values)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function handleCancel () {
|
|
56
|
+
window.store.toggleAIConfig()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handleChange (v) {
|
|
60
|
+
const options = getModelOptions(v)
|
|
61
|
+
setModelOptions(options)
|
|
62
|
+
form.setFieldsValue({
|
|
63
|
+
modelAI: options[0]?.value || ''
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!showAIConfig) {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
const title = 'AI ' + e('setting')
|
|
71
|
+
return (
|
|
72
|
+
<Modal
|
|
73
|
+
title={title}
|
|
74
|
+
open
|
|
75
|
+
onCancel={handleCancel}
|
|
76
|
+
footer={null}
|
|
77
|
+
>
|
|
78
|
+
<Alert
|
|
79
|
+
message={
|
|
80
|
+
<Link to={aiConfigWikiLink}>WIKI: {aiConfigWikiLink}</Link>
|
|
81
|
+
}
|
|
82
|
+
type='info'
|
|
83
|
+
className='mg2y'
|
|
84
|
+
/>
|
|
85
|
+
<Form
|
|
86
|
+
form={form}
|
|
87
|
+
onFinish={handleSubmit}
|
|
88
|
+
initialValues={initialValues}
|
|
89
|
+
>
|
|
90
|
+
<Form.Item
|
|
91
|
+
label='API URL'
|
|
92
|
+
name='baseURLAI'
|
|
93
|
+
rules={[
|
|
94
|
+
{ required: true, message: 'Please input or select API provider URL!' },
|
|
95
|
+
{ type: 'url', message: 'Please enter a valid URL!' }
|
|
96
|
+
]}
|
|
97
|
+
>
|
|
98
|
+
<AutoComplete
|
|
99
|
+
options={getBaseURLOptions()}
|
|
100
|
+
placeholder='Enter or select API provider URL'
|
|
101
|
+
filterOption={filter}
|
|
102
|
+
onChange={handleChange}
|
|
103
|
+
allowClear
|
|
104
|
+
/>
|
|
105
|
+
</Form.Item>
|
|
106
|
+
|
|
107
|
+
<Form.Item
|
|
108
|
+
label='Model'
|
|
109
|
+
name='modelAI'
|
|
110
|
+
rules={[{ required: true, message: 'Please input or select a model!' }]}
|
|
111
|
+
>
|
|
112
|
+
<AutoComplete
|
|
113
|
+
options={modelOptions}
|
|
114
|
+
placeholder='Enter or select AI model'
|
|
115
|
+
filterOption={filter}
|
|
116
|
+
/>
|
|
117
|
+
</Form.Item>
|
|
118
|
+
|
|
119
|
+
<Form.Item
|
|
120
|
+
label='API Key'
|
|
121
|
+
name='apiKeyAI'
|
|
122
|
+
>
|
|
123
|
+
<Input.Password placeholder='Enter your API key' />
|
|
124
|
+
</Form.Item>
|
|
125
|
+
|
|
126
|
+
<Form.Item
|
|
127
|
+
label='System Role'
|
|
128
|
+
name='roleAI'
|
|
129
|
+
rules={[{ required: true, message: 'Please input the AI role!' }]}
|
|
130
|
+
>
|
|
131
|
+
<Input.TextArea
|
|
132
|
+
placeholder='Enter AI role/system prompt'
|
|
133
|
+
rows={4}
|
|
134
|
+
/>
|
|
135
|
+
</Form.Item>
|
|
136
|
+
|
|
137
|
+
<Form.Item>
|
|
138
|
+
<Button type='primary' htmlType='submit'>
|
|
139
|
+
{e('save')}
|
|
140
|
+
</Button>
|
|
141
|
+
</Form.Item>
|
|
142
|
+
</Form>
|
|
143
|
+
</Modal>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useState, useMemo } 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, DownCircleOutlined } from '@ant-design/icons'
|
|
7
|
+
import getBrand from './get-brand'
|
|
8
|
+
|
|
9
|
+
const e = window.translate
|
|
10
|
+
|
|
11
|
+
export default function AIOutput ({ item }) {
|
|
12
|
+
const [showFull, setShowFull] = useState(false)
|
|
13
|
+
const {
|
|
14
|
+
response,
|
|
15
|
+
baseURLAI
|
|
16
|
+
} = item
|
|
17
|
+
if (!response) {
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { brand, brandUrl } = getBrand(baseURLAI)
|
|
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
|
+
|
|
35
|
+
const renderCode = (props) => {
|
|
36
|
+
const { node, className = '', children, ...rest } = props
|
|
37
|
+
const code = String(children).replace(/\n$/, '')
|
|
38
|
+
const inline = !className.includes('language-')
|
|
39
|
+
if (inline) {
|
|
40
|
+
return (
|
|
41
|
+
<code className={className} {...props}>
|
|
42
|
+
{children}
|
|
43
|
+
</code>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const copyToClipboard = () => {
|
|
48
|
+
copy(code)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const runInTerminal = () => {
|
|
52
|
+
window.store.runCommandInTerminal(code)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className='code-block'>
|
|
57
|
+
<pre>
|
|
58
|
+
<code className={className} {...rest}>
|
|
59
|
+
{children}
|
|
60
|
+
</code>
|
|
61
|
+
</pre>
|
|
62
|
+
<div className='code-block-actions'>
|
|
63
|
+
<CopyOutlined
|
|
64
|
+
className='code-action-icon pointer'
|
|
65
|
+
onClick={copyToClipboard}
|
|
66
|
+
title={e('copy')}
|
|
67
|
+
/>
|
|
68
|
+
<PlayCircleOutlined
|
|
69
|
+
className='code-action-icon pointer mg1l'
|
|
70
|
+
onClick={runInTerminal}
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function renderBrand () {
|
|
78
|
+
if (!brand) {
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
return (
|
|
82
|
+
<div className='pd1y'>
|
|
83
|
+
<Link to={brandUrl}>
|
|
84
|
+
<Tag>{brand}</Tag>
|
|
85
|
+
</Link>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
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
|
+
|
|
104
|
+
const mdProps = {
|
|
105
|
+
children: showFull ? response : truncatedResponse,
|
|
106
|
+
components: {
|
|
107
|
+
code: renderCode
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className='pd1'>
|
|
113
|
+
{renderBrand()}
|
|
114
|
+
<ReactMarkdown {...mdProps} />
|
|
115
|
+
{renderShowMore()}
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
min-height 0
|
|
11
|
+
&::-webkit-scrollbar
|
|
12
|
+
width 0
|
|
13
|
+
.ai-history-wrap
|
|
14
|
+
width 100%
|
|
15
|
+
overflow-y auto
|
|
16
|
+
overflow-x hidden
|
|
17
|
+
|
|
18
|
+
.ai-config-form
|
|
19
|
+
height 200px
|
|
20
|
+
overflow-y auto
|
|
21
|
+
|
|
22
|
+
.chat-history-item
|
|
23
|
+
.code-block
|
|
24
|
+
border 1px dashed text
|
|
25
|
+
padding 5px
|
|
26
|
+
border-radius 3px
|
|
27
|
+
pre
|
|
28
|
+
margin-bottom 0
|
|
29
|
+
.code-block-actions
|
|
30
|
+
display none
|
|
31
|
+
&:hover
|
|
32
|
+
.code-block-actions
|
|
33
|
+
display block
|
|
34
|
+
|
|
35
|
+
.ai-chat-input
|
|
36
|
+
position relative
|
|
37
|
+
margin-top 10px
|
|
38
|
+
|
|
39
|
+
.ant-input
|
|
40
|
+
padding-bottom 40px
|
|
41
|
+
|
|
42
|
+
.ai-chat-terminals
|
|
43
|
+
position absolute
|
|
44
|
+
bottom 5px
|
|
45
|
+
left 5px
|
|
46
|
+
right 5px
|
|
47
|
+
background transparent
|
|
48
|
+
z-index 1
|
|
49
|
+
font-size 16px
|
|
50
|
+
|
|
51
|
+
.code-block
|
|
52
|
+
width 100%
|
|
53
|
+
pre
|
|
54
|
+
white-space pre-wrap
|
|
55
|
+
word-wrap break-word
|
|
56
|
+
overflow-x auto
|
|
57
|
+
max-width 100%
|
|
58
|
+
code
|
|
59
|
+
white-space pre-wrap
|
|
60
|
+
word-break break-all
|
|
61
|
+
|
|
62
|
+
.clear-ai-icon
|
|
63
|
+
position relative
|
|
64
|
+
&::after
|
|
65
|
+
content '×'
|
|
66
|
+
position absolute
|
|
67
|
+
font-size 12px
|
|
68
|
+
top 8px
|
|
69
|
+
right -4px
|
|
70
|
+
font-weight bold
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
]
|