Compare commits
3 Commits
main
...
demo-0709-
Author | SHA1 | Date | |
---|---|---|---|
d6e70a4ef8 | |||
5b9db644e4
|
|||
39c581f3f9
|
2
.env.textbook-demo
Normal file
2
.env.textbook-demo
Normal file
@ -0,0 +1,2 @@
|
||||
DIFY_BASE_URL=https://service3.fenshenzhike.com/v1
|
||||
DIFY_API_KEY=app-mS8a08rlxQuxZpdqEfwxJppK
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -22,3 +22,5 @@ logs
|
||||
.env
|
||||
.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: {
|
||||
|
19
package.json
19
package.json
@ -4,28 +4,31 @@
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pinia/nuxt": "0.10.1",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/vite": "^4.0.14",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@uniiem/uuid": "^0.2.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"dotenv": "^16.5.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"markdown-it": "^14.1.0",
|
||||
"nuxt": "^3.16.0",
|
||||
"pinia": "^3.0.1",
|
||||
"tailwindcss": "^4.0.14",
|
||||
"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"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a",
|
||||
"devDependencies": {
|
||||
"@nuxt/ui": "^3.0.0",
|
||||
"@iconify-json/svg-spinners": "^1.2.2",
|
||||
"@iconify-json/tabler": "^1.2.17",
|
||||
"@nuxt/ui": "^3.0.2",
|
||||
"@types/markdown-it": "^14.1.2"
|
||||
}
|
||||
}
|
||||
|
170
pages/index.vue
170
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: "交通卡口监控系统需求分析包含哪些方面",
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -150,8 +177,7 @@ const getPopularInquiriesByRole = () => {
|
||||
};
|
||||
|
||||
const scrollArea = ref<HTMLDivElement | null>(null);
|
||||
// 滤除思考过程 details/think 标签正则
|
||||
const regex = /(?<=<\/details|think>\n\n)[\s\S]*/gm;
|
||||
const regex = /<\/think>\n\n([\s\S]*)/m;
|
||||
|
||||
// 常见问题
|
||||
const inquiries = computed(
|
||||
@ -178,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",
|
||||
}),
|
||||
}
|
||||
);
|
||||
gstate.insertOrUpdateMessage({
|
||||
id: botMessageId,
|
||||
role: "bot",
|
||||
message:
|
||||
resp.data.value?.data.outputs.message.match(regex)?.[0] ||
|
||||
"网络繁忙,请稍后再试",
|
||||
});
|
||||
// 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: "",
|
||||
});
|
||||
},
|
||||
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,
|
||||
@ -245,6 +325,12 @@ const scrollLastMessageIntoView = () => {
|
||||
:name="gstate.botName"
|
||||
:key="message.id"
|
||||
:message="message"
|
||||
@select-suggestion="
|
||||
(suggestion) => {
|
||||
if (responding) return;
|
||||
inquiryInput = suggestion;
|
||||
onInquirySubmit();
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
4544
pnpm-lock.yaml
generated
4544
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -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