From 78e1cda41cf6ad133e84efc78da50d77f2e43eca Mon Sep 17 00:00:00 2001
From: charles <981744753@qq.com>
Date: 星期五, 02 八月 2024 15:56:43 +0800
Subject: [PATCH] feat:完成新增会话的,会话记录的聊天模块

---
 src/views/dmx/model/components/addModel.vue                |   11 -
 src/views/session/sessionManager/components/addSession.vue |   75 +++++++++--
 src/api/session.ts                                         |    4 
 src/api/interceptor.ts                                     |    4 
 src/views/session/sessionManager/index.vue                 |   24 ++--
 .idea/codeStyles/Project.xml                               |    4 
 src/views/sessionRecords/sessionRecordsManager/index.vue   |  236 ++++++++++++++++++++++++++++----------
 7 files changed, 249 insertions(+), 109 deletions(-)

diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 3e22f4b..70dac63 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -3,7 +3,7 @@
     <HTMLCodeStyleSettings>
       <option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
     </HTMLCodeStyleSettings>
-    <JSCodeStyleSettings version="0">
+    <JSCodeStyleSettings>
       <option name="FORCE_SEMICOLON_STYLE" value="true" />
       <option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
       <option name="FORCE_QUOTE_STYlE" value="true" />
@@ -11,7 +11,7 @@
       <option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
       <option name="SPACES_WITHIN_IMPORTS" value="true" />
     </JSCodeStyleSettings>
-    <TypeScriptCodeStyleSettings version="0">
+    <TypeScriptCodeStyleSettings>
       <option name="FORCE_SEMICOLON_STYLE" value="true" />
       <option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
       <option name="FORCE_QUOTE_STYlE" value="true" />
diff --git a/src/api/interceptor.ts b/src/api/interceptor.ts
index f08ca35..0f73743 100644
--- a/src/api/interceptor.ts
+++ b/src/api/interceptor.ts
@@ -55,7 +55,7 @@
     const res = response.data;
 
     // if the custom code is not 20000, it is judged as an error.
-    if (
+    /*if (
       (res.retcode && res.retcode !== 0) ||
       (res.code && res.code !== 20000)
     ) {
@@ -88,7 +88,7 @@
       response.config.url === '/base/login'
     ) {
       setAuthorization(response.headers.authorization);
-    }
+    }*/
     return res;
   },
   (error) => {
diff --git a/src/api/session.ts b/src/api/session.ts
index fe2b47f..bd56742 100644
--- a/src/api/session.ts
+++ b/src/api/session.ts
@@ -26,4 +26,8 @@
 // 鑾峰彇浼氳瘽璇︽儏
 export function getSessionDetailsApi(conversation_id:string) {
     return axios.get<ISessionListResult>('/api/conversation/get?modeltype=localragflow',{params:{conversation_id}});
+}
+// 鑾峰彇鏅鸿兘鍔╂墜鍒楄〃
+export function getDialogListApi() {
+  return axios.get<ISessionListResult>('/api/dialog/list');
 }
\ No newline at end of file
diff --git a/src/views/dmx/model/components/addModel.vue b/src/views/dmx/model/components/addModel.vue
index eeab066..456a8fa 100644
--- a/src/views/dmx/model/components/addModel.vue
+++ b/src/views/dmx/model/components/addModel.vue
@@ -1,4 +1,3 @@
-
 <template>
   <a-button type="primary" @click="handleClick" style="margin-left: 10px">
     <template #icon>
@@ -138,7 +137,6 @@
         done(false)
       }else {
         console.log('璇锋眰鏁版嵁');
-
       }
     })
 };
@@ -176,12 +174,3 @@
 
 })
 </script>
-
-<script lang="ts">
-export default {
-  name: 'add',
-  methods: {
-
-  }
-};
-</script>
\ No newline at end of file
diff --git a/src/views/session/sessionManager/components/addSession.vue b/src/views/session/sessionManager/components/addSession.vue
index 0a1c997..37ebde1 100644
--- a/src/views/session/sessionManager/components/addSession.vue
+++ b/src/views/session/sessionManager/components/addSession.vue
@@ -1,34 +1,75 @@
 <script setup lang="ts">
