-
{{ gstate.botName }}
+
+
+
+
{{ gstate.botName }}
+
+
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 6af50d9..417e7b2 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -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: {
diff --git a/package.json b/package.json
index fd7ebb7..95d6d32 100644
--- a/package.json
+++ b/package.json
@@ -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"
diff --git a/pages/index.vue b/pages/index.vue
index 3580ccf..34b02be 100644
--- a/pages/index.vue
+++ b/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
(
- `${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)?.[1] ||
- "网络繁忙,请稍后再试",
- });
+ // const resp = await useFetch(
+ // `${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
+ }>(`${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,
@@ -244,6 +325,12 @@ const scrollLastMessageIntoView = () => {
:name="gstate.botName"
:key="message.id"
:message="message"
+ @select-suggestion="
+ (suggestion) => {
+ if (responding) return;
+ inquiryInput = suggestion;
+ onInquirySubmit();
+ }"
/>
=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
diff --git a/stores/state.ts b/stores/state.ts
index 0bd7769..7485275 100644
--- a/stores/state.ts
+++ b/stores/state.ts
@@ -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":
diff --git a/types/index.ts b/types/index.ts
index e1dad32..319e387 100644
--- a/types/index.ts
+++ b/types/index.ts
@@ -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 {
diff --git a/utils/http.ts b/utils/http.ts
new file mode 100644
index 0000000..0eda6d7
--- /dev/null
+++ b/utils/http.ts
@@ -0,0 +1,84 @@
+export const http_stream = async (
+ 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)
+}
\ No newline at end of file