feat(ui): 对话管理
BIN
gpt-vue/projects/vue-admin/public/images/avatar/artist.jpg
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/dou_yin.jpg
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/elon_musk.jpg
Normal file
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 20 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/girl_friend.jpg
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/good_comment.jpg
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/gpt.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/kong_zi.jpg
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/lu_xun.jpg
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/mid_journey.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/programmer.jpg
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/psychiatrist.jpg
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/red_book.jpg
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/seller.jpg
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/steve_jobs.jpg
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/teacher.jpg
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/translator.jpg
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/user.png
Normal file
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 19 KiB |
BIN
gpt-vue/projects/vue-admin/public/images/avatar/yi_yan.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
@ -8,6 +8,7 @@ function useRequest<T>(request: Request<T>) {
|
||||
const loading = ref(false)
|
||||
|
||||
const requestData = async (params?: any) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await request(params)
|
||||
result.value = res.data
|
||||
|
@ -51,6 +51,15 @@ const menu = [
|
||||
},
|
||||
component: () => import('@/views/Functions/FunctionsContainer.vue')
|
||||
},
|
||||
{
|
||||
path: '/chats',
|
||||
name: 'Chats',
|
||||
meta: {
|
||||
title: "对话管理",
|
||||
icon: IconCalendar,
|
||||
},
|
||||
component: () => import('@/views/Chats/ChatsContainer.vue')
|
||||
},
|
||||
{
|
||||
path: '/loginLog',
|
||||
name: 'LoginLog',
|
||||
|
@ -0,0 +1,110 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, h } from "vue";
|
||||
import { Message, Modal } from "@arco-design/web-vue";
|
||||
import SearchTable from "@/components/SearchTable/SearchTable.vue";
|
||||
import type { SearchTableColumns } from "@/components/SearchTable/type";
|
||||
import app from "@/main";
|
||||
import { getList, message, remove } from "./api";
|
||||
import ChatsLogs from "./ChatsLogs.vue";
|
||||
|
||||
const columns: SearchTableColumns[] = [
|
||||
{
|
||||
dataIndex: "user_id",
|
||||
title: "账户ID",
|
||||
search: {
|
||||
valueType: "input",
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "username",
|
||||
title: "账户",
|
||||
},
|
||||
{
|
||||
dataIndex: "title",
|
||||
title: "标题",
|
||||
search: {
|
||||
valueType: "input",
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "model",
|
||||
title: "模型",
|
||||
search: {
|
||||
valueType: "input",
|
||||
},
|
||||
},
|
||||
{
|
||||
dataIndex: "msg_num",
|
||||
title: "消息数量",
|
||||
},
|
||||
{
|
||||
dataIndex: "token",
|
||||
title: "消耗算力",
|
||||
},
|
||||
{
|
||||
dataIndex: "username",
|
||||
title: "账户",
|
||||
},
|
||||
{
|
||||
dataIndex: "created_at",
|
||||
title: "创建时间",
|
||||
search: {
|
||||
valueType: "range",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
fixed: "right",
|
||||
slotName: "actions",
|
||||
},
|
||||
];
|
||||
|
||||
const tabsList = [
|
||||
{ key: "1", title: "对话列表", api: getList, columns },
|
||||
{ key: "2", title: "消息记录", api: message, columns },
|
||||
];
|
||||
|
||||
const activeKey = ref(tabsList[0].key);
|
||||
|
||||
const handleRemove = async (chat_id, reload) => {
|
||||
await remove({ chat_id });
|
||||
Message.success("删除成功");
|
||||
await reload();
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleCheck = (record) => {
|
||||
if (activeKey.value === "1") {
|
||||
Modal._context = app._context;
|
||||
Modal.info({
|
||||
title: "对话详情",
|
||||
width: 800,
|
||||
content: () => h(ChatsLogs, { id: record.chat_id }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
Modal.info({
|
||||
title: "消息详情",
|
||||
content: record.content,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<a-tabs v-model:active-key="activeKey" lazy-load justify>
|
||||
<a-tab-pane v-for="item in tabsList" :key="item.key" :title="item.title">
|
||||
<SearchTable :request="item.api" :columns="item.columns">
|
||||
<template #actions="{ record, reload }">
|
||||
<a-link @click="handleCheck(record)">查看</a-link>
|
||||
<a-popconfirm
|
||||
content="是否删除?"
|
||||
position="left"
|
||||
type="warning"
|
||||
:on-before-ok="() => handleRemove(record.id, reload)"
|
||||
>
|
||||
<a-link status="danger">删除</a-link>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</SearchTable>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</template>
|
94
gpt-vue/projects/vue-admin/src/views/Chats/ChatsLogs.vue
Normal file
@ -0,0 +1,94 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from "vue";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import { dateFormat } from "@gpt-vue/packages/utils";
|
||||
import useRequest from "@/composables/useRequest";
|
||||
import { history } from "./api";
|
||||
|
||||
const props = defineProps({
|
||||
id: String,
|
||||
});
|
||||
|
||||
const [getData, data, loading] = useRequest(history);
|
||||
onMounted(async () => {
|
||||
await getData({ chat_id: props.id });
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<template v-if="loading">
|
||||
<div class="custom-skeleton">
|
||||
<a-skeleton-shape />
|
||||
<div style="flex: 1">
|
||||
<a-skeleton-line :rows="2" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-for="item in data" :key="item.id">
|
||||
<div class="item-container" :class="item.type">
|
||||
<div class="left">
|
||||
<a-avatar shape="square">
|
||||
<img :src="item.icon" />
|
||||
</a-avatar>
|
||||
</div>
|
||||
<a-space class="right" direction="vertical">
|
||||
<div>{{ item.content }}</div>
|
||||
<a-space>
|
||||
<div class="code">
|
||||
<icon-clock-circle />
|
||||
{{ dateFormat(item.created_at) }}
|
||||
</div>
|
||||
<div class="code">算力消耗: {{ item.tokens }}</div>
|
||||
<a-typography-text
|
||||
v-if="item.type === 'reply'"
|
||||
copyable
|
||||
:copy-delay="1000"
|
||||
:copy-text="item.content"
|
||||
@copy="Message.success('复制成功')"
|
||||
>
|
||||
<template #copy-icon>
|
||||
<a-button class="code" size="mini">
|
||||
<icon-copy />
|
||||
</a-button>
|
||||
</template>
|
||||
<template #copy-tooltip>复制回答</template>
|
||||
</a-typography-text>
|
||||
</a-space>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.item-container {
|
||||
display: flex;
|
||||
padding: 20px 10px;
|
||||
width: 100%;
|
||||
gap: 20px;
|
||||
border-bottom: 1px solid #d9d9e3;
|
||||
box-sizing: border-box;
|
||||
align-items: flex-start;
|
||||
&.reply {
|
||||
background: #f7f7f8;
|
||||
}
|
||||
.left {
|
||||
width: 40px;
|
||||
}
|
||||
.right {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
.code {
|
||||
background-color: #e7e7e8;
|
||||
color: #888;
|
||||
padding: 3px 5px;
|
||||
margin-right: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
.custom-skeleton {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
34
gpt-vue/projects/vue-admin/src/views/Chats/api.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import http from "@/http/config";
|
||||
|
||||
export const getList = (data) => {
|
||||
return http({
|
||||
url: "/api/admin/chat/list",
|
||||
method: "post",
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const message = (data) => {
|
||||
return http({
|
||||
url: "/api/admin/chat/message",
|
||||
method: "post",
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const history = (params) => {
|
||||
return http({
|
||||
url: "/api/admin/chat/history",
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const remove = (params) => {
|
||||
return http({
|
||||
url: "/api/admin/chat/remove",
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
}
|
||||
|
@ -34,11 +34,11 @@ const columns: TableColumnData[] = [
|
||||
const openFormModal = usePopup(FunctionsForm, {
|
||||
nodeProps: ([_, record]) => ({ record }),
|
||||
popupProps: ([reload, record], exposed) => ({
|
||||
title: `${record.id ? "编辑" : "新增"}函数`,
|
||||
title: `${record?.id ? "编辑" : "新增"}函数`,
|
||||
width: "800px",
|
||||
onBeforeOk: async (done) => {
|
||||
await exposed()?.handleSubmit(save, {
|
||||
id: record.id,
|
||||
id: record?.id,
|
||||
parameters: exposed()?.parameters(),
|
||||
});
|
||||
await reload();
|
||||
|
@ -1,7 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, watchEffect } from "vue";
|
||||
import { Message } from "@arco-design/web-vue";
|
||||
import useSubmit from "@/composables/useSubmit";
|
||||
import FunctionsFormTable from "./FunctionsFormTable.vue";
|
||||
import { token } from "./api";
|
||||
import translateTableData from "./translateTableData";
|
||||
|
||||
const props = defineProps({
|
||||
@ -16,7 +18,7 @@ const { formRef, formData, handleSubmit, submitting } = useSubmit({
|
||||
action: "",
|
||||
token: "",
|
||||
parameters: {},
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const rules = {
|
||||
@ -25,6 +27,12 @@ const rules = {
|
||||
description: [{ required: true, message: "请输入函数功能描述" }],
|
||||
};
|
||||
|
||||
const generateToken = async () => {
|
||||
const { data } = await token();
|
||||
Message.success("生成 Token 成功");
|
||||
formData.token = data;
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
Object.assign(formData, props.record ?? {});
|
||||
tableData.value = translateTableData.get(formData.parameters);
|
||||
@ -56,15 +64,15 @@ defineExpose({
|
||||
<a-input v-model="formData.action" placeholder="该函数实现的API地址,可以是第三方服务API" />
|
||||
</a-form-item>
|
||||
<a-form-item field="token" label="API Token">
|
||||
<a-input-search v-model="formData.token" placeholder="API授权Token">
|
||||
<a-input v-model="formData.token" placeholder="API授权Token">
|
||||
<template #append>
|
||||
<a-tooltip
|
||||
content="只有本地服务才可以使用自动生成Token第三方服务请填写第三方服务API Token"
|
||||
>
|
||||
<a-button>生成Token</a-button>
|
||||
<a-button type="text" @click="generateToken">生成Token</a-button>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-input-search>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item field="enabled" label="启用状态">
|
||||
<a-switch v-model="formData.enabled" />
|
||||
|
@ -1,6 +1,4 @@
|
||||
<script lang="ts" setup>
|
||||
import { IconDelete } from "@arco-design/web-vue/es/icon";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array,
|
||||
@ -33,7 +31,7 @@ const handleRemoveRow = (index) => {
|
||||
<a-input v-model="scope.record.name" />
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="参数类型" :width="120">
|
||||
<a-table-column title="参数类型" :width="150">
|
||||
<template #cell="scope">
|
||||
<a-select
|
||||
v-model="scope.record.type"
|
||||
@ -48,7 +46,7 @@ const handleRemoveRow = (index) => {
|
||||
</template>
|
||||
</a-table-column>
|
||||
|
||||
<a-table-column title="必填参数" :width="100">
|
||||
<a-table-column title="必填参数" :width="100" align="center">
|
||||
<template #cell="scope">
|
||||
<a-checkbox v-model="scope.record.required" />
|
||||
</template>
|
||||
@ -58,15 +56,19 @@ const handleRemoveRow = (index) => {
|
||||
<template #cell="scope">
|
||||
<a-button
|
||||
status="danger"
|
||||
:icon="IconDelete"
|
||||
circle
|
||||
shape="circle"
|
||||
@click="handleRemoveRow(scope.rowIndex)"
|
||||
size="small"
|
||||
/>
|
||||
>
|
||||
<icon-delete />
|
||||
</a-button>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-button type="primary" long @click="handleCreateRow">新增参数</a-button>
|
||||
<a-button type="primary" @click="handleCreateRow">
|
||||
<template #icon><icon-plus /></template>
|
||||
<span>新增参数</span>
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
@ -32,10 +32,10 @@ export const setStatus = (data) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const token = (data) => {
|
||||
export const token = (params) => {
|
||||
return http({
|
||||
url: "/api/admin/function/token",
|
||||
method: "post",
|
||||
data
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ const get = (origin) => {
|
||||
|
||||
const set = (tableData) => {
|
||||
const properties = tableData.reduce((prev, curr) => {
|
||||
if (curr.name) {
|
||||
return {
|
||||
...prev,
|
||||
[curr.name]: {
|
||||
@ -25,6 +26,8 @@ const set = (tableData) => {
|
||||
type: curr.type,
|
||||
},
|
||||
};
|
||||
}
|
||||
return prev
|
||||
}, {});
|
||||
const required = tableData.filter((i) => i.required).map((i) => i.name);
|
||||
return { properties, required, type: "object" }
|
||||
|