-    import { defineProps ,ref,defineEmits} from 'vue';
+    import { defineProps,ref,defineEmits, onMounted } from 'vue';
     import { Message } from '@arco-design/web-vue';
-    import { addSessionApi }from '@/api/session';
+    import { addSessionApi ,getDialogListApi }from '@/api/session';
     const props=defineProps({
         modalObj:Object
     });
-    const conversation_name=ref('');
+    const conversation=ref({ dialog_id:'', conversation_desc:'' });
+    const dialogList=ref([]);
     const emit = defineEmits(['addSession']);
-    const handleOk=async ()=>{
-        if(conversation_name.value){
-            const {code}=await addSessionApi({conversation_name:conversation_name.value});
-            if(code===200){
-                Message.success('娣诲姞鎴愬姛');
-                emit('addSession')
-            }
-        }else{
-            Message.warning('浼氳瘽鍚嶇О涓嶈兘涓虹┖');
-        }
+    const queryDialogList=async ()=>{
+      const { code, data } = await getDialogListApi();
+      if(code===200){
+        dialogList.value = data;
+      }
+    };
+
+   const  rules = {
+     dialog_id: [
+       {
+         required: true,
+         message: '鏅鸿兘鍔╂墜涓嶈兘涓虹┖',
+       },
+     ],
+     conversation_desc: [
+       {
+         required: true,
+         message: '鎻忚堪涓嶈兘涓虹┖',
+       }
+     ]
+   }
+    onMounted(()=>{
+      queryDialogList();
+    });
+    const formRef = ref();
+    const handleOk=()=>{
+      formRef.value.validate().then(async(res)=>{
+        if(!res){
+           const { code }=await addSessionApi({ ...conversation.value });
+           if(code===200){
+             Message.success('娣诲姞鎴愬姛');
+             emit('addSession');
+             setTimeout(()=>{
+               props.modalObj.add=false;
+             },500);
+           }else{
+             Message.warning('娣诲姞澶辫触');
+           }
+         }
+      });
+
+      return false;
+    }
+    const destroyData = ()=>{
+      formRef.value.resetFields();
     }
 </script>
 
 <template>
     <div>
-        <a-modal v-model:visible="modalObj.add" @ok="handleOk" @cancel="modalObj.add=false">
+        <a-modal v-model:visible="modalObj.add" @before-ok="handleOk" @cancel="modalObj.add=false"  @before-close="destroyData">
             <template #title>
                 鏂板浼氳瘽
             </template>
-            <a-form>
-                <a-form-item label="浼氳瘽鍚嶇О:">
-                    <a-input placeholder="璇疯緭鍏ヤ細璇濆悕绉�" v-model="conversation_name" style="width: 80%"></a-input>
+            <a-form ref="formRef" :model="conversation" :rules="rules">
+                <a-form-item label="鍔╂墜鍏宠仈:" field="dialog_id" @submit="handleSubmit">
+                    <a-select style="width: 80%" v-model="conversation.dialog_id" placeholder="璇烽�夋嫨鍏宠仈鍔╂墜">
+                        <a-option v-for="dialog in dialogList" :key="dialog.id" :value="dialog.id">{{dialog.name}}</a-option>
+                    </a-select>
+                </a-form-item>
+                <a-form-item label="鎻忚堪:" field="conversation_desc">
+                    <a-textarea placeholder="璇疯緭鍏ユ弿杩�" :max-length="100" show-word-limit   :auto-size="{minRows:4,maxRows:5}" v-model="conversation.conversation_desc" style="width: 80%"></a-textarea>
                 </a-form-item>
             </a-form>
         </a-modal>
diff --git a/src/views/session/sessionManager/index.vue b/src/views/session/sessionManager/index.vue
index 6cdb28a..1348516 100644
--- a/src/views/session/sessionManager/index.vue
+++ b/src/views/session/sessionManager/index.vue
@@ -25,20 +25,20 @@
     const streamStr=ref('');
     const modalObj=reactive({ add:false });
     //鏌ヨ浼氳瘽鍒楄〃
-  const querySessionList = async () => {
+    const querySessionList = async () => {
         const { code, data } =await sessionListApi();
-    if (code === 200) {
-      sessionList.value = data;
-            if(Array.isArray(data)&&data.length>0){
-                activeSessionId.value=data[0].id;
-                const res= await getSessionDetailsApi(data[0].id);
-                if(res.code===200){
-                    sessionDetailList.value=res.data.message;
-                    refreshScroll();
+        if (code === 200) {
+          sessionList.value = data;
+          if(Array.isArray(data)&&data.length>0){
+                    activeSessionId.value=data[0].id;
+                    const res= await getSessionDetailsApi(data[0].id);
+                    if(res.code===200){
+                        sessionDetailList.value=res.data.message;
+                        refreshScroll();
+                    }
                 }
-            }
         }else{
-            Message.warning('鏌ヨ澶辫触');
+          Message.warning('鏌ヨ澶辫触');
         }
     };
     //鏍规嵁浼氳瘽id鍒犻櫎浼氳瘽
@@ -142,7 +142,7 @@
                         </a-button>
                     </template>
                     <a-scrollbar class="left-list" style="height: 60vh;overflow-y: auto;">
-                        <div class="item" :class="{isLeftActive:activeSessionId===session.id}" v-for="session in sessionList" :key="session.id" @click="querySessionDetail(session)">
+                        <div class="item" :class="{ isLeftActive:activeSessionId===session.id }" v-for="session in sessionList" :key="session.id" @click="querySessionDetail(session)">
                             <div class="item-left">
                                 <IconQuestionCircleFill/>銆�
                                 {{session.name}}
diff --git a/src/views/sessionRecords/sessionRecordsManager/index.vue b/src/views/sessionRecords/sessionRecordsManager/index.vue
index 528c003..3b3a1e9 100644
--- a/src/views/sessionRecords/sessionRecordsManager/index.vue
+++ b/src/views/sessionRecords/sessionRecordsManager/index.vue
@@ -1,14 +1,65 @@
 <script setup lang="ts">
     import { IconSearch,IconTiktokColor ,IconSend,IconClose} from '@arco-design/web-vue/es/icon';
     import { useAppStore} from '@/store';
-    import {computed,ref,onMounted,reactive} from 'vue';
-    import {sessionListApi}from '@/api/session';
+    import { computed, ref, onMounted, reactive, nextTick } from 'vue';
     import { Message } from '@arco-design/web-vue';
     import moment from 'moment';
     import AddSession from '@/views/session/sessionManager/components/addSession.vue';
+    import { sessionListApi, deleteSessionApi,getSessionDetailsApi,chatApi }from '@/api/session';
+    const sessionDetailList=ref([]);//鏍规嵁浼氳瘽id鍑烘潵鐨勪細璇濊鎯�
     const sessionList=ref([]);//浼氳瘽鍒楄〃
-    const modalObj=reactive({add:false});
-    //鏌ヨ浼氳瘽鍒楄〃
+    const modalObj=reactive({ add:false });
+
+    const currIndex = ref(0)
+    const displayedText = ref('');// 姝e湪鏄剧ず鐨勬枃瀛�
+    let timer: number|null = null;
+    const streamStr=ref('');
+    const inputMsg=ref('');
+    const activeSessionId=ref('');
+
+    const sendMessage= async (event)=>{
+      event.preventDefault();
+      if(!activeSessionId.value){
+        Message.warning('璇烽�夋嫨浼氳瘽');
+        return;
+      }
+      if(inputMsg.value){
+        const {code,data} =await chatApi({conversation_id:activeSessionId.value,messages:inputMsg.value});
+        const res= await getSessionDetailsApi(activeSessionId.value);
+        if(res.code===200){
+          sessionDetailList.value=res.data.message.map((item,index)=>{
+            if(index===res.data.message.length-1){
+              item.role='last';
+              displayedText.value='';
+              currIndex.value=0;
+              streamStr.value=item.content;
+              startDisplayStr();
+            }
+            return item;
+          });
+          refreshScroll();
+        }
+        inputMsg.value='';
+      }else{
+        Message.warning('娑堟伅涓嶈兘涓虹┖');
+      }
+    };
+    const querySessionDetail=async (session)=>{
+      activeSessionId.value=session.id;
+      const {code,data}= await getSessionDetailsApi(session.id);
+      if(code===200){
+        sessionDetailList.value=data.message;
+        refreshScroll();//鍒锋柊婊氬姩鏉′綅缃�
+      }
+    };
+    const scrollbar = ref(null);
+    const refreshScroll=()=>{
+      nextTick(()=>{
+        const container = document.getElementById('home');
+        scrollbar.value.scrollTop(container.scrollHeight);
+      });
+    };
+    // 鏌ヨ浼氳瘽鍒楄〃
     const querySessionList=async ()=>{
         const {code,data} =await sessionListApi();
         if(code===200){
@@ -28,6 +79,22 @@
     const theme = computed(() => {
         return appStore.theme;
     });
+    //鏂囧瓧鍔ㄦ�佽緭鍑�
+    const startDisplayStr = () => {
+      if (timer) {
+        clearTimeout(timer!);
+      }
+      const res = streamStr.value;
+      // 灏嗘暟缁勪腑鐨勫瓧绗︿覆鎷兼帴璧锋潵
+      if (currIndex.value < res.length) {
+        displayedText.value += res[currIndex.value];
+        currIndex.value++;
+        setTimeout(startDisplayStr, 100);
+      } else {
+        clearTimeout(timer!);
+        timer = null
+      }
+    }
 </script>
 
 <template>
@@ -50,7 +117,7 @@
                 </a-card>
                 <a-card class="left">
                     <a-scrollbar class="left-list" style="height: calc(100vh - 160px);overflow-y: auto;overflow-x: hidden;">
-                        <div class="item" v-for="session in sessionList">
+                        <div class="item" v-for="session in sessionList" @click="querySessionDetail(session)" :class="{ isLeftActive:activeSessionId===session.id }">
                             <div class="text" :class="{light:theme==='dark'}">{{session.name}}</div>
                             <div class="time">{{moment(new Date(session.create_time)).format('YYYY-MM-DD HH:mm:ss')}}</div>
                         </div>
@@ -59,68 +126,102 @@
             </a-col>
             <a-col :span="15">
                 <a-card class="center">
-                    <div class="center-title">鏅鸿兘闂瓟</div>
-                    <div class="center-content">
-                        鎴戝彲浠ョ悊瑙e拰瀛︿範浜虹被鐨勮瑷�锛屽叿澶囧杞璇濈殑鑳藉姏锛岀幇鍦ㄥ拰鎴戝紑濮嬩氦娴佸惂~
-                    </div>
-                    <div class="center-question">
-                        <div class="center-question-left">璇曚竴璇曡繖鏍烽棶鎴�</div>
-                        <div class="center-question-right">
-                            <a-button type="primary">鎹竴鎹�</a-button>
+                    <div v-if="sessionDetailList.length===0">
+                        <div class="center-title">鏅鸿兘闂瓟</div>
+                        <div class="center-content">
+                            鎴戝彲浠ョ悊瑙e拰瀛︿範浜虹被鐨勮瑷�锛屽叿澶囧杞璇濈殑鑳藉姏锛岀幇鍦ㄥ拰鎴戝紑濮嬩氦娴佸惂~
                         </div>
+                        <div class="center-question">
+                            <div class="center-question-left">璇曚竴璇曡繖鏍烽棶鎴�</div>
+                            <div class="center-question-right">
+                                <a-button type="primary">鎹竴鎹�</a-button>
+                            </div>
+                        </div>
+                        <a-row  justify="space-around" class="center-list">
+                            <a-col :span="7" class="item">
+                                <div class="item-title">
+                                    <IconTiktokColor></IconTiktokColor>鎶栭煶甯﹁揣鑴氭湰
+                                </div>
+                                <div class="item-content" :class="{dark:theme==='dark'}">
+                                    缂栧啓寮曚汉娉ㄧ洰涓斿叿鏈夎鏈嶅姏鐨勩�侀�傜敤浜庝骇鍝佺殑...
+                                </div>
+                            </a-col>
+                            <a-col :span="7" class="item">
+                                <div class="item-title">
+                                    <IconTiktokColor></IconTiktokColor>鎶栭煶甯﹁揣鑴氭湰
+                                </div>
+                                <div class="item-content" :class="{dark:theme==='dark'}">
+                                    缂栧啓寮曚汉娉ㄧ洰涓斿叿鏈夎鏈嶅姏鐨勩�侀�傜敤浜庝骇鍝佺殑...
+                                </div>
+                            </a-col>
+                            <a-col :span="7" class="item">
+                                <div class="item-title">
+                                    <IconTiktokColor></IconTiktokColor>鎶栭煶甯﹁揣鑴氭湰
+                                </div>
+                                <div class="item-content" :class="{dark:theme==='dark'}">
+                                    缂栧啓寮曚汉娉ㄧ洰涓斿叿鏈夎鏈嶅姏鐨勩�侀�傜敤浜庝骇鍝佺殑...
+                                </div>
+                            </a-col>
+                            <a-col :span="7" class="item">
+                                <div class="item-title">
+                                    <IconTiktokColor></IconTiktokColor>鎶栭煶甯﹁揣鑴氭湰
+                                </div>
+                                <div class="item-content" :class="{dark:theme==='dark'}">
+                                    缂栧啓寮曚汉娉ㄧ洰涓斿叿鏈夎鏈嶅姏鐨勩�侀�傜敤浜庝骇鍝佺殑...
+                                </div>
+                            </a-col>
+                            <a-col :span="7" class="item">
+                                <div class="item-title">
+                                    <IconTiktokColor></IconTiktokColor>鎶栭煶甯﹁揣鑴氭湰
+                                </div>
+                                <div class="item-content" :class="{dark:theme==='dark'}">
+                                    缂栧啓寮曚汉娉ㄧ洰涓斿叿鏈夎鏈嶅姏鐨勩�侀�傜敤浜庝骇鍝佺殑...
+                                </div>
+                            </a-col>
+                            <a-col :span="7" class="item">
+                                <div class="item-title">
+                                    <IconTiktokColor></IconTiktokColor>鎶栭煶甯﹁揣鑴氭湰
+                                </div>
+                                <div class="item-content" :class="{dark:theme==='dark'}">
+                                    缂栧啓寮曚汉娉ㄧ洰涓斿叿鏈夎鏈嶅姏鐨勩�侀�傜敤浜庝骇鍝佺殑...
+                                </div>
+                            </a-col>
+                        </a-row>
                     </div>
-                    <a-row  justify="space-around" class="center-list">
-                        <a-col :span="7" class="item">
-                            <div class="item-title">
-                                <IconTiktokColor></IconTiktokColor>鎶栭煶甯﹁揣鑴氭湰
-                            </div>
-                            <div class="item-content" :class="{dark:theme==='dark'}">
-                                缂栧啓寮曚汉娉ㄧ洰涓斿叿鏈夎鏈嶅姏鐨勩�侀�傜敤浜庝骇鍝佺殑...
-                            </div>
-                        </a-col>
-                        <a-col :span="7" class="item">
-                            <div class="item-title">
-                                <IconTiktokColor></IconTiktokColor>鎶栭煶甯﹁揣鑴氭湰
-                            </div>
-                            <div class="item-content" :class="{dark:theme==='dark'}">
-                                缂栧啓寮曚汉娉ㄧ洰涓斿叿鏈夎鏈嶅姏鐨勩�侀�傜敤浜庝骇鍝佺殑...
-                            </div>
-                        </a-col>
-                        <a-col :span="7" class="item">
-                            <div class="item-title">
-                                <IconTiktokColor></IconTiktokColor>鎶栭煶甯﹁揣鑴氭湰
-                            </div>
-                            <div class="item-content" :class="{dark:theme==='dark'}">
-                                缂栧啓寮曚汉娉ㄧ洰涓斿叿鏈夎鏈嶅姏鐨勩�侀�傜敤浜庝骇鍝佺殑...
-                            </div>
-                        </a-col>
-                        <a-col :span="7" class="item">
-                            <div class="item-title">
-                                <IconTiktokColor></IconTiktokColor>鎶栭煶甯﹁揣鑴氭湰
-                            </div>
-                            <div class="item-content" :class="{dark:theme==='dark'}">
-                                缂栧啓寮曚汉娉ㄧ洰涓斿叿鏈夎鏈嶅姏鐨勩�侀�傜敤浜庝骇鍝佺殑...
-                            </div>
-                        </a-col>
-                        <a-col :span="7" class="item">
-                            <div class="item-title">
-                                <IconTiktokColor></IconTiktokColor>鎶栭煶甯﹁揣鑴氭湰
-                            </div>
-                            <div class="item-content" :class="{dark:theme==='dark'}">
-                                缂栧啓寮曚汉娉ㄧ洰涓斿叿鏈夎鏈嶅姏鐨勩�侀�傜敤浜庝骇鍝佺殑...
-                            </div>
-                        </a-col>
-                        <a-col :span="7" class="item">
-                            <div class="item-title">
-                                <IconTiktokColor></IconTiktokColor>鎶栭煶甯﹁揣鑴氭湰
-                            </div>
-                            <div class="item-content" :class="{dark:theme==='dark'}">
-                                缂栧啓寮曚汉娉ㄧ洰涓斿叿鏈夎鏈嶅姏鐨勩�侀�傜敤浜庝骇鍝佺殑...
-                            </div>
-                        </a-col>
-                    </a-row>
+                    <a-scrollbar ref="scrollbar" id="home" v-else class="chat-list" style="width:90%;overflow:auto;height: 60vh;margin: 0px auto">
+                        <div class="chat-item" v-for="sessionDetail in sessionDetailList">
+                            <a-comment
+                              v-if="sessionDetail.role==='user'"
+                              avatar="https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp"
+                            >
+                                <template #content>
+                                    <div :class="{light:theme==='light'}">{{sessionDetail.content}}</div>
+                                </template>
+                            </a-comment>
+                            <a-comment
+                              v-else-if="sessionDetail.role==='assistant'"
+                              avatar="https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/9eeb1800d9b78349b24682c3518ac4a3.png~tplv-uwbnlip3yd-webp.webp"
+                            >
+                                <template #content>
+                                    <a-card class="chat-item-answer"  style="background-color: rgba(63, 64, 79, 1);">
+                                        <div :class="{light:theme==='light'}">{{sessionDetail.content}}</div>
+                                    </a-card>
+                                </template>
+                            </a-comment>
+                            <a-comment
+                              v-else-if="sessionDetail.role==='last'"
+                              avatar="https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/9eeb1800d9b78349b24682c3518ac4a3.png~tplv-uwbnlip3yd-webp.webp"
+                            >
+                                <template #content>
+                                    <a-textarea readonly auto-size v-model="displayedText" class="chat-item-answer"  style="background-color: rgba(63, 64, 79, 1);">
+                                    </a-textarea>
+                                </template>
+                            </a-comment>
+                        </div>
+                    </a-scrollbar>
+
                     <div class="center-bottom">
-                        <a-textarea style="height: 180px" placeholder="杈撳叆鎮ㄦ兂浜嗚В鐨勫唴瀹癸紝Shift+Enter鎹㈣" :max-length="500" allow-clear show-word-limit>
+                        <a-textarea v-model="inputMsg"   @keydown.shift.enter="sendMessage" style="height: 180px" placeholder="杈撳叆鎮ㄦ兂浜嗚В鐨勫唴瀹癸紝Shift+Enter鍙戦��" :max-length="500" allow-clear show-word-limit>
                         </a-textarea>
                     </div>
                 </a-card>
@@ -181,6 +282,9 @@
 </template>
 
 <style scoped lang="scss">
+    .isLeftActive{
+        background-color:lightgrey;
+    }
     .light{
         color: white !important;
     }
@@ -210,10 +314,12 @@
                     }
                     .text{
                         color: black;
+                        padding-left: 10px;
                     }
                     .time{
                         color: gray;
                         font-size: 12px;
+                        padding-left: 10px;
                     }
                 }
             }
@@ -269,7 +375,7 @@
             .center-bottom{
                 position: absolute;
                 width: 90%;
-                bottom: 70px;
+                bottom: 20px;
                 left:5%;
             }
         }

--
Gitblit v1.8.0