AiCampus/pages/index.vue

281 lines
6.7 KiB
Vue

<script lang="ts" setup>
import { uuidv4 } from "@uniiem/uuid";
import type { IWorkflowResponse, LocalMessage, VisitorRole } from "~/types";
const gstate = useGState();
const runtimeConfig = useRuntimeConfig();
const getPopularInquiriesByRole = () => {
return {
stu: [
{
label: "学籍管理",
inquiries: [
{
question: "学籍注册的要求",
},
{
question: "病假请假的审批流程",
},
{
question: "转专业有哪些条件",
},
{
question: "办理休学的流程",
},
{
question: "人工智能专业毕业的要求",
},
],
},
{
label: "学生日常",
inquiries: [
{
question: "校医院服务时间和报销政策",
},
{
question: "学工部的联系方式",
},
{
question: "宿舍管理制度是怎样的",
},
{
question: "助学贷款申请指南",
},
{
question: "成立社团的流程",
},
],
},
{
label: "职业发展",
inquiries: [
{
question: "专升本的条件和要求",
},
{
question: "金融相关专业实习单位有哪些",
},
{
question: "创业孵化基地申请方式",
},
],
},
],
tea: [
{
label: "教师日常",
inquiries: [
{
question: "如何申请借用公共教室",
},
{
question: "教师每年最低教学学时要求是多少",
},
{
question: "出差北京的住宿标准是什么",
},
{
question: "副教授评审的基本条件是什么",
},
],
},
{
label: "教研活动",
inquiries: [
{
question: "“挑战杯”国赛一等奖的奖励政策",
},
{
question: "教学能力大赛的参赛规程是怎样的",
},
{
question: "校级精品课程项目结题的要求",
},
],
},
{
label: "科研活动",
inquiries: [
{
question: "横向项目合同签订的流程是怎样的",
},
{
question: "成果转化中经费是如何使用的",
},
{
question: "项目课题中团队占比的排序",
},
],
},
],
fans: [
{
label: "校园概况",
inquiries: [
{
question: "学校的发展历程是怎样的",
},
{
question: "入校预约的流程",
},
{
question: "学校有哪些专业",
},
{
question: "学校主要的教学成果",
},
],
},
{
label: "招生就业",
inquiries: [
{
question: "数字媒体专业历年的招生分数",
},
{
question: "学校招收外地学生的比例",
},
{
question: "学校近三年各个专业就业的情况",
},
],
},
],
} as Record<
VisitorRole,
{ label: string; inquiries?: Array<{ question: string }> }[]
>;
};
const scrollArea = ref<HTMLDivElement | null>(null);
// 滤除思考过程 details/think 标签正则
const regex = /(?<=<\/details|think>\n\n)[\s\S]*/gm;
// 常见问题
const inquiries = computed(
() => getPopularInquiriesByRole()[gstate.currentRole]
);
const responding = ref(false);
const inquiryInput = ref("");
const onInquirySubmit = async () => {
if (inquiryInput.value) {
gstate.insertOrUpdateMessage({
id: uuidv4(),
role: "user",
message: inquiryInput.value,
});
responding.value = true;
scrollLastMessageIntoView();
let botMessageId = uuidv4();
gstate.insertOrUpdateMessage({
id: botMessageId,
role: "bot",
});
try {
const resp = await useFetch<IWorkflowResponse>(
`${runtimeConfig.public.DifyBaseURL}/workflows/run`,
{
method: "post",
headers: {
Authorization: `Bearer ${runtimeConfig.public.DifyApiKey}`,
},
body: JSON.stringify({
inputs: {
question: inquiryInput.value,
role: gstate.currentRole,
},
response_mode: "blocking",
user: "abc-123",
}),
}
);
gstate.insertOrUpdateMessage({
id: botMessageId,
role: "bot",
message:
resp.data.value?.data.outputs.message.match(regex)?.[0] ||
"网络繁忙,请稍后再试",
});
} catch (error) {
gstate.insertOrUpdateMessage({
id: botMessageId,
role: "bot",
message: "网络繁忙,请稍后再试",
});
}
inquiryInput.value = "";
responding.value = false;
scrollLastMessageIntoView();
}
};
const scrollLastMessageIntoView = () => {
nextTick(() => {
scrollArea.value?.scrollTo({
top: scrollArea.value?.scrollHeight,
behavior: "smooth",
});
});
};
</script>
<template>
<div ref="scrollArea" class="h-full overflow-auto overflow-x-hidden mb-20">
<div class="p-4 h-full flex flex-col gap-4">
<PopularInquiries
:inquiries-list="inquiries"
@select-inquiry="
(inquiry) => {
if (responding) return;
inquiryInput = inquiry;
onInquirySubmit();
}
"
/>
<ChatMessage
v-for="message in gstate.messages"
:name="gstate.botName"
:key="message.id"
:message="message"
/>
</div>
<div
class="fixed inset-x-0 bottom-0 p-4 bg-white/90 rounded-t-2xl shadow-2xl dark:bg-neutral-800/90 backdrop-blur-2xl"
>
<img
src="~/assets/image/pattern/ai_glow.png"
class="absolute top-0 inset-x-0 opacity-50 pointer-events-none"
/>
<div class="w-full flex justify-between items-center gap-2">
<UInput
placeholder="请输入问题"
class="flex-1"
size="xl"
v-model="inquiryInput"
/>
<UButton
color="primary"
variant="solid"
size="xl"
trailing
trailing-icon="tabler:send-2"
:loading="responding"
loading-icon="svg-spinners:tadpole"
@click="onInquirySubmit"
>提问</UButton
>
</div>
</div>
</div>
</template>
<style scoped></style>