Compare commits
1 Commits
oem-hlbrzy
...
demo-0709-
Author | SHA1 | Date | |
---|---|---|---|
d6e70a4ef8 |
@ -1,2 +1,2 @@
|
||||
DIFY_BASE_URL=https://service3.fenshenzhike.com/v1
|
||||
DIFY_API_KEY=app-58uPYnqyLAtVtBYELHUGkqD9
|
||||
DIFY_API_KEY=app-mS8a08rlxQuxZpdqEfwxJppK
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -23,3 +23,4 @@ logs
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.oem-hlbrzy
|
||||
!.env.textbook-demo
|
||||
|
@ -15,6 +15,10 @@ defineProps({
|
||||
default: "tabler:robot-face",
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits<{
|
||||
(e: "select-suggestion", suggestion: string): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -35,7 +39,7 @@ defineProps({
|
||||
<span class="text-sm font-medium">{{ name }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-lg bg-white/50 p-2 text-sm dark:bg-neutral-800/50 break-all text-justify"
|
||||
class="rounded-lg bg-white/50 p-3 text-sm dark:bg-neutral-800/50 break-all text-justify"
|
||||
>
|
||||
<div v-if="message.message" class="prose prose-sm">
|
||||
<!-- {{ message.message }} -->
|
||||
@ -47,6 +51,24 @@ defineProps({
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="message.role === 'suggestion'">
|
||||
<div class="">
|
||||
<ul class="flex flex-col gap-2">
|
||||
<li
|
||||
v-for="suggestion in message.suggestions"
|
||||
class="rounded-lg bg-white/50 p-2 text-xs dark:bg-neutral-800/50 break-all w-fit inline-flex items-center gap-0.5"
|
||||
:key="suggestion"
|
||||
@click="$emit('select-suggestion', suggestion)"
|
||||
>
|
||||
<Icon
|
||||
name="tabler:question-mark"
|
||||
class="text-primary-500 hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 text-base"
|
||||
/>
|
||||
<span>{{ suggestion }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
@ -58,9 +58,14 @@ useHead({
|
||||
<template>
|
||||
<div class="w-full h-full flex flex-col dark:bg-neutral-900/80">
|
||||
<div
|
||||
class="sticky top-0 w-full px-4 py-3 bg-white/30 backdrop-blur-2xl z-30 dark:bg-neutral-900/80"
|
||||
class="sticky top-0 w-full px-4 py-3 bg-white/50 backdrop-blur-2xl z-30 dark:bg-neutral-900/80"
|
||||
>
|
||||
<h1 class="font-medium">{{ gstate.botName }}</h1>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Icon name="tabler:book" class="-mt-0.5 text-3xl text-primary-500" />
|
||||
<div class="flex items-end gap-1">
|
||||
<h1 class="text-base font-semibold text-primary-500 leading-none">{{ gstate.botName }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <USelectMenu v-model="value" color="primary" variant="none" :items="items" class="w-48" size="lg" :ui="{base: 'w-fit'}" /> -->
|
||||
</div>
|
||||
<slot></slot>
|
||||
|
@ -4,7 +4,7 @@ import tailwindcss from "@tailwindcss/vite";
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: "2024-11-01",
|
||||
devtools: { enabled: true },
|
||||
modules: ["@nuxt/ui", "@pinia/nuxt"],
|
||||
modules: ["@nuxt/ui", "@pinia/nuxt", "pinia-plugin-persistedstate/nuxt"],
|
||||
css: ["~/assets/css/main.css"],
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
|
@ -4,8 +4,8 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"dev": "nuxt dev --dotenv .env.textbook-demo",
|
||||
"generate": "nuxt generate --dotenv .env.textbook-demo",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
@ -19,6 +19,7 @@
|
||||
"markdown-it": "^14.1.0",
|
||||
"nuxt": "^3.16.2",
|
||||
"pinia": "^3.0.2",
|
||||
"pinia-plugin-persistedstate": "^4.4.1",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
|
159
pages/index.vue
159
pages/index.vue
@ -9,56 +9,83 @@ const getPopularInquiriesByRole = () => {
|
||||
return {
|
||||
stu: [
|
||||
{
|
||||
label: "学籍管理",
|
||||
label: "物联网基础",
|
||||
inquiries: [
|
||||
{
|
||||
question: "学籍注册的要求",
|
||||
question: "物联网的定义是什么",
|
||||
},
|
||||
{
|
||||
question: "病假请假的审批流程",
|
||||
question: "物联网与互联网的关系与区别",
|
||||
},
|
||||
{
|
||||
question: "转专业有哪些条件",
|
||||
question: "物联网有哪些典型应用场景",
|
||||
},
|
||||
{
|
||||
question: "办理休学的流程",
|
||||
question: "物联网系统的四层结构是什么",
|
||||
},
|
||||
{
|
||||
question: "人工智能专业毕业的要求",
|
||||
question: "物联网的本质是什么",
|
||||
},
|
||||
{
|
||||
question: "新基建和物联网的关系",
|
||||
},
|
||||
{
|
||||
question: "物联网工程的生命周期包括哪些阶段",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "学生日常",
|
||||
label: "智慧工地",
|
||||
inquiries: [
|
||||
{
|
||||
question: "校医院服务时间和报销政策",
|
||||
question: "智慧工地的概念与核心目标",
|
||||
},
|
||||
{
|
||||
question: "学工部的联系方式",
|
||||
question: "智慧工地系统的四层架构是什么",
|
||||
},
|
||||
{
|
||||
question: "宿舍管理制度是怎样的",
|
||||
question: "施工场地工人健康检测系统包括哪些功能",
|
||||
},
|
||||
{
|
||||
question: "助学贷款申请指南",
|
||||
question: "施工场地工人健康检测系统的作用是什么",
|
||||
},
|
||||
{
|
||||
question: "成立社团的流程",
|
||||
question: "智慧工地中常用的传感器有哪些",
|
||||
},
|
||||
{
|
||||
question: "选择传感器需要考虑哪些特性参数",
|
||||
},
|
||||
{
|
||||
question: "传感器安装调试的主要步骤有哪些",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "职业发展",
|
||||
label: "智能交通",
|
||||
inquiries: [
|
||||
{
|
||||
question: "专升本的条件和要求",
|
||||
question: "智能交通系统的定义和主要目标是什么",
|
||||
},
|
||||
{
|
||||
question: "金融相关专业实习单位有哪些",
|
||||
question: "智能交通系统的四层结构及其功能",
|
||||
},
|
||||
{
|
||||
question: "创业孵化基地申请方式",
|
||||
question: "我国智能交通的发展现状如何",
|
||||
},
|
||||
{
|
||||
question: "国外智能交通发展的主要特点是什么",
|
||||
},
|
||||
{
|
||||
question: "智能交通系统中常用的感知技术有哪些",
|
||||
},
|
||||
{
|
||||
question: "射频识别技术在智能交通中的应用有哪些",
|
||||
},
|
||||
{
|
||||
question: "自动识别技术包括哪些分类",
|
||||
},
|
||||
{
|
||||
question: "交通卡口监控系统需求分析包含哪些方面",
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -177,30 +204,84 @@ const onInquirySubmit = async () => {
|
||||
});
|
||||
|
||||
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",
|
||||
}),
|
||||
}
|
||||
);
|
||||
// const resp = await useFetch<IWorkflowResponse>(
|
||||
// `${runtimeConfig.public.DifyBaseURL}/workflows/run`,
|
||||
// {
|
||||
// method: "post",
|
||||
// headers: {
|
||||
// Authorization: `Bearer ${runtimeConfig.public.DifyApiKey}`,
|
||||
// },
|
||||
// body: JSON.stringify({
|
||||
// inputs: {
|
||||
// query: inquiryInput.value,
|
||||
// },
|
||||
// response_mode: "blocking",
|
||||
// user: "xsh",
|
||||
// }),
|
||||
// }
|
||||
// );
|
||||
// gstate.insertOrUpdateMessage({
|
||||
// id: botMessageId,
|
||||
// role: "bot",
|
||||
// message:
|
||||
// resp.data.value?.data.outputs.message.match(regex)?.[1] ||
|
||||
// "网络繁忙,请稍后再试",
|
||||
// });
|
||||
http_stream<{
|
||||
query: string
|
||||
response_mode: "blocking" | "streaming"
|
||||
user: string
|
||||
conversation_id: string
|
||||
inputs: Record<string, any>
|
||||
}>(`${runtimeConfig.public.DifyBaseURL}/chat-messages`, {
|
||||
query: inquiryInput.value,
|
||||
response_mode: "streaming",
|
||||
user: "xsh",
|
||||
conversation_id: "",
|
||||
inputs: {}
|
||||
}, {
|
||||
onStart: () => {
|
||||
responding.value = true;
|
||||
gstate.insertOrUpdateMessage({
|
||||
id: botMessageId,
|
||||
role: "bot",
|
||||
message:
|
||||
resp.data.value?.data.outputs.message.match(regex)?.[1] ||
|
||||
"网络繁忙,请稍后再试",
|
||||
message: "",
|
||||
});
|
||||
},
|
||||
onTextChunk: (message: string) => {
|
||||
gstate.insertOrUpdateMessage({
|
||||
id: botMessageId,
|
||||
role: "bot",
|
||||
message: gstate.messages.find(m => m.id === botMessageId)?.message + message,
|
||||
});
|
||||
},
|
||||
onComplete: (id, finished_at, messageId) => {
|
||||
responding.value = false;
|
||||
gstate.insertOrUpdateMessage({
|
||||
id: botMessageId,
|
||||
role: "bot",
|
||||
message: gstate.messages.find(m => m.id === botMessageId)?.message || "网络繁忙,请稍后再试",
|
||||
});
|
||||
scrollLastMessageIntoView();
|
||||
fetch(
|
||||
`${runtimeConfig.public.DifyBaseURL}/messages/${messageId}/suggested?user=xsh`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${runtimeConfig.public.DifyApiKey}`,
|
||||
},
|
||||
}
|
||||
).then(res => res.json()).then(data => {
|
||||
if (data?.data?.length) {
|
||||
gstate.insertOrUpdateMessage({
|
||||
id: uuidv4(),
|
||||
role: "suggestion",
|
||||
suggestions: data.data,
|
||||
});
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
gstate.insertOrUpdateMessage({
|
||||
id: botMessageId,
|
||||
@ -244,6 +325,12 @@ const scrollLastMessageIntoView = () => {
|
||||
:name="gstate.botName"
|
||||
:key="message.id"
|
||||
:message="message"
|
||||
@select-suggestion="
|
||||
(suggestion) => {
|
||||
if (responding) return;
|
||||
inquiryInput = suggestion;
|
||||
onInquirySubmit();
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
32
pnpm-lock.yaml
generated
32
pnpm-lock.yaml
generated
@ -35,6 +35,9 @@ importers:
|
||||
pinia:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))
|
||||
pinia-plugin-persistedstate:
|
||||
specifier: ^4.4.1
|
||||
version: 4.4.1(@nuxt/kit@3.16.2(magicast@0.3.5))(@pinia/nuxt@0.10.1(magicast@0.3.5)(pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))))(pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)))
|
||||
tailwindcss:
|
||||
specifier: ^4.1.4
|
||||
version: 4.1.4
|
||||
@ -1920,6 +1923,9 @@ packages:
|
||||
decache@4.6.2:
|
||||
resolution: {integrity: sha512-2LPqkLeu8XWHU8qNCS3kcF6sCcb5zIzvWaAHYSvPfwhdd7mHuah29NssMzrTYyHN4F5oFy2ko9OBYxegtU0FEw==}
|
||||
|
||||
deep-pick-omit@1.2.1:
|
||||
resolution: {integrity: sha512-2J6Kc/m3irCeqVG42T+SaUMesaK7oGWaedGnQQK/+O0gYc+2SP5bKh/KKTE7d7SJ+GCA9UUE1GRzh6oDe0EnGw==}
|
||||
|
||||
deepmerge@4.3.1:
|
||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -3252,6 +3258,20 @@ packages:
|
||||
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
pinia-plugin-persistedstate@4.4.1:
|
||||
resolution: {integrity: sha512-lmuMPpXla2zJKjxEq34e1E9P9jxkWEhcVwwioCCE0izG45kkTOvQfCzvwhW3i38cvnaWC7T1eRdkd15Re59ldw==}
|
||||
peerDependencies:
|
||||
'@nuxt/kit': '>=3.0.0'
|
||||
'@pinia/nuxt': '>=0.10.0'
|
||||
pinia: '>=3.0.0'
|
||||
peerDependenciesMeta:
|
||||
'@nuxt/kit':
|
||||
optional: true
|
||||
'@pinia/nuxt':
|
||||
optional: true
|
||||
pinia:
|
||||
optional: true
|
||||
|
||||
pinia@3.0.2:
|
||||
resolution: {integrity: sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g==}
|
||||
peerDependencies:
|
||||
@ -6511,6 +6531,8 @@ snapshots:
|
||||
dependencies:
|
||||
callsite: 1.0.0
|
||||
|
||||
deep-pick-omit@1.2.1: {}
|
||||
|
||||
deepmerge@4.3.1: {}
|
||||
|
||||
default-browser-id@5.0.0: {}
|
||||
@ -8012,6 +8034,16 @@ snapshots:
|
||||
|
||||
picomatch@4.0.2: {}
|
||||
|
||||
pinia-plugin-persistedstate@4.4.1(@nuxt/kit@3.16.2(magicast@0.3.5))(@pinia/nuxt@0.10.1(magicast@0.3.5)(pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))))(pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))):
|
||||
dependencies:
|
||||
deep-pick-omit: 1.2.1
|
||||
defu: 6.1.4
|
||||
destr: 2.0.5
|
||||
optionalDependencies:
|
||||
'@nuxt/kit': 3.16.2(magicast@0.3.5)
|
||||
'@pinia/nuxt': 0.10.1(magicast@0.3.5)(pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)))
|
||||
pinia: 3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))
|
||||
|
||||
pinia@3.0.2(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-api': 7.7.5
|
||||
|
@ -5,7 +5,7 @@ export const useGState = defineStore("global_state", () => {
|
||||
const botName = computed(() => {
|
||||
switch (currentRole.value) {
|
||||
case "stu":
|
||||
return "AI 辅导员";
|
||||
return "物联网工程导论";
|
||||
case "tea":
|
||||
return "AI 教研专家";
|
||||
case "fans":
|
||||
|
@ -2,8 +2,9 @@ export type VisitorRole = "stu" | "tea" | "fans";
|
||||
|
||||
export type LocalMessage = {
|
||||
id: string;
|
||||
role: "bot" | "user";
|
||||
role: "bot" | "user" | "suggestion";
|
||||
message?: string;
|
||||
suggestions?: string[];
|
||||
};
|
||||
|
||||
export interface IWorkflowResponse {
|
||||
|
84
utils/http.ts
Normal file
84
utils/http.ts
Normal file
@ -0,0 +1,84 @@
|
||||
export const http_stream = async <ReqT>(
|
||||
url: string,
|
||||
params: ReqT,
|
||||
events: {
|
||||
onStart?: (id: string, created_at?: number, messageId?: string | null) => void
|
||||
onTextChunk?: (message: string) => void
|
||||
onComplete?: (id: string | null, finished_at?: number, messageId?: string | null) => void
|
||||
}) => {
|
||||
const { onStart, onTextChunk, onComplete } = events
|
||||
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${runtimeConfig.public.DifyApiKey}`,
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
})
|
||||
|
||||
if (!response) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader()
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
let buffer = ''
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader?.read() || {}
|
||||
if (done) break
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
|
||||
const parts = buffer.split('\n\n')
|
||||
buffer = parts.pop()!
|
||||
|
||||
for (const part of parts) {
|
||||
if (!part.trim().startsWith('data:')) continue
|
||||
const payload = part.replace(/^data:\s*/, '').trim()
|
||||
|
||||
try {
|
||||
const obj = JSON.parse(payload)
|
||||
if (obj?.event && obj.event === 'workflow_started') {
|
||||
onStart?.(obj?.data?.id || null, obj?.data?.created_at || 0, obj?.message_id || null)
|
||||
}
|
||||
if (obj?.event && obj.event === 'workflow_finished') {
|
||||
onComplete?.(obj?.data?.id || null, obj?.data?.finished_at || 0, obj?.message_id || null)
|
||||
return
|
||||
}
|
||||
// Workflow
|
||||
if (obj?.event && obj.event === 'text_chunk') {
|
||||
let text_chunk = obj.data?.text as string
|
||||
if (text_chunk) {
|
||||
if (text_chunk.startsWith('<') && text_chunk.endsWith('>')) {
|
||||
const endTag = text_chunk.match(/<\/[^>]*>/)
|
||||
if (endTag) {
|
||||
text_chunk = text_chunk.replace(endTag[0], '\n' + endTag[0])
|
||||
}
|
||||
}
|
||||
onTextChunk?.(text_chunk)
|
||||
}
|
||||
}
|
||||
// Chatflow
|
||||
if (obj?.event && obj.event === 'message') {
|
||||
let ans = obj.answer
|
||||
if (ans) {
|
||||
if (ans.startsWith('<') && ans.endsWith('>')) {
|
||||
const endTag = ans.match(/<\/[^>]*>/)
|
||||
if (endTag) {
|
||||
ans = ans.replace(endTag[0], '\n' + endTag[0])
|
||||
}
|
||||
}
|
||||
onTextChunk?.(ans)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
onComplete?.(null)
|
||||
}
|
Reference in New Issue
Block a user