feat: add models
This commit is contained in:
parent
be71f659ac
commit
392dfcee13
131
src/renderer/src/components/Popups/ModalListPopup.tsx
Normal file
131
src/renderer/src/components/Popups/ModalListPopup.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
import { Button, Modal } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import { TopView } from '../TopView'
|
||||
import { Model, Provider } from '@renderer/types'
|
||||
import { groupBy } from 'lodash'
|
||||
import styled from 'styled-components'
|
||||
import { MinusOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import { SYSTEM_MODELS } from '@renderer/config/models'
|
||||
|
||||
interface ShowParams {
|
||||
provider: Provider
|
||||
}
|
||||
|
||||
interface Props extends ShowParams {
|
||||
resolve: (data: any) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const { provider, addModel, removeModel } = useProvider(_provider.id)
|
||||
|
||||
const systemModels = SYSTEM_MODELS[_provider.id]
|
||||
const systemModelGroups = groupBy(systemModels, 'group')
|
||||
|
||||
const onOk = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
resolve({})
|
||||
}
|
||||
|
||||
const onAddModel = (model: Model) => {
|
||||
addModel(model)
|
||||
}
|
||||
|
||||
const onRemoveModel = (model: Model) => {
|
||||
removeModel(model)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={String(provider.name + ' Models').toUpperCase()}
|
||||
open={open}
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
footer={null}
|
||||
width="600px"
|
||||
styles={{
|
||||
content: { padding: 0 },
|
||||
header: { padding: 22, paddingBottom: 15 }
|
||||
}}>
|
||||
<ListContainer>
|
||||
{Object.keys(systemModelGroups).map((group) => (
|
||||
<div key={group}>
|
||||
<ListHeader key={group}>{group}</ListHeader>
|
||||
{systemModelGroups[group].map((model) => {
|
||||
const hasModel = provider.models.find((m) => m.id === model.id)
|
||||
return (
|
||||
<ListItem key={model.id}>
|
||||
<ListItemName>{model.id}</ListItemName>
|
||||
{hasModel ? (
|
||||
<Button type="default" onClick={() => onRemoveModel(model)} icon={<MinusOutlined />} />
|
||||
) : (
|
||||
<Button type="primary" onClick={() => onAddModel(model)} icon={<PlusOutlined />} />
|
||||
)}
|
||||
</ListItem>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</ListContainer>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const ListContainer = styled.div`
|
||||
max-height: 70vh;
|
||||
overflow-y: scroll;
|
||||
padding-bottom: 20px;
|
||||
`
|
||||
|
||||
const ListHeader = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--color-background-soft);
|
||||
padding: 8px 22px;
|
||||
color: #ffffff50;
|
||||
`
|
||||
|
||||
const ListItem = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 22px;
|
||||
`
|
||||
|
||||
const ListItemName = styled.div`
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
`
|
||||
|
||||
export default class ModalListPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(this.topviewId)
|
||||
}
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<any>((resolve) => {
|
||||
this.topviewId = TopView.show(
|
||||
<PopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
this.hide()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
54
src/renderer/src/components/Popups/TemplatePopup.tsx
Normal file
54
src/renderer/src/components/Popups/TemplatePopup.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import { Modal } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import { TopView } from '../TopView'
|
||||
import { Box } from '../Layout'
|
||||
|
||||
interface ShowParams {
|
||||
title: string
|
||||
}
|
||||
|
||||
interface Props extends ShowParams {
|
||||
resolve: (data: any) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
|
||||
const onOk = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
resolve({})
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal title={title} open={open} onOk={onOk} onCancel={onCancel} afterClose={onClose}>
|
||||
<Box mb={8}>Name</Box>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default class TemplatePopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(this.topviewId)
|
||||
}
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<any>((resolve) => {
|
||||
this.topviewId = TopView.show(
|
||||
<PopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
this.hide()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -28,7 +28,7 @@ const Sidebar: FC = () => {
|
||||
</Menus>
|
||||
</MainMenus>
|
||||
<Menus>
|
||||
<StyledLink to="/settings/general">
|
||||
<StyledLink to="/settings/common">
|
||||
<Icon className={pathname.startsWith('/settings') ? 'active' : ''}>
|
||||
<i className="iconfont icon-setting"></i>
|
||||
</Icon>
|
||||
|
||||
144
src/renderer/src/config/assistant.ts
Normal file
144
src/renderer/src/config/assistant.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import { SystemAssistant } from '@renderer/types'
|
||||
|
||||
export const SYSTEM_ASSISTANTS: SystemAssistant[] = [
|
||||
// Software Engineer
|
||||
{
|
||||
id: '43CEDACF-C9EB-431B-848C-4D08EC26EB90',
|
||||
name: '软件工程师',
|
||||
description: '你是一个高级软件工程师,你需要帮我解答各种技术难题',
|
||||
prompt:
|
||||
'你是一个高级软件工程师,你需要帮我解答各种技术难题、设计技术方案以及编写代码。你编写的代码必须可以正常运行,而且没有任何 Bug 和其他问题。',
|
||||
group: 'Software Engineer'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D2A',
|
||||
name: '前端工程师',
|
||||
description: '你是一个高级前端工程师,你需要帮我解答各种技术难题',
|
||||
prompt:
|
||||
'你擅长使用 TypeScript, JavaScript, HMLT, CSS 等编程语言。同时你还会使用 Node.js 及各种包来解决开发中遇到的问题。你还会使用 React, Vue 等前端框架。对于我的问题希望你能给出具体的代码示例,最好能够封装成一个函数方便我复制运行测试。',
|
||||
group: 'Software Engineer'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D2B',
|
||||
name: '后端工程师',
|
||||
description: '你是一个高级后端工程师,你需要帮我解答各种技术难题',
|
||||
prompt: '你是一个高级后端工程师,你需要帮我解答各种技术难题',
|
||||
group: 'Software Engineer'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D2C',
|
||||
name: '全栈工程师',
|
||||
description: '你是一个高级全栈工程师,你需要帮我解答各种技术难题',
|
||||
prompt: '你是一个高级全栈工程师,你需要帮我解答各种技术难题',
|
||||
group: 'Software Engineer'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D2D',
|
||||
name: '测试工程师',
|
||||
description: '你是一个高级测试工程师,你需要帮我解答各种技术难题',
|
||||
prompt: '你是一个高级测试工程师,你需要帮我解答各种技术难题',
|
||||
group: 'Software Engineer'
|
||||
},
|
||||
// Programming Languages Assistants
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D2E',
|
||||
name: 'Python',
|
||||
description: '你是一个高级Python工程师,你需要帮我解答各种技术难题',
|
||||
prompt: '你是一个高级Python工程师,你需要帮我解答各种技术难题',
|
||||
group: 'Programming Languages'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D2F',
|
||||
name: 'Java',
|
||||
description: '你是一个高级Java工程师,你需要帮我解答各种技术难题',
|
||||
prompt: '你是一个高级Java工程师,你需要帮我解答各种技术难题',
|
||||
group: 'Programming Languages'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D30',
|
||||
name: 'C#',
|
||||
description: '你是一个高级C#工程师,你需要帮我解答各种技术难题',
|
||||
prompt: '你是一个高级C#工程师,你需要帮我解答各种技术难题',
|
||||
group: 'Programming Languages'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D31',
|
||||
name: 'C++',
|
||||
description: '你是一个高级C++工程师,你需要帮我解答各种技术难题',
|
||||
prompt: '你是一个高级C++工程师,你需要帮我解答各种技术难题',
|
||||
group: 'Programming Languages'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D32',
|
||||
name: 'C',
|
||||
description: '你是一个高级C工程师,你需要帮我解答各种技术难题',
|
||||
prompt: '你是一个高级C工程师,你需要帮我解答各种技术难题',
|
||||
group: 'Programming Languages'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D33',
|
||||
name: 'Go',
|
||||
description: '你是一个高级Go工程师,你需要帮我解答各种技术难题',
|
||||
prompt: '你是一个高级Go工程师,你需要帮我解答各种技术难题',
|
||||
group: 'Programming Languages'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D34',
|
||||
name: 'Rust',
|
||||
description: '你是一个高级Rust工程师,你需要帮我解答各种技术难题',
|
||||
prompt: '你是一个高级Rust工程师,你需要帮我解答各种技术难题',
|
||||
group: 'Programming Languages'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D35',
|
||||
name: 'PHP',
|
||||
description: '你是一个高级PHP工程师,你需要帮我解答各种技术难题',
|
||||
prompt: '你是一个高级PHP工程师,你需要帮我解答各种技术难题',
|
||||
group: 'Programming Languages'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D36',
|
||||
name: 'Ruby',
|
||||
description: '你是一个高级Ruby工程师,你需要帮我解答各种技术难题',
|
||||
prompt: '你是一个高级Ruby工程师,你需要帮我解答各种技术难题',
|
||||
group: 'Programming Languages'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D37',
|
||||
name: 'Swift',
|
||||
description: '你是一个高级Swift工程师,你需要帮我解答各种技术难题',
|
||||
prompt: '你是一个高级Swift工程师,你需要帮我解答各种技术难题',
|
||||
group: 'Programming Languages'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D38',
|
||||
name: 'Kotlin',
|
||||
description: '你是一个高级Kotlin工程师,你需要帮我解答各种技术难题',
|
||||
prompt: '你是一个高级Kotlin工程师,你需要帮我解答各种技术难题',
|
||||
group: 'Programming Languages'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D39',
|
||||
name: 'Dart',
|
||||
description: '你是一个高级Dart工程师,你需要帮我解答各种技术难题',
|
||||
prompt: '你是一个高级Dart工程师,你需要帮我解答各种技术难题',
|
||||
group: 'Programming Languages'
|
||||
},
|
||||
// Translation
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D40',
|
||||
name: '翻译成中文',
|
||||
description: '你是一个好用的翻译助手, 可以把任何语言翻译成中文',
|
||||
prompt:
|
||||
'你是一个好用的翻译助手。请将我的英文翻译成中文,将所有非中文的翻译成中文。我发给你所有的话都是需要翻译的内容,你只需要回答翻译结果。翻译结果请符合中文的语言习惯。',
|
||||
group: 'Translation'
|
||||
},
|
||||
{
|
||||
id: '6B1D8E9F-9B7F-4E2B-8FBB-0F5B6F7B0D41',
|
||||
name: '翻译成英文',
|
||||
description: '你是一个好用的翻译助手, 可以把任何语言翻译成英文',
|
||||
prompt:
|
||||
'你是一个好用的翻译助手。请将我的中文翻译成英文,将所有非中文的翻译成英文。我发给你所有的话都是需要翻译的内容,你只需要回答翻译结果。翻译结果请符合英文的语言习惯。',
|
||||
group: 'Translation'
|
||||
}
|
||||
]
|
||||
222
src/renderer/src/config/models.ts
Normal file
222
src/renderer/src/config/models.ts
Normal file
@ -0,0 +1,222 @@
|
||||
import { Model } from '@renderer/types'
|
||||
|
||||
export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
openai: [
|
||||
{
|
||||
id: 'gpt-3.5-turbo',
|
||||
name: 'gpt-3.5-turbo',
|
||||
group: 'GPT 3.5',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'gpt-3.5-turbo-0301',
|
||||
name: 'gpt-3.5-turbo',
|
||||
group: 'GPT 3.5',
|
||||
temperature: 0.3
|
||||
},
|
||||
{
|
||||
id: 'gpt-4',
|
||||
name: 'gpt-4',
|
||||
group: 'GPT 4',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'gpt-4-0314',
|
||||
name: 'gpt-4',
|
||||
group: 'GPT 4',
|
||||
temperature: 0.3
|
||||
},
|
||||
{
|
||||
id: 'gpt-4-32k',
|
||||
name: 'gpt-4-32k',
|
||||
group: 'GPT 4',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'gpt-4-32k-0314',
|
||||
name: 'gpt-4-32k',
|
||||
group: 'GPT 4',
|
||||
temperature: 0.3
|
||||
}
|
||||
],
|
||||
silicon: [
|
||||
{
|
||||
id: 'deepseek-ai/DeepSeek-V2-Chat',
|
||||
name: 'DeepSeek-V2-Chat',
|
||||
group: 'DeepSeek',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'deepseek-ai/DeepSeek-Coder-V2-Instruct',
|
||||
name: 'DeepSeek-Coder-V2-Instruct',
|
||||
group: 'DeepSeek',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'deepseek-ai/deepseek-llm-67b-chat',
|
||||
name: 'deepseek-llm-67b-chat',
|
||||
group: 'DeepSeek',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'google/gemma-2-27b-it',
|
||||
name: 'gemma-2-27b-it',
|
||||
group: 'Gemma',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'google/gemma-2-9b-it',
|
||||
name: 'gemma-2-9b-it',
|
||||
group: 'Gemma',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen2-7B-Instruct',
|
||||
name: 'Qwen2-7B-Instruct',
|
||||
group: 'Qwen2',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen2-1.5B-Instruct',
|
||||
name: 'Qwen2-1.5B-Instruct',
|
||||
group: 'Qwen2',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen1.5-7B-Chat',
|
||||
name: 'Qwen1.5-7B-Chat',
|
||||
group: 'Qwen1.5',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen2-72B-Instruct',
|
||||
name: 'Qwen2-72B-Instruct',
|
||||
group: 'Qwen2',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen2-57B-A14B-Instruct',
|
||||
name: 'Qwen2-57B-A14B-Instruct',
|
||||
group: 'Qwen2',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen1.5-110B-Chat',
|
||||
name: 'Qwen1.5-110B-Chat',
|
||||
group: 'Qwen1.5',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen1.5-32B-Chat',
|
||||
name: 'Qwen1.5-32B-Chat',
|
||||
group: 'Qwen1.5',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'Qwen/Qwen1.5-14B-Chat',
|
||||
name: 'Qwen1.5-14B-Chat',
|
||||
group: 'Qwen1.5',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'THUDM/glm-4-9b-chat',
|
||||
name: 'glm-4-9b-chat',
|
||||
group: 'GLM',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'THUDM/chatglm3-6b',
|
||||
name: 'chatglm3-6b',
|
||||
group: 'GLM',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: '01-ai/Yi-1.5-9B-Chat-16K',
|
||||
name: 'Yi-1.5-9B-Chat-16K',
|
||||
group: 'Yi',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: '01-ai/Yi-1.5-6B-Chat',
|
||||
name: 'Yi-1.5-6B-Chat',
|
||||
group: 'Yi',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: '01-ai/Yi-1.5-34B-Chat-16K',
|
||||
name: 'Yi-1.5-34B-Chat-16K',
|
||||
group: 'Yi',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'OpenAI/GPT-4o',
|
||||
name: 'GPT-4o',
|
||||
group: 'OpenAI',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'OpenAI/GPT-3.5 Turbo',
|
||||
name: 'GPT-3.5 Turbo',
|
||||
group: 'OpenAI',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'Anthropic/claude-3-5-sonnet',
|
||||
name: 'claude-3-5-sonnet',
|
||||
group: 'Claude',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'meta-llama/Meta-Llama-3-8B-Instruct',
|
||||
name: 'Meta-Llama-3-8B-Instruct',
|
||||
group: 'Meta Llama',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'meta-llama/Meta-Llama-3-70B-Instruct',
|
||||
name: 'Meta-Llama-3-70B-Instruct',
|
||||
group: 'Meta Llama',
|
||||
temperature: 0.7
|
||||
}
|
||||
],
|
||||
deepseek: [
|
||||
{
|
||||
id: 'deepseek-chat',
|
||||
name: 'deepseek-chat',
|
||||
group: 'Deepseek Chat',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'deepseek-coder',
|
||||
name: 'deepseek-coder',
|
||||
group: 'Deepseek Coder',
|
||||
temperature: 1.0
|
||||
}
|
||||
],
|
||||
groq: [
|
||||
{
|
||||
id: 'llama3-8b-8192',
|
||||
name: 'LLaMA3 8b',
|
||||
group: 'Llama3',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'llama3-70b-8192',
|
||||
name: 'LLaMA3 70b',
|
||||
group: 'Llama3',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'mixtral-8x7b-32768',
|
||||
name: 'Mixtral 8x7b',
|
||||
group: 'Mixtral',
|
||||
temperature: 0.7
|
||||
},
|
||||
{
|
||||
id: 'gemma-7b-it',
|
||||
name: 'Gemma 7b',
|
||||
group: 'Gemma',
|
||||
temperature: 0.7
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -11,7 +11,7 @@ import {
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import localforage from 'localforage'
|
||||
|
||||
export default function useAssistants() {
|
||||
export function useAssistants() {
|
||||
const { assistants } = useAppSelector((state) => state.assistants)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
32
src/renderer/src/hooks/useProvider.ts
Normal file
32
src/renderer/src/hooks/useProvider.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
addModel as _addModel,
|
||||
removeModel as _removeModel,
|
||||
updateProvider as _updateProvider
|
||||
} from '@renderer/store/llm'
|
||||
import { Model, Provider } from '@renderer/types'
|
||||
|
||||
export function useProviders() {
|
||||
return useAppSelector((state) => state.llm.providers)
|
||||
}
|
||||
|
||||
export function useProvider(id: string) {
|
||||
const provider = useAppSelector((state) => state.llm.providers.find((p) => p.id === id) as Provider)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return {
|
||||
provider,
|
||||
models: provider.models,
|
||||
updateProvider: (provider: Provider) => dispatch(_updateProvider(provider)),
|
||||
addModel: (model: Model) => dispatch(_addModel({ providerId: id, model })),
|
||||
removeModel: (model: Model) => dispatch(_removeModel({ providerId: id, model }))
|
||||
}
|
||||
}
|
||||
|
||||
export function useDefaultProvider() {
|
||||
return useAppSelector((state) => state.llm.providers.find((p) => p.isDefault))
|
||||
}
|
||||
|
||||
export function useSystemProviders() {
|
||||
return useAppSelector((state) => state.llm.providers.filter((p) => p.isSystem))
|
||||
}
|
||||
@ -1,13 +1,81 @@
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { SYSTEM_ASSISTANTS } from '@renderer/config/assistant'
|
||||
import { Button, Col, message, Row, Tooltip, Typography } from 'antd'
|
||||
import { find, groupBy } from 'lodash'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { CheckOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import { SystemAssistant } from '@renderer/types'
|
||||
import { getDefaultAssistant } from '@renderer/services/assistant'
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
const AppsPage: FC = () => {
|
||||
const { assistants, addAssistant } = useAssistants()
|
||||
const assistantGroups = groupBy(SYSTEM_ASSISTANTS, 'group')
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
|
||||
const onAddAssistant = (assistant: SystemAssistant) => {
|
||||
addAssistant({
|
||||
...getDefaultAssistant(),
|
||||
...assistant
|
||||
})
|
||||
messageApi.destroy()
|
||||
messageApi.open({
|
||||
type: 'success',
|
||||
content: 'Assistant added successfully',
|
||||
style: {
|
||||
marginTop: '5vh'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{contextHolder}
|
||||
<Navbar>
|
||||
<NavbarCenter>Assistant Market</NavbarCenter>
|
||||
</Navbar>
|
||||
<ContentContainer>
|
||||
{Object.keys(assistantGroups).map((group) => (
|
||||
<div key={group}>
|
||||
<Title level={3} key={group} style={{ marginBottom: 16 }}>
|
||||
{group}
|
||||
</Title>
|
||||
<Row gutter={16}>
|
||||
{assistantGroups[group].map((assistant, index) => {
|
||||
const added = find(assistants, { id: assistant.id })
|
||||
return (
|
||||
<Col span={6} key={group + index} style={{ marginBottom: 16 }}>
|
||||
<AssistantCard>
|
||||
<AssistantHeader>
|
||||
<Title level={5} style={{ marginBottom: 0, color: '#00b96b' }}>
|
||||
{assistant.name}
|
||||
</Title>
|
||||
{added && <Button type="primary" shape="circle" size="small" icon={<CheckOutlined />} />}
|
||||
{!added && (
|
||||
<Tooltip placement="top" title=" Add to assistant list " arrow>
|
||||
<Button
|
||||
type="default"
|
||||
shape="circle"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => onAddAssistant(assistant)}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</AssistantHeader>
|
||||
<AssistantCardDescription>{assistant.description}</AssistantCardDescription>
|
||||
<AssistantCardPrompt>{assistant.prompt}</AssistantCardPrompt>
|
||||
</AssistantCard>
|
||||
</Col>
|
||||
)
|
||||
})}
|
||||
</Row>
|
||||
</div>
|
||||
))}
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@ -15,6 +83,46 @@ const AppsPage: FC = () => {
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
padding: 20px;
|
||||
overflow-y: scroll;
|
||||
`
|
||||
|
||||
const AssistantCard = styled.div`
|
||||
margin-bottom: 16px;
|
||||
background-color: #141414;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
`
|
||||
|
||||
const AssistantHeader = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const AssistantCardDescription = styled.div`
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.5;
|
||||
`
|
||||
|
||||
const AssistantCardPrompt = styled.div`
|
||||
color: white;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.5;
|
||||
`
|
||||
|
||||
export default AppsPage
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Navbar, NavbarCenter, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import useAssistants from '@renderer/hooks/useAssistants'
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { FC, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Chat from './components/Chat/Chat'
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { FC, useRef } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import useAssistants from '@renderer/hooks/useAssistants'
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { Dropdown, MenuProps } from 'antd'
|
||||
import { last } from 'lodash'
|
||||
|
||||
@ -5,7 +5,7 @@ import Inputbar from './Inputbar'
|
||||
import Conversations from './Conversations'
|
||||
import { Flex } from 'antd'
|
||||
import TopicList from './TopicList'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistants'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useActiveTopic } from '@renderer/hooks/useTopic'
|
||||
|
||||
interface Props {
|
||||
|
||||
@ -7,7 +7,7 @@ import MessageItem from './Message'
|
||||
import { reverse } from 'lodash'
|
||||
import hljs from 'highlight.js'
|
||||
import { fetchChatCompletion, fetchConversationSummary } from '@renderer/services/api'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistants'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { DEFAULT_TOPIC_NAME } from '@renderer/config/constant'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import LocalStorage from '@renderer/services/storage'
|
||||
|
||||
@ -6,7 +6,7 @@ import styled from 'styled-components'
|
||||
import { MoreOutlined } from '@ant-design/icons'
|
||||
import { Button, Popconfirm, Tooltip } from 'antd'
|
||||
import { useShowRightSidebar } from '@renderer/hooks/useStore'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistants'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { ClearOutlined, HistoryOutlined, PlusCircleOutlined } from '@ant-design/icons'
|
||||
|
||||
interface Props {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistants'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useShowRightSidebar } from '@renderer/hooks/useStore'
|
||||
import { fetchConversationSummary } from '@renderer/services/api'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
|
||||
@ -5,6 +5,8 @@ const AboutSettings: FC = () => {
|
||||
return <Container>About</Container>
|
||||
}
|
||||
|
||||
const Container = styled.div``
|
||||
const Container = styled.div`
|
||||
padding: 20px;
|
||||
`
|
||||
|
||||
export default AboutSettings
|
||||
|
||||
@ -5,6 +5,7 @@ const CommonSettings: FC = () => {
|
||||
return <Container>Common Settings</Container>
|
||||
}
|
||||
|
||||
const Container = styled.div``
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 20px;
|
||||
`
|
||||
export default CommonSettings
|
||||
|
||||
@ -5,6 +5,7 @@ const DefaultAssistantSetting: FC = () => {
|
||||
return <Container>Default Assistant</Container>
|
||||
}
|
||||
|
||||
const Container = styled.div``
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 20px;
|
||||
`
|
||||
export default DefaultAssistantSetting
|
||||
|
||||
@ -5,6 +5,7 @@ const DeveloperSetting: FC = () => {
|
||||
return <Container>Developer</Container>
|
||||
}
|
||||
|
||||
const Container = styled.div``
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 20px;
|
||||
`
|
||||
export default DeveloperSetting
|
||||
|
||||
@ -1,36 +1,64 @@
|
||||
import { Collapse } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useSystemProviders } from '@renderer/hooks/useProvider'
|
||||
import { Provider } from '@renderer/types'
|
||||
import { FC, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import ModalProviderSetting from './components/ModalProviderSetting'
|
||||
|
||||
// OpenAI Silicon deepseek Groq
|
||||
const LanguageModelsSettings: FC = () => {
|
||||
const providers = useSystemProviders()
|
||||
const [selectedProvider, setSelectedProvider] = useState<Provider>(providers[0])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Collapse style={{ width: '100%', marginBottom: 10 }}>
|
||||
<Collapse.Panel header="OpenAI" key="openai">
|
||||
<p>OpenAI</p>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
<Collapse style={{ width: '100%', marginBottom: 10 }}>
|
||||
<Collapse.Panel header="Silicon" key="silicon">
|
||||
<p>Silicon</p>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
<Collapse style={{ width: '100%', marginBottom: 10 }}>
|
||||
<Collapse.Panel header="deepseek" key="deepseek">
|
||||
<p>deepseek</p>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
<Collapse style={{ width: '100%', marginBottom: 10 }}>
|
||||
<Collapse.Panel header="Groq" key="groq">
|
||||
<p>Groq</p>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
<ProviderListContainer>
|
||||
{providers.map((provider) => (
|
||||
<ProviderListItem
|
||||
key={JSON.stringify(provider)}
|
||||
className={provider.id === selectedProvider?.id ? 'active' : ''}
|
||||
onClick={() => setSelectedProvider(provider)}>
|
||||
{provider.name}
|
||||
</ProviderListItem>
|
||||
))}
|
||||
</ProviderListContainer>
|
||||
<ModalProviderSetting provider={selectedProvider} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`
|
||||
|
||||
const ProviderListContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: var(--assistants-width);
|
||||
height: 100%;
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
padding: 10px;
|
||||
`
|
||||
|
||||
const ProviderListItem = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 6px 10px;
|
||||
margin-bottom: 5px;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
background: #135200;
|
||||
}
|
||||
&.active {
|
||||
background: #135200;
|
||||
font-weight: bold;
|
||||
}
|
||||
`
|
||||
|
||||
export default LanguageModelsSettings
|
||||
|
||||
@ -96,8 +96,7 @@ const SettingContent = styled.div`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
border-right: 1px solid var(--color-border);
|
||||
padding: 20px;
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
`
|
||||
|
||||
export default SettingsPage
|
||||
|
||||
@ -5,6 +5,7 @@ const SystemAssistantSettings: FC = () => {
|
||||
return <Container>System Assistant</Container>
|
||||
}
|
||||
|
||||
const Container = styled.div``
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 20px;
|
||||
`
|
||||
export default SystemAssistantSettings
|
||||
|
||||
@ -0,0 +1,112 @@
|
||||
import { Provider } from '@renderer/types'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Button, Card, Divider, Input } from 'antd'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import ModalListPopup from '@renderer/components/Popups/ModalListPopup'
|
||||
import { groupBy } from 'lodash'
|
||||
|
||||
interface Props {
|
||||
provider: Provider
|
||||
}
|
||||
|
||||
const ModalProviderSetting: FC<Props> = ({ provider }) => {
|
||||
const [apiKey, setApiKey] = useState(provider.apiKey)
|
||||
const [apiHost, setApiHost] = useState(provider.apiHost)
|
||||
const [apiPath, setApiPath] = useState(provider.apiPath)
|
||||
const { updateProvider, models } = useProvider(provider.id)
|
||||
|
||||
const modelGroups = groupBy(models, 'group')
|
||||
|
||||
useEffect(() => {
|
||||
setApiKey(provider.apiKey)
|
||||
setApiHost(provider.apiHost)
|
||||
setApiPath(provider.apiPath)
|
||||
}, [provider])
|
||||
|
||||
const onUpdateApiKey = () => {
|
||||
updateProvider({ ...provider, apiKey })
|
||||
}
|
||||
|
||||
const onUpdateApiHost = () => {
|
||||
updateProvider({ ...provider, apiHost })
|
||||
}
|
||||
|
||||
const onUpdateApiPath = () => {
|
||||
updateProvider({ ...provider, apiHost })
|
||||
}
|
||||
|
||||
const onAddModal = () => {
|
||||
ModalListPopup.show({ provider })
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Title>{provider.name}</Title>
|
||||
<Divider style={{ width: '100%', margin: '10px 0' }} />
|
||||
<SubTitle>API Key</SubTitle>
|
||||
<Input value={apiKey} placeholder="API Key" onChange={(e) => setApiKey(e.target.value)} onBlur={onUpdateApiKey} />
|
||||
<SubTitle>API Host</SubTitle>
|
||||
<Input
|
||||
value={apiHost}
|
||||
placeholder="API Host"
|
||||
onChange={(e) => setApiHost(e.target.value)}
|
||||
onBlur={onUpdateApiHost}
|
||||
/>
|
||||
<SubTitle>API Path</SubTitle>
|
||||
<Input
|
||||
value={apiPath}
|
||||
placeholder="API Path"
|
||||
onChange={(e) => setApiPath(e.target.value)}
|
||||
onBlur={onUpdateApiPath}
|
||||
/>
|
||||
<SubTitle>Models</SubTitle>
|
||||
{Object.keys(modelGroups).map((group) => (
|
||||
<Card key={group} type="inner" title={group} style={{ marginBottom: '10px' }} size="small">
|
||||
{modelGroups[group].map((model) => (
|
||||
<ModelListItem key={model.id}>{model.id}</ModelListItem>
|
||||
))}
|
||||
</Card>
|
||||
))}
|
||||
<Button type="primary" style={{ width: '100px', marginTop: '10px' }} onClick={onAddModal}>
|
||||
Edit Models
|
||||
</Button>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
padding: 15px;
|
||||
overflow-y: scroll;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const Title = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const SubTitle = styled.div`
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
margin: 10px 0;
|
||||
`
|
||||
|
||||
const ModelListItem = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 5px 0;
|
||||
`
|
||||
|
||||
export default ModalProviderSetting
|
||||
@ -6,5 +6,6 @@ export const EVENT_NAMES = {
|
||||
SEND_MESSAGE: 'SEND_MESSAGE',
|
||||
AI_CHAT_COMPLETION: 'AI_CHAT_COMPLETION',
|
||||
AI_AUTO_RENAME: 'AI_AUTO_RENAME',
|
||||
CLEAR_CONVERSATION: 'CLEAR_CONVERSATION'
|
||||
CLEAR_CONVERSATION: 'CLEAR_CONVERSATION',
|
||||
ADD_ASSISTANT: 'ADD_ASSISTANT'
|
||||
}
|
||||
|
||||
@ -4,10 +4,12 @@ import { FLUSH, PAUSE, PERSIST, persistReducer, persistStore, PURGE, REGISTER, R
|
||||
import storage from 'redux-persist/lib/storage'
|
||||
import assistants from './assistants'
|
||||
import settings from './settings'
|
||||
import llm from './llm'
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
assistants,
|
||||
settings
|
||||
settings,
|
||||
llm
|
||||
})
|
||||
|
||||
const store = configureStore({
|
||||
|
||||
@ -1,31 +1,88 @@
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
|
||||
type Provider = {
|
||||
id: string
|
||||
name: string
|
||||
apiKey: string
|
||||
apiUrl: string
|
||||
url: string
|
||||
}
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
import { Model, Provider } from '@renderer/types'
|
||||
import { uniqBy } from 'lodash'
|
||||
|
||||
export interface LlmState {
|
||||
providers: Provider[]
|
||||
}
|
||||
|
||||
const initialState: LlmState = {
|
||||
providers: []
|
||||
providers: [
|
||||
{
|
||||
id: 'openai',
|
||||
name: 'OpenAI',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.openai.com',
|
||||
apiPath: '/v1/chat/completions',
|
||||
isSystem: true,
|
||||
models: []
|
||||
},
|
||||
{
|
||||
id: 'silicon',
|
||||
name: 'Silicon',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.siliconflow.cn',
|
||||
apiPath: '/v1/chat/completions',
|
||||
isSystem: true,
|
||||
models: []
|
||||
},
|
||||
{
|
||||
id: 'deepseek',
|
||||
name: 'deepseek',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.deepseek.com',
|
||||
apiPath: '/v1/chat/completions',
|
||||
isSystem: true,
|
||||
models: []
|
||||
},
|
||||
{
|
||||
id: 'groq',
|
||||
name: 'Groq',
|
||||
apiKey: '',
|
||||
apiHost: 'https://api.groq.com',
|
||||
apiPath: '/v1/chat/completions',
|
||||
isSystem: true,
|
||||
models: []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const settingsSlice = createSlice({
|
||||
name: 'settings',
|
||||
initialState,
|
||||
reducers: {
|
||||
updateProvider: () => {
|
||||
//
|
||||
updateProvider: (state, action: PayloadAction<Provider>) => {
|
||||
state.providers = state.providers.map((p) => (p.id === action.payload.id ? { ...p, ...action.payload } : p))
|
||||
},
|
||||
addProvider: (state, action: PayloadAction<Provider>) => {
|
||||
state.providers.push(action.payload)
|
||||
},
|
||||
removeProvider: (state, action: PayloadAction<{ id: string }>) => {
|
||||
state.providers = state.providers.filter((p) => p.id !== action.payload.id && !p.isSystem)
|
||||
},
|
||||
addModel: (state, action: PayloadAction<{ providerId: string; model: Model }>) => {
|
||||
state.providers = state.providers.map((p) =>
|
||||
p.id === action.payload.providerId
|
||||
? {
|
||||
...p,
|
||||
models: uniqBy(p.models.concat(action.payload.model), 'id')
|
||||
}
|
||||
: p
|
||||
)
|
||||
},
|
||||
removeModel: (state, action: PayloadAction<{ providerId: string; model: Model }>) => {
|
||||
state.providers = state.providers.map((p) =>
|
||||
p.id === action.payload.providerId
|
||||
? {
|
||||
...p,
|
||||
models: p.models.filter((m) => m.id !== action.payload.model.id)
|
||||
}
|
||||
: p
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const { updateProvider } = settingsSlice.actions
|
||||
export const { updateProvider, addProvider, removeProvider, addModel, removeModel } = settingsSlice.actions
|
||||
|
||||
export default settingsSlice.reducer
|
||||
|
||||
@ -27,3 +27,29 @@ export type User = {
|
||||
avatar: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export type Provider = {
|
||||
id: string
|
||||
name: string
|
||||
apiKey: string
|
||||
apiHost: string
|
||||
apiPath: string
|
||||
models: Model[]
|
||||
isSystem?: boolean
|
||||
isDefault?: boolean
|
||||
}
|
||||
|
||||
export type Model = {
|
||||
id: string
|
||||
name: string
|
||||
group: string
|
||||
temperature: number
|
||||
}
|
||||
|
||||
export type SystemAssistant = {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
prompt: string
|
||||
group: string
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user