| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113 |
- <template>
- <div class="app-container">
- <!-- 主要内容区域 -->
- <div class="main-content">
- <!-- 左侧知识库列表 -->
- <div class="left-panel">
- <div class="panel-header">
- <h3>知识库列表</h3>
- <span class="knowledge-count">{{ knowledgeList.length }} 个知识库</span>
- <el-button type="primary" size="small" @click="handleAdd" v-hasPermi="['llm:knowledge:add']"
- icon="el-icon-plus">
- 新增知识库
- </el-button>
- </div>
-
- <!-- 搜索框 -->
- <div class="search-box">
- <el-input v-model="queryParams.collectionName" placeholder="请输入知识库名称" clearable @clear="handleQuery"
- @keyup.enter.native="handleQuery">
- <template slot="append">
- <el-button @click="handleQuery" icon="el-icon-search">
- </el-button>
- </template>
- </el-input>
- </div>
-
- <div class="knowledge-cards" v-loading="loading">
- <div v-for="(item, index) in knowledgeList" :key="index" class="knowledge-card"
- :class="{ 'active': (selectedKnowledge ? selectedKnowledge.collectionName : '') === item.collectionName }"
- @click="selectKnowledge(item)">
- <div class="card-header">
- <div class="card-title">
- <i class="el-icon-folder folder-icon">
- </i>
- <span class="title-text">{{ item.collectionName }}</span>
- </div>
- <div class="card-actions">
- <el-dropdown trigger="click" @command="handleCardAction">
- <el-button type="text" size="small" icon="el-icon-more">
- </el-button>
- <el-dropdown-menu slot="dropdown">
- <el-dropdown-item :command="{ action: 'edit', data: item }" icon="el-icon-edit">编辑</el-dropdown-item>
- <el-dropdown-item :command="{ action: 'upload', data: item }"
- icon="el-icon-upload">上传文件</el-dropdown-item>
- <el-dropdown-item :command="{ action: 'delete', data: item }" icon="el-icon-delete"
- divided>删除</el-dropdown-item>
- </el-dropdown-menu>
- </el-dropdown>
- </div>
- </div>
-
- <div class="card-content">
- <p class="description">{{ item.description || '暂无描述' }}</p>
- <div class="meta-info">
- <span class="create-time">{{ parseTime(item.createdTime, '{y}-{m}-{d}') }}</span>
- <span class="file-count">{{ item.fileCount || 0 }} 个文件</span>
- </div>
- </div>
- </div>
-
- <!-- 空状态 - 无论是否搜索,当列表为空且未加载时都显示创建按钮 -->
- <div v-if="knowledgeList.length === 0 && !loading" class="empty-state">
- <i class="el-icon-folder-opened empty-icon"></i>
- <p>{{ queryParams.collectionName ? '搜索结果为空' : '暂无知识库' }}</p>
- <el-button type="primary" @click="handleAdd">{{ queryParams.collectionName ? '创建知识库' : '创建第一个知识库'
- }}</el-button>
- </div>
- </div>
- </div>
-
- <!-- 右侧面板 -->
- <div class="right-panel">
- <div class="panel-header">
- <h3>{{ isChatMode ? '知识库对话' : (isTitleMode ? '标题列表' : '文件列表') }}</h3>
- <div class="header-actions">
- <el-button v-if="!isChatMode && !isTitleMode" type="primary" icon="el-icon-chat-round" @click="handleChat(selectedKnowledge)"
- :disabled="!selectedKnowledge" v-hasPermi="['llm:knowledge:chat']">
- 开始对话
- </el-button>
- <el-button v-if="!isChatMode && !isTitleMode" type="primary" icon="el-icon-upload" @click="handleUpload(selectedKnowledge)"
- :disabled="!selectedKnowledge" v-hasPermi="['llm:knowledge:upload']">
- 上传文件
- </el-button>
- <el-button v-if="!isChatMode && !isTitleMode" type="default" icon="el-icon-notebook-2" @click="switchToTitleMode"
- :disabled="!selectedKnowledge">
- 查看内容
- </el-button>
- <el-button v-if="isChatMode" type="default" icon="el-icon-document" @click="switchToFileMode">
- 返回文件列表
- </el-button>
- <el-button v-if="isTitleMode" type="default" icon="el-icon-document" @click="switchToFileMode">
- 返回文件列表
- </el-button>
- </div>
- </div>
-
- <!-- 文件列表模式 -->
- <div v-if="!isChatMode && !isTitleMode" class="file-content" v-loading="fileLoading">
- <div v-if="selectedKnowledge" class="selected-knowledge">
- <i class="el-icon-folder folder-icon">
- </i>
- <span class="knowledge-name">{{ selectedKnowledge.collectionName }}</span>
- </div>
-
- <div v-if="!selectedKnowledge" class="empty-state">
- <i class="el-icon-document empty-icon">
- </i>
- <p>请选择一个知识库查看文件</p>
- </div>
-
- <div v-else-if="fileList.length === 0" class="empty-state">
- <i class="el-icon-document empty-icon">
- </i>
- <p>该知识库暂无文件</p>
- <el-button type="primary" @click="handleUpload(selectedKnowledge)">上传文件</el-button>
- </div>
-
- <div v-else class="file-list">
- <div v-for="(file, index) in pagedFileList" :key="index" class="file-item">
- <div class="file-info">
- <i class="el-icon-document file-icon">
- </i>
- <div class="file-details">
- <div class="file-name">{{ file }}</div>
- <div class="file-meta">
- <span class="file-type">{{ getFileType(file) }}</span>
- </div>
- </div>
- </div>
- <div class="file-actions">
- <el-button type="danger" size="small" icon="el-icon-delete" @click="handleDeleteFile(file)">
- 删除
- </el-button>
- </div>
- </div>
- </div>
-
- <!-- 文件列表分页 -->
- <pagination v-show="fileTotal > 0" :total="fileTotal" :page.sync="filePageNum" :limit.sync="filePageSize"
- @pagination="handleFilePagination" :autoScroll="false" />
- </div>
-
- <!-- 标题列表模式 -->
- <div v-if="!isChatMode && isTitleMode" class="title-content" v-loading="titleLoading">
- <div v-if="selectedKnowledge" class="selected-knowledge">
- <i class="el-icon-folder folder-icon">
- </i>
- <span class="knowledge-name">{{ selectedKnowledge.collectionName }}</span>
- <span class="title-mode-badge">标题模式</span>
- </div>
-
- <div v-if="!selectedKnowledge" class="empty-state">
- <i class="el-icon-notebook-2 empty-icon">
- </i>
- <p>请选择一个知识库查看内容</p>
- </div>
-
- <div v-else-if="titleList.length === 0" class="empty-state">
- <i class="el-icon-notebook-2 empty-icon">
- </i>
- <p>该知识库暂无标题</p>
- </div>
-
- <div v-else class="title-content-container">
- <!-- 左侧标题列表 -->
- <div class="title-list">
- <div v-for="(title, index) in titleList" :key="index" class="title-item"
- :class="{ 'active': selectedTitle === title }"
- @click="selectTitle(title)">
- <i class="el-icon-notebook-2 title-icon"></i>
- <span class="title-text">{{ title }}</span>
- </div>
- </div>
-
- <!-- 右侧内容列表 -->
- <div class="content-list" v-loading="contentLoading">
- <div v-if="!selectedTitle" class="empty-state">
- <i class="el-icon-document empty-icon"></i>
- <p>请选择一个标题查看内容</p>
- </div>
- <div v-else-if="contentList.length === 0" class="empty-state">
- <i class="el-icon-document empty-icon"></i>
- <p>该标题暂无内容</p>
- </div>
- <div v-else class="content-items">
- <div v-for="(content, index) in contentList" :key="index" class="content-item">
- <div class="content-header">
- <span class="content-index">{{ index + 1 }}</span>
- </div>
- <div class="content-body">
- {{ content }}
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
-
- <!-- 聊天模式 -->
- <div v-else class="chat-content">
- <div v-if="selectedKnowledge" class="selected-knowledge">
- <i class="el-icon-folder folder-icon">
- </i>
- <span class="knowledge-name">{{ selectedKnowledge.collectionName }}</span>
- <span class="chat-mode-badge">对话模式</span>
- </div>
-
- <!-- 聊天消息区域 -->
- <div class="chat-messages" ref="chatMessagesRef">
- <div v-if="chatMessages.length === 0" class="welcome-message">
- <div class="welcome-content">
- <div class="welcome-icon">🤖</div>
- <h3>知识库助手</h3>
- <p>我是基于「{{ selectedKnowledge ? selectedKnowledge.collectionName : "" }}」知识库的AI助手</p>
- <p>您可以询问关于该知识库内容的问题,我会为您提供准确的答案</p>
- </div>
- </div>
-
- <div v-else class="message-list">
- <div v-for="(message, index) in chatMessages" :key="index" class="message-item"
- :class="[message.type, { 'typing': message.type === 'ai' && isSending && index === chatMessages.length - 1 }]">
- <div class="message-avatar">
- <div v-if="message.type === 'user'" class="user-avatar">
- <i class="el-icon-user"></i>
- </div>
- <div v-else class="ai-avatar">
- 🤖
- </div>
- </div>
- <div class="message-content">
- <div class="message-bubble" :class="message.type">
- <div class="message-text" v-html="formatMessage(message.content)"></div>
-
- <!-- AI回答的引用文件信息 -->
- <div v-if="message.type === 'ai' && message.references && message.references.length > 0"
- class="message-references">
- <div class="references-header">
- <i class="el-icon-document reference-icon"></i>
- <span>参考文件 ({{ message.references.length }})</span>
- <el-button v-if="message.references.length > 3" type="text" size="small" class="toggle-references"
- @click="toggleReferences(index)">
- {{ message.showAllReferences ? '收起' : '展开全部' }}
- </el-button>
- </div>
- <div class="references-list">
- <div v-for="(ref, refIndex) in getDisplayedReferences(message)" :key="refIndex"
- class="reference-item">
- <div class="reference-info">
- <span class="reference-filename" :title="ref.fileName">{{ ref.fileName }}</span>
- <span class="reference-similarity" :class="getSimilarityClass(ref.similarity)">
- 相似度: {{ formatSimilarity(ref.similarity) }}%
- </span>
- </div>
- </div>
- </div>
- </div>
-
- <div class="message-time">{{ message.time }}</div>
- </div>
- </div>
- </div>
-
- <!-- 加载状态:仅在尚未收到首段流内容时显示,避免出现两个AI气泡 -->
- <div v-if="isSending && !streamingStarted" class="message-item ai">
- <div class="message-avatar">
- <div class="ai-avatar">
- 🤖
- </div>
- </div>
- <div class="message-content">
- <div class="message-bubble ai">
- <div class="loading-dots">
- <span></span><span></span><span></span>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
-
- <!-- 输入区域 -->
- <div class="chat-input-area">
- <div class="input-container">
- <el-input v-model="chatInput" type="textarea" :rows="3"
- :placeholder="isSending ? 'AI正在思考中...' : '请输入您的问题...'" @keyup.enter.native="handleEnter"
- @keyup.ctrl.enter.native="handleCtrlEnter" :disabled="isSending" resize="none" />
- <div class="input-actions">
- <el-button v-if="!isSending" type="primary" icon="el-icon-promotion" @click="sendMessage"
- :disabled="!chatInput.trim() || chatInput.length > 2000">
- 发送
- </el-button>
- <el-button v-else type="danger" icon="el-icon-delete" @click="stopGenerating">
- 停止生成
- </el-button>
- </div>
- </div>
- <div class="input-tips">
- <span>按 Enter 发送,Ctrl + Enter 换行</span>
- <span v-if="chatInput" class="char-count" :class="{ 'warning': chatInput.length > 1500 }">
- {{ chatInput.length }} 字符
- <span v-if="chatInput.length > 2000" class="error-text">(超出限制)</span>
- </span>
- </div>
- </div>
- </div>
- </div>
- </div>
-
- <!-- 添加或修改知识库对话框 -->
- <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
- <el-form ref="knowledgeFormRef" :model="form" :rules="rules" label-width="120px">
- <el-form-item label="知识库名称" prop="collectionName">
- <el-input v-model="form.collectionName" placeholder="请输入知识库名称" :disabled="isModify" />
- </el-form-item>
- <el-form-item label="新知识库名称" prop="newCollectionName" v-if="isModify">
- <el-input v-model="form.newCollectionName" placeholder="请输入新知识库名称" />
- </el-form-item>
- <el-form-item label="描述" prop="description" v-if="!isModify">
- <el-input type="textarea" v-model="form.description" placeholder="请输入描述" />
- </el-form-item>
- </el-form>
- <div slot="footer" class="dialog-footer">
- <el-button type="primary" @click="submitForm">确 定</el-button>
- <el-button @click="cancel">取 消</el-button>
- </div>
- </el-dialog>
-
- <!-- 上传文件对话框 -->
- <el-dialog title="上传文件到知识库" :visible.sync="uploadOpen" width="500px" append-to-body>
- <el-form ref="uploadFormRef" :model="uploadForm" :rules="uploadRules" label-width="80px">
- <el-form-item label="知识库" prop="collectionName">
- <el-input v-model="uploadForm.collectionName" disabled />
- </el-form-item>
- <el-form-item label="选择文件" prop="file">
- <el-upload ref="knowledgeUpload" multiple accept=".docx,.doc,.pdf" :headers="upload.headers" :action="''"
- :disabled="upload.isUploading" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess"
- :auto-upload="false" drag :on-change="handleFileChange">
- <i class="el-icon-upload"></i>
- <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
- <div slot="tip" class="el-upload__tip text-center">
- <div class="el-upload__tip">
- <el-checkbox v-model="upload.updateSupport" />
- 是否更新已经存在的文件数据
- </div>
- <span>支持 .docx、.doc、.pdf 格式文件</span>
- </div>
- </el-upload>
- </el-form-item>
- </el-form>
- <div slot="footer" class="dialog-footer">
- <el-button type="primary" @click="submitUpload">确 定</el-button>
- <el-button @click="cancelUpload">取 消</el-button>
- </div>
-
- <div v-if="percentageDisplay">
- <el-progress :percentage="percentage" :stroke-width="25" :text-inside="true">
- <el-button text>{{ progress }}</el-button>
- </el-progress>
- </div>
- </el-dialog>
- </div>
- </template>
-
- <script>
- import { parseTime } from "@/utils/ruoyi";
- import { Message } from 'element-ui'
- import { getToken } from "@/utils/auth";
- import { listKnowledge, listKnowLedgeByCollectionName, addKnowledge, updateKnowledge, delKnowledge, insertKnowledgeFile, listKnowledgeDocument, deleteKnowledgeFile, getProcessValue, listTiles, listByTitle } from "@/api/llm/knowLedge";
- import { getAnswer, getAnswerStream, getContextFile } from '@/api/llm/rag';
- import { marked } from 'marked';
-
- function createTypewriter(appender, options = {}) {
- const intervalMs = typeof options.intervalMs === 'number' ? options.intervalMs : 25 // 约 40 字/秒
- const maxCharsPerTick = typeof options.maxCharsPerTick === 'number' ? options.maxCharsPerTick : 1
-
- let queue = ''
- let timer = null
- let ended = false
- let onDrained = null
-
- const tick = () => {
- if (!queue) {
- if (ended) {
- if (timer) clearInterval(timer)
- timer = null
- if (onDrained) onDrained()
- }
- return
- }
-
- const n = Math.min(maxCharsPerTick, queue.length)
- const chunk = queue.slice(0, n)
- queue = queue.slice(n)
- appender(chunk)
- }
-
- return {
- push(text) {
- if (!text) return
- queue += text
- if (!timer) timer = setInterval(tick, intervalMs)
- },
- end(cb) {
- ended = true
- onDrained = cb
- if (!timer) timer = setInterval(tick, intervalMs)
- },
- stop() {
- ended = true
- queue = ''
- if (timer) clearInterval(timer)
- timer = null
- }
- }
- }
-
- function trimToLastSentenceEnd(text) {
- const s = String(text || '')
- // 句末标点:中文/英文句号、问号、叹号,或换行
- const last = Math.max(
- s.lastIndexOf('。'),
- s.lastIndexOf('!'),
- s.lastIndexOf('?'),
- s.lastIndexOf('.'),
- s.lastIndexOf('!'),
- s.lastIndexOf('?'),
- s.lastIndexOf('\n')
- )
- if (last === -1) return s.trim()
- return s.slice(0, last + 1).trim()
- }
-
- export default {
- name: 'KnowledgeManager',
- // 处理引用文件的显示状态
- data() {
- return {
- knowledgeList: [],
- open: false,
- uploadOpen: false,
- loading: true,
- fileLoading: false,
- percentage: 0,
- progress: "",
- percentageDisplay: false,
- showSearch: true,
- ids: [],
- single: true,
- multiple: true,
- total: 0,
- title: "",
- isModify: false,
- selectedKnowledge: null,
- fileList: [],
-
- // 文件分页相关状态
- fileTotal: 0,
- filePageNum: 1,
- filePageSize: 10,
-
- // 聊天相关状态
- isChatMode: false,
- chatMessages: [],
- chatInput: '',
- isSending: false,
- streamingStarted: false,
- chatMessagesRef: null,
-
- // 表单数据
- form: {
- collectionName: '',
- description: '',
- newCollectionName: ''
- },
-
- // 查询参数
- queryParams: {
- collectionName: ''
- },
-
- // 表单验证规则
- rules: {
- collectionName: [
- { required: true, message: "知识库名称不能为空", trigger: "blur" }
- ]
- },
-
- // 上传配置
- upload: {
- file: null,
- fileList: [],
- // 是否禁用上传
- isUploading: false,
- // 是否更新已经存在的文件数据
- updateSupport: true,
- // 设置上传的请求头部
- headers: { Authorization: "Bearer " + getToken() }
- },
-
- // 上传表单
- uploadForm: {
- collectionName: '',
- file: null
- },
-
- // 上传验证规则
- uploadRules: {
- file: [
- { required: true, message: "请选择要上传的文件", trigger: "blur" }
- ],
- collectionName: [
- { required: true, message: "请选择知识库", trigger: "blur" }
- ]
- },
-
- // 定时器
- timer: null,
-
- // 标题和内容相关状态
- isTitleMode: false, // 是否显示标题模式
- titleList: [], // 标题列表
- contentList: [], // 内容列表
- selectedTitle: null, // 选中的标题
- titleLoading: false, // 标题加载状态
- contentLoading: false // 内容加载状态
- }
- },
- mounted() {
- // 重置状态,确保页面刷新后不会停留在对话模式或标题模式
- this.isChatMode = false;
- this.isTitleMode = false;
- this.chatMessages = [];
- this.chatInput = '';
- this.streamingStarted = false;
- this.titleList = [];
- this.contentList = [];
- this.selectedTitle = null;
-
- this.getList();
- },
-
- computed: {
- pagedFileList() {
- const start = (this.filePageNum - 1) * this.filePageSize;
- const end = start + this.filePageSize;
- return this.fileList.slice(start, end);
- }
- },
-
- // 生命周期钩子
- beforeDestroy() {
- // 组件卸载时清理请求控制器和超时定时器
- if (window.currentController) {
- window.currentController.abort();
- window.currentController = null;
- }
- if (window.responseTimeout) {
- clearTimeout(window.responseTimeout);
- window.responseTimeout = null;
- }
- },
- methods: {
- /** 查询知识库列表 */
- getList() {
- this.loading = true;
- // 将查询参数传递给API调用
- if (this.queryParams.collectionName == '') {
- listKnowledge().then(response => {
- // 确保返回的数据是数组格式
- if (Array.isArray(response.data)) {
- this.knowledgeList = response.data;
- console.log(this.knowledgeList);
- this.knowledgeList.map(item => {
- listKnowledgeDocument(item.collectionName).then(response => {
- item.fileCount = response.data.length;
- })
- })
- } else {
- this.knowledgeList = [];
- }
- this.loading = false;
- }).catch(error => {
- this.loading = false;
- this.$modal.msgError("获取知识库列表失败:" + error.message);
- });
- }
- else {
- listKnowLedgeByCollectionName({ collectionName: this.queryParams.collectionName }).then(response => {
- // 确保返回的数据是数组格式
- if (Array.isArray(response.data)) {
- this.knowledgeList = response.data;
- this.knowledgeList.map(item => {
- listKnowledgeDocument(item.collectionName).then(response => {
- item.fileCount = response.data.length;
- })
- })
- } else {
- this.knowledgeList = [];
- }
- this.loading = false;
- }).catch(error => {
- this.loading = false;
- this.$modal.msgError("获取知识库列表失败:" + error.message);
- });
- }
- },
- /** 选择知识库 */
- selectKnowledge(knowledge) {
- // 如果正在生成回答,先停止
- if (this.isSending) {
- this.stopGenerating();
- }
-
- this.selectedKnowledge = knowledge;
- // 重置聊天模式
- this.isChatMode = false;
- this.chatMessages = [];
- this.chatInput = '';
- this.streamingStarted = false;
-
- // 重置文件分页状态
- this.filePageNum = 1;
- this.filePageSize = 10;
-
- this.fileLoading = true;
- listKnowledgeDocument(knowledge.collectionName).then(response => {
- this.fileList = response.data;
- this.fileTotal = response.data.length;
- this.fileLoading = false;
- }).catch(error => {
- this.fileLoading = false;
- this.$modal.msgError("获取文件列表失败:" + error.message);
- });
- },
-
- /** 开始对话 */
- handleChat(knowledge) {
- if (!knowledge) {
- this.$modal.msgWarning("请先选择要对话的知识库");
- return;
- }
- // 如果正在生成回答,先停止
- if (this.isSending) {
- this.stopGenerating();
- }
-
- this.isChatMode = true;
- this.chatMessages = [];
- this.chatInput = '';
- this.streamingStarted = false;
- // 滚动到底部
- this.$nextTick(() => {
- this.scrollToBottom();
- });
- },
-
- /** 切换到文件模式 */
- switchToFileMode() {
- // 如果正在生成回答,先停止
- if (this.isSending) {
- this.stopGenerating();
- }
-
- this.isChatMode = false;
- this.isTitleMode = false;
- },
-
- /** 切换到标题模式 */
- switchToTitleMode() {
- this.isTitleMode = true;
- this.isChatMode = false;
- this.titleList = [];
- this.contentList = [];
- this.selectedTitle = null;
-
- // 获取标题列表
- this.titleLoading = true;
- listTiles(this.selectedKnowledge.collectionName).then(response => {
- if (Array.isArray(response.data)) {
- this.titleList = response.data;
- } else {
- this.titleList = [];
- }
- this.titleLoading = false;
- }).catch(error => {
- this.titleLoading = false;
- this.$modal.msgError("获取标题列表失败:" + error.message);
- });
- },
-
- /** 选择标题 */
- selectTitle(title) {
- this.selectedTitle = title;
- this.contentList = [];
-
- // 获取内容列表
- this.contentLoading = true;
- listByTitle(this.selectedKnowledge.collectionName, title).then(response => {
- if (Array.isArray(response.data)) {
- this.contentList = response.data;
- } else {
- this.contentList = [];
- }
- this.contentLoading = false;
- }).catch(error => {
- this.contentLoading = false;
- this.$modal.msgError("获取内容列表失败:" + error.message);
- });
- },
-
- /** 发送消息 */
- sendMessage() {
- if (!this.chatInput.trim()) return;
-
- // 如果正在发送,先停止当前请求
- if (this.isSending) {
- this.stopGenerating();
- }
-
- const userMessage = {
- type: 'user',
- content: this.chatInput.trim(),
- time: parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}')
- };
-
- this.chatMessages.push(userMessage);
- const currentInput = this.chatInput;
- this.chatInput = '';
- this.isSending = true;
- this.streamingStarted = false;
-
- // 滚动到底部
- this.$nextTick(() => {
- this.scrollToBottom();
- });
-
- // 创建AI消息占位符
- const aiMessage = {
- type: 'ai',
- content: '',
- time: parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}'),
- references: [], // 引用的文件信息
- showAllReferences: false // 是否显示所有引用文件
- };
- this.chatMessages.push(aiMessage);
-
- const typewriter = createTypewriter((chunk) => {
- aiMessage.content += chunk
- aiMessage.time = parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}')
- this.$nextTick(() => this.scrollToBottom())
- }, {
- intervalMs: 25,
- maxCharsPerTick: 1
- })
-
- // 使用流式API获取回答
- const eventSource = getAnswerStream(
- currentInput,
- this.selectedKnowledge.collectionName,
- // onMessage: 接收到每个字符时的回调
- (content) => {
- const that = this;
- if (!that.streamingStarted) that.streamingStarted = true;
-
- if (!content || !String(content).trim()) {
- return;
- }
-
- typewriter.push(String(content))
-
- if (window.responseTimeout) {
- clearTimeout(window.responseTimeout);
- }
- window.responseTimeout = setTimeout(() => {
- if (that.isSending) {
- console.log('=== 响应超时强制结束 ===');
- that.isSending = false;
- typewriter.stop()
- if (window.responseTimeout) {
- clearTimeout(window.responseTimeout);
- window.responseTimeout = null;
- }
- }
- }, 300000);
-
- },
- // onError: 发生错误时的回调
- (error) => {
- const that = this;
- console.error('=== 流式回答错误 ===', error);
-
- if (window.responseTimeout) {
- clearTimeout(window.responseTimeout);
- window.responseTimeout = null;
- }
-
- if (aiMessage.content === '') {
- aiMessage.content = '抱歉,我暂时无法回答您的问题,请稍后再试。';
- } else {
- aiMessage.content += '\n\n[回答生成中断]';
- }
- that.isSending = false;
- typewriter.stop()
-
- that.$nextTick(() => {
- that.scrollToBottom();
- });
- },
- // onComplete: 回答完成时的回调
- () => {
- const that = this;
- console.log('=== 回答完成 ===');
-
- if (window.responseTimeout) {
- clearTimeout(window.responseTimeout);
- window.responseTimeout = null;
- }
-
- typewriter.end(async () => {
- aiMessage.content = trimToLastSentenceEnd(aiMessage.content)
- try {
- // 获取上下文引用文件
- const response = await getContextFile(currentInput, that.selectedKnowledge.collectionName);
- console.log('=== 上下文文件 ===', response)
- if (response && Array.isArray(response)) {
- aiMessage.references = response.map(item => ({
- fileName: item.file_name,
- similarity: item.score,
- content: item.content
- }));
- that.$forceUpdate(); // 强制更新以显示引用文件
- }
- } catch (error) {
- console.error('获取上下文文件失败:', error);
- }
-
- that.isSending = false;
- that.$nextTick(() => {
- that.scrollToBottom();
- });
- })
- }
- );
- // 如果用户快速发送多条消息,取消之前的请求
- if (window.currentController) {
- window.currentController.abort();
- }
- if (window.responseTimeout) {
- clearTimeout(window.responseTimeout);
- }
-
- window.currentController = eventSource;
- },
-
- // 设置初始超时定时器(30秒无任何响应才超时)
- initResponseTimeout() {
- window.responseTimeout = setTimeout(() => {
- if (this.isSending) {
- console.log('=== 初始连接超时强制结束 ===')
- this.isSending = false;
- if (window.currentController) {
- window.currentController.abort();
- window.currentController = null;
- }
- window.responseTimeout = null;
- }
- }, 300000); // 5分钟超时
- },
-
- /** 停止生成回答 */
- stopGenerating() {
- // 清除超时定时器
- if (window.responseTimeout) {
- clearTimeout(window.responseTimeout);
- window.responseTimeout = null;
- }
-
- if (window.currentController) {
- window.currentController.abort();
- window.currentController = null;
- }
- this.isSending = false;
- },
-
- /** 滚动到底部 */
- scrollToBottom() {
- // 确保在 DOM 更新后执行滚动操作
- this.$nextTick(() => {
- const container = this.$refs.chatMessagesRef;
- if (container) {
- container.scrollTop = container.scrollHeight;
- }
- });
- },
-
- /** 处理Enter键 */
- handleEnter(event) {
- if (!event.shiftKey) {
- event.preventDefault();
- this.sendMessage();
- }
- },
-
- /** 处理Ctrl+Enter键 */
- handleCtrlEnter() {
- this.chatInput += '\n';
- },
-
- /** 格式化消息内容 */
- formatMessage(content) {
- // 直接使用marked解析markdown内容
- return marked(content);
- },
-
- /** 格式化相似度 */
- formatSimilarity(similarity) {
- // 如果相似度已经是百分比形式(>1),直接返回
- if (similarity > 1) {
- return similarity.toFixed(1);
- }
- // 如果是0-1之间的值,转换为百分比
- return (similarity * 100).toFixed(1);
- },
-
- /** 获取相似度的CSS类 */
- getSimilarityClass(similarity) {
- const percentage = similarity > 1 ? similarity : similarity * 100;
- if (percentage >= 80) return 'high-similarity';
- if (percentage >= 60) return 'medium-similarity';
- return 'low-similarity';
- },
-
- /** 切换引用文件的展开/收起状态 */
- toggleReferences(messageIndex) {
- const message = this.chatMessages[messageIndex];
- message.showAllReferences = !message.showAllReferences;
- this.$forceUpdate();
- },
-
- /** 获取要显示的引用文件列表 */
- getDisplayedReferences(message) {
- if (!message.references || message.references.length === 0) {
- return [];
- }
-
- // 如果引用文件数量 <= 3 或者设置为显示全部,则显示所有
- if (message.references.length <= 3 || message.showAllReferences) {
- return message.references;
- }
-
- // 否则只显示前3个
- return message.references.slice(0, 3);
- },
-
- /** 处理卡片操作 */
- handleCardAction(command) {
- const { action, data } = command;
- switch (action) {
- case 'edit':
- this.handleUpdate(data);
- break;
- case 'upload':
- this.handleUpload(data);
- break;
- case 'delete':
- this.handleDelete(data);
- break;
- }
- },
-
- /** 删除文件 */
- handleDeleteFile(file) {
- this.$modal.confirm(`是否确认删除文件"${file}"?`).then(() => {
- deleteKnowledgeFile(file, this.selectedKnowledge.collectionName).then(response => {
- this.$modal.msgSuccess("删除成功");
- // 刷新文件列表
- if (this.selectedKnowledge) {
- this.fileLoading = true;
- listKnowledgeDocument(this.selectedKnowledge.collectionName).then(response => {
- this.fileList = response.data;
- this.fileTotal = response.data.length;
-
- // 如果当前页没有数据了,回到上一页
- const maxPage = Math.ceil(this.fileTotal / this.filePageSize);
- if (this.filePageNum > maxPage && maxPage > 0) {
- this.filePageNum = maxPage;
- }
-
- this.fileLoading = false;
- }).catch(error => {
- this.fileLoading = false;
- this.$modal.msgError("刷新文件列表失败:" + error.message);
- });
- }
- }).catch(error => {
- this.$modal.msgError("删除失败:" + error.message);
- });
- }).catch(() => { });
- },
-
- /** 获取文件类型 */
- getFileType(fileName) {
- if (!fileName) return '未知';
- const extension = fileName.split('.').pop()?.toLowerCase();
- switch (extension) {
- case 'pdf':
- return 'PDF文档';
- case 'docx':
- return 'Word文档';
- case 'doc':
- return 'Word文档';
- default:
- return '文档';
- }
- },
-
- /** 文件分页处理 */
- handleFilePagination() {
- this.fileLoading = true;
- listKnowledgeDocument(this.selectedKnowledge.collectionName).then(response => {
- this.fileList = response.data;
- this.fileTotal = response.data.length;
- this.fileLoading = false;
- }).catch(error => {
- this.fileLoading = false;
- this.$modal.msgError("获取文件列表失败:" + error.message);
- });
- },
-
- // 取消按钮
- cancel() {
- this.open = false;
- this.reset();
- },
-
- // 表单重置
- reset() {
- this.form.collectionName = '';
- this.form.description = '';
- this.form.newCollectionName = '';
- // 强制更新DOM
- this.$nextTick(() => {
- this.$refs.form.resetFields();
- });
- },
-
- /** 重置表单 */
- resetForm(refName) {
- if (this.$refs[refName]) {
- this.$refs[refName].resetFields();
- }
- },
- /** 搜索按钮操作 */
- handleQuery() {
- // 重置页码
- this.queryParams.pageNum = 1;
- this.getList();
- },
-
- /** 重置按钮操作 */
- resetQuery() {
- this.resetForm("queryForm");
- this.handleQuery();
- },
-
- /** 新增按钮操作 */
- handleAdd() {
- this.reset();
- this.open = true;
- this.title = "添加知识库";
- this.isModify = false;
- },
-
- /** 修改按钮操作 */
- handleUpdate(row) {
- this.reset();
- const collectionName = row?.collectionName || this.ids[0];
- if (!collectionName) {
- this.$modal.msgWarning("请先选择要修改的知识库");
- return;
- }
- this.form.collectionName = collectionName;
- this.form.newCollectionName = '';
- this.isModify = true;
- this.open = true;
- this.title = "修改知识库";
- },
-
- /** 提交按钮 */
- submitForm() {
- this.$refs["knowledgeFormRef"].validate(valid => {
- if (valid) {
- if (this.form.newCollectionName) {
- // 修改操作
- updateKnowledge(this.form.collectionName, this.form.newCollectionName).then(response => {
- this.$modal.msgSuccess("修改成功");
- this.open = false;
- this.getList();
- }).catch(error => {
- this.$modal.msgError("修改失败:" + error.message);
- });
- } else {
- // 新增操作
- addKnowledge(this.form.collectionName, this.form.description).then(response => {
- this.$modal.msgSuccess("新增成功");
- this.open = false;
- this.getList();
- }).catch(error => {
- this.$modal.msgError("新增失败:" + error.message);
- });
- }
- }
- });
- },
-
- /** 删除按钮操作 */
- handleDelete(row) {
- const collectionNames = row?.collectionName || this.ids;
- if (!collectionNames) {
- this.$modal.msgWarning("请先选择要删除的知识库");
- return;
- }
- this.$modal.confirm('是否确认删除知识库名称为"' + collectionNames + '"的数据项?').then(() => {
- return delKnowledge(collectionNames);
- }).then(() => {
- this.getList();
- // 如果删除的是当前选中的知识库,清空选择
- if (this.selectedKnowledge?.collectionName === collectionNames) {
- // 如果正在生成回答,先停止
- if (this.isSending) {
- this.stopGenerating();
- }
- this.selectedKnowledge = null;
- this.fileList = [];
- this.isChatMode = false;
- this.chatMessages = [];
- }
- this.$modal.msgSuccess("删除成功");
- }).catch((error) => {
- if (error !== 'cancel') {
- this.$modal.msgError("删除失败:" + error.message);
- }
- });
- },
-
- /** 上传文件按钮操作 */
- handleUpload(row) {
- this.percentage = 0;
- this.percentageDisplay = false;
- const collectionName = row?.collectionName;
- if (!collectionName) {
- this.$modal.msgWarning("请先选择要上传文件的知识库");
- return;
- }
- this.uploadForm.collectionName = collectionName;
- this.uploadOpen = true;
- },
-
- // 文件上传中处理
- handleFileUploadProgress(event, file, fileList) {
- this.upload.isUploading = true;
- },
-
- handleFileChange(file, fileList) {
- this.upload.fileList = fileList.map(f => f.raw)
- },
-
- // 文件上传成功处理
- handleFileSuccess(response, file, fileList) {
- this.upload.isUploading = false;
- this.$refs.upload.clearFiles();
- this.$alert(response.msg, "导入结果", { dangerouslyUseHTMLString: true });
- this.uploadOpen = false;
- this.getList();
- // 如果当前有选中的知识库,刷新文件列表
- if (this.selectedKnowledge) {
- this.fileLoading = true;
- listKnowledgeDocument(this.selectedKnowledge.collectionName).then((response) => {
- this.fileList = response.data;
- this.fileTotal = response.data.length;
- this.fileLoading = false;
- }).catch((error) => {
- this.fileLoading = false;
- this.$modal.msgError("刷新文件列表失败:" + error.message);
- });
- }
- },
-
- // 提交上传
- submitUpload() {
- try {
- this.percentageDisplay = true;
- var timer;
- clearInterval(timer);
- this.getProcess()
- } catch (error) {
- console.error('文件上传失败:', error)
- this.$message.error('文件上传失败')
- }
- },
-
- getProcess() {
- var timer = setInterval(() => { //隔2000毫秒获取进度
- getProcessValue().then((res) => {
- if (res.code == 200 && res.msg.includes(":")) {
- this.progress = res.msg;
- this.percentage = Number(res.msg.split(":")[1].replace("%", ""))
- }
- });
- }, 30000);
-
- insertKnowledgeFile(this.upload.fileList, this.uploadForm.collectionName).then((response) => {
- this.percentageDisplay = false;
- this.$modal.msgSuccess("上传成功");
- this.uploadOpen = false;
- this.$refs.knowledgeUpload.clearFiles();
- this.getList();
- }).catch((error) => {
- this.$modal.msgError("上传失败:" + error.message);
- });
- },
-
- // 取消上传
- cancelUpload() {
- this.uploadOpen = false;
- this.$refs.knowledgeUpload.clearFiles();
- },
-
- }
-
- }
- </script>
-
- <style lang="scss" scoped>
- .app-container {
- height: 100vh;
- display: flex;
- flex-direction: column;
- background: #f5f7fa;
- }
-
- .main-content {
- flex: 1;
- display: flex;
- gap: 16px;
- padding: 16px;
- overflow: hidden;
- }
-
- .left-panel {
- width: 400px;
- background: white;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- display: flex;
- flex-direction: column;
- overflow: hidden;
- }
-
- .right-panel {
- flex: 1;
- background: white;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- display: flex;
- flex-direction: column;
- overflow: hidden;
- }
-
- .panel-header {
- padding: 20px 24px;
- border-bottom: 1px solid #e4e7ed;
- display: flex;
- justify-content: space-between;
- align-items: center;
- background: #fafbfc;
-
- h3 {
- margin: 0;
- font-size: 16px;
- font-weight: 600;
- color: #303133;
- }
-
- .knowledge-count {
- font-size: 12px;
- color: #909399;
- background: #f0f2f5;
- padding: 4px 8px;
- border-radius: 4px;
- }
-
- .search-box {
- padding: 16px;
- border-bottom: 1px solid #e4e4e4;
- }
-
- .header-actions {
- display: flex;
- gap: 8px;
- }
- }
-
- .knowledge-cards {
- flex: 1;
- padding: 16px;
- overflow-y: auto;
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
-
- .knowledge-card {
- background: white;
- border: 1px solid #e4e7ed;
- border-radius: 8px;
- padding: 16px;
- cursor: pointer;
- transition: all 0.3s ease;
- position: relative;
-
- &:hover {
- border-color: #409eff;
- box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
- transform: translateY(-2px);
- }
-
- &.active {
- border-color: #409eff;
- background: #f0f9ff;
- box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
- }
-
- .card-header {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- margin-bottom: 12px;
-
- .card-title {
- display: flex;
- align-items: center;
- gap: 8px;
- flex: 1;
-
- .folder-icon {
- color: #409eff;
- font-size: 18px;
- }
-
- .title-text {
- font-weight: 600;
- color: #303133;
- font-size: 14px;
- line-height: 1.4;
- }
- }
-
- .card-actions {
- opacity: 0;
- transition: opacity 0.3s ease;
- }
- }
-
- &:hover .card-actions {
- opacity: 1;
- }
-
- .card-content {
- .description {
- color: #606266;
- font-size: 13px;
- line-height: 1.5;
- margin: 0 0 12px 0;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
- }
-
- .meta-info {
- display: flex;
- justify-content: space-between;
- align-items: center;
- font-size: 12px;
- color: #909399;
-
- .create-time {
- background: #f0f2f5;
- padding: 2px 6px;
- border-radius: 3px;
- }
-
- .file-count {
- background: #e1f3d8;
- color: #67c23a;
- padding: 2px 6px;
- border-radius: 3px;
- }
- }
- }
- }
-
- .file-content {
- flex: 1;
- padding: 20px;
- overflow-y: auto;
- }
-
- .title-content {
- flex: 1;
- padding: 20px;
- overflow: hidden;
- }
-
- .title-content-container {
- display: flex;
- height: calc(100vh - 240px);
- gap: 20px;
- overflow: hidden;
- }
-
- .title-list {
- width: 300px;
- background: #fafbfc;
- border-radius: 8px;
- border: 1px solid #e4e7ed;
- padding: 12px;
- overflow-y: auto;
- }
-
- .title-item {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 10px 12px;
- margin-bottom: 8px;
- border-radius: 6px;
- cursor: pointer;
- transition: all 0.3s ease;
- background: white;
- border: 1px solid #e4e7ed;
-
- &:hover {
- border-color: #409eff;
- background: #f0f9ff;
- }
-
- &.active {
- border-color: #409eff;
- background: #e6f7ff;
- box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
- }
-
- .title-icon {
- color: #409eff;
- font-size: 16px;
- }
-
- .title-text {
- flex: 1;
- font-size: 14px;
- color: #303133;
- line-height: 1.4;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- }
-
- .content-list {
- flex: 1;
- background: #fafbfc;
- border-radius: 8px;
- border: 1px solid #e4e7ed;
- padding: 20px;
- overflow-y: auto;
- }
-
- .content-items {
- display: flex;
- flex-direction: column;
- gap: 16px;
- }
-
- .content-item {
- background: white;
- border: 1px solid #e4e7ed;
- border-radius: 8px;
- padding: 16px;
- transition: all 0.3s ease;
-
- &:hover {
- border-color: #409eff;
- box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
- }
-
- .content-header {
- display: flex;
- align-items: center;
- margin-bottom: 12px;
-
- .content-index {
- background: #409eff;
- color: white;
- width: 24px;
- height: 24px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 12px;
- font-weight: 600;
- }
- }
-
- .content-body {
- font-size: 14px;
- color: #303133;
- line-height: 1.6;
- white-space: pre-wrap;
- word-break: break-word;
- }
- }
-
- .title-mode-badge {
- background: #67c23a;
- color: white;
- padding: 2px 8px;
- border-radius: 12px;
- font-size: 12px;
- margin-left: auto;
- }
-
- // 聊天模式样式
- .chat-content {
- flex: 1;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- }
-
- .selected-knowledge {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 12px 16px;
- background: #f0f9ff;
- border-radius: 6px;
- margin-bottom: 20px;
-
- .folder-icon {
- color: #409eff;
- font-size: 16px;
- }
-
- .knowledge-name {
- font-weight: 600;
- color: #303133;
- }
-
- .chat-mode-badge {
- background: #409eff;
- color: white;
- padding: 2px 8px;
- border-radius: 12px;
- font-size: 12px;
- margin-left: auto;
- }
- }
-
- .chat-messages {
- flex: 1;
- padding: 20px;
- overflow-y: auto;
- background: #fafbfc;
- }
-
- .welcome-message {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 100%;
- text-align: center;
-
- .welcome-content {
- max-width: 400px;
-
- .welcome-icon {
- font-size: 48px;
- margin-bottom: 16px;
- }
-
- h3 {
- margin: 0 0 12px 0;
- color: #303133;
- font-size: 18px;
- }
-
- p {
- margin: 0 0 8px 0;
- color: #606266;
- font-size: 14px;
- line-height: 1.6;
- }
- }
- }
-
- .message-list {
- display: flex;
- flex-direction: column;
- gap: 20px;
- }
-
- .message-item {
- display: flex;
- gap: 12px;
-
- &.user {
- flex-direction: row-reverse;
-
- .message-content {
- align-items: flex-end;
- }
-
- .message-bubble {
- background: #409eff;
- color: white;
- border-radius: 18px 18px 4px 18px;
- }
- }
-
- &.ai {
- .message-bubble {
- background: white;
- color: #303133;
- border: 1px solid #e4e7ed;
- border-radius: 18px 18px 18px 4px;
- }
- }
- }
-
- .message-avatar {
- width: 40px;
- height: 40px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
-
- .user-avatar {
- // background: #409eff;
- color: #409eff;
- font-size: 18px;
- }
-
- .ai-avatar {
- // background: #67c23a;
- color: white;
- font-size: 18px;
- }
- }
-
- .message-content {
- display: flex;
- flex-direction: column;
- max-width: 70%;
- }
-
- .message-bubble {
- padding: 12px 16px;
- line-height: 1.5;
- word-wrap: break-word;
-
- .message-text {
- margin-bottom: 8px;
- white-space: pre-wrap;
- }
-
- .message-references {
- margin: 12px 0;
- padding: 12px;
- background: rgba(0, 0, 0, 0.05);
- border-radius: 6px;
- border-left: 3px solid #409eff;
-
- .references-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 6px;
- margin-bottom: 8px;
- font-size: 13px;
- font-weight: 600;
- color: #409eff;
-
- .reference-icon {
- font-size: 14px;
- }
-
- .toggle-references {
- font-size: 11px;
- color: #909399;
- padding: 0;
- height: auto;
-
- &:hover {
- color: #409eff;
- }
- }
- }
-
- .references-list {
- display: flex;
- flex-direction: column;
- gap: 6px;
- }
-
- .reference-item {
- padding: 6px 8px;
- background: rgba(255, 255, 255, 0.8);
- border-radius: 4px;
- border: 1px solid rgba(0, 0, 0, 0.1);
-
- .reference-info {
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 8px;
-
- .reference-filename {
- font-size: 12px;
- color: #303133;
- font-weight: 500;
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
-
- .reference-similarity {
- font-size: 11px;
- padding: 2px 6px;
- border-radius: 10px;
- white-space: nowrap;
- font-weight: 500;
- transition: all 0.3s ease;
-
- &.high-similarity {
- color: #67c23a;
- background: #e1f3d8;
- }
-
- &.medium-similarity {
- color: #e6a23c;
- background: #fdf5e6;
- }
-
- &.low-similarity {
- color: #f56c6c;
- background: #fef0f0;
- }
- }
- }
- }
- }
-
- .message-time {
- font-size: 12px;
- opacity: 0.7;
- }
- }
-
- // 打字机效果的光标 - 只在正在生成时显示
- .message-bubble.ai.typing .message-text:after {
- content: '|';
- animation: blink 1s infinite;
- color: #409eff;
- }
-
- @keyframes blink {
-
- 0%,
- 50% {
- opacity: 1;
- }
-
- 51%,
- 100% {
- opacity: 0;
- }
- }
-
- .chat-input-area {
- padding: 20px;
- border-top: 1px solid #e4e7ed;
- background: white;
- }
-
- .input-container {
- display: flex;
- gap: 12px;
- align-items: flex-end;
-
- .el-textarea {
- flex: 1;
- }
-
- .input-actions {
- flex-shrink: 0;
- }
- }
-
- .input-tips {
- margin-top: 8px;
- text-align: center;
- font-size: 12px;
- color: #999;
- display: flex;
- justify-content: space-between;
- align-items: center;
-
- .char-count {
- color: #409eff;
- font-weight: 500;
-
- &.warning {
- color: #e6a23c;
- }
-
- .error-text {
- color: #f56c6c;
- font-size: 11px;
- }
- }
- }
-
- .file-list {
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
-
- .file-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 16px;
- background: #fafbfc;
- border: 1px solid #e4e7ed;
- border-radius: 6px;
- transition: all 0.3s ease;
-
- &:hover {
- border-color: #409eff;
- box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
- }
-
- .file-info {
- display: flex;
- align-items: center;
- gap: 12px;
- flex: 1;
-
- .file-icon {
- color: #409eff;
- font-size: 20px;
- }
-
- .file-details {
- .file-name {
- font-weight: 500;
- color: #303133;
- margin-bottom: 4px;
- }
-
- .file-meta {
- display: flex;
- gap: 12px;
- font-size: 12px;
- color: #909399;
-
- .file-type {
- background: #e1f3d8;
- color: #67c23a;
- padding: 2px 6px;
- border-radius: 3px;
- }
- }
- }
- }
-
- .file-actions {
- opacity: 0;
- transition: opacity 0.3s ease;
- }
-
- &:hover .file-actions {
- opacity: 1;
- }
- }
-
- // 文件列表分页样式
- .file-content {
- :deep(.pagination-container) {
- margin-top: 20px;
- padding: 20px 0;
- text-align: center;
- background: transparent;
- }
- }
-
- .empty-state {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 60px 20px;
- text-align: center;
- color: #909399;
-
- .empty-icon {
- font-size: 48px;
- margin-bottom: 16px;
- opacity: 0.5;
- }
-
- p {
- margin: 0 0 16px 0;
- font-size: 14px;
- }
- }
-
- .dialog-footer {
- text-align: right;
- }
-
- .el-upload__tip {
- font-size: 12px;
- color: #606266;
- margin-top: 7px;
- }
-
- :deep(.el-upload-dragger) {
- width: 350px;
- }
-
- :deep(.el-upload__text) {
- margin: 10px 0 16px;
- color: #606266;
- font-size: 14px;
- }
-
- :deep(.el-upload__tip) {
- font-size: 12px;
- color: #606266;
- margin-top: 7px;
- }
-
- // 修复上传文件列表中文件名过长的问题
- :deep(.el-upload-list) {
- max-width: 100%;
-
- .el-upload-list__item {
- max-width: 100%;
- overflow: hidden;
-
- .el-upload-list__item-name {
- width: 350px; // 为删除按钮等留出空间
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- display: inline-block;
- vertical-align: middle;
- }
- }
- }
-
- // 修复拖拽区域内文件名显示问题
- :deep(.el-upload-dragger) {
- .el-upload-list {
- .el-upload-list__item {
- margin: 0 10px 10px 10px;
- padding: 8px 12px;
- background: #f5f7fa;
- border-radius: 4px;
-
- .el-upload-list__item-name {
- color: #606266;
- font-size: 13px;
- max-width: calc(100% - 60px);
- word-wrap: break-word;
- word-break: break-all;
- white-space: normal;
- line-height: 1.4;
- }
- }
- }
- }
-
- // 响应式设计
- @media (max-width: 1200px) {
- .main-content {
- flex-direction: column;
- }
-
- .left-panel {
- width: 100%;
- height: 300px;
- }
- }
-
- @media (max-width: 768px) {
- .top-toolbar {
- padding: 12px 16px;
- }
-
- .main-content {
- padding: 12px;
- gap: 12px;
- }
-
- .panel-header {
- padding: 16px 20px;
- }
-
- .knowledge-cards {
- padding: 12px;
- }
-
- .file-content {
- padding: 16px;
- }
-
- .chat-messages {
- padding: 16px;
- }
-
- .chat-input-area {
- padding: 16px;
- }
-
- .message-content {
- max-width: 85%;
- }
-
- .message-references {
- .references-header {
- flex-direction: column;
- align-items: flex-start;
- gap: 4px;
-
- .toggle-references {
- font-size: 10px;
- }
- }
-
- .reference-item {
- .reference-info {
- flex-direction: column;
- align-items: flex-start;
- gap: 4px;
-
- .reference-filename {
- font-size: 11px;
- }
-
- .reference-similarity {
- font-size: 10px;
- align-self: flex-end;
- }
- }
- }
- }
- }
-
- // Loading动画样式
- .loading-dots {
- display: flex;
- gap: 4px;
- padding: 8px 0;
-
- span {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background-color: #ccc;
- animation: loading 1.4s infinite ease-in-out;
-
- &:nth-child(1) {
- animation-delay: -0.32s;
- }
-
- &:nth-child(2) {
- animation-delay: -0.16s;
- }
- }
- }
-
- @keyframes loading {
-
- 0%,
- 80%,
- 100% {
- transform: scale(0);
- }
-
- 40% {
- transform: scale(1);
- }
- }
- </style>
|