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 loading = ref(false)
|
||||||
|
|
||||||
const requestData = async (params?: any) => {
|
const requestData = async (params?: any) => {
|
||||||
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await request(params)
|
const res = await request(params)
|
||||||
result.value = res.data
|
result.value = res.data
|
||||||
|
@ -51,6 +51,15 @@ const menu = [
|
|||||||
},
|
},
|
||||||
component: () => import('@/views/Functions/FunctionsContainer.vue')
|
component: () => import('@/views/Functions/FunctionsContainer.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/chats',
|
||||||
|
name: 'Chats',
|
||||||
|
meta: {
|
||||||
|
title: "对话管理",
|
||||||
|
icon: IconCalendar,
|
||||||
|
},
|
||||||
|
component: () => import('@/views/Chats/ChatsContainer.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/loginLog',
|
path: '/loginLog',
|
||||||
name: '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, {
|
const openFormModal = usePopup(FunctionsForm, {
|
||||||
nodeProps: ([_, record]) => ({ record }),
|
nodeProps: ([_, record]) => ({ record }),
|
||||||
popupProps: ([reload, record], exposed) => ({
|
popupProps: ([reload, record], exposed) => ({
|
||||||
title: `${record.id ? "编辑" : "新增"}函数`,
|
title: `${record?.id ? "编辑" : "新增"}函数`,
|
||||||
width: "800px",
|
width: "800px",
|
||||||
onBeforeOk: async (done) => {
|
onBeforeOk: async (done) => {
|
||||||
await exposed()?.handleSubmit(save, {
|
await exposed()?.handleSubmit(save, {
|
||||||
id: record.id,
|
id: record?.id,
|
||||||
parameters: exposed()?.parameters(),
|
parameters: exposed()?.parameters(),
|
||||||
});
|
});
|
||||||
await reload();
|
await reload();
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watchEffect } from "vue";
|
import { ref, watchEffect } from "vue";
|
||||||
|
import { Message } from "@arco-design/web-vue";
|
||||||
import useSubmit from "@/composables/useSubmit";
|
import useSubmit from "@/composables/useSubmit";
|
||||||
import FunctionsFormTable from "./FunctionsFormTable.vue";
|
import FunctionsFormTable from "./FunctionsFormTable.vue";
|
||||||
|
import { token } from "./api";
|
||||||
import translateTableData from "./translateTableData";
|
import translateTableData from "./translateTableData";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -16,7 +18,7 @@ const { formRef, formData, handleSubmit, submitting } = useSubmit({
|
|||||||
action: "",
|
action: "",
|
||||||
token: "",
|
token: "",
|
||||||
parameters: {},
|
parameters: {},
|
||||||
enabled: false,
|
enabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
@ -25,6 +27,12 @@ const rules = {
|
|||||||
description: [{ required: true, message: "请输入函数功能描述" }],
|
description: [{ required: true, message: "请输入函数功能描述" }],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generateToken = async () => {
|
||||||
|
const { data } = await token();
|
||||||
|
Message.success("生成 Token 成功");
|
||||||
|
formData.token = data;
|
||||||
|
};
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
Object.assign(formData, props.record ?? {});
|
Object.assign(formData, props.record ?? {});
|
||||||
tableData.value = translateTableData.get(formData.parameters);
|
tableData.value = translateTableData.get(formData.parameters);
|
||||||
@ -56,15 +64,15 @@ defineExpose({
|
|||||||
<a-input v-model="formData.action" placeholder="该函数实现的API地址,可以是第三方服务API" />
|
<a-input v-model="formData.action" placeholder="该函数实现的API地址,可以是第三方服务API" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item field="token" label="API Token">
|
<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>
|
<template #append>
|
||||||
<a-tooltip
|
<a-tooltip
|
||||||
content="只有本地服务才可以使用自动生成Token第三方服务请填写第三方服务API Token"
|
content="只有本地服务才可以使用自动生成Token第三方服务请填写第三方服务API Token"
|
||||||
>
|
>
|
||||||
<a-button>生成Token</a-button>
|
<a-button type="text" @click="generateToken">生成Token</a-button>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
</a-input-search>
|
</a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item field="enabled" label="启用状态">
|
<a-form-item field="enabled" label="启用状态">
|
||||||
<a-switch v-model="formData.enabled" />
|
<a-switch v-model="formData.enabled" />
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { IconDelete } from "@arco-design/web-vue/es/icon";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@ -33,7 +31,7 @@ const handleRemoveRow = (index) => {
|
|||||||
<a-input v-model="scope.record.name" />
|
<a-input v-model="scope.record.name" />
|
||||||
</template>
|
</template>
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
<a-table-column title="参数类型" :width="120">
|
<a-table-column title="参数类型" :width="150">
|
||||||
<template #cell="scope">
|
<template #cell="scope">
|
||||||
<a-select
|
<a-select
|
||||||
v-model="scope.record.type"
|
v-model="scope.record.type"
|
||||||
@ -48,7 +46,7 @@ const handleRemoveRow = (index) => {
|
|||||||
</template>
|
</template>
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
|
|
||||||
<a-table-column title="必填参数" :width="100">
|
<a-table-column title="必填参数" :width="100" align="center">
|
||||||
<template #cell="scope">
|
<template #cell="scope">
|
||||||
<a-checkbox v-model="scope.record.required" />
|
<a-checkbox v-model="scope.record.required" />
|
||||||
</template>
|
</template>
|
||||||
@ -58,15 +56,19 @@ const handleRemoveRow = (index) => {
|
|||||||
<template #cell="scope">
|
<template #cell="scope">
|
||||||
<a-button
|
<a-button
|
||||||
status="danger"
|
status="danger"
|
||||||
:icon="IconDelete"
|
shape="circle"
|
||||||
circle
|
|
||||||
@click="handleRemoveRow(scope.rowIndex)"
|
@click="handleRemoveRow(scope.rowIndex)"
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
>
|
||||||
|
<icon-delete />
|
||||||
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</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>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
|
@ -32,10 +32,10 @@ export const setStatus = (data) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const token = (data) => {
|
export const token = (params) => {
|
||||||
return http({
|
return http({
|
||||||
url: "/api/admin/function/token",
|
url: "/api/admin/function/token",
|
||||||
method: "post",
|
method: "get",
|
||||||
data
|
params
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -18,13 +18,16 @@ const get = (origin) => {
|
|||||||
|
|
||||||
const set = (tableData) => {
|
const set = (tableData) => {
|
||||||
const properties = tableData.reduce((prev, curr) => {
|
const properties = tableData.reduce((prev, curr) => {
|
||||||
return {
|
if (curr.name) {
|
||||||
...prev,
|
return {
|
||||||
[curr.name]: {
|
...prev,
|
||||||
description: curr.description,
|
[curr.name]: {
|
||||||
type: curr.type,
|
description: curr.description,
|
||||||
},
|
type: curr.type,
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return prev
|
||||||
}, {});
|
}, {});
|
||||||
const required = tableData.filter((i) => i.required).map((i) => i.name);
|
const required = tableData.filter((i) => i.required).map((i) => i.name);
|
||||||
return { properties, required, type: "object" }
|
return { properties, required, type: "object" }
|
||||||
|