Files
geekai/web/src/views/admin/ChatModel.vue

441 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="container model-list" v-loading="loading">
<div class="handle-box">
<el-input v-model="query.name" placeholder="模型名称" class="handle-input" />
<el-select v-model="query.type" placeholder="模型类型" class="handle-input" clearable>
<el-option v-for="v in modelTypes" :value="v.value" :label="v.label" :key="v.value">
{{ v.label }}
</el-option>
</el-select>
<el-button :icon="Search" @click="fetchData">搜索</el-button>
<el-button type="primary" :icon="Plus" @click="add">新增</el-button>
</div>
<el-row>
<el-table :data="items" :row-key="(row) => row.id" table-layout="auto">
<el-table-column type="selection" width="38"></el-table-column>
<el-table-column prop="name" label="模型名称">
<template #default="scope">
<span class="sort" :data-id="scope.row.id">
<i class="iconfont icon-drag"></i>
{{ scope.row.name }}
</span>
</template>
</el-table-column>
<el-table-column prop="type" label="模型类型">
<template #default="scope">
<el-tag type="primary" v-if="scope.row.type === 'img'">绘图</el-tag>
<el-tag type="success" v-else>聊天</el-tag>
</template>
</el-table-column>
<el-table-column prop="tag" label="标签" />
<el-table-column prop="value" label="模型值">
<template #default="scope">
<span>{{ scope.row.value }}</span>
<el-icon class="copy-model" :data-clipboard-text="scope.row.value">
<DocumentCopy />
</el-icon>
</template>
</el-table-column>
<el-table-column prop="power" label="费率" />
<el-table-column prop="max_tokens" label="最大响应长度" />
<el-table-column prop="max_context" label="最大上下文长度" />
<el-table-column prop="temperature" label="创意度" />
<el-table-column prop="enabled" label="启用状态">
<template #default="scope">
<el-switch v-model="scope.row['enabled']" @change="modelSet('enabled', scope.row)" />
</template>
</el-table-column>
<el-table-column prop="enabled" label="开放状态">
<template #default="scope">
<el-switch v-model="scope.row['open']" @change="modelSet('open', scope.row)" />
</template>
</el-table-column>
<el-table-column prop="key_name" label="绑定API-KEY" />
<el-table-column label="操作" width="180">
<template #default="scope">
<el-button size="small" type="primary" @click="edit(scope.row)">编辑</el-button>
<el-popconfirm title="确定要删除当前记录吗?" @confirm="remove(scope.row)" :width="200">
<template #reference>
<el-button size="small" type="danger">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</el-row>
<el-dialog
v-model="showDialog"
:title="title"
:close-on-click-modal="false"
style="width: 90%; max-width: 600px"
>
<el-form :model="item" label-width="120px" ref="formRef" :rules="rules">
<el-form-item label="模型类型" prop="type">
<el-select v-model="item.type" placeholder="请选择模型类型">
<el-option v-for="v in modelTypes" :value="v.value" :label="v.label" :key="v.value">
{{ v.label }}
</el-option>
</el-select>
</el-form-item>
<el-form-item label="模型名称" prop="name">
<el-input v-model="item.name" autocomplete="off" />
</el-form-item>
<el-form-item label="模型值" prop="value">
<el-input v-model="item.value" autocomplete="off" />
</el-form-item>
<el-form-item label="模型标签" prop="tag">
<el-input v-model="item.tag" autocomplete="off" />
</el-form-item>
<el-form-item label="消耗算力" prop="power">
<template #label>
<div class="flex items-center">
<span class="mr-1">消耗算力</span>
</div>
</template>
<el-input v-model.number="item.power" autocomplete="off" placeholder="消耗算力" />
</el-form-item>
<div v-if="item.type === 'chat'">
<el-form-item label="最长响应" prop="max_tokens">
<el-input
v-model.number="item.max_tokens"
autocomplete="off"
placeholder="模型最大响应长度"
/>
</el-form-item>
<el-form-item>
<template #label>
<div class="flex items-center">
<span class="mr-1">最大上下文</span>
<el-tooltip
effect="dark"
content="去各大模型的官方 API 文档查询模型支持的最大上下文长度"
raw-content
placement="right"
>
<el-icon>
<InfoFilled />
</el-icon>
</el-tooltip>
</div>
</template>
<el-input
v-model.number="item.max_context"
autocomplete="off"
placeholder="模型最大上下文长度"
/>
</el-form-item>
<el-form-item label="模型简介" prop="desc">
<el-input v-model="item.desc" type="textarea" :rows="3" autocomplete="off" />
</el-form-item>
<el-form-item label="创意度" prop="temperature">
<template #label>
<div class="flex items-center">
<span class="mr-1">创意度</span>
<el-tooltip
effect="dark"
content="OpenAI 0-2其他模型 0-1"
raw-content
placement="right"
>
<el-icon>
<InfoFilled />
</el-icon>
</el-tooltip>
</div>
</template>
<el-input v-model="item.temperature" autocomplete="off" placeholder="模型创意度" />
</el-form-item>
</div>
<div v-if="item.type === 'tts'">
<el-form-item label="音色" prop="voice">
<el-select v-model="item.options.voice" placeholder="请选择音色">
<el-option v-for="v in voices" :value="v.value" :label="v.label" :key="v.value">
{{ v.label }}
</el-option>
</el-select>
</el-form-item>
</div>
<el-form-item label="绑定API-KEY" prop="apikey">
<el-select v-model="item.key_id" placeholder="请选择 API KEY" filterable clearable>
<el-option v-for="v in apiKeys" :value="v.id" :label="v.name" :key="v.id">
{{ v.name }}
<el-tag type="primary" size="small" class="ml-1 mr-1">{{ v.type }}</el-tag>
<el-text type="info" size="small">{{ substr(v.api_url, 50) }}</el-text>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="启用状态" prop="enable">
<el-switch v-model="item.enabled" />
</el-form-item>
<el-form-item>
<template #label>
<div class="flex items-center">
<span class="mr-1">开放状态</span>
<el-tooltip
effect="dark"
content="开放后该模型将对所有用户可见<br/> 如果模型没有启用则当前设置无效"
raw-content
placement="right"
>
<el-icon>
<InfoFilled />
</el-icon>
</el-tooltip>
</div>
</template>
<el-switch v-model="item.open" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="save">提交</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { httpGet, httpPost } from '@/utils/http'
import { dateFormat, removeArrayItem, substr } from '@/utils/libs'
import { DocumentCopy, InfoFilled, Plus, Search } from '@element-plus/icons-vue'
import ClipboardJS from 'clipboard'
import { ElMessage } from 'element-plus'
import { Sortable } from 'sortablejs'
import { onMounted, onUnmounted, reactive, ref } from 'vue'
// 变量定义
const items = ref([])
const query = ref({ name: '' })
const item = ref({})
const showDialog = ref(false)
const title = ref('')
const rules = reactive({
type: [{ required: true, message: '请选择模型类型', trigger: 'change' }],
name: [{ required: true, message: '请输入模型名称', trigger: 'change' }],
value: [{ required: true, message: '请输入模型值', trigger: 'change' }],
power: [{ required: true, message: '请输入消耗算力', trigger: 'change' }],
})
const loading = ref(true)
const formRef = ref(null)
const modelTypes = ref([
{ label: '聊天', value: 'chat' },
{ label: '绘图', value: 'img' },
{ label: '语音', value: 'tts' },
])
const voices = ref([
{ label: 'Echo', value: 'echo' },
{ label: 'Fable', value: 'fable' },
{ label: 'Onyx', value: 'onyx' },
{ label: 'Nova', value: 'nova' },
{ label: 'Shimmer', value: 'shimmer' },
])
// 获取 API KEY
const apiKeys = ref([])
httpGet('/api/admin/apikey/list')
.then((res) => {
apiKeys.value = res.data
})
.catch((e) => {
ElMessage.error('获取 API KEY 失败:' + e.message)
})
// 获取数据
const fetchData = () => {
httpGet('/api/admin/model/list', query.value)
.then((res) => {
if (res.data) {
res.data.forEach((item) => {
if (!item.options) {
item.options = {}
}
item.last_used_at = dateFormat(item.last_used_at)
})
items.value = res.data
}
loading.value = false
})
.catch((e) => {
console.error(e)
ElMessage.error('获取数据失败')
})
}
const clipboard = ref(null)
onMounted(() => {
fetchData()
const drawBodyWrapper = document.querySelector('.el-table__body tbody')
// 初始化拖动排序插件
Sortable.create(drawBodyWrapper, {
sort: true,
animation: 500,
onEnd({ newIndex, oldIndex, from }) {
if (oldIndex === newIndex) {
return
}
const sortedData = Array.from(from.children).map((row) =>
row.querySelector('.sort').getAttribute('data-id')
)
const ids = []
const sorts = []
sortedData.forEach((id, index) => {
ids.push(parseInt(id))
sorts.push(index + 1)
items.value[index].sort_num = index + 1
})
httpPost('/api/admin/model/sort', { ids: ids, sorts: sorts })
.then(() => {})
.catch((e) => {
ElMessage.error('排序失败:' + e.message)
})
},
})
clipboard.value = new ClipboardJS('.copy-model')
clipboard.value.on('success', () => {
ElMessage.success('复制成功!')
})
clipboard.value.on('error', () => {
ElMessage.error('复制失败!')
})
})
onUnmounted(() => {
clipboard.value.destroy()
})
const add = function () {
title.value = '新增模型'
showDialog.value = true
item.value = {
enabled: true,
power: 1,
open: true,
description: '',
max_tokens: 1024,
max_context: 8192,
temperature: 0.9,
}
}
const edit = function (row) {
title.value = '修改模型'
showDialog.value = true
item.value = Object.assign({}, row)
}
const save = function () {
formRef.value.validate((valid) => {
item.value.temperature = parseFloat(item.value.temperature)
if (valid) {
showDialog.value = false
item.value.key_id = parseInt(item.value.key_id)
httpPost('/api/admin/model/save', item.value)
.then(() => {
ElMessage.success('操作成功!')
fetchData()
})
.catch((e) => {
ElMessage.error('操作失败,' + e.message)
})
} else {
return false
}
})
}
const modelSet = (filed, row) => {
httpPost('/api/admin/model/set', { id: row.id, filed: filed, value: row[filed] })
.then(() => {
ElMessage.success('操作成功!')
})
.catch((e) => {
ElMessage.error('操作失败:' + e.message)
})
}
const remove = function (row) {
httpGet('/api/admin/model/remove?id=' + row.id)
.then(() => {
ElMessage.success('删除成功!')
items.value = removeArrayItem(items.value, row, (v1, v2) => {
return v1.id === v2.id
})
})
.catch((e) => {
ElMessage.error('删除失败:' + e.message)
})
}
</script>
<style lang="stylus" scoped>
@import "../../assets/css/admin/form.styl";
.model-list {
.handle-box {
margin-bottom 20px
.handle-input {
max-width 150px;
margin-right 10px;
}
}
.cell {
.copy-model {
margin-left 6px
cursor pointer
}
}
.description-cell {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 160px;
}
.el-select {
width: 100%
}
.sort {
cursor move
.iconfont {
position relative
top 1px
}
}
.pagination {
padding 20px 0
display flex
justify-content right
}
}
</style>