report.vue 60 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820
  1. <template>
  2. <div class="layout">
  3. <div class="menu" :class="{ collapsed: isMenuCollapsed }">
  4. <!-- 添加收缩按钮 -->
  5. <div
  6. class="collapse-left-button"
  7. :class="{ 'menu-collapsed': isMenuCollapsed }"
  8. @click="toggleMenu"
  9. >
  10. <img
  11. :src="isMenuCollapsed ? rightIcon : leftIcon"
  12. alt="toggle"
  13. class="collapse-icon"
  14. />
  15. </div>
  16. <!-- Logo -->
  17. <div class="logo">
  18. <img
  19. src="https://ecsaas.raycos.com.cn/web/uploads/20230713/3690c0badc2c7be4552a253f72d6d701.png"
  20. :draggable="false"
  21. alt="logo"
  22. class="logo-img"
  23. />
  24. <span>Raycos Tech</span>
  25. </div>
  26. <!-- New Conversation 按钮 -->
  27. <button class="new-conversation-btn" @click="addNewConversation">
  28. <img class="tab-icon" src="../assets/svg/plus.svg" alt="" />
  29. <span>New Conversation</span>
  30. </button>
  31. <!-- 会话列表 -->
  32. <div class="conversation-list">
  33. <div
  34. v-for="conversation in conversations"
  35. :key="conversation.id"
  36. :class="[
  37. 'conversation-item',
  38. { active: currentConversationId === conversation.id },
  39. ]"
  40. @click="switchConversation(conversation.id)"
  41. >
  42. {{ conversation.title }}
  43. </div>
  44. </div>
  45. <!-- 添加模型配置和语音配置的容器 -->
  46. <div class="config-container">
  47. <KnowledgeConfig />
  48. <ModelConfig />
  49. <VoiceConfig />
  50. </div>
  51. </div>
  52. <div class="chat">
  53. <!-- 欢迎消息 -->
  54. <div v-if="messages.length === 0" class="welcome-section">
  55. <!-- Do you want 部分 -->
  56. <div class="suggestion-section">
  57. <h3>嗨,我是你的智能助手</h3>
  58. <div class="cards-container">
  59. <div class="suggestion-card">
  60. <div class="card-header">
  61. <img
  62. src="../assets/svg/hot-for-ux.svg"
  63. alt="hot topics"
  64. class="tab-icon"
  65. />
  66. <h4>推荐问题</h4>
  67. </div>
  68. <p class="card-subtitle">为您精选的热门问题</p>
  69. <div class="suggestion-items">
  70. <!-- Combine both QA pairs and guides -->
  71. <div
  72. v-for="(item, index) in [
  73. ...currentQaPairs,
  74. ...currentDesignGuides,
  75. ]"
  76. :key="index"
  77. class="suggestion-item"
  78. @click="handleQaClick(item)"
  79. >
  80. {{ item.question }}
  81. </div>
  82. </div>
  83. </div>
  84. <!-- Hot Topics 卡片 --><!-- Design Guide 卡片 -->
  85. <!-- <div class="suggestion-card">
  86. <div class="card-header">
  87. <img
  88. src="../assets/svg/hot-for-ux.svg"
  89. alt="hot topics"
  90. class="tab-icon"
  91. />
  92. <h4>知识库相关问题:</h4>
  93. </div>
  94. <p class="card-subtitle">我们可以这样向知识库提问</p>
  95. <div class="suggestion-items">
  96. <div
  97. v-for="(qa, index) in currentQaPairs"
  98. :key="index"
  99. class="suggestion-item"
  100. @click="handleQaClick(qa)"
  101. >
  102. {{ qa.question }}
  103. </div>
  104. </div>
  105. </div>
  106. <div class="suggestion-card">
  107. <div class="card-header">
  108. <img
  109. src="../assets/svg/read.svg"
  110. alt="hot topics"
  111. class="tab-icon"
  112. />
  113. <h4>网上热搜问答</h4>
  114. </div>
  115. <p class="card-subtitle">搜索网上的热门回答</p>
  116. <div class="suggestion-items">
  117. <div
  118. v-for="(guide, index) in currentDesignGuides"
  119. :key="index"
  120. class="suggestion-item"
  121. @click="handleQaClick(guide)"
  122. >
  123. {{ guide.question }}
  124. </div>
  125. </div>
  126. </div> -->
  127. </div>
  128. </div>
  129. </div>
  130. <!-- 修改消息列表部分 -->
  131. <div v-else class="message-list" ref="messageList">
  132. <div
  133. v-for="message in messages"
  134. :key="message.id"
  135. :class="[
  136. 'message-item',
  137. message.role === 'user' ? 'user' : 'assistant',
  138. ]"
  139. >
  140. <div class="message-bubble">
  141. <div class="message-content">
  142. <template v-if="message.role === 'user' && message.files">
  143. <div
  144. v-for="file in message.files"
  145. :key="file.name"
  146. class="file-message"
  147. >
  148. <!-- 图片文件显示 -->
  149. <template v-if="isImageFile(file)">
  150. <div class="image-preview">
  151. <img :src="file.url" class="preview-image" />
  152. </div>
  153. </template>
  154. <!-- 视频文件显示 -->
  155. <template v-else-if="isVideoFile(file)">
  156. <div class="video-preview">
  157. <video
  158. controls
  159. class="preview-video"
  160. :src="file.url"
  161. preload="metadata"
  162. >
  163. Your browser does not support the video tag.
  164. </video>
  165. </div>
  166. </template>
  167. <!-- 文档文件显示 -->
  168. <template v-else>
  169. <div class="file-info-display">
  170. <img :src="getFileIcon(file)" class="file-type-icon" />
  171. <div class="file-details">
  172. <span class="file-name" :title="file.name">{{
  173. file.name
  174. }}</span>
  175. <span class="file-size">{{ file.size }}</span>
  176. </div>
  177. </div>
  178. </template>
  179. </div>
  180. </template>
  181. <div
  182. class="message-text"
  183. v-if="message.role === 'assistant'"
  184. v-html="renderMarkdown(message.displayContent)"
  185. ></div>
  186. <div v-else>{{ message.content }}</div>
  187. <!-- 音频控制按钮 -->
  188. <div
  189. v-if="message.role === 'assistant' && message.audioData"
  190. class="audio-controls"
  191. >
  192. <button class="audio-btn" @click="handleAudioClick(message)">
  193. <!-- 只在当前消息正在播放时显示波形动画 -->
  194. <div
  195. v-if="currentPlayingId === message.id"
  196. class="wave-animation"
  197. >
  198. <span class="wave-bar"></span>
  199. <span class="wave-bar"></span>
  200. <span class="wave-bar"></span>
  201. <span class="wave-bar"></span>
  202. </div>
  203. <!-- 其他情况显示播放图标 -->
  204. <img v-else :src="playIcon" alt="播放" class="audio-icon" />
  205. </button>
  206. </div>
  207. </div>
  208. </div>
  209. </div>
  210. </div>
  211. <!-- 输入框 -->
  212. <div class="input-container">
  213. <!-- 标签栏 -->
  214. <div class="tabs-wrapper">
  215. <div class="tab-item active">
  216. <img
  217. src="../assets/svg/hot-for-ux.svg"
  218. alt="hot topics"
  219. class="tab-icon"
  220. />
  221. <span>Hot Topics</span>
  222. </div>
  223. <div class="tab-item">
  224. <img
  225. src="../assets/svg/read.svg"
  226. alt="hot topics"
  227. class="tab-icon"
  228. />
  229. <span>Design Guide</span>
  230. </div>
  231. <!-- 添加互联网搜索开关 -->
  232. <div class="web-search-toggle">
  233. <input
  234. type="checkbox"
  235. id="webSearchToggle"
  236. v-model="enableWebSearch"
  237. :disabled="attachedFiles.length > 0"
  238. class="toggle-input"
  239. />
  240. <label
  241. for="webSearchToggle"
  242. class="toggle-label"
  243. :class="{ disabled: attachedFiles.length > 0 }"
  244. >
  245. 启用互联网搜索
  246. <!-- <span v-if="attachedFiles.length > 0" class="disabled-hint">
  247. (文件上传时不可用)
  248. </span> -->
  249. </label>
  250. </div>
  251. </div>
  252. <!-- 添加附件面板 -->
  253. <div v-if="showAttachments" class="attachments-panel">
  254. <div class="panel-content">
  255. <div class="attachments-header">
  256. <span>Attachments</span>
  257. <button class="close-btn" @click="toggleAttachments">
  258. <img src="../assets/svg/close.svg" alt="" />
  259. </button>
  260. </div>
  261. <div
  262. v-if="attachedFiles.length === 0"
  263. style="display: flex; justify-content: center"
  264. >
  265. <div
  266. class="upload-area"
  267. :class="{ 'has-files': attachedFiles.length > 0 }"
  268. @click="triggerFileInput"
  269. @drop.prevent="handleFileDrop"
  270. @dragover.prevent
  271. @dragenter.prevent
  272. >
  273. <!-- 移除条件判断,让整个区域始终可点击 -->
  274. <template v-if="isUploading">
  275. <div class="upload-loading">
  276. <div class="loading-spinner"></div>
  277. <div class="upload-text">Uploading...</div>
  278. </div>
  279. </template>
  280. <template v-else>
  281. <img
  282. src="../assets/svg/upload.svg"
  283. alt="upload"
  284. class="upload-icon"
  285. />
  286. <div class="upload-text">Upload files</div>
  287. <div class="upload-hint">
  288. Click or drag files to this area to upload
  289. </div>
  290. </template>
  291. <input
  292. type="file"
  293. ref="fileInput"
  294. style="display: none"
  295. @change="handleFileUpload"
  296. multiple
  297. />
  298. </div>
  299. </div>
  300. <!-- 上传内容列表 -->
  301. <div v-if="attachedFiles.length !== 0">
  302. <div class="file-list">
  303. <div
  304. v-for="file in attachedFiles"
  305. :key="file.name"
  306. class="file-item"
  307. :class="{ 'is-image': isImageFile(file) }"
  308. >
  309. <div :class="isImageFile(file) ? 'file-wrapper' : ''">
  310. <!-- 添加文件 loading 状态 -->
  311. <div v-if="file.isUploading" class="file-loading">
  312. <div class="loading-spinner"></div>
  313. </div>
  314. <!-- 其他文件显示逻辑保持不变 -->
  315. <img
  316. v-else-if="isImageFile(file)"
  317. :src="file.url"
  318. alt="file"
  319. class="file-icon"
  320. />
  321. <video
  322. v-else-if="isVideoFile(file)"
  323. :src="file.url"
  324. class="file-icon"
  325. preload="metadata"
  326. >
  327. Your browser does not support the video tag.
  328. </video>
  329. <img
  330. v-else
  331. :src="getFileIcon(file)"
  332. alt="file"
  333. :class="
  334. isImageFile(file) || isVideoFile(file)
  335. ? 'file-icon'
  336. : ''
  337. "
  338. />
  339. <button
  340. class="remove-btn"
  341. @click.stop="removeFile(file)"
  342. v-if="!file.isUploading"
  343. >
  344. ×
  345. </button>
  346. </div>
  347. <div class="file-info" v-if="!isImageFile(file)">
  348. <span class="file-name">{{ file.name }}</span>
  349. <span class="file-size">{{ file.sizeFormatted }}</span>
  350. </div>
  351. </div>
  352. <button class="add-more-btn" @click.stop="triggerFileInput">
  353. <img src="../assets/svg/plus.svg" alt="add" />
  354. </button>
  355. </div>
  356. <input
  357. type="file"
  358. ref="fileInput"
  359. style="display: none"
  360. @change="handleFileUpload"
  361. multiple
  362. />
  363. </div>
  364. <!-- <div v-if="attachedFiles.length" class="attached-files">
  365. <div
  366. v-for="file in attachedFiles"
  367. :key="file.name"
  368. class="file-item"
  369. >
  370. <FileOutlined />
  371. <span>{{ file.name }}</span>
  372. <button class="remove-btn" @click="removeFile(file)">
  373. </button>
  374. </div>
  375. </div> -->
  376. </div>
  377. </div>
  378. <!-- 输入框部分 -->
  379. <div class="input-wrapper">
  380. <button class="attachment-btn" @click="toggleAttachments">
  381. <img
  382. src="../assets/svg/attachment.svg"
  383. alt="attachment"
  384. class="tab-icon"
  385. />
  386. </button>
  387. <input
  388. type="text"
  389. class="message-input"
  390. v-model="inputContent"
  391. @keyup.enter="sendMessage"
  392. placeholder="Type a message..."
  393. />
  394. <button
  395. class="send-btn"
  396. @click="sendMessage"
  397. :disabled="!inputContent && attachedFiles.length === 0"
  398. >
  399. <img src="../assets/svg/up.svg" alt="send" />
  400. </button>
  401. </div>
  402. </div>
  403. </div>
  404. <div class="right_menu" :class="{ collapsed: isRightMenuCollapsed }">
  405. <div class="collapse-button" @click="toggleRightMenu">
  406. <img
  407. :src="isRightMenuCollapsed ? leftIcon : rightIcon"
  408. alt="toggle"
  409. class="collapse-icon"
  410. />
  411. </div>
  412. <div class="right-menu-content">
  413. <SearchResults
  414. :searchResults="searchResults"
  415. :loading="isSearchLoading"
  416. :total="searchTotal"
  417. />
  418. </div>
  419. </div>
  420. </div>
  421. </template>
  422. <script setup>
  423. import { ref, reactive, nextTick, onUnmounted, onMounted } from "vue";
  424. import {
  425. PlusOutlined,
  426. FireOutlined,
  427. ReadOutlined,
  428. HeartOutlined,
  429. SmileOutlined,
  430. CommentOutlined,
  431. CloudUploadOutlined,
  432. PaperClipOutlined,
  433. ShareAltOutlined,
  434. EllipsisOutlined,
  435. CloseOutlined,
  436. FileOutlined,
  437. ArrowUpOutlined,
  438. UploadOutlined,
  439. } from "@ant-design/icons-vue";
  440. import leftIcon from "../assets/svg/left.svg";
  441. import rightIcon from "../assets/svg/right.svg";
  442. import plusIcon from "../assets/svg/plus.svg";
  443. import hotForUxIcon from "../assets/svg/hot-for-ux.svg";
  444. import readIcon from "../assets/svg/read.svg";
  445. import attachmentIcon from "../assets/svg/attachment.svg";
  446. import uploadIcon from "../assets/svg/upload.svg";
  447. import closeIcon from "../assets/svg/close.svg";
  448. import upIcon from "../assets/svg/up.svg";
  449. import wordIcon from "../assets/svg/word.svg";
  450. import pauseIcon from "../assets/svg/pause.svg";
  451. import playIcon from "../assets/svg/play.svg";
  452. import axios from "axios";
  453. import { useStore } from "vuex";
  454. import MarkdownIt from "markdown-it";
  455. import ModelConfig from "../components/ModelConfig/index.vue";
  456. import SearchResults from "../components/SearchResults/index.vue";
  457. import VoiceConfig from "../components/VoiceConfig/index.vue";
  458. import KnowledgeConfig from "../components/KnowledgeConfig/index.vue";
  459. import ExportButton from "../components/ExportButton/index.vue";
  460. // 初始化 markdown-it
  461. const md = new MarkdownIt({
  462. html: true,
  463. linkify: true,
  464. typographer: true,
  465. });
  466. // 响应式状态
  467. const headerOpen = ref(false);
  468. const inputContent = ref("");
  469. const showAttachments = ref(false);
  470. const isRightMenuCollapsed = ref(false);
  471. const fileInput = ref(null);
  472. const attachedFiles = ref([]);
  473. const messages = ref([]);
  474. const isLoading = ref(false);
  475. const currentConversationId = ref(1);
  476. const conversationCounter = ref(1);
  477. const messageList = ref(null);
  478. const store = useStore();
  479. // 会话数据
  480. const conversations = ref([
  481. {
  482. id: 1,
  483. title: "当前会话",
  484. messages: [],
  485. },
  486. ]);
  487. // 添加打字效果相关的状态
  488. const typingSpeed = 50; // 打字速度(毫秒/字符)
  489. const isTyping = ref(false);
  490. // 添加音频播放相关的响应式变量
  491. const audioPlayer = ref(null);
  492. const isPlaying = ref(false);
  493. // 添加当前播放消息的 ID
  494. const currentPlayingId = ref(null);
  495. // 添加新的响应式变量
  496. const isUploading = ref(false);
  497. // 添加新的响应式状态
  498. const documentSummary = ref(null);
  499. const isLoadingSummary = ref(false);
  500. const currentQaPairs = ref([]);
  501. const currentDesignGuides = ref([]);
  502. let rotationInterval = null;
  503. // 添加新的响应式状态
  504. const searchResults = ref([]);
  505. const isSearchLoading = ref(false);
  506. const searchTotal = ref(0);
  507. // 添加互联网搜索开关状态
  508. const enableWebSearch = ref(false);
  509. // 添加新的响应式变量
  510. const isMenuCollapsed = ref(false);
  511. // 添加移动端检测
  512. const isMobile = ref(false);
  513. // 检测设备类型
  514. const checkMobile = () => {
  515. isMobile.value = window.innerWidth <= 768;
  516. };
  517. // 监听窗口大小变化
  518. onMounted(() => {
  519. checkMobile();
  520. window.addEventListener('resize', checkMobile);
  521. });
  522. onUnmounted(() => {
  523. window.removeEventListener('resize', checkMobile);
  524. });
  525. // 添加遮罩层点击处理
  526. const handleOverlayClick = () => {
  527. if (isMobile.value) {
  528. isMenuCollapsed.value = true;
  529. isRightMenuCollapsed.value = true;
  530. document.body.style.overflow = 'auto';
  531. }
  532. };
  533. // 修改菜单切换逻辑
  534. const toggleMenu = () => {
  535. isMenuCollapsed.value = !isMenuCollapsed.value;
  536. if (isMobile.value) {
  537. document.body.style.overflow = isMenuCollapsed.value ? 'auto' : 'hidden';
  538. if (!isMenuCollapsed.value) {
  539. isRightMenuCollapsed.value = true; // 确保右侧菜单关闭
  540. }
  541. }
  542. };
  543. // 修改右侧菜单切换逻辑
  544. const toggleRightMenu = () => {
  545. isRightMenuCollapsed.value = !isRightMenuCollapsed.value;
  546. if (isMobile.value) {
  547. document.body.style.overflow = isRightMenuCollapsed.value ? 'auto' : 'hidden';
  548. if (!isRightMenuCollapsed.value) {
  549. isMenuCollapsed.value = true; // 确保左侧菜单关闭
  550. }
  551. }
  552. };
  553. // 修改获取文档摘要的方法
  554. const fetchDocumentSummary = async () => {
  555. isLoadingSummary.value = true;
  556. try {
  557. const response = await axios.get(
  558. `${import.meta.env.VITE_BASE_AI_API}/api/document/summary`
  559. );
  560. console.log("API Response:", response.data);
  561. if (response.data.code === 200) {
  562. documentSummary.value = response.data.data.items;
  563. console.log("Document Summary:", documentSummary.value);
  564. startRotation();
  565. }
  566. } catch (error) {
  567. console.error("获取文档摘要失败:", error);
  568. } finally {
  569. isLoadingSummary.value = false;
  570. }
  571. };
  572. // 修改轮换显示逻辑
  573. const startRotation = () => {
  574. if (rotationInterval) {
  575. clearInterval(rotationInterval);
  576. }
  577. const updateItems = () => {
  578. if (!documentSummary.value?.length) {
  579. console.log("No document summary data available");
  580. return;
  581. }
  582. // 处理第一项数据(Hot Topics)
  583. try {
  584. const hotTopicsData = documentSummary.value[0];
  585. if (hotTopicsData?.questions) {
  586. const parsedQaPairs =
  587. /* JSON.parse( */ hotTopicsData.questions; /* .replace(/'/g, '"')); */
  588. const formattedQaPairs = parsedQaPairs.map((question) => ({
  589. question,
  590. }));
  591. // 随机选择3个问题
  592. const shuffled = [...formattedQaPairs].sort(() => 0.5 - Math.random());
  593. currentQaPairs.value = shuffled.slice(0, 3);
  594. }
  595. } catch (error) {
  596. console.error("解析 Hot Topics 失败:", error);
  597. }
  598. // 处理第二项数据(Design Guide)
  599. try {
  600. const designGuideData = documentSummary.value[1];
  601. if (designGuideData?.questions) {
  602. const parsedGuides =
  603. /* JSON.parse( */ designGuideData.questions; /* .replace(/'/g, '"')); */
  604. const formattedGuides = parsedGuides.map((question) => ({ question }));
  605. // 随机选择3个指南
  606. const shuffled = [...formattedGuides].sort(() => 0.5 - Math.random());
  607. currentDesignGuides.value = shuffled.slice(0, 3);
  608. }
  609. } catch (error) {
  610. console.error("解析 Design Guide 失败:", error);
  611. }
  612. };
  613. // 初始更新
  614. updateItems();
  615. // 每5秒更新一次
  616. rotationInterval = setInterval(updateItems, 5000);
  617. };
  618. // 处理QA点击事件
  619. const handleQaClick = (qa) => {
  620. console.log("QA clicked:", qa); // 添加日志
  621. if (qa?.question) {
  622. inputContent.value = qa.question;
  623. sendMessage();
  624. }
  625. };
  626. // 在组件卸载时清理定时器
  627. onUnmounted(() => {
  628. if (rotationInterval) {
  629. clearInterval(rotationInterval);
  630. }
  631. });
  632. // 在组件挂载时获取摘要
  633. onMounted(() => {
  634. fetchDocumentSummary();
  635. });
  636. // 方法
  637. const toggleAttachments = () => {
  638. showAttachments.value = !showAttachments.value;
  639. };
  640. const removeFile = (file) => {
  641. attachedFiles.value = attachedFiles.value.filter((f) => f !== file);
  642. };
  643. const triggerFileInput = () => {
  644. // 如果正在上传,不触发新的上传
  645. if (isUploading.value) return;
  646. fileInput.value?.click();
  647. };
  648. const uploadFile = async (file) => {
  649. try {
  650. const formData = new FormData();
  651. formData.append("file", file);
  652. const response = await axios.post(
  653. `${import.meta.env.VITE_BASE_AI_API}/upload/file`,
  654. formData,
  655. {
  656. headers: {
  657. "Content-Type": "multipart/form-data",
  658. },
  659. }
  660. );
  661. if (response.data.status === 200) {
  662. return response.data.data.fileUrl; // 返回文件上传后的URL
  663. } else {
  664. throw new Error(response.data.message || "文件上传失败");
  665. }
  666. } catch (error) {
  667. console.error("文件上传失败:", error);
  668. throw error;
  669. }
  670. };
  671. const handleFileUpload = async (event) => {
  672. const files = Array.from(event.target.files);
  673. if (files.length === 0) return;
  674. // 添加文件大小检查
  675. const MAX_VIDEO_SIZE = 10 * 1024 * 1024; // 10MB in bytes
  676. const invalidFiles = files.filter((file) => {
  677. if (isVideoFile(file) && file.size > MAX_VIDEO_SIZE) {
  678. return true;
  679. }
  680. return false;
  681. });
  682. if (invalidFiles.length > 0) {
  683. // 如果有超出大小限制的视频文件,显示错误提示
  684. alert("Video files must not exceed 10MB");
  685. event.target.value = ""; // 清空input
  686. return;
  687. }
  688. isUploading.value = true;
  689. try {
  690. for (const file of files) {
  691. // 创建一个带有上传状态的文件对象
  692. const fileWithStatus = {
  693. name: file.name,
  694. size: file.size,
  695. sizeFormatted: formatFileSize(file.size),
  696. type: file.type,
  697. isUploading: true,
  698. };
  699. // 先添加到文件列表中
  700. attachedFiles.value.push(fileWithStatus);
  701. try {
  702. const url = await uploadFile(file);
  703. // 直接修改数组中的对象
  704. const index = attachedFiles.value.findIndex(
  705. (f) => f.name === file.name
  706. );
  707. if (index !== -1) {
  708. // 使用数组替换方式更新,确保响应式更新
  709. attachedFiles.value.splice(index, 1, {
  710. ...fileWithStatus,
  711. url,
  712. isUploading: false,
  713. });
  714. }
  715. } catch (error) {
  716. // 如果上传失败,从列表中移除该文件
  717. attachedFiles.value = attachedFiles.value.filter(
  718. (f) => f.name !== file.name
  719. );
  720. console.error("单个文件上传失败:", error);
  721. }
  722. }
  723. } catch (error) {
  724. console.error("文件上传失败:", error);
  725. } finally {
  726. isUploading.value = false;
  727. event.target.value = ""; // 清空input以允许重复上传相同文件
  728. }
  729. if (files.length > 0 && enableWebSearch.value) {
  730. enableWebSearch.value = false; // 如果有文件上传,自动关闭互联网搜索
  731. }
  732. };
  733. const formatFileSize = (bytes) => {
  734. if (!bytes || isNaN(bytes)) return "0 B";
  735. const k = 1024;
  736. const sizes = ["B", "KB", "MB", "GB"];
  737. const i = Math.floor(Math.log(bytes) / Math.log(k));
  738. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
  739. };
  740. const getFileIcon = (file) => {
  741. // 如果文件已经有 URL 且是图片类型,返回 URL
  742. if (file.url && isImageFile(file)) {
  743. return file.url;
  744. }
  745. // 其他所有情况返回文档图标
  746. return wordIcon;
  747. };
  748. const isImageFile = (file) => {
  749. const imageTypes = ["jpg", "jpeg", "png", "gif", "webp"];
  750. const ext = file.name?.split(".")?.pop()?.toLowerCase() || "";
  751. return imageTypes.includes(ext);
  752. };
  753. const isVideoFile = (file) => {
  754. const videoTypes = ["mp4", "webm", "ogg"];
  755. const ext = file.name?.split(".")?.pop()?.toLowerCase() || "";
  756. return videoTypes.includes(ext);
  757. };
  758. const getFileType = (file) => {
  759. const ext = file.name.split(".").pop().toLowerCase();
  760. const imageTypes = ["jpg", "jpeg", "png", "gif", "webp"];
  761. const videoTypes = ["mp4", "webm", "ogg"];
  762. if (imageTypes.includes(ext)) return "image";
  763. if (videoTypes.includes(ext)) return "video";
  764. return "document";
  765. };
  766. // 添加滚动到底部的方法
  767. const scrollToBottom = () => {
  768. if (messageList.value) {
  769. nextTick(() => {
  770. messageList.value.scrollTop = messageList.value.scrollHeight;
  771. });
  772. }
  773. };
  774. // 添加 session_id 的响应式引用
  775. const session_id = ref("");
  776. // 修改发送消息方法
  777. const sendMessage = async () => {
  778. if (
  779. (!inputContent.value.trim() && attachedFiles.value.length === 0) ||
  780. isTyping.value
  781. )
  782. return;
  783. // 构建消息内容,包含文件信息
  784. let messageContent = inputContent.value.trim();
  785. // 收集文件URLs
  786. const imageUrls = [];
  787. const videoUrls = [];
  788. const documentUrls = [];
  789. attachedFiles.value.forEach((file) => {
  790. const fileType = getFileType(file);
  791. if (fileType === "image") {
  792. imageUrls.push(file.url);
  793. } else if (fileType === "video") {
  794. videoUrls.push(file.url);
  795. } else {
  796. documentUrls.push(file.url);
  797. }
  798. });
  799. // 添加用户消息
  800. const userMessage = {
  801. id: Date.now(),
  802. role: "user",
  803. content: messageContent,
  804. displayContent: messageContent,
  805. files: attachedFiles.value.map((file) => ({
  806. name: file.name,
  807. size: file.sizeFormatted, // 使用格式化后的大小
  808. type: file.type,
  809. url: file.url,
  810. })),
  811. };
  812. messages.value.push(userMessage);
  813. scrollToBottom();
  814. // 异步执行搜索,但不等待结果
  815. sendMessageS(messageContent).catch((error) => {
  816. console.error("Web search failed:", error);
  817. });
  818. // 保存当前输入内容并清空输入框和附件
  819. const currentInput = inputContent.value;
  820. showAttachments.value = false;
  821. inputContent.value = "";
  822. const currentFiles = [...attachedFiles.value];
  823. attachedFiles.value = [];
  824. // 添加临时AI消息
  825. const tempAiMessage = {
  826. id: Date.now() + 1,
  827. role: "assistant",
  828. content: "Thinking...",
  829. displayContent: "Thinking...",
  830. isLoading: true,
  831. };
  832. messages.value.push(tempAiMessage);
  833. scrollToBottom();
  834. try {
  835. // 根据开关状态选择不同的 API 端点
  836. const apiEndpoint = enableWebSearch.value
  837. ? `${import.meta.env.VITE_BASE_AI_API}/api/chat/web-search-llm/`
  838. : `${import.meta.env.VITE_BASE_AI_API}/chatbot/multimodal`;
  839. const response = await axios.post(apiEndpoint, {
  840. message: currentInput,
  841. chat_config_id: "2",
  842. user_id: "13365429324",
  843. session_id: session_id.value,
  844. source: "pc",
  845. image_urls: imageUrls,
  846. video_urls: videoUrls,
  847. documents: documentUrls,
  848. });
  849. // 更新 session_id
  850. if (response.data.data.session_id) {
  851. session_id.value = response.data.data.session_id;
  852. }
  853. // 移除时消息并添加AI响应
  854. messages.value = messages.value.filter(
  855. (msg) => msg.id !== tempAiMessage.id
  856. );
  857. const aiMessage = reactive({
  858. id: Date.now() + 2,
  859. role: "assistant",
  860. content: response.data.data.answer,
  861. displayContent: "",
  862. audioData: response.data.data.audio_info?.audio || null, // 保存音频数据
  863. });
  864. enableWebSearch.value=false
  865. messages.value.push(aiMessage);
  866. // 如果有音频数据,自动播放
  867. if (aiMessage.audioData) {
  868. playAudioMessage(aiMessage.audioData);
  869. }
  870. await typeMessage(aiMessage);
  871. } catch (error) {
  872. console.error("发送消息失败:", error);
  873. messages.value = messages.value.filter(
  874. (msg) => msg.id !== tempAiMessage.id
  875. );
  876. messages.value.push({
  877. id: Date.now() + 2,
  878. role: "assistant",
  879. content: "Sorry, there was an error processing your request.",
  880. displayContent: "Sorry, there was an error processing your request.",
  881. });
  882. } finally {
  883. isTyping.value = false;
  884. await nextTick();
  885. scrollToBottom();
  886. }
  887. };
  888. // 修改 sendMessageS 方法
  889. const sendMessageS = async (query) => {
  890. isSearchLoading.value = true;
  891. try {
  892. const response = await axios.post(
  893. `${import.meta.env.VITE_BASE_AI_API}/api/web-search-results/`,
  894. {
  895. query: query,
  896. num_results: 20,
  897. page: 1,
  898. page_size: 20,
  899. engine: "bing",
  900. }
  901. );
  902. if (response.data && response.data.code === 200) {
  903. searchResults.value = response.data.data.results || [];
  904. searchTotal.value = response.data.data.total_results || 0;
  905. }
  906. } catch (error) {
  907. console.error("Web search failed:", error);
  908. searchResults.value = [];
  909. searchTotal.value = 0;
  910. } finally {
  911. isSearchLoading.value = false;
  912. }
  913. };
  914. // 修改打字效果方法
  915. const typeMessage = async (message) => {
  916. if (!message || !message.content) {
  917. console.error("Invalid message or content is missing");
  918. return;
  919. }
  920. return new Promise((resolve) => {
  921. const delay = 30; // 打字延迟时间
  922. let i = 0;
  923. message.displayContent = ""; // 确保开始时是空的
  924. const typeChar = () => {
  925. if (i < message.content.length) {
  926. message.displayContent = message.content.substring(0, i + 1);
  927. i++;
  928. nextTick(() => {
  929. scrollToBottom();
  930. setTimeout(typeChar, delay);
  931. });
  932. } else {
  933. resolve();
  934. }
  935. };
  936. typeChar();
  937. });
  938. };
  939. // 模拟API请求
  940. const mockApiRequest = async (message) => {
  941. await new Promise((resolve) => setTimeout(resolve, 1000));
  942. return `This is a response to: ${message}`;
  943. };
  944. // 会话管理
  945. const addNewConversation = () => {
  946. conversationCounter.value++;
  947. const newConversation = {
  948. id: Date.now(),
  949. title: `New Conversation ${conversationCounter.value}`,
  950. messages: [],
  951. };
  952. conversations.value.push(newConversation);
  953. session_id.value = ""; // 重置 session_id
  954. switchConversation(newConversation.id);
  955. };
  956. const switchConversation = (conversationId) => {
  957. currentConversationId.value = conversationId;
  958. const conversation = conversations.value.find((c) => c.id === conversationId);
  959. messages.value = conversation ? conversation.messages : [];
  960. session_id.value = ""; // 重置 session_id
  961. };
  962. const handleFileDrop = async (event) => {
  963. if (isUploading.value) return;
  964. const files = Array.from(event.dataTransfer.files);
  965. if (files.length === 0) return;
  966. // 添加文件大小检查
  967. const MAX_VIDEO_SIZE = 10 * 1024 * 1024; // 10MB in bytes
  968. const invalidFiles = files.filter((file) => {
  969. if (isVideoFile(file) && file.size > MAX_VIDEO_SIZE) {
  970. return true;
  971. }
  972. return false;
  973. });
  974. isUploading.value = true;
  975. try {
  976. for (const file of files) {
  977. const fileWithStatus = {
  978. name: file.name,
  979. size: file.size,
  980. sizeFormatted: formatFileSize(file.size),
  981. type: file.type,
  982. isUploading: true,
  983. };
  984. attachedFiles.value.push(fileWithStatus);
  985. try {
  986. const url = await uploadFile(file);
  987. // 直接修改数组中的对象
  988. const index = attachedFiles.value.findIndex(
  989. (f) => f.name === file.name
  990. );
  991. if (index !== -1) {
  992. // 使用数组替换方式更新,确保响应式更新
  993. attachedFiles.value.splice(index, 1, {
  994. ...fileWithStatus,
  995. url,
  996. isUploading: false,
  997. });
  998. }
  999. } catch (error) {
  1000. attachedFiles.value = attachedFiles.value.filter(
  1001. (f) => f.name !== file.name
  1002. );
  1003. console.error("单个文件上传失败:", error);
  1004. }
  1005. }
  1006. } catch (error) {
  1007. console.error("文件上传失败:", error);
  1008. } finally {
  1009. isUploading.value = false;
  1010. }
  1011. if (files.length > 0 && enableWebSearch.value) {
  1012. enableWebSearch.value = false; // 如果有文件上传,自动关闭互联网搜索
  1013. }
  1014. };
  1015. // 添加音频点击处理方法
  1016. const handleAudioClick = (message) => {
  1017. if (currentPlayingId.value === message.id) {
  1018. stopAudio();
  1019. } else {
  1020. playAudioMessage(message.audioData, message.id);
  1021. }
  1022. };
  1023. // 修改音频播放方法
  1024. const playAudioMessage = async (audioBase64, messageId) => {
  1025. try {
  1026. // 停止当前播放的音频
  1027. stopAudio();
  1028. // 创建新的音频元素
  1029. const audio = new Audio(audioBase64);
  1030. audioPlayer.value = audio;
  1031. currentPlayingId.value = messageId;
  1032. // 添加事件监听
  1033. audio.addEventListener("ended", () => {
  1034. stopAudio();
  1035. });
  1036. audio.addEventListener("error", (e) => {
  1037. console.error("音频播放错误:", e);
  1038. stopAudio();
  1039. });
  1040. // 播放音频
  1041. await audio.play();
  1042. isPlaying.value = true;
  1043. } catch (error) {
  1044. console.error("音频播放失败:", error);
  1045. stopAudio();
  1046. }
  1047. };
  1048. // 修改停止播放方法
  1049. const stopAudio = () => {
  1050. if (audioPlayer.value) {
  1051. audioPlayer.value.pause();
  1052. audioPlayer.value = null;
  1053. }
  1054. isPlaying.value = false;
  1055. currentPlayingId.value = null;
  1056. };
  1057. // 在组件卸载时清理
  1058. onUnmounted(() => {
  1059. stopAudio();
  1060. });
  1061. // 添加 renderMarkdown 方法
  1062. const renderMarkdown = (content) => {
  1063. if (!content) return "";
  1064. return md.render(content);
  1065. };
  1066. </script>
  1067. <style scoped>
  1068. .layout {
  1069. display: flex;
  1070. width: 100%;
  1071. height: 100vh;
  1072. overflow: hidden;
  1073. position: relative;
  1074. }
  1075. .menu {
  1076. background: #f7f7f8;
  1077. width: 260px;
  1078. height: 100%;
  1079. display: flex;
  1080. flex-direction: column;
  1081. border-right: 1px solid rgba(0, 0, 0, 0.06);
  1082. position: absolute;
  1083. left: 0;
  1084. transform: translateX(0);
  1085. transition: transform 0.3s ease;
  1086. z-index: 2;
  1087. }
  1088. .menu.collapsed {
  1089. transform: translateX(-260px);
  1090. width: 260px;
  1091. }
  1092. .conversations {
  1093. padding: 0 12px;
  1094. flex: 1;
  1095. overflow-y: auto;
  1096. }
  1097. .chat {
  1098. flex: 1;
  1099. padding: 24px;
  1100. display: flex;
  1101. flex-direction: column;
  1102. gap: 24px;
  1103. overflow-y: auto;
  1104. overflow-x: hidden;
  1105. margin-left: 0;
  1106. margin-right: 0;
  1107. transition: margin 0.3s ease;
  1108. position: absolute;
  1109. left: 0;
  1110. right: 0;
  1111. top: 0;
  1112. bottom: 0;
  1113. }
  1114. .menu:not(.collapsed) + .chat {
  1115. left: 260px;
  1116. }
  1117. .chat {
  1118. right: 0;
  1119. transition: left 0.3s ease, right 0.3s ease;
  1120. }
  1121. .layout:has(.right_menu:not(.collapsed)) .chat {
  1122. right: 600px;
  1123. }
  1124. .right_menu:not(.collapsed) ~ .chat {
  1125. right: 600px;
  1126. }
  1127. .messages {
  1128. flex: 1;
  1129. overflow-y: auto;
  1130. padding-right: 16px;
  1131. }
  1132. .sender {
  1133. box-shadow: none;
  1134. border: 1px solid #e5e6eb;
  1135. border-radius: 8px;
  1136. padding: 8px;
  1137. }
  1138. .logo {
  1139. height: 64px;
  1140. display: flex;
  1141. align-items: center;
  1142. padding: 0 20px;
  1143. }
  1144. .logo-img {
  1145. width: 24px;
  1146. height: 24px;
  1147. }
  1148. .logo span {
  1149. margin-left: 12px;
  1150. font-size: 16px;
  1151. font-weight: 600;
  1152. color: rgba(0, 0, 0, 0.88);
  1153. }
  1154. .new-conversation-btn {
  1155. margin: 0 12px 12px;
  1156. height: 40px;
  1157. background: rgba(235, 245, 255, 0.7);
  1158. border: none;
  1159. border-radius: 8px;
  1160. color: #1677ff;
  1161. font-size: 14px;
  1162. display: flex;
  1163. align-items: center;
  1164. justify-content: center;
  1165. gap: 8px;
  1166. cursor: pointer;
  1167. transition: background-color 0.2s;
  1168. }
  1169. .new-conversation-btn:hover {
  1170. background: rgba(235, 245, 255, 0.9);
  1171. }
  1172. .conversation-list {
  1173. flex: 1;
  1174. padding: 0 8px;
  1175. overflow-y: auto;
  1176. }
  1177. .conversation-item {
  1178. padding: 12px;
  1179. margin: 4px 0;
  1180. border-radius: 8px;
  1181. color: rgba(0, 0, 0, 0.88);
  1182. font-size: 14px;
  1183. cursor: pointer;
  1184. transition: background-color 0.2s;
  1185. }
  1186. .conversation-item:hover {
  1187. background: rgba(0, 0, 0, 0.04);
  1188. }
  1189. .conversation-item.active {
  1190. background: #fff;
  1191. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
  1192. }
  1193. .conversation-list::-webkit-scrollbar {
  1194. width: 4px;
  1195. }
  1196. .conversation-list::-webkit-scrollbar-thumb {
  1197. background: rgba(0, 0, 0, 0.15);
  1198. border-radius: 2px;
  1199. }
  1200. .conversation-list::-webkit-scrollbar-track {
  1201. background: transparent;
  1202. }
  1203. :deep(.welcome-message) {
  1204. background: #fff;
  1205. border-radius: 8px;
  1206. padding: 16px;
  1207. display: flex;
  1208. align-items: flex-start;
  1209. gap: 16px;
  1210. }
  1211. :deep(.welcome-message .title) {
  1212. font-size: 16px;
  1213. font-weight: 500;
  1214. color: rgba(0, 0, 0, 0.88);
  1215. margin-bottom: 4px;
  1216. }
  1217. :deep(.welcome-message .description) {
  1218. color: rgba(0, 0, 0, 0.45);
  1219. font-size: 14px;
  1220. }
  1221. :deep(.prompt-card) {
  1222. background: #f7f8fa;
  1223. border-radius: 8px;
  1224. padding: 16px;
  1225. margin-bottom: 12px;
  1226. }
  1227. :deep(.prompt-card-title) {
  1228. font-size: 16px;
  1229. font-weight: 500;
  1230. margin-bottom: 8px;
  1231. display: flex;
  1232. align-items: center;
  1233. gap: 8px;
  1234. }
  1235. :deep(.prompt-option) {
  1236. background: #f5f5f5;
  1237. padding: 8px 12px;
  1238. border-radius: 4px;
  1239. margin-top: 8px;
  1240. cursor: pointer;
  1241. transition: background 0.3s;
  1242. }
  1243. :deep(.prompt-option:hover) {
  1244. background: #e8e8e8;
  1245. }
  1246. .input-container {
  1247. width: 50%;
  1248. margin: auto auto 0;
  1249. display: flex;
  1250. flex-direction: column;
  1251. gap: 8px;
  1252. }
  1253. .tabs-wrapper {
  1254. display: flex;
  1255. gap: 8px;
  1256. margin-bottom: 4px;
  1257. }
  1258. .tab-item {
  1259. display: flex;
  1260. align-items: center;
  1261. gap: 6px;
  1262. padding: 4px 12px;
  1263. border-radius: 4px;
  1264. cursor: pointer;
  1265. font-size: 14px;
  1266. color: rgba(0, 0, 0, 0.88);
  1267. background: transparent;
  1268. transition: background-color 0.2s;
  1269. }
  1270. .tab-item:hover {
  1271. background: rgba(0, 0, 0, 0.04);
  1272. }
  1273. .tab-item.active {
  1274. background: #f5f5f5;
  1275. }
  1276. .tab-item .anticon {
  1277. font-size: 14px;
  1278. }
  1279. .input-wrapper {
  1280. display: flex;
  1281. align-items: center;
  1282. padding: 8px 12px;
  1283. border: 1px solid #e5e6eb;
  1284. border-radius: 8px;
  1285. background: white;
  1286. }
  1287. .message-input {
  1288. flex: 1;
  1289. border: none;
  1290. outline: none;
  1291. padding: 8px;
  1292. font-size: 14px;
  1293. background: transparent;
  1294. }
  1295. .message-input::placeholder {
  1296. color: rgba(0, 0, 0, 0.45);
  1297. }
  1298. .attachment-btn,
  1299. .send-btn {
  1300. display: flex;
  1301. align-items: center;
  1302. justify-content: center;
  1303. width: 32px;
  1304. height: 32px;
  1305. border: none;
  1306. background: transparent;
  1307. cursor: pointer;
  1308. color: rgba(0, 0, 0, 0.45);
  1309. border-radius: 4px;
  1310. }
  1311. .attachment-btn:hover,
  1312. .send-btn:hover {
  1313. background: rgba(0, 0, 0, 0.04);
  1314. color: rgba(0, 0, 0, 0.88);
  1315. }
  1316. .attachments-panel {
  1317. background: white;
  1318. border-radius: 12px;
  1319. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
  1320. overflow: hidden;
  1321. }
  1322. .panel-content {
  1323. background: white;
  1324. }
  1325. .attachments-header {
  1326. padding: 12px 16px;
  1327. border-bottom: 1px solid #f0f0f0;
  1328. display: flex;
  1329. justify-content: space-between;
  1330. align-items: center;
  1331. }
  1332. .attachments-header span {
  1333. font-size: 14px;
  1334. font-weight: 500;
  1335. color: rgba(0, 0, 0, 0.88);
  1336. }
  1337. .close-btn {
  1338. border: none;
  1339. background: transparent;
  1340. cursor: pointer;
  1341. padding: 4px;
  1342. display: flex;
  1343. align-items: center;
  1344. justify-content: center;
  1345. color: rgba(0, 0, 0, 0.45);
  1346. }
  1347. .upload-area {
  1348. padding: 24px;
  1349. border-radius: 8px;
  1350. text-align: center;
  1351. cursor: pointer;
  1352. transition: all 0.3s;
  1353. }
  1354. .upload-area.has-files {
  1355. padding: 12px;
  1356. background: #fff;
  1357. }
  1358. .upload-icon {
  1359. width: 32px;
  1360. height: 32px;
  1361. margin-bottom: 8px;
  1362. }
  1363. .upload-text {
  1364. font-size: 14px;
  1365. color: rgba(0, 0, 0, 0.88);
  1366. margin-bottom: 4px;
  1367. }
  1368. .upload-hint {
  1369. font-size: 12px;
  1370. color: rgba(0, 0, 0, 0.45);
  1371. }
  1372. .file-list {
  1373. display: flex;
  1374. flex-wrap: wrap;
  1375. gap: 8px;
  1376. padding: 10px;
  1377. }
  1378. .file-item {
  1379. position: relative;
  1380. width: 260px;
  1381. padding: 8px 12px;
  1382. margin: 4px 0;
  1383. background: #f5f5f5;
  1384. border-radius: 8px;
  1385. }
  1386. .file-item.is-image {
  1387. width: 68px;
  1388. padding: 0;
  1389. margin: 4px 0;
  1390. }
  1391. .file-wrapper {
  1392. position: relative;
  1393. width: 100%;
  1394. height: 100%;
  1395. }
  1396. .file-icon {
  1397. width: 68px;
  1398. height: 68px;
  1399. object-fit: cover;
  1400. border-radius: 4px;
  1401. }
  1402. .file-wrapper video.file-icon {
  1403. background: #000;
  1404. }
  1405. .remove-btn {
  1406. position: absolute;
  1407. top: -8px;
  1408. right: -8px;
  1409. width: 16px;
  1410. height: 16px;
  1411. padding: 0;
  1412. border: none;
  1413. background: #545454;
  1414. border-radius: 50%;
  1415. cursor: pointer;
  1416. display: flex;
  1417. align-items: center;
  1418. justify-content: center;
  1419. font-size: 14px;
  1420. line-height: 1;
  1421. color: #fff;
  1422. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  1423. }
  1424. .remove-btn:hover {
  1425. opacity: 0.8;
  1426. }
  1427. .file-info {
  1428. margin-top: 8px;
  1429. }
  1430. .file-name {
  1431. display: block;
  1432. max-width: 150px;
  1433. color: rgba(0, 0, 0, 0.88);
  1434. font-size: 14px;
  1435. white-space: nowrap;
  1436. overflow: hidden;
  1437. text-overflow: ellipsis;
  1438. }
  1439. .file-info .file-name {
  1440. max-width: 150px;
  1441. }
  1442. .file-name:hover {
  1443. cursor: pointer;
  1444. }
  1445. .file-size {
  1446. display: block;
  1447. color: rgba(0, 0, 0, 0.45);
  1448. font-size: 12px;
  1449. margin-top: 4px;
  1450. }
  1451. .add-more-btn {
  1452. display: flex;
  1453. align-items: center;
  1454. justify-content: center;
  1455. width: 68px;
  1456. height: 68px;
  1457. border: 1px dashed #d9d9d9;
  1458. border-radius: 6px;
  1459. background: transparent;
  1460. cursor: pointer;
  1461. transition: all 0.3s;
  1462. margin: 4px 0;
  1463. }
  1464. .add-more-btn:hover {
  1465. border-color: #1677ff;
  1466. background: #f0f7ff;
  1467. }
  1468. /* 欢迎区域样式 */
  1469. .welcome-section {
  1470. max-width: 800px;
  1471. margin: 0 auto;
  1472. width: 50%;
  1473. }
  1474. .welcome-header {
  1475. display: flex;
  1476. align-items: flex-start;
  1477. gap: 16px;
  1478. margin-bottom: 24px;
  1479. }
  1480. .avatar {
  1481. width: 40px;
  1482. height: 40px;
  1483. border-radius: 50%;
  1484. }
  1485. .welcome-info {
  1486. flex: 1;
  1487. }
  1488. .welcome-info h2 {
  1489. font-size: 20px;
  1490. font-weight: 500;
  1491. color: rgba(0, 0, 0, 0.88);
  1492. margin: 0 0 4px 0;
  1493. }
  1494. .welcome-info p {
  1495. font-size: 14px;
  1496. color: rgba(0, 0, 0, 0.45);
  1497. margin: 0;
  1498. }
  1499. .action-buttons {
  1500. display: flex;
  1501. gap: 8px;
  1502. }
  1503. .icon-btn {
  1504. padding: 8px;
  1505. border: none;
  1506. background: transparent;
  1507. cursor: pointer;
  1508. color: rgba(0, 0, 0, 0.45);
  1509. }
  1510. /* 建议区域样式 */
  1511. .suggestion-section {
  1512. margin-top: 32px;
  1513. }
  1514. .suggestion-section h3 {
  1515. font-size: 14px;
  1516. color: rgba(0, 0, 0, 0.45);
  1517. margin-bottom: 16px;
  1518. }
  1519. .cards-container {
  1520. display: flex;
  1521. gap: 16px;
  1522. }
  1523. .suggestion-card {
  1524. flex: 1;
  1525. border-radius: 8px;
  1526. padding: 16px;
  1527. background: #ffffff; /* 替换 var(--ant-color-bg-container) */
  1528. border-radius: 8px; /* 替换 var(--ant-border-radius-lg) */
  1529. transition: border 0.3s, background 0.3s; /* 替换 var(--ant-motion-duration-slow) */
  1530. border: 1px solid #f0f0f0; /* 替换合的 border 属性 */
  1531. }
  1532. :root {
  1533. --ant-color-bg-container: #ffffff;
  1534. --ant-border-radius-lg: 8px;
  1535. --ant-motion-duration-slow: 0.3s;
  1536. --ant-line-width: 1px;
  1537. --ant-line-type: solid;
  1538. --ant-color-border-secondary: #f0f0f0;
  1539. }
  1540. .card-header {
  1541. display: flex;
  1542. align-items: center;
  1543. gap: 8px;
  1544. margin-bottom: 8px;
  1545. }
  1546. .card-header h4 {
  1547. font-size: 16px;
  1548. font-weight: 500;
  1549. color: rgba(0, 0, 0, 0.88);
  1550. margin: 0;
  1551. }
  1552. .icon.red {
  1553. color: #ff4d4f;
  1554. }
  1555. .icon.blue {
  1556. color: #1890ff;
  1557. }
  1558. .card-subtitle {
  1559. font-size: 14px;
  1560. color: rgba(0, 0, 0, 0.45);
  1561. margin-bottom: 16px;
  1562. }
  1563. .suggestion-items {
  1564. display: flex;
  1565. flex-direction: column;
  1566. gap: 8px;
  1567. }
  1568. .suggestion-item {
  1569. padding: 8px 12px;
  1570. background: #f5f5f5;
  1571. border-radius: 4px;
  1572. font-size: 12px;
  1573. color: rgba(0, 0, 0, 0.88);
  1574. display: flex;
  1575. align-items: center;
  1576. gap: 8px;
  1577. cursor: pointer;
  1578. transition: opacity 0.3s ease;
  1579. }
  1580. .suggestion-item:hover {
  1581. background: #e8e8e8;
  1582. }
  1583. .tab-icon {
  1584. width: 16px;
  1585. }
  1586. .attached-files {
  1587. padding: 16px;
  1588. }
  1589. .file-item {
  1590. display: flex;
  1591. align-items: center;
  1592. gap: 8px;
  1593. padding: 8px;
  1594. border-radius: 4px;
  1595. }
  1596. .file-item:hover {
  1597. background: #f5f5f5;
  1598. }
  1599. .remove-btn {
  1600. margin-left: auto;
  1601. padding: 4px;
  1602. border: none;
  1603. background: transparent;
  1604. cursor: pointer;
  1605. color: rgba(0, 0, 0, 0.45);
  1606. }
  1607. .remove-btn:hover {
  1608. color: rgba(0, 0, 0, 0.88);
  1609. }
  1610. .send-btn:disabled {
  1611. opacity: 0.5;
  1612. cursor: not-allowed;
  1613. }
  1614. .upload-area {
  1615. /* border: 2px dashed #d9d9d9; */
  1616. border-radius: 8px;
  1617. padding: 20px;
  1618. text-align: center;
  1619. cursor: pointer;
  1620. transition: border-color 0.3s;
  1621. }
  1622. /* .upload-area:hover,
  1623. .upload-area.drag-over {
  1624. border-color: #1677ff;
  1625. } */
  1626. .file-item {
  1627. display: flex;
  1628. align-items: center;
  1629. width: 236px;
  1630. height: 68px;
  1631. padding: 0;
  1632. margin: 4px 0;
  1633. background: #f5f5f5;
  1634. border-radius: 4px;
  1635. }
  1636. .file-item .remove-btn {
  1637. margin-left: auto;
  1638. padding: 4px 8px;
  1639. background: transparent;
  1640. border: none;
  1641. cursor: pointer;
  1642. }
  1643. /* 聊天内容显示框 */
  1644. .message-list {
  1645. width: 60%;
  1646. overflow-y: auto;
  1647. overflow-x: hidden;
  1648. padding: 20px;
  1649. display: flex;
  1650. flex-direction: column;
  1651. gap: 16px;
  1652. margin: 0 auto;
  1653. }
  1654. .message-item {
  1655. display: flex;
  1656. margin-bottom: 12px;
  1657. }
  1658. /* 用户消息靠右显示 */
  1659. .user {
  1660. display: flex;
  1661. justify-content: flex-end;
  1662. }
  1663. .message-bubble {
  1664. max-width: 80%;
  1665. padding: 12px 16px;
  1666. border-radius: 12px;
  1667. background: #fff;
  1668. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
  1669. }
  1670. /* 用户消息的气泡样式 */
  1671. .user .message-bubble {
  1672. background: #fff;
  1673. color: rgba(0, 0, 0, 0.88);
  1674. }
  1675. /* AI消息的气泡样式 */
  1676. .assistant .message-bubble {
  1677. background: #f7f7f8;
  1678. color: rgba(0, 0, 0, 0.88);
  1679. }
  1680. .message-content {
  1681. font-size: 14px;
  1682. line-height: 1.5;
  1683. white-space: pre-wrap;
  1684. word-break: break-word;
  1685. }
  1686. .right_menu {
  1687. width: 600px;
  1688. height: 100%;
  1689. background: #f7f7f8;
  1690. border-left: 1px solid rgba(0, 0, 0, 0.06);
  1691. position: absolute; /* 改为绝对定位 */
  1692. right: 0;
  1693. transform: translateX(0);
  1694. transition: transform 0.3s ease;
  1695. z-index: 2;
  1696. }
  1697. .right_menu.collapsed {
  1698. transform: translateX(100%); /* 使用 transform 代替 width */
  1699. width: 600px; /* 保持固定宽度 */
  1700. }
  1701. /* 当右侧菜单展开时,给聊天区域添加右边距 */
  1702. .right_menu:not(.collapsed) + .chat {
  1703. margin-right: 600px;
  1704. }
  1705. .collapse-left-button{
  1706. position: absolute;
  1707. left: 260px;
  1708. top: 50%;
  1709. transform: translateY(-50%);
  1710. width: 20px;
  1711. height: 40px;
  1712. background: #fff;
  1713. border: 1px solid rgba(0, 0, 0, 0.06);
  1714. border-right: none;
  1715. border-radius: 4px 0 0 4px;
  1716. display: flex;
  1717. align-items: center;
  1718. justify-content: center;
  1719. cursor: pointer;
  1720. z-index: 2;
  1721. transition: left 0.3s ease;
  1722. }
  1723. .collapse-left-button.menu-collapsed {
  1724. left: 260px;
  1725. /* border-right: 1px solid rgba(0, 0, 0, 0.06);
  1726. border-left: none;
  1727. border-radius: 0 4px 4px 0; */
  1728. }
  1729. .collapse-button {
  1730. position: absolute;
  1731. left: -20px;
  1732. top: 50%;
  1733. transform: translateY(-50%);
  1734. width: 20px;
  1735. height: 40px;
  1736. background: #fff;
  1737. border: 1px solid rgba(0, 0, 0, 0.06);
  1738. border-right: none;
  1739. border-radius: 4px 0 0 4px;
  1740. display: flex;
  1741. align-items: center;
  1742. justify-content: center;
  1743. cursor: pointer;
  1744. z-index: 1;
  1745. }
  1746. .collapse-button:hover {
  1747. background: #f5f5f5;
  1748. }
  1749. .collapse-icon {
  1750. width: 12px;
  1751. height: 12px;
  1752. }
  1753. .right-menu-content {
  1754. height: 100%;
  1755. overflow-y: auto;
  1756. }
  1757. .right_menu.collapsed .right-menu-content {
  1758. display: none;
  1759. }
  1760. /* 添加打字效果的光标样式 */
  1761. /* .assistant .message-content {
  1762. position: relative;
  1763. }
  1764. .assistant .message-content::after {
  1765. content: '|';
  1766. position: absolute;
  1767. margin-left: 2px;
  1768. animation: cursor-blink 1s infinite;
  1769. }
  1770. @keyframes cursor-blink {
  1771. 0%, 100% { opacity: 1; }
  1772. 50% { opacity: 0; }
  1773. }*/
  1774. .file-message {
  1775. background: #f7f7f8;
  1776. border-radius: 8px;
  1777. padding: 12px;
  1778. margin-bottom: 8px;
  1779. }
  1780. .file-info-display {
  1781. display: flex;
  1782. align-items: center;
  1783. gap: 12px;
  1784. margin-bottom: 8px;
  1785. }
  1786. .file-type-icon {
  1787. width: 24px;
  1788. height: 24px;
  1789. object-fit: contain;
  1790. }
  1791. .file-details {
  1792. display: flex;
  1793. flex-direction: column;
  1794. }
  1795. /* .file-name {
  1796. font-size: 14px;
  1797. color: rgba(0, 0, 0, 0.88);
  1798. margin-bottom: 4px;
  1799. } */
  1800. .file-size {
  1801. font-size: 12px;
  1802. color: rgba(0, 0, 0, 0.45);
  1803. }
  1804. .file-action {
  1805. font-size: 12px;
  1806. color: rgba(0, 0, 0, 0.45);
  1807. padding-left: 36px;
  1808. }
  1809. /* 修改用户消息气泡样式,确保文件信息正确显示 */
  1810. .user .message-bubble {
  1811. background: #fff;
  1812. color: rgba(0, 0, 0, 0.88);
  1813. }
  1814. .user .file-message {
  1815. background: #f7f7f8;
  1816. }
  1817. /* 图片预览样式 */
  1818. .image-preview {
  1819. position: relative;
  1820. width: 200px;
  1821. border-radius: 8px;
  1822. overflow: hidden;
  1823. background: #fff;
  1824. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
  1825. }
  1826. .preview-image {
  1827. width: 100%;
  1828. height: auto;
  1829. display: block;
  1830. }
  1831. .image-info {
  1832. position: absolute;
  1833. bottom: 0;
  1834. left: 0;
  1835. right: 0;
  1836. padding: 8px;
  1837. background: rgba(255, 255, 255, 0.9);
  1838. font-size: 12px;
  1839. color: rgba(0, 0, 0, 0.45);
  1840. text-align: center;
  1841. border-top: 1px solid rgba(0, 0, 0, 0.06);
  1842. }
  1843. /* 文档显示样式保持不变 */
  1844. .file-info-display {
  1845. display: flex;
  1846. align-items: center;
  1847. gap: 12px;
  1848. margin-bottom: 8px;
  1849. }
  1850. .file-type-icon {
  1851. width: 24px;
  1852. height: 24px;
  1853. object-fit: contain;
  1854. }
  1855. .file-details {
  1856. display: flex;
  1857. flex-direction: column;
  1858. }
  1859. /* .file-name {
  1860. font-size: 14px;
  1861. color: rgba(0, 0, 0, 0.88);
  1862. margin-bottom: 4px;
  1863. } */
  1864. .file-size {
  1865. font-size: 12px;
  1866. color: rgba(0, 0, 0, 0.45);
  1867. }
  1868. .file-action {
  1869. font-size: 12px;
  1870. color: rgba(0, 0, 0, 0.45);
  1871. padding-left: 36px;
  1872. }
  1873. /* 添加音频控制相关样式 */
  1874. .audio-controls {
  1875. margin-top: 8px;
  1876. }
  1877. .audio-btn {
  1878. background: transparent;
  1879. border: none;
  1880. padding: 4px 8px;
  1881. cursor: pointer;
  1882. display: flex;
  1883. align-items: center;
  1884. justify-content: center;
  1885. border-radius: 4px;
  1886. transition: background-color 0.2s;
  1887. min-width: 32px;
  1888. height: 32px;
  1889. }
  1890. .audio-btn:hover {
  1891. background: rgba(0, 0, 0, 0.04);
  1892. }
  1893. .audio-icon {
  1894. width: 20px;
  1895. height: 20px;
  1896. }
  1897. /* 音频波形动画样式 */
  1898. .wave-animation {
  1899. display: flex;
  1900. align-items: center;
  1901. height: 20px;
  1902. gap: 2px;
  1903. }
  1904. .wave-bar {
  1905. display: inline-block;
  1906. width: 3px;
  1907. height: 100%;
  1908. background: #1677ff;
  1909. border-radius: 1px;
  1910. animation: wave 1s ease-in-out infinite;
  1911. }
  1912. .wave-bar:nth-child(1) {
  1913. animation-delay: 0s;
  1914. }
  1915. .wave-bar:nth-child(2) {
  1916. animation-delay: 0.2s;
  1917. }
  1918. .wave-bar:nth-child(3) {
  1919. animation-delay: 0.4s;
  1920. }
  1921. .wave-bar:nth-child(4) {
  1922. animation-delay: 0.6s;
  1923. }
  1924. @keyframes wave {
  1925. 0%,
  1926. 100% {
  1927. height: 6px;
  1928. }
  1929. 50% {
  1930. height: 18px;
  1931. }
  1932. }
  1933. .audio-btn {
  1934. background: transparent;
  1935. border: none;
  1936. padding: 4px 8px;
  1937. cursor: pointer;
  1938. display: flex;
  1939. align-items: center;
  1940. justify-content: center;
  1941. border-radius: 4px;
  1942. transition: background-color 0.2s;
  1943. min-width: 32px;
  1944. height: 32px;
  1945. }
  1946. .audio-btn:hover {
  1947. background: rgba(0, 0, 0, 0.04);
  1948. }
  1949. .audio-icon {
  1950. width: 20px;
  1951. height: 20px;
  1952. }
  1953. .message-text {
  1954. margin: 0;
  1955. white-space: normal; /* 确保文本会换行 */
  1956. word-wrap: break-word; /* 长单词或 URL 会被断开以适应容器宽度 */
  1957. overflow: auto;
  1958. font-size: 12px;
  1959. line-height: 20px;
  1960. }
  1961. .message-text ::v-deep p {
  1962. font-size: 12px !important;
  1963. margin: 5px 0 10px;
  1964. padding: 0;
  1965. font-size: inherit;
  1966. line-height: 22px;
  1967. font-weight: inherit;
  1968. }
  1969. .message-text ::v-deep ol {
  1970. padding-bottom: 10px;
  1971. }
  1972. .message-text ::v-deep .source-section h3 {
  1973. margin: 20px 0 3px;
  1974. color: #333;
  1975. font-size: 16px;
  1976. }
  1977. .message-text ::v-deep ol li {
  1978. padding-top: 3px;
  1979. }
  1980. .message-text ::v-deep ol li a {
  1981. font-size: 14px;
  1982. color: #1296db;
  1983. }
  1984. .video-preview {
  1985. position: relative;
  1986. width: 300px;
  1987. border-radius: 8px;
  1988. overflow: hidden;
  1989. background: #fff;
  1990. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
  1991. margin-bottom: 8px;
  1992. }
  1993. .preview-video {
  1994. width: 100%;
  1995. height: auto;
  1996. display: block;
  1997. max-height: 300px;
  1998. }
  1999. /* 添加 loading 相关样式 */
  2000. .upload-loading {
  2001. display: flex;
  2002. flex-direction: column;
  2003. align-items: center;
  2004. gap: 12px;
  2005. }
  2006. .loading-spinner {
  2007. width: 24px;
  2008. height: 24px;
  2009. border: 2px solid #f3f3f3;
  2010. border-top: 2px solid #1677ff;
  2011. border-radius: 50%;
  2012. animation: spin 1s linear infinite;
  2013. }
  2014. .file-loading {
  2015. position: absolute;
  2016. top: 0;
  2017. left: 0;
  2018. right: 0;
  2019. bottom: 0;
  2020. background: rgba(255, 255, 255, 0.8);
  2021. display: flex;
  2022. align-items: center;
  2023. justify-content: center;
  2024. z-index: 1;
  2025. }
  2026. @keyframes spin {
  2027. 0% {
  2028. transform: rotate(0deg);
  2029. }
  2030. 100% {
  2031. transform: rotate(360deg);
  2032. }
  2033. }
  2034. /* 上传区域在上传时的样式 */
  2035. .upload-area {
  2036. position: relative;
  2037. cursor: pointer; /* 始终保持指针样式 */
  2038. }
  2039. .upload-area.uploading {
  2040. opacity: 0.7;
  2041. }
  2042. /* 添加配置容器的样式 */
  2043. .config-container {
  2044. margin-top: auto;
  2045. border-top: 1px solid rgba(0, 0, 0, 0.06);
  2046. }
  2047. .web-search-toggle {
  2048. display: flex;
  2049. align-items: center;
  2050. margin-left: auto;
  2051. padding: 0 12px;
  2052. }
  2053. .toggle-input {
  2054. position: relative;
  2055. width: 36px;
  2056. height: 20px;
  2057. margin-right: 8px;
  2058. appearance: none;
  2059. background: #ccc;
  2060. border-radius: 10px;
  2061. cursor: pointer;
  2062. transition: background 0.3s;
  2063. }
  2064. .toggle-input:checked {
  2065. background: #1677ff;
  2066. }
  2067. .toggle-input::before {
  2068. content: "";
  2069. position: absolute;
  2070. top: 2px;
  2071. left: 2px;
  2072. width: 16px;
  2073. height: 16px;
  2074. background: white;
  2075. border-radius: 50%;
  2076. transition: transform 0.3s;
  2077. }
  2078. .toggle-input:checked::before {
  2079. transform: translateX(16px);
  2080. }
  2081. .toggle-label {
  2082. font-size: 14px;
  2083. color: rgba(0, 0, 0, 0.88);
  2084. cursor: pointer;
  2085. }
  2086. .toggle-input:disabled {
  2087. opacity: 0.5;
  2088. cursor: not-allowed;
  2089. }
  2090. .toggle-label.disabled {
  2091. color: rgba(0, 0, 0, 0.45);
  2092. cursor: not-allowed;
  2093. }
  2094. .disabled-hint {
  2095. font-size: 12px;
  2096. color: rgba(0, 0, 0, 0.45);
  2097. margin-left: 4px;
  2098. }
  2099. /* 添加菜单收缩相关样式 */
  2100. .menu {
  2101. /* 现有样式保持不变 */
  2102. transition: width 0.3s ease;
  2103. position: relative;
  2104. }
  2105. .menu.collapsed {
  2106. width: 0;
  2107. padding: 0;
  2108. }
  2109. /* 添加收缩按钮样式 */
  2110. .collapse-button {
  2111. position: absolute;
  2112. right: -20px;
  2113. top: 50%;
  2114. transform: translateY(-50%);
  2115. width: 20px;
  2116. height: 40px;
  2117. background: #fff;
  2118. border: 1px solid rgba(0, 0, 0, 0.06);
  2119. border-left: none;
  2120. border-radius: 0 4px 4px 0;
  2121. display: flex;
  2122. align-items: center;
  2123. justify-content: center;
  2124. cursor: pointer;
  2125. z-index: 1;
  2126. }
  2127. .collapse-button:hover {
  2128. background: #f5f5f5;
  2129. }
  2130. .collapse-icon {
  2131. width: 12px;
  2132. height: 12px;
  2133. }
  2134. /* 添加响应式布局样式 */
  2135. @media screen and (max-width: 768px) {
  2136. .layout {
  2137. /* 移动端布局调整 */
  2138. flex-direction: column;
  2139. }
  2140. .menu {
  2141. /* 移动端菜单调整 */
  2142. position: fixed;
  2143. width: 100%;
  2144. height: 100%;
  2145. z-index: 1000;
  2146. transform: translateX(-100%);
  2147. }
  2148. .menu.collapsed {
  2149. transform: translateX(-100%);
  2150. }
  2151. .collapse-left-button {
  2152. /* 移动端收缩按钮调整 */
  2153. display: none;
  2154. }
  2155. .chat {
  2156. /* 移动端聊天区域调整 */
  2157. width: 100%;
  2158. margin: 0;
  2159. padding: 16px;
  2160. left: 0 !important;
  2161. right: 0 !important;
  2162. }
  2163. .message-list {
  2164. /* 移动端消息列表调整 */
  2165. width: 100%;
  2166. padding: 10px;
  2167. }
  2168. .input-container {
  2169. /* 移动端输入区域调整 */
  2170. width: 100%;
  2171. padding: 10px;
  2172. }
  2173. .cards-container {
  2174. /* 移动端卡片容器调整 */
  2175. flex-direction: column;
  2176. }
  2177. .suggestion-card {
  2178. /* 移动端建议卡片调整 */
  2179. width: 100%;
  2180. }
  2181. .welcome-section {
  2182. /* 移动端欢迎区域调整 */
  2183. width: 100%;
  2184. padding: 16px;
  2185. }
  2186. .right_menu {
  2187. /* 移动端右侧菜单调整 */
  2188. position: fixed;
  2189. width: 100%;
  2190. height: 100%;
  2191. z-index: 1000;
  2192. }
  2193. .right_menu.collapsed {
  2194. transform: translateX(100%);
  2195. }
  2196. /* 移动端文件预览调整 */
  2197. .image-preview,
  2198. .video-preview {
  2199. width: 100%;
  2200. max-width: 300px;
  2201. }
  2202. /* 移动端文件列表调整 */
  2203. .file-list {
  2204. justify-content: center;
  2205. }
  2206. .file-item {
  2207. width: 100%;
  2208. max-width: 300px;
  2209. }
  2210. /* 移动端标签栏调整 */
  2211. .tabs-wrapper {
  2212. flex-wrap: wrap;
  2213. gap: 4px;
  2214. }
  2215. .web-search-toggle {
  2216. width: 100%;
  2217. justify-content: flex-start;
  2218. margin-top: 8px;
  2219. }
  2220. /* 移动端消息气泡调整 */
  2221. .message-bubble {
  2222. max-width: 90%;
  2223. }
  2224. }
  2225. /* 添加移动端手势支持 */
  2226. @media (hover: none) and (pointer: coarse) {
  2227. .menu {
  2228. /* 支持触摸滑动 */
  2229. touch-action: pan-y;
  2230. }
  2231. .message-list {
  2232. /* 支持触摸滚动 */
  2233. -webkit-overflow-scrolling: touch;
  2234. }
  2235. }
  2236. /* 优化移动端字体大小 */
  2237. @media screen and (max-width: 480px) {
  2238. .message-content {
  2239. font-size: 14px;
  2240. }
  2241. .suggestion-item {
  2242. font-size: 13px;
  2243. }
  2244. .file-name {
  2245. font-size: 13px;
  2246. }
  2247. }
  2248. /* 修改现有的响应式样式 */
  2249. @media screen and (max-width: 768px) {
  2250. .layout {
  2251. overflow-x: hidden;
  2252. }
  2253. .chat {
  2254. /* 调整聊天区域以保持一致的宽度 */
  2255. padding: 24px 16px;
  2256. margin: 0;
  2257. width: 100%;
  2258. }
  2259. .message-list {
  2260. /* 保持消息列表的宽度一致 */
  2261. width: 100%;
  2262. max-width: 800px;
  2263. margin: 0 auto;
  2264. padding: 0 10px;
  2265. }
  2266. .input-container {
  2267. /* 保持输入区域的宽度一致 */
  2268. width: 100%;
  2269. max-width: 800px;
  2270. margin: 0 auto;
  2271. padding: 0 10px;
  2272. }
  2273. .welcome-section {
  2274. /* 保持欢迎区域的宽度一致 */
  2275. width: 100%;
  2276. max-width: 800px;
  2277. margin: 0 auto;
  2278. padding: 0 16px;
  2279. }
  2280. .cards-container {
  2281. /* 保持卡片布局 */
  2282. flex-direction: row;
  2283. flex-wrap: wrap;
  2284. gap: 16px;
  2285. }
  2286. .suggestion-card {
  2287. /* 调整卡片大小 */
  2288. flex: 1 1 300px;
  2289. min-width: 300px;
  2290. }
  2291. .message-bubble {
  2292. /* 保持消息气泡的样式 */
  2293. max-width: 80%;
  2294. padding: 12px 16px;
  2295. }
  2296. /* 调整文件预览区域 */
  2297. .image-preview,
  2298. .video-preview {
  2299. width: 200px;
  2300. margin: 0 auto;
  2301. }
  2302. /* 调整文件列表布局 */
  2303. .file-list {
  2304. display: flex;
  2305. flex-wrap: wrap;
  2306. gap: 8px;
  2307. justify-content: flex-start;
  2308. }
  2309. .file-item {
  2310. width: calc(50% - 8px);
  2311. min-width: 200px;
  2312. }
  2313. /* 调整标签栏布局 */
  2314. .tabs-wrapper {
  2315. flex-wrap: nowrap;
  2316. overflow-x: auto;
  2317. -webkit-overflow-scrolling: touch;
  2318. padding-bottom: 8px;
  2319. }
  2320. /* 调整搜索开关位置 */
  2321. .web-search-toggle {
  2322. position: relative;
  2323. width: auto;
  2324. margin-left: auto;
  2325. }
  2326. /* 调整菜单样式 */
  2327. .menu {
  2328. position: fixed;
  2329. left: 0;
  2330. top: 0;
  2331. bottom: 0;
  2332. width: 260px;
  2333. transform: translateX(-100%);
  2334. transition: transform 0.3s ease;
  2335. z-index: 1000;
  2336. }
  2337. .menu.collapsed {
  2338. transform: translateX(-100%);
  2339. }
  2340. .menu:not(.collapsed) {
  2341. transform: translateX(0);
  2342. }
  2343. /* 调整右侧菜单样式 */
  2344. .right_menu {
  2345. position: fixed;
  2346. right: 0;
  2347. top: 0;
  2348. bottom: 0;
  2349. width: 300px;
  2350. transform: translateX(100%);
  2351. transition: transform 0.3s ease;
  2352. z-index: 1000;
  2353. }
  2354. .right_menu.collapsed {
  2355. transform: translateX(100%);
  2356. }
  2357. .right_menu:not(.collapsed) {
  2358. transform: translateX(0);
  2359. }
  2360. /* 添加遮罩层样式 */
  2361. .menu-overlay {
  2362. position: fixed;
  2363. top: 0;
  2364. left: 0;
  2365. right: 0;
  2366. bottom: 0;
  2367. background: rgba(0, 0, 0, 0.5);
  2368. z-index: 999;
  2369. display: none;
  2370. }
  2371. .menu:not(.collapsed) ~ .menu-overlay,
  2372. .right_menu:not(.collapsed) ~ .menu-overlay {
  2373. display: block;
  2374. }
  2375. }
  2376. /* 添加更细致的断点控制 */
  2377. @media screen and (max-width: 480px) {
  2378. .message-content {
  2379. font-size: 14px;
  2380. line-height: 1.5;
  2381. }
  2382. .suggestion-item {
  2383. font-size: 14px;
  2384. padding: 10px;
  2385. }
  2386. .input-wrapper {
  2387. padding: 8px;
  2388. }
  2389. .message-input {
  2390. font-size: 14px;
  2391. }
  2392. }
  2393. /* 确保滚动流畅 */
  2394. * {
  2395. -webkit-overflow-scrolling: touch;
  2396. }
  2397. /* 防止页面横向滚动 */
  2398. body {
  2399. overflow-x: hidden;
  2400. width: 100%;
  2401. }
  2402. /* 优化触摸体验 */
  2403. button,
  2404. .suggestion-item,
  2405. .conversation-item {
  2406. touch-action: manipulation;
  2407. }
  2408. </style>
  2409. <style lang="less" scoped>
  2410. ::v-deep .message-content {
  2411. /* 确保 scoped 样式能够渗透到动态生成的内容 */
  2412. h1,
  2413. h2,
  2414. h3,
  2415. h4,
  2416. h5,
  2417. h6 {
  2418. margin-top: 16px;
  2419. margin-bottom: 8px;
  2420. font-weight: 600;
  2421. }
  2422. p {
  2423. margin-bottom: 8px;
  2424. }
  2425. code {
  2426. background: #f5f5f5;
  2427. padding: 2px 4px;
  2428. border-radius: 4px;
  2429. font-family: monospace;
  2430. }
  2431. pre {
  2432. background: #f5f5f5;
  2433. padding: 16px;
  2434. border-radius: 8px;
  2435. overflow-x: auto;
  2436. }
  2437. ul,
  2438. ol {
  2439. padding-left: 24px;
  2440. margin-bottom: 8px;
  2441. }
  2442. blockquote {
  2443. border-left: 4px solid #ddd;
  2444. padding-left: 16px;
  2445. margin: 8px 0;
  2446. color: rgba(0, 0, 0, 0.65);
  2447. }
  2448. a {
  2449. color: #1677ff;
  2450. text-decoration: none;
  2451. }
  2452. a:hover {
  2453. text-decoration: underline;
  2454. }
  2455. table {
  2456. border-collapse: collapse;
  2457. width: 100%;
  2458. margin-bottom: 16px;
  2459. }
  2460. th,
  2461. td {
  2462. border: 1px solid #ddd;
  2463. padding: 8px;
  2464. text-align: left;
  2465. }
  2466. th {
  2467. background: #f5f5f5;
  2468. }
  2469. }
  2470. .message-actions {
  2471. display: flex;
  2472. justify-content: flex-end;
  2473. margin-top: 8px;
  2474. padding-top: 8px;
  2475. border-top: 1px solid rgba(0, 0, 0, 0.06);
  2476. }
  2477. .message-actions :deep(.export-button) {
  2478. opacity: 0.6;
  2479. transition: opacity 0.2s;
  2480. }
  2481. .message-actions :deep(.export-button:hover) {
  2482. opacity: 1;
  2483. }
  2484. </style>