@electerm/electerm-react 1.51.21 → 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.
- package/client/common/constants.js +1 -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 +164 -0
- package/client/components/ai/ai-config.jsx +133 -0
- package/client/components/ai/ai-output.jsx +123 -0
- package/client/components/ai/ai.styl +69 -0
- package/client/components/ai/providers.js +14 -0
- 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 +14 -2
- 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/sidebar/app-running-time.jsx +35 -0
- package/client/components/sidebar/info-modal.jsx +2 -0
- package/client/components/tabs/app-drag.jsx +1 -1
- package/client/components/tabs/index.jsx +8 -17
- 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 +298 -0
- package/client/store/tab.js +54 -1
- package/client/store/watch.js +9 -2
- package/package.json +1 -1
|
@@ -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
|
+
]
|