feat(ui): 对话管理

This commit is contained in:
廖彦棋 2024-03-07 15:32:32 +08:00
parent 586a174d6c
commit 721a8f1ecb
30 changed files with 285 additions and 24 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -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

View File

@ -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',

View File

@ -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>

View 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>

View 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
})
}

View File

@ -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();

View File

@ -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" />

View File

@ -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>

View File

@ -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
})
}

View File

@ -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" }