【实战】 Vue 3、Anything LLM + DeepSeek本地化项目(四)

117 阅读9分钟

本章节在基于《# 【实战】 Vue 3、Anything LLM + DeepSeek本地化项目(三)》的基础上新增了如下功能:

  1. 智能小助手按工作空间的线程分别展示
  2. 配置了各个工作空间所引入的知识库内容
  3. 支持管理工作空间(新增、删除、修改及给工作空间增加现有的知识库内容)

本章节关键词汇:工作空间 聊天线程 知识库

本期实现目标

  1. 聊天线程的实现

image.png 2. 知识库列表实现

image.png 3. 工作空间管理

image.png

Anything LLM 工作空间

工作空间(Workspace)的定义

Anything LLM中,工作空间(Workspace) 是一个独立的环境,用于存储和管理与特定主题或项目相关的文件和数据。它类似于一个容器,可以将文档、资源和上下文信息组织在一起,方便用户对不同主题或项目进行分类管理。

工作空间的主要功能

  • 文档管理:
    • 文档容器化:工作区可以存储和管理各种文档,如 PDF、TXT、DOCX 等格式的文件。
    • 文档共享:同一个工作区内的文档可以被共享和访问,但不同工作区之间的内容是隔离的。
    • 文档嵌入:工作区支持将文档嵌入到向量数据库中,以便在聊天过程中使用。
  • 上下文隔离:
    • 独立上下文:每个工作区都有自己的上下文,互不干扰。这确保了不同项目或主题的对话和数据保持清晰和独立。
  • 多用户支持:
    • 多用户管理:工作区支持多用户实例和权限管理,适合团队协作。
    • 用户隔离:不同用户可以在自己的工作区内进行操作,而不会影响其他用户。
  • 智能代理(AI Agent):
    • 智能任务执行:工作区内可以使用智能代理,执行网页浏览、代码运行等复杂任务。

工作空间的使用场景

  • 企业知识库:企业可以将内部文档、手册、项目资料等上传到不同的工作区,构建私有的知识库,方便员工查询和协作。
  • 个人项目管理:个人用户可以为不同的项目创建独立的工作区,管理项目相关的文件和数据。
  • 教育和研究:教育机构或研究人员可以将研究资料、课程文档等分类存储在不同的工作区,便于教学和研究。

工作空间的优势

  • 高效管理:工作区可以高效地管理大量文档,支持多种文件格式。
  • 成本优化:通过一次性嵌入大型文档,降低了运营成本。
  • 隐私保护:工作区支持完全离线运行,确保用户数据的安全和隐私。

工作空间相关概念在Vue3中的实现

Vue3组件相关内容

智能小助手组件(views/ChatMsgStream/index.vue)中增加线程概念,实现展示工作空间下的多个线程及聊天信息

<template>
  <el-row class="chat-window">
    <el-col :span="2">
      <el-checkbox v-model="searchLibrary" @change="searchLibraryChange"
        >检索知识库</el-checkbox
      >
    </el-col>
    <el-col class="chat-title" :span="20">
      智能小助手<span v-if="isThinking"><i class="el-icon-loading"></i>(思考中……)</span>
    </el-col>
    <template v-if="searchLibrary">
      <el-col :span="24">
        <el-tabs
          v-model="activeSlug"
          type="card"
          class="chat-tabs"
          @tab-click="handleClick"
        >
          <el-tab-pane
            v-for="tabItem in tabList"
            :label="tabItem.name"
            :name="tabItem.slug"
            :key="`tab-${tabItem.slug}`"
          />
        </el-tabs>
      </el-col>
      <el-col :span="24">
        <el-tabs v-model="activeThreadSlug" @tab-click="threadHandleClick">
          <el-tab-pane
            v-for="thread in activeInfo.threads"
            :label="thread.name"
            :name="thread.slug"
            :key="thread.slug"
          />
        </el-tabs>
      </el-col>
    </template>
    <el-col class="chat-content" :span="24">
      <template v-for="(message, index) in messages" :key="index">
        <el-collapse v-if="message.role === 'assistant'">
          <el-collapse-item
            :title="`思考信息${
              (message.thinkTime && '-' + message.thinkTime + 's') || ''
            }`"
          >
            {{ message.thinkInfo }}
          </el-collapse-item>
        </el-collapse>
        <div class="chat-message">
          <div
            v-if="message.content"
            class="chat-picture"
            :class="{ 'chat-picture-user': message.role === 'user' }"
          >
            {{ message.role === "user" ? "用户" : "助手" }}
          </div>
          <v-md-preview :text="message.content"></v-md-preview>
        </div>
      </template>
    </el-col>
    <el-col class="chat-input" :span="20">
      <el-input
        v-model="userInput"
        type="textarea"
        :rows="4"
        placeholder="请输入消息"
      ></el-input>
    </el-col>
    <el-col class="chat-btn" :span="4">
      <el-button type="primary" plain @click="sendMessage">发送</el-button>
    </el-col>
  </el-row>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { callDeepSeekAPI } from "@/apis/chatAPIStream";
import {
  getAllWorkspaces,
  getWorkspaceChatsBySlug,
  getWorkspaceThreadChatsBySlug,
} from "@/apis/anythionChatAPIs.ts";
const messages = ref<
  { role: string; content: string; thinkTime?: number; thinkInfo?: string }[]
>([]);
const userInput = ref<string>("");
let thinkStartTime = 0; // 思考开始时间
let thinkEndTime = 0; // 思考结束时间
let isThinking = ref<boolean>(false); // 是否处于思考状态
let searchLibrary = ref<boolean>(false);
let activeSlug = ref("" as string);
let activeInfo = ref({} as any);
let activeThreadSlug = ref("");
let tabList = ref([] as any);
/* 切换tab */
const handleClick = () => {
  activeInfo.value = tabList.value.find((item: any) => item.slug == activeSlug.value);
  if (!activeInfo.value.threads.find((item: any) => item.name === "default")) {
    activeInfo.value.threads.unshift({
      name: "default",
      slug: activeInfo.value.slug,
    });
  }
  activeThreadSlug.value = activeInfo.value.threads[0].slug;
  getChatsMsgBySlug();
};
const threadHandleClick = () => {
  let _currentThread = activeInfo.value.threads.find(
    (item: any) => item.slug == activeThreadSlug.value
  );
  if (_currentThread.name === "default") {
    getChatsMsgBySlug();
  } else {
    getTheadsChartsMsgBySlug();
  }
};
/* 检索知识库 */
const searchLibraryChange = () => {
  getAllWrokspaces();
};
const _initSetHistoryMessages = (result: any) => {
  messages.value = result.history.map(
    (chartInfo: {
      role: string;
      content: string;
      chatId?: number;
      sentAt?: string;
      thinkInfo: string;
      attachments?: Array<any>;
    }) => {
      let _exp = new RegExp("<think>.*?</think>", "gs");
      let _thinkInfo = chartInfo.content.match(_exp);
      if (_thinkInfo) {
        // 记录思考过程
        chartInfo.thinkInfo = _thinkInfo[0]
          .replace("<think>", "")
          .replace("</think>", "");
      }
      // 处理 <think> 标签
      chartInfo.content = chartInfo.content.replace(_exp, "");
      return chartInfo;
    }
  );
};
/* 获取工作空间的聊天记录 */
const getChatsMsgBySlug = async () => {
  const result: any = await getWorkspaceChatsBySlug(activeThreadSlug.value);
  _initSetHistoryMessages(result);
};
const getTheadsChartsMsgBySlug = async () => {
  const result: any = await getWorkspaceThreadChatsBySlug(
    activeSlug.value,
    activeThreadSlug.value
  );
  _initSetHistoryMessages(result);
};
/* 获取工作空间列表信息 */
const getAllWrokspaces = async () => {
  const result: any = await getAllWorkspaces();
  tabList.value = result.workspaces;
  // 默认选中第一项
  activeSlug.value = tabList.value[0].slug;
  handleClick();
};
const formatDuring = (millisecond: number): number => {
  let seconds: number = (millisecond % (1000 * 60)) / 1000;
  return seconds;
};
const contentFactory = (assistantContent: string) => {
  // 处理 <think> 标签
  if (/<think>(.*?)/gs.test(assistantContent) && !/<\/think>/gs.test(assistantContent)) {
    let _thinkInfo = assistantContent.replace(/<think>/gs, "");
    if (!thinkStartTime) {
      thinkStartTime = Date.now();
    }
    messages.value[messages.value.length - 1].thinkInfo = _thinkInfo;

    isThinking.value = true;
    return;
  } else if (/<\/think>/gs.test(assistantContent)) {
    assistantContent = assistantContent.replace(/<think>(.*?)<\/think>/gs, "");
    isThinking.value = false;
    if (!thinkEndTime) {
      thinkEndTime = Date.now();
    }
    messages.value[messages.value.length - 1].thinkTime = formatDuring(
      thinkEndTime - thinkStartTime
    );
  }
  // 逐字输出动画
  let currentContent = "";
  const chars = assistantContent.split("");
  chars.forEach((char, i) => {
    currentContent += char;
    messages.value[messages.value.length - 1].content = currentContent;
  });
};
const sendMessage = async () => {
  if (!userInput.value.trim()) return;

  // 添加用户消息
  messages.value.push({ role: "user", content: userInput.value, thinkTime: 0 });

  try {
    // 调用 DeepSeek API
    const stream = await callDeepSeekAPI(messages.value, searchLibrary.value);
    const decoder = new TextDecoder("utf-8");
    let assistantContent = ""; // 初始化助手内容
    const reader = stream.getReader();
    messages.value.push({ role: "assistant", content: "", thinkTime: 0, thinkInfo: "" });
    // 读取流式数据
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      // console.log(value, "value");
      const chunk = decoder.decode(value, { stream: true });
      // console.log(chunk, "chunk");
      let _chunkArr = chunk.split("\n").filter(Boolean);
      _chunkArr.forEach((item: string) => {
        let _content = "";
        if (searchLibrary.value) {
          item = item.replace(/^data:\s?/g, "");
          let { choices } = JSON.parse(item);
          _content = choices[0].delta.content;
        } else {
          let {
            message: { content },
          } = JSON.parse(item);
          _content = content;
        }
        assistantContent += _content; // 拼接流式数据
      });
      // 处理消息
      contentFactory(assistantContent);
    }
    thinkStartTime = 0;
    thinkEndTime = 0;
  } catch (error) {
    console.error("API 调用失败:", error);
  } finally {
    userInput.value = ""; // 清空输入框
  }
};
</script>

<style scoped lang="scss">
.user {
  color: blue;
}

.assistant {
  color: green;
}

.chat-window {
  width: 60%;
  padding: 10px;
  height: 700px;
  margin: 100px auto;
  box-shadow: 0 0 10px #6cb4ffcf;
  overflow: hidden;
  .chat-tabs {
    :deep() {
      .el-tabs__header {
        margin-bottom: 0;
      }
    }
  }
  .chat-title {
    text-align: center;
    font-size: 18px;
    font-weight: bold;
    margin-bottom: 10px;
    height: 30px;
  }

  .chat-content {
    overflow-y: auto;
    border: 1px solid #e4e7ed;
    padding: 10px;
    margin-bottom: 10px;
    width: 100%;
    height: 436px;
    .chat-message {
      position: relative;
    }
    .chat-picture {
      width: 35px;
      height: 35px;
      background: #d44512;
      color: #fff;
      overflow: hidden;
      border-radius: 25px;
      font-size: 20px;
      line-height: 35px;
      text-align: center;
      position: absolute;
      top: 12px;
      left: -6px;
      &.chat-picture-user {
        background: #0079ff;
      }
    }
  }
  .chat-input,
  .chat-btn {
    height: 94px;
  }
  .chat-input {
  }

  .chat-btn {
    text-align: center;
    button {
      width: 100%;
      height: 100%;
    }
  }
}
</style>

新增src/views/Library/myLibrary.vue组件来实现工作空间下的知识库列表展示

<template>
  <el-row class="my-library-main">
    <el-col :span="24">
      <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
        <el-tab-pane
          v-for="tabItem in tabList"
          :label="tabItem.name"
          :name="tabItem.slug"
          :key="tabItem.slug"
        />
      </el-tabs>
    </el-col>
    <el-col :span="24">
      <el-table border :data="tableData">
        <el-table-column align="center" type="index" width="80" label="序号" />
        <el-table-column prop="title" label="标题" />
        <el-table-column prop="filename" label="文件名称" />
        <el-table-column prop="docpath" label="URL" />
        <el-table-column prop="published" label="发布时间" />
        <el-table-column prop="createdAt" label="创建时间" />
        <el-table-column prop="lastUpdatedAt" label="创建时间" />
      </el-table>
    </el-col>
  </el-row>
</template>

<script setup lang="ts">
import { onMounted, ref } from "vue";
import {
  getAllDocuments,
  getAllWorkspaces,
  getWorkspaceBySlug,
} from "@/apis/anythionChatAPIs.ts";
let tableData = ref([]);
let activeName = ref("");
let tabList = ref([] as any);
onMounted(() => {
  // 初始化获取知识库信息
  getLibrary();
  // 初始化获取所有工作空间信息
  getWorkspaceList();
});
/* 切换tab */
const handleClick = () => {
  getWorkspaceLibraryBySlug();
};
/* 获取指定会话中的知识库 */
const getWorkspaceLibraryBySlug = async () => {
  let result: any = await getWorkspaceBySlug(activeName.value);
  tableData.value =
    (result.workspace &&
      result.workspace.length > 0 &&
      result.workspace[0].documents.map((item: any) => {
        let _matData = JSON.parse(item.metadata);
        return {
          ...item,
          ..._matData,
        };
      })) ||
    [];
};
/* 获取工作空间列表信息 */
const getWorkspaceList = async () => {
  const result: any = await getAllWorkspaces();
  tabList.value = result.workspaces;
  // 默认选中第一项
  activeName.value = tabList.value[0].slug;
  getWorkspaceLibraryBySlug();
};
/* 获取知识库 */
const getLibrary = async () => {
  const result: any = await getAllDocuments();
  tableData.value = result.localFiles.items[0].items;
};
</script>

<style scoped lang="scss">
.my-library-main {
  padding: 10px;
}
</style

新增src/views/Library/workspace.vue组件来实现工作空间管理

<template>
  <el-row class="workspace-main">
    <el-col class="workspace-btn-tool">
      <EditWorkspacePlugin @confirmWorkspace="getWorkspaceList" />
    </el-col>
    <el-col class="workspace-list">
      <el-table border :data="workspaceList" class="workspace-table">
        <el-table-column
          type="index"
          width="60"
          align="center"
          label="序号"
        ></el-table-column>
        <el-table-column prop="name" label="工作空间名称"></el-table-column>
        <el-table-column align="center" label="工作空间线程数">
          <template #default="{ row }">
            <el-popover
              width="200"
              :disabled="!row.threads.length"
              class="box-item"
              title="工作线程"
              placement="right"
            >
              <ul>
                <li v-for="item in row.threads" :key="item.slug">
                  {{ item.name }}
                </li>
              </ul>
              <template #reference>
                <span class="workspace-threads-num">
                  {{ row.threads.length }}
                </span>
              </template>
            </el-popover>
          </template>
        </el-table-column>
        <el-table-column prop="createdAt" label="创建时间"></el-table-column>
        <el-table-column prop="lastUpdatedAt" label="最后修改时间"></el-table-column>
        <el-table-column class-name="workspace-edit" label="编辑">
          <template #default="{ row }">
            <EditWorkspacePlugin
              :key="row.slug"
              v-bind="getBindOption(row)"
              @confirmWorkspace="getWorkspaceList"
            />
            <el-link type="primary" class="edit-item" @click="delWorkspace(row)"
              >删除</el-link
            >
            <AddLibraryPlugin :slug="row.slug" />
          </template>
        </el-table-column>
      </el-table>
    </el-col>
  </el-row>
</template>

<script setup lang="ts">
import { onMounted, ref } from "vue";
import { getAllWorkspaces, delWorkspaceBySlug } from "@/apis/anythionChatAPIs.ts";
import EditWorkspacePlugin from "./components/workspace/editWorkspacePlugin.vue";
import AddLibraryPlugin from "./components/workspace/addLibraryPlugin.vue";
import { ElMessage, ElMessageBox } from "element-plus";
let workspaceList = ref([] as Array<any>);
onMounted(() => {
  getWorkspaceList();
});
/* 获取工作空间列表信息 */
const getWorkspaceList = async () => {
  const res: any = await getAllWorkspaces();
  workspaceList.value = res.workspaces;
};
/* 获取绑定的属性 */
const getBindOption = (row: any) => {
  return {
    bizType: "update",
    title: "修改工作空间",
    btnTitle: "修改",
    btnOptions: {
      btnType: "a",
      bind: {
        className: "edit-item el-link el-link--primary is-underline edit-item",
      },
      onEvent: {
        click: () => {
          return row;
        },
      },
    },
  };
};
/* 删除工作空间 */
const delWorkspace = (row: any) => {
  ElMessageBox.confirm("是否删除该工作空间?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(async () => {
    let res: any = await delWorkspaceBySlug(row.slug);
    if (res === "OK") {
      ElMessage.success("删除成功");
      // 移除已经删掉的工作空间
      workspaceList.value = workspaceList.value.filter(
        (item: any) => item.slug !== row.slug
      );
    } else {
      ElMessage.error("删除失败");
    }
  });
};
</script>

<style lang="scss" scoped>
.workspace-main {
  padding: 8px;

  .workspace-btn-tool {
    margin-bottom: 4px;
  }
}

:deep(.workspace-edit) {
  .edit-item {
    margin-right: 4px;

    &:last-child {
      margin-right: 0;
    }
  }
}
</style>

工作空间管理关联组件:addLibraryPlugin(添加知识库组件)

<template>
  <el-link class="edit-item" type="primary" @click="dialogFormVisible = true"
    >添加知识库</el-link
  >
  <el-dialog
    append-to-body
    title="添加知识库"
    v-model="dialogFormVisible"
    v-if="dialogFormVisible"
  >
    <el-transfer
      v-model="activeDocumentList"
      :data="data"
      :titles="['知识库', '工作空间知识库']"
      :button-texts="['删除', '添加']"
    >
      <template #default="{ option }">
        <el-tooltip
          class="box-item"
          effect="dark"
          :content="option.label"
          placement="top"
        >
          {{ option.label }}
        </el-tooltip>
      </template>
      <template #left-empty>
        <el-empty :image-size="60" description="暂无数据" />
      </template>
      <template #right-empty>
        <el-empty :image-size="60" description="暂无数据" />
      </template>
    </el-transfer>
    <el-row class="add-libray-plugin-history-group">
      <el-col class="add-libray-plugin-history-title">
        <span>历史已选知识库</span>
        <el-tooltip
          class="box-item"
          effect="dark"
          content="存放添加过且已经被删除的知识库信息"
          placement="top"
        >
          <el-icon :size="20">
            <QuestionFilled />
          </el-icon>
        </el-tooltip>
      </el-col>
      <el-col class="add-libray-plugin-history-content">
        <el-tag
          v-if="historyDocumentList.length > 0"
          v-for="tag in historyDocumentList"
          :key="tag.key"
          closable
          effect="dark"
        >
          {{ tag.title }}
        </el-tag>
        <el-empty v-else :image-size="30" description="暂无数据" />
      </el-col>
    </el-row>
    <template #footer>
      <el-button @click="dialogFormVisible = false">取消</el-button>
      <el-button type="primary" @click="saveDocument">保存</el-button>
    </template>
  </el-dialog>
</template>

<script setup lang="ts">
import { ref, watch } from "vue";
import type { TransferOption } from "../workspace/type/addLibraryPlugin.d.ts";
import {
  getAllDocuments,
  getWorkspaceBySlug,
  updateWorkspaceEmbeddingsBySlug,
} from "@/apis/anythionChatAPIs.ts";
import { ElMessage } from "element-plus";
const props = defineProps({
  slug: {
    type: String,
    default: "",
  },
});
let data = ref<TransferOption[]>([]);
const dialogFormVisible = ref(false);
const activeDocumentList = ref([]);
const historyDocumentList = ref<TransferOption[]>([]);
let defaultDocumentList = [] as string[];
watch(dialogFormVisible, (val) => {
  if (val) {
    generateData();
  }
});
/* 获取知识库、工作区已选及历史数据 */
const generateData = async () => {
  // 清空重新计算历史信息
  historyDocumentList.value = [];
  // 获取所有文档内容供工作空间选择
  const {
    localFiles: { items: resultLsit },
  }: any = await getAllDocuments();
  let _documentList = [] as TransferOption[];
  resultLsit.forEach((info: any) => {
    _documentList.push(
      info.items.map((item: any) => {
        return {
          key: item.id,
          id: item.id,
          name: item.name,
          title: item.title,
          published: item.published,
          token_count_estimate: item.token_count_estimate,
          type: item.type,
          label: `${item.title}(${info.name})`,
          disabled: false,
          docpath: `${info.name}/${item.name}`,
        };
      })
    );
  });
  data.value = _documentList.flat();
  // 获取当前操作的工作空间已经选有的文档内容
  const res: any = await getWorkspaceBySlug(props.slug);
  const activeList = res.workspace[0].documents.map((item: any) => {
    let _metadata = JSON.parse(item.metadata);
    let _resultItem: TransferOption = {
      key: _metadata.id,
      id: _metadata.id,
      name: item.filename,
      title: _metadata.title,
      published: _metadata.published || "",
      token_count_estimate: _metadata.token_count_estimate,
      type: _metadata.type || "",
      label: `${_metadata.title}`,
      disabled: false,
      docpath: item.docpath,
    };
    // 处理历史知识库信息
    if (!data.value.some((item: any) => item.key === _resultItem.key)) {
      historyDocumentList.value.push(_resultItem);
    }
    return _resultItem;
  });
  activeDocumentList.value = activeList.map((item: any) => item.key);
  // 备份记录当前默认的已选知识库
  defaultDocumentList = activeDocumentList.value;
};
/* 保存知识库 */
const saveDocument = async () => {
  let _params: any = {
    adds: [],
    deletes: [],
  };
  // 排除历史中的数据
  let _currentDocumentList: string[] = activeDocumentList.value.filter(
    (item) => !historyDocumentList.value.find((citem) => citem.key === item)
  );
  if (_currentDocumentList.length > 0) {
    console.log(_currentDocumentList, "新增");
    _currentDocumentList.forEach((item: any) => {
      // 新增项
      if (!defaultDocumentList.includes(item)) {
        // 查询文档信息并标记为新增
        let _documentInfo: TransferOption = data.value.find(
          (citem) => citem.key === item
        ) as TransferOption;
        _params.adds.push(_documentInfo.docpath);
      }
    });
    // 删除项
    defaultDocumentList.forEach((item: string) => {
      if (
        !historyDocumentList.value.find((citem) => citem.key === item) &&
        !_currentDocumentList.includes(item)
      ) {
        // 查询文档信息并标记为删除
        let _documentInfo: TransferOption = data.value.find(
          (citem) => citem.key === item
        ) as TransferOption;
        _params.deletes.push(_documentInfo.docpath);
      }
    });
    console.log(_params, "_params");
  } else {
    // 删除全部
    let _delInfo = data.value.filter((item: any) =>
      defaultDocumentList.includes(item.key)
    );
    _params.deletes = _delInfo.map((item: any) => item.docpath);
  }
  const res: any = await updateWorkspaceEmbeddingsBySlug(props.slug, _params);
  if (res) {
    ElMessage.success("操作成功");
    dialogFormVisible.value = false;
  } else {
    ElMessage.error("操作失败");
  }
};
</script>

<style scoped lang="scss">
.add-libray-plugin-history-group {
  border: solid 1px #dcdfe6;
  margin-top: 8px;
  padding: 0px 8px 8px;
  .add-libray-plugin-history-title {
    border-bottom: 1px solid #dcdfe6;
    font-size: 16px;
    height: 30px;
    align-items: center;
    display: flex;
    overflow: hidden;
    i {
      margin-left: 4px;
    }
  }
  .add-libray-plugin-history-content {
    margin-top: 8px;
    :deep(.el-tag) {
      padding-right: 5px;
      width: 200px;
      text-overflow: ellipsis;
      overflow: hidden;
      white-space: nowrap;
      display: inline-block;
      margin: 4px;
      position: relative;
      height: 20px;
      line-height: 20px;
      padding-right: 22px;
      .el-tag__close {
        margin-left: 6px;
        position: absolute;
        right: 6px;
        top: 50%;
        transform: translateY(-50%);
      }
    }
  }
}
:deep(.el-transfer-panel) {
  width: 351px;
}
</style>

工作空间管理关联组件editWorkspacePlugin.vue(新增工作空间、修改组件)

<template>
  <component :is="btnOptions.btnType" v-bind="btnOptions.bind" v-on="btnOptions.onEvent">
    {{ props.btnTitle }}
  </component>
  <el-dialog
    append-to-body
    v-if="dialogFormVisible"
    v-model="dialogFormVisible"
    :title="props.title"
  >
    <el-form :data="workspaceForm" label-width="110px">
      <el-row :gutter="20">
        <el-col :span="12">
          <el-form-item prop="name" label="工作空间名称">
            <el-input v-model="workspaceForm.name"></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item prop="similarityThreshold">
            <template #label>
              <el-tooltip
                class="box-item"
                effect="dark"
                content="相似性阈值通常用于衡量两个文本之间的相似度,常用于文本检索、抄袭检测等场景。OpenAI的模型可以通过计算文本嵌入向量之间的相似度来判断文本之间的相似性。相似性阈值是一个数值,用于决定两个文本是否足够相似。"
                placement="top"
              >
                <el-icon :size="20">
                  <QuestionFilled />
                </el-icon>
              </el-tooltip>
              相似性阈值
            </template>
            <el-input-number
              v-model="workspaceForm.similarityThreshold"
              controls-position="right"
              :min="0"
            ></el-input-number>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item prop="openAiTemp">
            <template #label>
              <el-tooltip
                class="box-item"
                effect="dark"
                content="温度是一个控制模型输出随机性和创造性的参数。它决定了模型在生成文本时的概率分布的“尖锐度”或“平滑度”。取值范围:通常在0到2之间"
                placement="top"
              >
                <el-icon :size="20">
                  <QuestionFilled />
                </el-icon>
              </el-tooltip>
              温度
            </template>
            <el-input-number
              v-model="workspaceForm.openAiTemp"
              :max="2"
              :min="0"
              controls-position="right"
            ></el-input-number>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item prop="openAiHistory">
            <template #label>
              <el-tooltip
                class="box-item"
                effect="dark"
                content="历史通常指的是模型在生成文本时考虑的上下文信息。在对话场景中,历史信息可以帮助模型生成更连贯和相关的回答。"
                placement="top"
              >
                <el-icon :size="20">
                  <QuestionFilled />
                </el-icon>
              </el-tooltip>
              历史
            </template>
            <el-input-number
              v-model="workspaceForm.openAiHistory"
              :min="0"
              controls-position="right"
            ></el-input-number>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item prop="topN">
            <template #label>
              <el-tooltip
                class="box-item"
                effect="dark"
                content="常用于控制生成文本时的候选词数量."
                placement="top"
              >
                <el-icon :size="20">
                  <QuestionFilled />
                </el-icon>
              </el-tooltip>
              概率阈值
            </template>
            <el-input-number
              :min="0"
              v-model="workspaceForm.topN"
              controls-position="right"
            ></el-input-number>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item prop="chatMode" label="模型">
            <el-input disabled v-model="workspaceForm.chatMode"></el-input>
          </el-form-item>
        </el-col>

        <el-col :span="24">
          <el-form-item prop="openAiPrompt" label="成功后响应内容">
            <el-input
              v-model="workspaceForm.openAiPrompt"
              type="textarea"
              :rows="4"
            ></el-input>
          </el-form-item>
        </el-col>
        <el-col :span="24">
          <el-form-item prop="queryRefusalResponse" label="失败后响应内容">
            <el-input
              v-model="workspaceForm.queryRefusalResponse"
              type="textarea"
              :rows="4"
            ></el-input>
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>
    <template #footer>
      <el-button @click="cancelWorkspace">取消</el-button>
      <el-button type="primary" @click="confirmWorkspace">确定</el-button>
    </template>
  </el-dialog>
</template>

<script setup lang="ts">
import { reactive, ref } from "vue";
import {
  createNewWorkspace,
  getWorkspaceBySlug,
  updateWorkspaceBySlug,
} from "@/apis/anythionChatAPIs.ts";
import type { WorkspaceForm } from "../workspace/type/workspace.ts";
import { ElMessage } from "element-plus";

const emit = defineEmits(["confirmWorkspace", "cancelWorkspace"]);
const props = defineProps({
  bizType: {
    type: String,
    defalut: (rawProps: any) => {
      if (["add", "update"].includes(rawProps.bizType)) {
        return rawProps.bizType;
      } else {
        return "add";
      }
    },
  },
  title: {
    type: String,
    default: "新增工作空间",
  },
  btnTitle: {
    type: String,
    default: "新增工作空间",
  },
  btnOptions: {
    type: Object,
    default: () => ({
      btnType: "el-button",
      bind: {
        type: "primary",
      },
      onEvent: {
        click: () => {},
      },
    }),
  },
});
let dialogFormVisible = ref(false);
let workspaceForm = reactive({
  name: "",
  similarityThreshold: 0,
  openAiTemp: 0,
  openAiHistory: 0,
  openAiPrompt: "",
  queryRefusalResponse: "",
  chatMode: "chat",
  topN: 0,
} as WorkspaceForm);
let editSlug = ref("");
let btnOptions = reactive(props.btnOptions);
Object.keys(btnOptions.onEvent).forEach((fun) => {
  let _funResult = null;
  if (typeof props.btnOptions.onEvent[fun] === "function") {
    _funResult = props.btnOptions.onEvent[fun]();
  }
  btnOptions.onEvent[fun] = () => {
    if (_funResult && props.bizType === "update") {
      // 获取指定的工作空间信息
      getWorkspaceBySlug(_funResult.slug).then((res: any) => {
        if (res.workspace && res.workspace.length > 0) {
          editSlug.value = _funResult.slug;
          workspaceForm = reactive(res.workspace[0]);
          // 修改
          dialogFormVisible.value = true;
        }
      });
    } else {
      // 新增
      dialogFormVisible.value = true;
    }
  };
});
/* 点击确定触发 */
const confirmWorkspace = async () => {
  if (props.bizType === "update") {
    // 确定修改
    updateWorkspace();
  } else {
    // 确定新增
    addWorkspace();
  }
};
/* 修改工作空间 */
const updateWorkspace = async () => {
  const res: any = await updateWorkspaceBySlug(editSlug.value, workspaceForm);
  if (res && res.workspace) {
    ElMessage.success("修改成功");
    dialogFormVisible.value = false;
    emit("confirmWorkspace", workspaceForm);
  } else {
    ElMessage.error("修改失败");
  }
};
/* 添加工作空间 */
const addWorkspace = async () => {
  const res: any = await createNewWorkspace(workspaceForm);
  if (res) {
    ElMessage.success("保存成功");
    dialogFormVisible.value = false;
    emit("confirmWorkspace", workspaceForm);
  } else {
    ElMessage.error("保存失败");
  }
};
/* 取消工作空间 */
const cancelWorkspace = () => {
  dialogFormVisible.value = false;
  emit("cancelWorkspace");
};
</script>

<style scoped lang="scss">
:deep() {
  .el-form-item {
    .el-form-item__label {
      align-items: center;

      i {
        margin-right: 4px;
        cursor: pointer;
      }
    }

    .el-form-item__content {
      .el-input,
      .el-input-number {
        width: 100%;
      }
    }
  }
}
</style>

工作空间管理相关TS类型脚本(workspace.d.ts

export interface WorkspaceForm {
  name: string;
  similarityThreshold: number;
  openAiTemp: number;
  openAiHistory: number;
  openAiPrompt: string;
  queryRefusalResponse: string;
  chatMode: string;
  topN: number;
}

工作空间管理相关TS类型脚本(addLibraryPlugin.d.ts

export interface TransferOption {
  key: string
  label: string
  disabled: boolean
  id: string
  name: string
  title: string
  published: string,
  token_count_estimate: number
  type: string,
  docpath: string
}
export interface DocumentListState{
  key:string,
  state:string
}

Vue3接口调用

anythionChatAPIs.ts脚本中新增接口

import axios from "@/anytingAxios.ts"

/**
 * /api/v1/workspaces
 * 获取当前所有工作空间
 * @returns 
 */
export function getAllWorkspaces(){
  return axios.get("/api/v1/workspaces")
}
// 以下内容为新增接口
/**
 * /api/v1/workspace/{slug}/chats
 * 通过工作空间的slug,获得工作区聊天信息
 * @param slug 工作空间的slugID
 * @returns 
 */
export function getWorkspaceChatsBySlug(slug:string){
  return axios.get(`/api/v1/workspace/${slug}/chats`)
}
/**
 * /api/v1/workspace/{slug}
 * 根据slug删除工作空间
 * @param slug
 * @returns 
 */
export function delWorkspaceBySlug(slug:string){
  return axios.delete(`/api/v1/workspace/${slug}`)
}
/**
 * /api/v1/workspace/{slug}
 * 通过其slug获得工作空间
 * @param slug
 * @returns 
 */
export function getWorkspaceBySlug(slug:string){
  return axios.get(`/api/v1/workspace/${slug}`)
}
/**
 * /api/v1/workspace/new
 * 创建一个工作空间
 * @param data {
  "name": "My New Workspace",
  "similarityThreshold": 0.7,
  "openAiTemp": 0.7,
  "openAiHistory": 20,
  "openAiPrompt": "Custom prompt for responses",
  "queryRefusalResponse": "Custom refusal message",
  "chatMode": "chat",
  "topN": 4
}
 * @returns 
 */
export function createNewWorkspace(data:any){
  return axios.post("/api/v1/workspace/new",data)
}
/**
 * /api/v1/workspace/{slug}/update
 * 通过slug更新工作区设置
 * @param slug 
 * @param data {
  "name": "Updated Workspace Name",
  "openAiTemp": 0.2,
  "openAiHistory": 20,
  "openAiPrompt": "Respond to all inquires and questions in binary - do not respond in any other format."
}
 * @returns 
 */
export function updateWorkspaceBySlug(slug:string,data:any){
  return axios.post(`/api/v1/workspace/${slug}/update`,data)
}
/**
 * /api/v1/workspace/{slug}/update-embeddings
 * 通过slug在工作区中添加或删除文档。
 * @param slug 
 * @param data {
  "adds": [
    "custom-documents/my-pdf.pdf-hash.json"
  ],
  "deletes": [
    "custom-documents/anythingllm.txt-hash.json"
  ]
}
 * @returns 
 */
export function updateWorkspaceEmbeddingsBySlug(slug:string,data:any){
  return axios.post(`/api/v1/workspace/${slug}/update-embeddings`,data)
}

至此,完成了本次的预期目标,实现了可以设置自己的工作空间且可以通过自己的知识库与自己的智能小助手进行聊天,下一期计划对知识库内容进行上传维护,达到模型的聊天信息可以从知识库中获取,敬请期待~