report.vue 128 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718
  1. <template>
  2. <div class="layout" :class="{ 'dark-theme': isDarkTheme }">
  3. <!-- 添加密码修改弹窗组件 -->
  4. <ChangePasswordDialog
  5. v-model:visible="showPasswordDialog"
  6. />
  7. <div class="header">
  8. <div class="user-profile-bar">
  9. <div class="logo">
  10. <img
  11. src="../assets/logoc.png"
  12. alt="logo"
  13. class="logo-img"
  14. />
  15. <!-- <span>Raycos Tech</span> -->
  16. </div>
  17. <div class="user-info-bar">
  18. <!-- 添加主题切换按钮 -->
  19. <div class="theme-switch" @click="toggleTheme" :title="isDarkTheme ? '切换到浅色模式' : '切换到深色模式'">
  20. <transition name="theme-icon" mode="out-in">
  21. <img
  22. v-if="isDarkTheme"
  23. :key="isDarkTheme"
  24. src="../assets/sun2.png"
  25. alt="切换到浅色模式"
  26. class="theme-icon"
  27. />
  28. <img v-else class="theme-icon" src="../assets/night2.png" alt="切换到深色模式">
  29. </transition>
  30. </div>
  31. <div class="user-info" v-if="!isMobile" @click="toggleDropdown">
  32. <div class="user-name">商户名称:{{ userInfo.merchant_name || '未登录用户' }}</div>
  33. <div class="user-name">当前用户:{{ userInfo.username || '未登录用户' }}</div>
  34. <!-- <div class="user-role">{{ userInfo.role || '未设置角色' }}</div> -->
  35. </div>
  36. <div class="avatar" @click="toggleDropdown">
  37. <img src="../assets/avter.png" alt="用户头像" />
  38. </div>
  39. </div>
  40. <!-- Logo :draggable="false"-->
  41. <!-- 下拉菜单 -->
  42. <div class="dropdown-menu" v-show="showDropdown">
  43. <div class="dropdown-item" @click="handleModifyPassword">
  44. <i class="fas fa-key"></i>
  45. 修改密码
  46. </div>
  47. <div class="dropdown-item" @click="handleCreateInvitation">
  48. <i class="fas fa-user-plus"></i>
  49. 创建邀请
  50. </div>
  51. <div class="dropdown-item" @click="handleLogout">
  52. <i class="fas fa-sign-out-alt"></i>
  53. 退出登录
  54. </div>
  55. </div>
  56. </div>
  57. </div>
  58. <div
  59. class="collapse-left-button"
  60. :class="{ 'menu-collapsed': isMenuCollapsed }"
  61. @click="toggleMenu"
  62. >
  63. <img
  64. :src="isMenuCollapsed ? rightIcon : leftIcon"
  65. alt="toggle"
  66. class="collapse-icon"
  67. />
  68. </div>
  69. <div class="menu" :class="{ collapsed: isMenuCollapsed }">
  70. <!-- 添加收缩按钮 -->
  71. <!-- New Conversation 按钮 -->
  72. <button class="new-conversation-btn" @click="addNewConversation">
  73. <img class="tab-icon" src="../assets/svg/plus.svg" alt="" />
  74. <span>New Conversation</span>
  75. </button>
  76. <!-- 会话列表 -->
  77. <div class="conversation-list">
  78. <div
  79. v-for="conversation in conversations"
  80. :key="conversation.id"
  81. :class="[
  82. 'conversation-item',
  83. { active: currentConversationId === conversation.id },
  84. ]"
  85. @click="switchConversation(conversation.id)"
  86. >
  87. {{ conversation.title }}
  88. </div>
  89. </div>
  90. <!-- 添加模型配置和语音配置的容器 -->
  91. <div class="config-container">
  92. <KnowledgeConfig />
  93. <ModelConfig />
  94. <VoiceConfig />
  95. </div>
  96. </div>
  97. <div class="chat">
  98. <!-- 欢迎消息 -->
  99. <div v-if="messages.length === 0" class="welcome-section">
  100. <!-- Do you want 部分 -->
  101. <div class="suggestion-section">
  102. <h3>嗨,我是你的智能助手</h3>
  103. <div class="cards-container">
  104. <div class="suggestion-card">
  105. <div class="card-header">
  106. <img
  107. src="../assets/svg/hot-for-ux.svg"
  108. alt="hot topics"
  109. class="tab-icon"
  110. />
  111. <h4>推荐问题</h4>
  112. </div>
  113. <p class="card-subtitle">为您精选的热门问题</p>
  114. <div class="suggestion-items">
  115. <!-- Combine both QA pairs and guides -->
  116. <div
  117. v-for="(item, index) in [
  118. ...currentQaPairs,
  119. ...currentDesignGuides,
  120. ]"
  121. :key="index"
  122. class="suggestion-item"
  123. @click="handleQaClick(item)"
  124. >
  125. {{ item.question }}
  126. </div>
  127. </div>
  128. </div>
  129. <!-- Hot Topics 卡片 --><!-- Design Guide 卡片 -->
  130. <!-- <div class="suggestion-card">
  131. <div class="card-header">
  132. <img
  133. src="../assets/svg/hot-for-ux.svg"
  134. alt="hot topics"
  135. class="tab-icon"
  136. />
  137. <h4>知识库相关问题:</h4>
  138. </div>
  139. <p class="card-subtitle">我们可以这样向知识库提问</p>
  140. <div class="suggestion-items">
  141. <div
  142. v-for="(qa, index) in currentQaPairs"
  143. :key="index"
  144. class="suggestion-item"
  145. @click="handleQaClick(qa)"
  146. >
  147. {{ qa.question }}
  148. </div>
  149. </div>
  150. </div>
  151. <div class="suggestion-card">
  152. <div class="card-header">
  153. <img
  154. src="../assets/svg/read.svg"
  155. alt="hot topics"
  156. class="tab-icon"
  157. />
  158. <h4>网上热搜问答</h4>
  159. </div>
  160. <p class="card-subtitle">搜索网上的热门回答</p>
  161. <div class="suggestion-items">
  162. <div
  163. v-for="(guide, index) in currentDesignGuides"
  164. :key="index"
  165. class="suggestion-item"
  166. @click="handleQaClick(guide)"
  167. >
  168. {{ guide.question }}
  169. </div>
  170. </div>
  171. </div> -->
  172. </div>
  173. </div>
  174. </div>
  175. <!-- 修改消息列表部分 -->
  176. <div v-else class="message-list" ref="messageList">
  177. <!-- 添加流式思考步骤显示 -->
  178. <div v-if="currentThinkingSteps && currentThinkingSteps.length > 0" class="thinking-steps-stream">
  179. <div v-for="(step, index) in currentThinkingSteps"
  180. :key="index"
  181. class="thinking-step-stream"
  182. :class="[step.type, {'fade-in': true}]">
  183. {{ step.content }}
  184. <div v-if="index === currentThinkingSteps.length - 1" class="typing-indicator">
  185. <span></span>
  186. <span></span>
  187. <span></span>
  188. </div>
  189. </div>
  190. </div>
  191. <div
  192. v-for="message in messages"
  193. :key="message.id"
  194. :class="[
  195. 'message-item',
  196. message.role === 'user' ? 'user' : 'assistant',
  197. ]"
  198. >
  199. <div class="message-bubble">
  200. <div class="message-content">
  201. <!-- 添加thinking steps显示 -->
  202. <div v-if="message.thinkingSteps && message.thinkingSteps.length > 0" class="thinking-steps">
  203. <div v-for="(step, index) in message.thinkingSteps"
  204. :key="index"
  205. class="thinking-step"
  206. :class="step.type">
  207. {{ step.content }}
  208. </div>
  209. </div>
  210. <template v-if="message.role === 'user' && message.files">
  211. <div
  212. v-for="file in message.files"
  213. :key="file.name"
  214. class="file-message"
  215. >
  216. <!-- 图片文件显示 -->
  217. <template v-if="isImageFile(file)">
  218. <div class="image-preview">
  219. <img :src="file.url" class="preview-image" />
  220. </div>
  221. </template>
  222. <!-- 视频文件显示 -->
  223. <template v-else-if="isVideoFile(file)">
  224. <div class="video-preview">
  225. <video
  226. controls
  227. class="preview-video"
  228. :src="file.url"
  229. preload="metadata"
  230. >
  231. Your browser does not support the video tag.
  232. </video>
  233. </div>
  234. </template>
  235. <!-- 文档文件显示 -->
  236. <template v-else>
  237. <div class="file-info-display">
  238. <img :src="getFileIcon(file)" class="file-type-icon" />
  239. <div class="file-details">
  240. <span class="file-name" :title="file.name">{{
  241. file.name
  242. }}</span>
  243. <span class="file-size">{{ file.size }}</span>
  244. </div>
  245. </div>
  246. </template>
  247. </div>
  248. </template>
  249. <div
  250. class="message-text"
  251. v-if="message.role === 'assistant'"
  252. v-html="renderMarkdown(message.displayContent)"
  253. ></div>
  254. <div v-else>{{ message.content }}</div>
  255. <!-- 音频控制按钮 -->
  256. <div
  257. v-if="message.role === 'assistant'"
  258. class="audio-controls"
  259. >
  260. <button class="audio-btn" @click="handleAudioClick(message)">
  261. <!-- 只在当前消息正在播放时显示波形动画 -->
  262. <div
  263. v-if="currentPlayingId === message.id"
  264. class="wave-animation"
  265. >
  266. <span class="wave-bar"></span>
  267. <span class="wave-bar"></span>
  268. <span class="wave-bar"></span>
  269. <span class="wave-bar"></span>
  270. </div>
  271. <!-- 其他情况显示播放图标 -->
  272. <img v-else :src="playIcon" alt="播放" class="audio-icon" />
  273. </button>
  274. </div>
  275. </div>
  276. </div>
  277. </div>
  278. </div>
  279. <!-- 输入框 -->
  280. <div class="input-container">
  281. <!-- 搜索开关组 -->
  282. <div class="search-toggles" :class="{ 'compact': isCompact }">
  283. <div class="left_item" style="display: flex;">
  284. <div class="search-toggle-item">
  285. <div class="toggle-content">
  286. <GlobalOutlined />
  287. <span class="toggle-text" >互联网</span><!-- v-show="!isCompact" -->
  288. <div class="toggle-switch">
  289. <input
  290. type="checkbox"
  291. id="webSearchToggle"
  292. v-model="enableWebSearch"
  293. @change="handleWebSearchToggle"
  294. :disabled="attachedFiles.length > 0"
  295. class="toggle-input"
  296. />
  297. <label
  298. for="webSearchToggle"
  299. class="toggle-label"
  300. :class="{ disabled: attachedFiles.length > 0 }"
  301. >
  302. <span class="toggle-slider"></span>
  303. </label>
  304. </div>
  305. </div>
  306. </div>
  307. <div class="search-toggle-item">
  308. <div class="toggle-content">
  309. <DatabaseOutlined />
  310. <span class="toggle-text" >知识库</span><!-- v-show="!isCompact" -->
  311. <div class="toggle-switch">
  312. <input
  313. type="checkbox"
  314. id="knowledgeSearchToggle"
  315. v-model="enableKnowledgeSearch"
  316. @change="handleKnowledgeToggle"
  317. class="toggle-input"
  318. />
  319. <label
  320. for="knowledgeSearchToggle"
  321. class="toggle-label"
  322. >
  323. <span class="toggle-slider"></span>
  324. </label>
  325. </div>
  326. </div>
  327. </div>
  328. <div class="search-toggle-item">
  329. <div class="toggle-content">
  330. <DatabaseOutlined />
  331. <span class="toggle-text" >推理模式</span><!-- v-show="!isCompact" -->
  332. <div class="toggle-switch">
  333. <input
  334. type="checkbox"
  335. id="inferenceModelId"
  336. v-model="inferenceModel"
  337. class="toggle-input"
  338. />
  339. <label
  340. for="inferenceModelId"
  341. class="toggle-label"
  342. >
  343. <span class="toggle-slider"></span>
  344. </label>
  345. </div>
  346. </div>
  347. </div>
  348. </div>
  349. <div class="search-toggle-item">
  350. <div class="toggle-content">
  351. <!-- <SelectOutlined /> -->
  352. <!-- <span class="toggle-text">模型选择</span> -->
  353. <select
  354. class="model-select"
  355. :value="selectedModel"
  356. @change="handleModelChange($event.target.value)"
  357. :disabled="isLoadingModels"
  358. >
  359. <option v-if="isLoadingModels" value="">加载中...</option>
  360. <option v-else-if="models.length === 0" value="">无可用模型</option>
  361. <option
  362. v-for="model in models"
  363. :key="model.id"
  364. :value="model.id"
  365. >
  366. {{ model.name }}
  367. </option>
  368. </select>
  369. </div>
  370. </div>
  371. </div>
  372. <!-- 添加附件面板 -->
  373. <div v-if="showAttachments" class="attachments-panel">
  374. <div class="panel-content">
  375. <div class="attachments-header">
  376. <span>Attachments</span>
  377. <button class="close-btn" @click="toggleAttachments">
  378. <img src="../assets/svg/close.svg" alt="" />
  379. </button>
  380. </div>
  381. <div
  382. v-if="attachedFiles.length === 0"
  383. style="display: flex; justify-content: center"
  384. >
  385. <div
  386. class="upload-area"
  387. :class="{ 'has-files': attachedFiles.length > 0 }"
  388. @click="triggerFileInput"
  389. @drop.prevent="handleFileDrop"
  390. @dragover.prevent
  391. @dragenter.prevent
  392. >
  393. <!-- 移除条件判断,让整个区域始终可点击 -->
  394. <template v-if="isUploading">
  395. <div class="upload-loading">
  396. <div class="loading-spinner"></div>
  397. <div class="upload-text">Uploading...</div>
  398. </div>
  399. </template>
  400. <template v-else>
  401. <img
  402. src="../assets/svg/upload.svg"
  403. alt="upload"
  404. class="upload-icon"
  405. />
  406. <div class="upload-text">Upload files</div>
  407. <div class="upload-hint">
  408. Click or drag files to this area to upload
  409. </div>
  410. </template>
  411. <input
  412. type="file"
  413. ref="fileInput"
  414. style="display: none"
  415. @change="handleFileUpload"
  416. multiple
  417. />
  418. </div>
  419. </div>
  420. <!-- 上传内容列表 -->
  421. <div v-if="attachedFiles.length !== 0">
  422. <div class="file-list">
  423. <div
  424. v-for="file in attachedFiles"
  425. :key="file.name"
  426. class="file-item"
  427. :class="{ 'is-image': isImageFile(file) }"
  428. >
  429. <div :class="isImageFile(file) ? 'file-wrapper' : ''">
  430. <!-- 添加文件 loading 状态 -->
  431. <div v-if="file.isUploading" class="file-loading">
  432. <div class="loading-spinner"></div>
  433. </div>
  434. <!-- 其他文件显示逻辑保持不变 -->
  435. <img
  436. v-else-if="isImageFile(file)"
  437. :src="file.url"
  438. alt="file"
  439. class="file-icon"
  440. />
  441. <video
  442. v-else-if="isVideoFile(file)"
  443. :src="file.url"
  444. class="file-icon"
  445. preload="metadata"
  446. >
  447. Your browser does not support the video tag.
  448. </video>
  449. <img
  450. v-else
  451. :src="getFileIcon(file)"
  452. alt="file"
  453. :class="
  454. isImageFile(file) || isVideoFile(file)
  455. ? 'file-icon'
  456. : ''
  457. "
  458. />
  459. <button
  460. class="remove-btn"
  461. @click.stop="removeFile(file)"
  462. v-if="!file.isUploading"
  463. >
  464. ×
  465. </button>
  466. </div>
  467. <div class="file-info" v-if="!isImageFile(file)">
  468. <span class="file-name">{{ file.name }}</span>
  469. <span class="file-size">{{ file.sizeFormatted }}</span>
  470. </div>
  471. </div>
  472. <button class="add-more-btn" @click.stop="triggerFileInput">
  473. <img src="../assets/svg/plus.svg" alt="add" />
  474. </button>
  475. </div>
  476. <input
  477. type="file"
  478. ref="fileInput"
  479. style="display: none"
  480. @change="handleFileUpload"
  481. multiple
  482. />
  483. </div>
  484. <!-- <div v-if="attachedFiles.length" class="attached-files">
  485. <div
  486. v-for="file in attachedFiles"
  487. :key="file.name"
  488. class="file-item"
  489. >
  490. <FileOutlined />
  491. <span>{{ file.name }}</span>
  492. <button class="remove-btn" @click="removeFile(file)">
  493. </button>
  494. </div>
  495. </div> -->
  496. </div>
  497. </div>
  498. <!-- 输入框部分 -->
  499. <div class="input-wrapper">
  500. <button class="attachment-btn" @click="toggleAttachments">
  501. <img
  502. src="../assets/svg/attachment.svg"
  503. alt="attachment"
  504. class="tab-icon"
  505. />
  506. </button>
  507. <input
  508. type="text"
  509. class="message-input"
  510. v-model="inputContent"
  511. @keyup.enter="sendMessage"
  512. placeholder="Type a message..."
  513. /><!-- @input="handleInputChange" :disabled="(!inputContent && attachedFiles.length === 0)"-->
  514. <button
  515. class="send-btn"
  516. @click="sendMessage"
  517. >
  518. <template v-if="isTyping">
  519. <span class="loading-btn">
  520. <span class="loading-square"></span>
  521. </span>
  522. </template>
  523. <template v-else>
  524. <img src="../assets/svg/up.svg" alt="send" />
  525. </template>
  526. </button>
  527. </div>
  528. </div>
  529. </div>
  530. <div
  531. v-if="!isMobile"
  532. class="right_menu"
  533. :class="{
  534. collapsed: !isRightMenuVisible
  535. }"
  536. :style="{ width: rightMenuWidth + 'px' }"
  537. >
  538. <div v-if="inferenceModel">
  539. <div style="height: 91vh;overflow: auto;" class="file-list" v-html="formatted_data">
  540. </div>
  541. </div>
  542. <!-- 修改收缩按钮的位置和样式 -->
  543. <!-- <div
  544. class="collapse-button"
  545. :class="{
  546. 'menu-expanded': !isRightMenuCollapsed,
  547. 'menu-collapsed': isRightMenuCollapsed
  548. }"
  549. @click="toggleRightMenu"
  550. >
  551. <img
  552. :src="isRightMenuCollapsed ? leftIcon : rightIcon"
  553. alt="toggle"
  554. class="collapse-icon"
  555. />
  556. </div> -->
  557. <!-- 添加拖动条 -->
  558. <div
  559. class="resize-handle"
  560. @mousedown="startResize"
  561. @touchstart="startResize"
  562. ></div>
  563. <div class="right-menu-content">
  564. <!-- 添加 Tab 切换 -->
  565. <!-- 搜索面板 -->
  566. <div v-show="activeTab === 'search'" class="panel-container">
  567. <!-- <div class="search-header">
  568. <span>搜索结果</span>
  569. <div class="web-search-toggle">
  570. <input
  571. type="checkbox"
  572. id="searchToggle"
  573. v-model="enableWebSearch"
  574. class="toggle-input"
  575. />
  576. <label for="searchToggle" class="toggle-label">
  577. {{ enableWebSearch ? '搜索已开启' : '搜索已关闭' }}
  578. </label>
  579. </div>
  580. </div> -->
  581. <!-- v-if="enableWebSearch" -->
  582. <SearchResults
  583. :searchResults="searchResults"
  584. :loading="isSearchLoading"
  585. :total="searchTotal"
  586. :enableWebSearch="enableWebSearch"
  587. :message="currentSearchQuery"
  588. @update:enableWebSearch="handleSearchToggle"
  589. />
  590. </div>
  591. <!-- 文档预览面板 -->
  592. <div v-show="activeTab === 'preview'" class="panel-container">
  593. <div class="preview-header">
  594. <span>文档预览</span>
  595. </div>
  596. <div class="document-preview">
  597. <div class="preview-container">
  598. <!-- 文档列表 -->
  599. <!-- <div class="document-list">
  600. <div v-if="attachedFiles.length === 0" class="empty-state">
  601. <FileOutlined class="empty-icon" />
  602. <p>暂无文档</p>
  603. </div>
  604. <div v-else v-for="doc in attachedFiles" :key="doc.name" class="document-item"
  605. :class="{ 'active': currentDoc?.name === doc.name }"
  606. @click="handleDocClick(doc)">
  607. <div class="document-info">
  608. <div class="doc-icon">
  609. <img
  610. v-if="isImageFile(doc)"
  611. :src="doc.url"
  612. class="preview-thumbnail"
  613. alt="preview"
  614. />
  615. <video
  616. v-else-if="isVideoFile(doc)"
  617. :src="doc.url"
  618. class="preview-thumbnail"
  619. preload="metadata"
  620. />
  621. <img
  622. v-else
  623. :src="getFileIcon(doc)"
  624. alt="file"
  625. class="file-icon"
  626. />
  627. </div>
  628. <div class="info">
  629. <div class="name" :title="doc.name">{{ doc.name }}</div>
  630. <div class="size">{{ doc.sizeFormatted || formatFileSize(doc.size) }}</div>
  631. </div>
  632. </div>
  633. <div class="actions">
  634. <button class="action-btn" @click.stop="handleDownload(doc)" title="下载">
  635. <DownloadOutlined />
  636. </button>
  637. </div>
  638. </div>
  639. </div> -->
  640. <!-- 直接预览区域 -->
  641. <div v-if="currentDoc" class="direct-preview">
  642. <div class="preview-header">
  643. <span>{{ currentDoc.name }}</span>
  644. <button class="close-btn" @click="closePreview">
  645. <CloseOutlined />
  646. </button>
  647. </div>
  648. <div class="preview-body">
  649. <!-- 图片预览 -->
  650. <img
  651. v-if="isImageFile(currentDoc)"
  652. :src="currentDoc.url"
  653. class="preview-image"
  654. alt="preview"
  655. />
  656. <!-- 视频预览 -->
  657. <video
  658. v-else-if="isVideoFile(currentDoc)"
  659. :src="currentDoc.url"
  660. controls
  661. class="preview-video"
  662. />
  663. <!-- Word文档预览 -->
  664. <div v-else-if="isWordFile(currentDoc)">
  665. <vue-office-docx
  666. :src="currentDoc.url"
  667. class="doc-preview"
  668. @rendered="handleRendered"
  669. @error="handleError"
  670. />
  671. </div>
  672. <!-- Excel文档预览 -->
  673. <div v-else-if="isExcelFile(currentDoc)">
  674. <vue-office-excel
  675. :options="excelOptions"
  676. @rendered="handleRendered"
  677. @error="handleExcelError"
  678. :src="currentDoc.url"
  679. class="doc-preview"
  680. />
  681. </div>
  682. <!-- PDF预览-->
  683. <iframe v-else-if="isPdfFile(currentDoc)" :src="currentDoc.url" style="width: 100%; height: 100%;" frameborder="0"></iframe>
  684. <!-- 其他文件预览 -->
  685. <div v-else class="file-preview">
  686. <FileOutlined class="large-icon" />
  687. <p>该文件类型暂不支持预览</p>
  688. </div>
  689. </div>
  690. </div>
  691. </div>
  692. </div>
  693. </div>
  694. <!-- 添加知识库结果面板 -->
  695. <div v-show="activeTab === 'knowledge'" class="panel-container">
  696. <!-- <div class="search-header">
  697. <span>知识库搜索结果</span>
  698. </div> -->
  699. <!-- v-if="enableKnowledgeSearch" -->
  700. <KnowledgeResults
  701. :searchResults="knowledgeResults"
  702. :loading="isKnowledgeLoading"
  703. :total="knowledgeTotal"
  704. :message="currentSearchQuery"
  705. />
  706. </div>
  707. </div>
  708. </div>
  709. </div>
  710. </template>
  711. <script setup>
  712. import { ref, reactive, nextTick, onUnmounted, onMounted, computed, watch } from "vue";
  713. import {
  714. PlusOutlined,
  715. FireOutlined,
  716. ReadOutlined,
  717. HeartOutlined,
  718. SmileOutlined,
  719. CommentOutlined,
  720. CloudUploadOutlined,
  721. PaperClipOutlined,
  722. ShareAltOutlined,
  723. EllipsisOutlined,
  724. CloseOutlined,
  725. FileOutlined,
  726. ArrowUpOutlined,
  727. UploadOutlined,
  728. FileSearchOutlined,
  729. GlobalOutlined,
  730. DatabaseOutlined,
  731. FolderOutlined,
  732. SelectOutlined,
  733. DownloadOutlined,
  734. } from "@ant-design/icons-vue";
  735. import leftIcon from "../assets/svg/left.svg";
  736. import rightIcon from "../assets/svg/right.svg";
  737. import plusIcon from "../assets/svg/plus.svg";
  738. import hotForUxIcon from "../assets/svg/hot-for-ux.svg";
  739. import readIcon from "../assets/svg/read.svg";
  740. import attachmentIcon from "../assets/svg/attachment.svg";
  741. import uploadIcon from "../assets/svg/upload.svg";
  742. import closeIcon from "../assets/svg/close.svg";
  743. import upIcon from "../assets/svg/up.svg";
  744. import wordIcon from "../assets/svg/word.svg";
  745. import pauseIcon from "../assets/svg/pause.svg";
  746. import playIcon from "../assets/svg/play.svg";
  747. import axios from "axios";
  748. import { useStore } from "vuex";
  749. import MarkdownIt from "markdown-it";
  750. import ModelConfig from "../components/ModelConfig/index.vue";
  751. import SearchResults from "../components/SearchResults/index.vue";
  752. import VoiceConfig from "../components/VoiceConfig/index.vue";
  753. import KnowledgeConfig from "../components/KnowledgeConfig/index.vue";
  754. import ExportButton from "../components/ExportButton/index.vue";
  755. import dayjs from 'dayjs';
  756. import DocumentPreview from "../components/DocumentPreview/index.vue";
  757. import KnowledgeResults from "../components/KnowledgeResults/index.vue";
  758. import ChangePasswordDialog from '../components/ChangePasswordDialog.vue'
  759. import { useRouter } from 'vue-router'
  760. import { message } from 'ant-design-vue';
  761. import VueOfficeDocx from '@vue-office/docx';
  762. import VueOfficeExcel from '@vue-office/excel';
  763. /* import '@vue-office/docx/lib/index.css';
  764. import '@vue-office/excel/lib/index.css'; */
  765. // 初始化 markdown-it
  766. const md = new MarkdownIt({
  767. html: true,
  768. linkify: true,
  769. typographer: true,
  770. });
  771. // 响应式状态
  772. const headerOpen = ref(false);
  773. const inputContent = ref("");
  774. const showAttachments = ref(false);
  775. const isRightMenuCollapsed = ref(false);
  776. const fileInput = ref(null);
  777. const attachedFiles = ref([]);
  778. const messages = ref([]);
  779. const isLoading = ref(false);
  780. const currentConversationId = ref(1);
  781. const conversationCounter = ref(1);
  782. const messageList = ref(null);
  783. const store = useStore();
  784. const router = useRouter()
  785. // 会话数据
  786. const conversations = ref([
  787. {
  788. id: 1,
  789. title: "当前会话",
  790. messages: [],
  791. },
  792. ]);
  793. // 添加打字效果相关的状态
  794. const typingSpeed = 50; // 打字速度(毫秒/字符)
  795. const isTyping = ref(false);
  796. const typingTimeout = ref(null); // 添加打字超时引用
  797. // 添加音频播放相关的响应式变量
  798. const audioPlayer = ref(null);
  799. const isPlaying = ref(false);
  800. // 添加当前播放消息的 ID
  801. const currentPlayingId = ref(null);
  802. const currentAudioSource = ref(null);
  803. // 添加新的响应式变量
  804. const isUploading = ref(false);
  805. // 添加新的响应式状态
  806. const documentSummary = ref(null);
  807. const isLoadingSummary = ref(false);
  808. const currentQaPairs = ref([]);
  809. const currentDesignGuides = ref([]);
  810. let rotationInterval = null;
  811. // 添加新的响应式状态
  812. const searchResults = ref([]);
  813. const isSearchLoading = ref(false);
  814. const searchTotal = ref(0);
  815. // 添加互联网搜索开关状态
  816. const enableWebSearch = ref(false);
  817. // 添加新的响应式变量
  818. const isMenuCollapsed = ref(false);
  819. // 添加移动端检测
  820. const isMobile = ref(false);
  821. //知识库搜索
  822. const enableKnowledgeSearch = ref(false);
  823. const knowledgeResults = ref([]);
  824. const knowledgeTotal = ref(0);
  825. const isKnowledgeLoading = ref(false);
  826. const formatted_data = ref('')
  827. // 推理模式
  828. const inferenceModel = ref(false);
  829. // 检测设备类型
  830. const checkMobile = () => {
  831. isMobile.value = window.innerWidth <= 768;
  832. if (isMobile.value) {
  833. isRightMenuCollapsed.value = true; // 在移动设备下自动收起右侧菜单
  834. isMenuCollapsed.value = true;
  835. }else{
  836. isRightMenuCollapsed.value = false;
  837. isMenuCollapsed.value = false;
  838. }
  839. };
  840. // 添加窗口大小改变事件监听
  841. onMounted(() => {
  842. checkMobile(); // 初始检查
  843. window.addEventListener('resize', checkMobile);
  844. });
  845. const showDropdown = ref(false);
  846. const toggleDropdown = () => {
  847. console.log(showDropdown.value);
  848. showDropdown.value = !showDropdown.value;
  849. };
  850. // 添加遮罩层点击处理
  851. const handleOverlayClick = () => {
  852. if (isMobile.value) {
  853. isMenuCollapsed.value = true;
  854. isRightMenuCollapsed.value = true;
  855. document.body.style.overflow = 'auto';
  856. }
  857. };
  858. // 修改菜单切换逻辑
  859. const toggleMenu = () => {
  860. isMenuCollapsed.value = !isMenuCollapsed.value;
  861. if (isMobile.value) {
  862. document.body.style.overflow = isMenuCollapsed.value ? 'auto' : 'hidden';
  863. if (!isMenuCollapsed.value) {
  864. isRightMenuCollapsed.value = true; // 确保右侧菜单关闭
  865. }
  866. }
  867. // 更新聊天区域的位置
  868. if (document.querySelector('.chat')) {
  869. document.querySelector('.chat').style.left = isMenuCollapsed.value ? '0' : '260px';
  870. }
  871. };
  872. // 修改右侧菜单切换逻辑
  873. const toggleRightMenu = () => {
  874. isRightMenuCollapsed.value = false;
  875. if (isRightMenuCollapsed.value) {
  876. // 收起菜单时的处理
  877. if (document.querySelector('.chat')) {
  878. document.querySelector('.chat').style.right = '0';
  879. }
  880. } else {
  881. enableWebSearch.value = true;
  882. // 展开菜单时的处理
  883. if (document.querySelector('.chat')) {
  884. document.querySelector('.chat').style.right = `${rightMenuWidth.value}px`;
  885. }
  886. }
  887. if (isMobile.value) {
  888. document.body.style.overflow = isRightMenuCollapsed.value ? 'auto' : 'hidden';
  889. if (!isRightMenuCollapsed.value) {
  890. isMenuCollapsed.value = true;
  891. }
  892. }
  893. };
  894. // 修改自动展开右侧菜单的方法
  895. const expandRightMenu = () => {
  896. isRightMenuCollapsed.value = false;
  897. if (document.querySelector('.chat')) {
  898. document.querySelector('.chat').style.right = `${rightMenuWidth.value}px`;
  899. }
  900. };
  901. // 修改获取文档摘要的方法
  902. const fetchDocumentSummary = async () => {
  903. isLoadingSummary.value = true;
  904. try {
  905. const response = await axios.get(
  906. `${import.meta.env.VITE_BASE_AI_API}/api/document/summary?limit=2`,
  907. {
  908. headers: {
  909. 'Authorization': `JWT ${localStorage.getItem('token')}`
  910. }
  911. }
  912. );
  913. console.log("API Response:", response.data);
  914. if (response.data.status === 200) {
  915. documentSummary.value = response.data.data;
  916. console.log("Document Summary:", documentSummary.value);
  917. startRotation();
  918. }
  919. } catch (error) {
  920. console.error("获取文档摘要失败:", error);
  921. } finally {
  922. isLoadingSummary.value = false;
  923. }
  924. };
  925. // 修改轮换显示逻辑
  926. const startRotation = () => {
  927. if (rotationInterval) {
  928. clearInterval(rotationInterval);
  929. }
  930. const updateItems = () => {
  931. if (!documentSummary.value?.length) {
  932. console.log("没有可用的文档摘要数据");
  933. return;
  934. }
  935. // 处理第一篇文档的问题(如:热门话题)
  936. try {
  937. const firstDocData = documentSummary.value[0];
  938. if (firstDocData?.questions) {
  939. // 直接使用questions数组中的问题
  940. const formattedQaPairs = firstDocData.questions.map((question) => ({
  941. question,
  942. }));
  943. // 随机抽取3个问题显示
  944. const shuffled = [...formattedQaPairs].sort(() => 0.5 - Math.random());
  945. currentQaPairs.value = shuffled.slice(0, 3);
  946. }
  947. } catch (error) {
  948. console.error("处理第一篇文档问题失败:", error);
  949. }
  950. // 处理第二篇文档的问题(如:设计指南)
  951. try {
  952. const secondDocData = documentSummary.value[1];
  953. if (secondDocData?.questions) {
  954. // 直接使用questions数组中的问题
  955. const formattedGuides = secondDocData.questions.map((question) => ({
  956. question,
  957. }));
  958. // 随机抽取3个问题显示
  959. const shuffled = [...formattedGuides].sort(() => 0.5 - Math.random());
  960. currentDesignGuides.value = shuffled.slice(0, 3);
  961. }
  962. } catch (error) {
  963. console.error("处理第二篇文档问题失败:", error);
  964. }
  965. };
  966. // 初始更新
  967. updateItems();
  968. // 每5秒更新一次
  969. rotationInterval = setInterval(updateItems, 5000);
  970. };
  971. // 处理QA点击事件
  972. const handleQaClick = (qa) => {
  973. console.log("QA clicked:", qa); // 添加日志
  974. if (qa?.question) {
  975. inputContent.value = qa.question;
  976. sendMessage();
  977. }
  978. };
  979. // 在组件卸载时清理定时器
  980. onUnmounted(() => {
  981. if (rotationInterval) {
  982. clearInterval(rotationInterval);
  983. }
  984. });
  985. // 方法
  986. const toggleAttachments = () => {
  987. showAttachments.value = !showAttachments.value;
  988. };
  989. const removeFile = (file) => {
  990. attachedFiles.value = attachedFiles.value.filter((f) => f !== file);
  991. if(attachedFiles.value.length === 0){
  992. rightMenuWidth.value = 0;
  993. if(document.querySelector('.chat')){
  994. document.querySelector('.chat').style.right = `0px`;
  995. }
  996. }else{
  997. rightMenuWidth.value = 450;
  998. if(document.querySelector('.chat')){
  999. document.querySelector('.chat').style.right = `${rightMenuWidth.value}px`;
  1000. }
  1001. }
  1002. console.log(attachedFiles.value.length);
  1003. };
  1004. const triggerFileInput = () => {
  1005. // 如果正在上传,不触发新的上传
  1006. if (isUploading.value) return;
  1007. fileInput.value?.click();
  1008. };
  1009. const uploadFile = async (file) => {
  1010. try {
  1011. const formData = new FormData();
  1012. formData.append("file", file);
  1013. const response = await axios.post(
  1014. `${import.meta.env.VITE_BASE_AI_API}/upload/file`,
  1015. formData,
  1016. {
  1017. headers: {
  1018. "Content-Type": "multipart/form-data",
  1019. },
  1020. }
  1021. );
  1022. if (response.data.status === 2000) {
  1023. return response.data.data.fileUrl; // 返回文件上传后的URL
  1024. } else {
  1025. throw new Error(response.data.message || "文件上传失败");
  1026. }
  1027. } catch (error) {
  1028. console.error("文件上传失败:", error);
  1029. throw error;
  1030. }
  1031. };
  1032. const handleFileUpload = async (event) => {
  1033. const files = Array.from(event.target.files);
  1034. if (files.length === 0) return;
  1035. // 添加文件大小检查
  1036. const MAX_VIDEO_SIZE = 10 * 1024 * 1024; // 10MB in bytes
  1037. const invalidFiles = files.filter((file) => {
  1038. if (isVideoFile(file) && file.size > MAX_VIDEO_SIZE) {
  1039. return true;
  1040. }
  1041. return false;
  1042. });
  1043. if (invalidFiles.length > 0) {
  1044. // 如果有超出大小限制的视频文件,显示错误提示
  1045. alert("Video files must not exceed 10MB");
  1046. event.target.value = ""; // 清空input
  1047. return;
  1048. }
  1049. isUploading.value = true;
  1050. rightMenuWidth.value = 450;
  1051. try {
  1052. for (const file of files) {
  1053. // 创建一个带有上传状态的文件对象
  1054. const fileWithStatus = {
  1055. name: file.name,
  1056. size: file.size,
  1057. sizeFormatted: formatFileSize(file.size),
  1058. type: file.type,
  1059. isUploading: true,
  1060. };
  1061. // 先添加到文件列表中
  1062. attachedFiles.value.push(fileWithStatus);
  1063. try {
  1064. const url = await uploadFile(file);
  1065. // 直接修改数组中的对象
  1066. const index = attachedFiles.value.findIndex(
  1067. (f) => f.name === file.name
  1068. );
  1069. if (index !== -1) {
  1070. // 使用数组替换方式更新,确保响应式更新
  1071. attachedFiles.value.splice(index, 1, {
  1072. ...fileWithStatus,
  1073. url,
  1074. isUploading: false,
  1075. });
  1076. }
  1077. } catch (error) {
  1078. // 如果上传失败,从列表中移除该文件
  1079. attachedFiles.value = attachedFiles.value.filter(
  1080. (f) => f.name !== file.name
  1081. );
  1082. console.error("单个文件上传失败:", error);
  1083. }
  1084. }
  1085. } catch (error) {
  1086. console.error("文件上传失败:", error);
  1087. } finally {
  1088. isUploading.value = false;
  1089. event.target.value = ""; // 清空input以允许重复上传相同文件
  1090. // 修改文件上传后的处理逻辑
  1091. if (attachedFiles.value.length > 0) {
  1092. // 切换到预览标签页
  1093. activeTab.value = 'preview';
  1094. // 展开右侧菜单
  1095. isRightMenuCollapsed.value = false;
  1096. // 如果在移动端,调整布局
  1097. if (document.querySelector('.chat')) {
  1098. document.querySelector('.chat').style.right = `${rightMenuWidth.value}px`;
  1099. }
  1100. }
  1101. }
  1102. if (files.length > 0 && enableWebSearch.value) {
  1103. enableWebSearch.value = false; // 如果有文件上传,自动关闭互联网搜索
  1104. }
  1105. };
  1106. const formatFileSize = (bytes) => {
  1107. if (!bytes || isNaN(bytes)) return "0 B";
  1108. const k = 1024;
  1109. const sizes = ["B", "KB", "MB", "GB"];
  1110. const i = Math.floor(Math.log(bytes) / Math.log(k));
  1111. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
  1112. };
  1113. const getFileIcon = (file) => {
  1114. // 如果文件已经有 URL 且是图片类型,返回 URL
  1115. if (file.url && isImageFile(file)) {
  1116. return file.url;
  1117. }
  1118. // 其他所有情况返回文档图标
  1119. return wordIcon;
  1120. };
  1121. const isImageFile = (file) => {
  1122. const imageTypes = ["jpg", "jpeg", "png", "gif", "webp"];
  1123. const ext = file.name?.split(".")?.pop()?.toLowerCase() || "";
  1124. return imageTypes.includes(ext);
  1125. };
  1126. const isVideoFile = (file) => {
  1127. const videoTypes = ["mp4", "webm", "ogg"];
  1128. const ext = file.name?.split(".")?.pop()?.toLowerCase() || "";
  1129. return videoTypes.includes(ext);
  1130. };
  1131. const getFileType = (file) => {
  1132. const ext = file.name.split(".").pop().toLowerCase();
  1133. const imageTypes = ["jpg", "jpeg", "png", "gif", "webp"];
  1134. const videoTypes = ["mp4", "webm", "ogg"];
  1135. if (imageTypes.includes(ext)) return "image";
  1136. if (videoTypes.includes(ext)) return "video";
  1137. return "document";
  1138. };
  1139. // 添加滚动到底部的方法
  1140. const scrollToBottom = () => {
  1141. if (messageList.value) {
  1142. nextTick(() => {
  1143. messageList.value.scrollTop = messageList.value.scrollHeight;
  1144. });
  1145. }
  1146. };
  1147. // 添加 session_id 的响应式引用
  1148. const session_id = ref("");
  1149. // 添加取消控制器
  1150. const abortController = ref(null);
  1151. // 修改发送消息方法
  1152. const sendMessage = async () => {
  1153. // 检查登录状态
  1154. const token = localStorage.getItem('token');
  1155. if (!token) {
  1156. router.push('/login');
  1157. return;
  1158. }
  1159. if(isTyping.value){
  1160. isTyping.value = false;
  1161. // 如果存在正在进行的请求,取消它
  1162. if (abortController.value) {
  1163. abortController.value.abort();
  1164. abortController.value = null;
  1165. }
  1166. // 清除打字效果的定时器
  1167. if (typingTimeout.value) {
  1168. clearTimeout(typingTimeout.value);
  1169. typingTimeout.value = null;
  1170. }
  1171. return;
  1172. }
  1173. if (
  1174. (!inputContent.value.trim() && attachedFiles.value.length === 0) ||
  1175. isTyping.value
  1176. )
  1177. return;
  1178. isTyping.value = true;
  1179. // 保存当前输入内容
  1180. const currentMessage = inputContent.value.trim();
  1181. // 立即清空输入框
  1182. inputContent.value = '';
  1183. // 根据开启的搜索类型执行相应的搜索
  1184. if (enableWebSearch.value) {
  1185. currentSearchQuery.value = currentMessage;
  1186. // 执行互联网搜索
  1187. sendMessageS(currentMessage).catch((error) => {
  1188. console.error("Web search failed:", error);
  1189. });
  1190. } else if (enableKnowledgeSearch.value) {
  1191. currentSearchQuery.value = currentMessage;
  1192. // 执行知识库搜索
  1193. handleKnowledgeSearch({
  1194. query: currentMessage,
  1195. page: 1,
  1196. pageSize: 20
  1197. });
  1198. }
  1199. // 构建消息内容,包含文件信息
  1200. let messageContent = currentMessage;
  1201. // 收集文件URLs
  1202. const imageUrls = [];
  1203. const videoUrls = [];
  1204. const documentUrls = [];
  1205. attachedFiles.value.forEach((file) => {
  1206. const fileType = getFileType(file);
  1207. if (fileType === "image") {
  1208. imageUrls.push(file.url);
  1209. } else if (fileType === "video") {
  1210. videoUrls.push(file.url);
  1211. } else {
  1212. documentUrls.push(file.url);
  1213. }
  1214. });
  1215. // 添加用户消息
  1216. const userMessage = {
  1217. id: Date.now(),
  1218. role: "user",
  1219. content: currentMessage,
  1220. displayContent: currentMessage,
  1221. files: attachedFiles.value.map((file) => ({
  1222. name: file.name,
  1223. size: file.sizeFormatted,
  1224. type: file.type,
  1225. url: file.url,
  1226. })),
  1227. };
  1228. messages.value.push(userMessage);
  1229. // 添加临时AI消息
  1230. const tempAiMessage = {
  1231. id: Date.now() + 1,
  1232. role: "assistant",
  1233. content: "思考中...",
  1234. displayContent: "思考中...",
  1235. isLoading: true,
  1236. };
  1237. messages.value.push(tempAiMessage);
  1238. try {
  1239. // 创建新的 AbortController
  1240. abortController.value = new AbortController();
  1241. if(!inferenceModel.value){
  1242. // 构建请求参数
  1243. /* const requestData = {
  1244. message: currentMessage,
  1245. chat_config_id: "2",
  1246. user_id: "13365429324",
  1247. session_id: session_id.value || "",
  1248. source: "pc",
  1249. image_urls: attachedFiles.value.filter(f => isImageFile(f)).map(f => f.url),
  1250. video_urls: attachedFiles.value.filter(f => isVideoFile(f)).map(f => f.url),
  1251. documents: attachedFiles.value.filter(f => !isImageFile(f) && !isVideoFile(f)).map(f => f.url),
  1252. merchant_id: JSON.parse(localStorage.getItem('userInfo')).merchant.merchant_id||1,
  1253. model_type: selectedModelInfo.value.type,
  1254. model_name: selectedModelInfo.value.name
  1255. }; */
  1256. const apiEndpoint = enableWebSearch.value
  1257. ? `${import.meta.env.VITE_BASE_AI_API}/api/chat/web-search-llm/`
  1258. : `${import.meta.env.VITE_BASE_AI_API}/api/chat/online/multimodal`;
  1259. const response = await axios.post(apiEndpoint, {
  1260. message: currentMessage,
  1261. chat_config_id: "2",
  1262. user_id: "13365429324",
  1263. session_id: session_id.value,
  1264. source: "pc",
  1265. image_urls: imageUrls,
  1266. video_urls: videoUrls,
  1267. documents: documentUrls,
  1268. merchant_id:JSON.parse(localStorage.getItem('userInfo')).merchant.merchant_id,
  1269. model_type: selectedModelInfo.value.type,
  1270. model_name: selectedModelInfo.value.name
  1271. },
  1272. {
  1273. headers: {
  1274. 'Authorization': `JWT ${localStorage.getItem('token')}`
  1275. },
  1276. signal: abortController.value.signal
  1277. }
  1278. );
  1279. // 移除临时消息
  1280. messages.value = messages.value.filter(
  1281. (msg) => msg.id !== tempAiMessage.id
  1282. );
  1283. // 统一处理响应数据
  1284. const responseData = enableWebSearch.value ? response.data.data : response.data;
  1285. const messageContent = enableWebSearch.value ? responseData.answer : responseData.choices[0].message.content;
  1286. const audioInfo = enableWebSearch.value ? responseData.audio_info?.audio : responseData.audio_info?.audio;
  1287. const sessionId = enableWebSearch.value ? responseData.session_id : responseData.session_id;
  1288. // 更新 session_id
  1289. if (sessionId) {
  1290. session_id.value = sessionId;
  1291. }
  1292. // 创建新的AI消息
  1293. const aiMessage = reactive({
  1294. id: Date.now() + 2,
  1295. role: "assistant",
  1296. content: messageContent,
  1297. displayContent: "",
  1298. audioData: audioInfo || null,
  1299. });
  1300. messages.value.push(aiMessage);
  1301. // 如果有音频数据,自动播放
  1302. if (aiMessage.audioData && !isPlaying.value) {
  1303. await playAudioMessage(aiMessage.audioData, aiMessage.id);
  1304. }
  1305. // 显示打字效果
  1306. await typeMessage(aiMessage);
  1307. }else{
  1308. // 构建请求参数
  1309. const requestData = {
  1310. query: currentMessage,
  1311. /* user_id: "13365429324",
  1312. session_id: session_id.value || "", */
  1313. };
  1314. // 发送初始请求获取SSE URL
  1315. const response = await fetch(`${import.meta.env.VITE_BASE_AI_API}/api/agents/database/`, {
  1316. method: 'POST',
  1317. headers: {
  1318. 'Authorization': `JWT ${token}`,
  1319. 'Content-Type': 'application/json'
  1320. },
  1321. body: JSON.stringify(requestData)
  1322. });
  1323. if (!response.ok) {
  1324. throw new Error('请求失败');
  1325. }
  1326. // 获取SSE URL
  1327. const responseData = await response.json();
  1328. console.log(responseData)//.result.summary
  1329. // 移除临时消息
  1330. messages.value = messages.value.filter(
  1331. (msg) => msg.id !== tempAiMessage.id
  1332. );
  1333. formatted_data.value = responseData.result.html
  1334. const messageContent = responseData.result.summary
  1335. // 创建新的AI消息
  1336. const aiMessage = reactive({
  1337. id: Date.now() + 2,
  1338. role: "assistant",
  1339. content: messageContent,
  1340. displayContent: "",
  1341. audioData: null,
  1342. });
  1343. messages.value.push(aiMessage);
  1344. // 显示打字效果
  1345. await typeMessage(aiMessage);
  1346. }
  1347. } catch (error) {
  1348. console.error("发送消息失败:", error);
  1349. // 移除临时消息
  1350. messages.value = messages.value.filter(msg => msg.id !== tempAiMessage.id);
  1351. // 添加错误消息
  1352. messages.value.push({
  1353. id: Date.now() + 2,
  1354. role: "assistant",
  1355. content: "发送消息失败,请重试",
  1356. displayContent: "发送消息失败,请重试",
  1357. });
  1358. } finally {
  1359. isTyping.value = false;
  1360. abortController.value = null;
  1361. }
  1362. };
  1363. // 修改 sendMessageS 方法
  1364. const sendMessageS = async (query) => {
  1365. isSearchLoading.value = true;
  1366. try {
  1367. const response = await axios.post(
  1368. `${import.meta.env.VITE_BASE_AI_API}/api/web-search-results/`,
  1369. {
  1370. query: query,
  1371. num_results: 20,
  1372. page: 1,
  1373. page_size: 20,
  1374. engine: "tavily",
  1375. }
  1376. );
  1377. console.log(response)
  1378. if (response.data && response.data.code === 200) {
  1379. searchResults.value = response.data.data.result || [];
  1380. console.log(searchResults.value)
  1381. searchTotal.value = response.data.data.total || 0;
  1382. }
  1383. } catch (error) {
  1384. console.error("Web search failed:", error);
  1385. searchResults.value = [];
  1386. searchTotal.value = 0;
  1387. } finally {
  1388. isSearchLoading.value = false;
  1389. }
  1390. };
  1391. // 修改打字效果方法
  1392. const typeMessage = async (message) => {
  1393. if (!message || !message.content) {
  1394. console.error("Invalid message or content is missing");
  1395. return;
  1396. }
  1397. return new Promise((resolve) => {
  1398. const minDelay = 20; // 最小打字延迟
  1399. const maxDelay = 50; // 最大打字延迟
  1400. let i = 0;
  1401. message.displayContent = ""; // 确保开始时是空的
  1402. const typeChar = () => {
  1403. if (i < message.content.length && isTyping.value) {
  1404. // 处理特殊字符和标点符号
  1405. let chunk = "";
  1406. const char = message.content[i];
  1407. // 如果是中文字符,整个字符一起显示
  1408. if (/[\u4e00-\u9fa5]/.test(char)) {
  1409. chunk = char;
  1410. }
  1411. // 如果是标点符号,与前面的字符一起显示
  1412. else if (/[.,!?;,。!?;]/.test(char)) {
  1413. chunk = char;
  1414. }
  1415. // 如果是空格,与下一个单词一起显示
  1416. else if (char === " " && i + 1 < message.content.length) {
  1417. let j = i + 1;
  1418. while (j < message.content.length && !/[\s\n]/.test(message.content[j])) {
  1419. j++;
  1420. }
  1421. chunk = message.content.substring(i, j);
  1422. }
  1423. // 其他情况,单个字符显示
  1424. else {
  1425. chunk = char;
  1426. }
  1427. message.displayContent = message.content.substring(0, i) + chunk;
  1428. i += chunk.length;
  1429. // 根据字符类型动态调整延迟
  1430. const delay = chunk.length > 1 ? maxDelay : minDelay;
  1431. typingTimeout.value = setTimeout(typeChar, delay);
  1432. } else {
  1433. if (!isTyping.value) {
  1434. // 如果打字被取消,直接显示完整内容
  1435. message.displayContent = message.content;
  1436. }
  1437. resolve();
  1438. }
  1439. };
  1440. typeChar();
  1441. });
  1442. };
  1443. // 模拟API请求
  1444. const mockApiRequest = async (message) => {
  1445. await new Promise((resolve) => setTimeout(resolve, 1000));
  1446. return `This is a response to: ${message}`;
  1447. };
  1448. // 会话管理
  1449. const addNewConversation = () => {
  1450. conversationCounter.value++;
  1451. const newConversation = {
  1452. id: Date.now(),
  1453. title: `New Conversation ${conversationCounter.value}`,
  1454. messages: [],
  1455. };
  1456. conversations.value.push(newConversation);
  1457. session_id.value = ""; // 重置 session_id
  1458. switchConversation(newConversation.id);
  1459. };
  1460. const switchConversation = (conversationId) => {
  1461. currentConversationId.value = conversationId;
  1462. const conversation = conversations.value.find((c) => c.id === conversationId);
  1463. messages.value = conversation ? conversation.messages : [];
  1464. session_id.value = ""; // 重置 session_id
  1465. };
  1466. const handleFileDrop = async (event) => {
  1467. if (isUploading.value) return;
  1468. const files = Array.from(event.dataTransfer.files);
  1469. if (files.length === 0) return;
  1470. // 添加文件大小检查
  1471. const MAX_VIDEO_SIZE = 10 * 1024 * 1024; // 10MB in bytes
  1472. const invalidFiles = files.filter((file) => {
  1473. if (isVideoFile(file) && file.size > MAX_VIDEO_SIZE) {
  1474. return true;
  1475. }
  1476. return false;
  1477. });
  1478. isUploading.value = true;
  1479. try {
  1480. for (const file of files) {
  1481. const fileWithStatus = {
  1482. name: file.name,
  1483. size: file.size,
  1484. sizeFormatted: formatFileSize(file.size),
  1485. type: file.type,
  1486. isUploading: true,
  1487. };
  1488. attachedFiles.value.push(fileWithStatus);
  1489. try {
  1490. const url = await uploadFile(file);
  1491. // 直接修改数组中的对象
  1492. const index = attachedFiles.value.findIndex(
  1493. (f) => f.name === file.name
  1494. );
  1495. if (index !== -1) {
  1496. // 使用数组替换方式更新,确保响应式更新
  1497. attachedFiles.value.splice(index, 1, {
  1498. ...fileWithStatus,
  1499. url,
  1500. isUploading: false,
  1501. });
  1502. }
  1503. } catch (error) {
  1504. attachedFiles.value = attachedFiles.value.filter(
  1505. (f) => f.name !== file.name
  1506. );
  1507. console.error("单个文件上传失败:", error);
  1508. }
  1509. }
  1510. } catch (error) {
  1511. console.error("文件上传失败:", error);
  1512. } finally {
  1513. isUploading.value = false;
  1514. // 修改文件上传后的处理逻辑
  1515. if (attachedFiles.value.length > 0) {
  1516. // 切换到预览标签页
  1517. activeTab.value = 'preview';
  1518. // 展开右侧菜单
  1519. isRightMenuCollapsed.value = false;
  1520. // 如果在移动端,调整布局
  1521. if (document.querySelector('.chat')) {
  1522. document.querySelector('.chat').style.right = `${rightMenuWidth.value}px`;
  1523. }
  1524. }
  1525. }
  1526. if (files.length > 0 && enableWebSearch.value) {
  1527. enableWebSearch.value = false; // 如果有文件上传,自动关闭互联网搜索
  1528. }
  1529. };
  1530. // 添加音频点击处理方法
  1531. const handleAudioClick = (message) => {
  1532. if(message.audioData){
  1533. if (currentPlayingId.value === message.id) {
  1534. stopAudio();
  1535. } else {
  1536. playAudioMessage(message.audioData, message.id);
  1537. }
  1538. }else{
  1539. // 用户消息的文本转语音处理
  1540. if (currentPlayingId.value === message.id) {
  1541. stopAudio();
  1542. } else {
  1543. playUserMessage(message.content, message.id);
  1544. }
  1545. }
  1546. };
  1547. // 添加播放用户消息的方法
  1548. const playUserMessage = async (text, messageId) => {
  1549. try {
  1550. // 如果当前有其他音频在播放,先停止
  1551. if (currentPlayingId.value && currentPlayingId.value !== messageId) {
  1552. stopAudio();
  1553. }
  1554. // 如果已经在播放这条消息,直接返回(这种情况应该不会发生,因为会被上面的 handleAudioClick 处理)
  1555. if (currentPlayingId.value === messageId) {
  1556. return;
  1557. }
  1558. const response = await axios.post(`${import.meta.env.VITE_BASE_AI_API}/api/voices/play_text`, {
  1559. text: text
  1560. }, {
  1561. headers: {
  1562. 'Authorization': `JWT ${localStorage.getItem('token')}`
  1563. },
  1564. });
  1565. if(response.data.code === 200){
  1566. // 创建音频元素
  1567. const audio = new Audio(`${import.meta.env.VITE_BASE_AI_API}${response.data.audio_url}`);
  1568. // 保存当前播放的音频和ID
  1569. currentAudioSource.value = audio;
  1570. currentPlayingId.value = messageId;
  1571. isPlaying.value = true;
  1572. // 播放结束时清理
  1573. audio.onended = () => {
  1574. currentPlayingId.value = null;
  1575. currentAudioSource.value = null;
  1576. isPlaying.value = false;
  1577. };
  1578. // 播放错误时清理
  1579. audio.onerror = (e) => {
  1580. console.error('播放文本失败:', e);
  1581. message.error('播放失败,请稍后重试');
  1582. currentPlayingId.value = null;
  1583. currentAudioSource.value = null;
  1584. isPlaying.value = false;
  1585. };
  1586. // 开始播放
  1587. await audio.play();
  1588. }
  1589. } catch (error) {
  1590. console.error('播放文本失败:', error);
  1591. message.error('播放失败,请稍后重试');
  1592. currentPlayingId.value = null;
  1593. currentAudioSource.value = null;
  1594. isPlaying.value = false;
  1595. }
  1596. };
  1597. // 修改停止播放方法
  1598. const stopAudio = () => {
  1599. if (audioPlayer.value) {
  1600. audioPlayer.value.pause();
  1601. audioPlayer.value.currentTime = 0;
  1602. audioPlayer.value = null;
  1603. }
  1604. if (currentAudioSource.value) {
  1605. currentAudioSource.value.pause();
  1606. currentAudioSource.value.currentTime = 0;
  1607. currentAudioSource.value = null;
  1608. }
  1609. isPlaying.value = false;
  1610. currentPlayingId.value = null;
  1611. };
  1612. // 在组件卸载时清理
  1613. onUnmounted(() => {
  1614. stopAudio();
  1615. });
  1616. // 添加 renderMarkdown 方法
  1617. const renderMarkdown = (content) => {
  1618. if (!content) return "";
  1619. /* // 处理代码块
  1620. content = content.replace(/```([\s\S]*?)```/g, (match, code) => {
  1621. return `<pre class="code-block"><code>${code}</code></pre>`;
  1622. });
  1623. // 处理行内代码
  1624. content = content.replace(/`([^`]+)`/g, '<code>$1</code>');
  1625. // 处理标题
  1626. content = content.replace(/^(#{1,6})\s+(.*)$/gm, (match, hashes, title) => {
  1627. const level = hashes.length;
  1628. return `<h${level}>${title}</h${level}>`;
  1629. }); */
  1630. return md.render(content);
  1631. };
  1632. // 添加新的响应式变量
  1633. const rightMenuWidth = ref(0); // 默认宽度
  1634. const minWidth = 0; // 最小宽度
  1635. const maxWidth = 800; // 最大宽度
  1636. // 添加拖动相关方法
  1637. const startResize = (e) => {
  1638. e.preventDefault();
  1639. const startX = e.type === 'mousedown' ? e.clientX : e.touches[0].clientX;
  1640. const startWidth = rightMenuWidth.value;
  1641. const handleMove = (e) => {
  1642. if (isRightMenuCollapsed.value) return;
  1643. const currentX = e.type === 'mousemove' ? e.clientX : e.touches[0].clientX;
  1644. const diff = startX - currentX;
  1645. let newWidth = Math.min(Math.max(startWidth + diff, minWidth), maxWidth);
  1646. rightMenuWidth.value = newWidth;
  1647. // 更新聊天区域的右边距和按钮位置
  1648. if (document.querySelector('.chat')) {
  1649. document.querySelector('.chat').style.right = `${newWidth}px`;
  1650. }
  1651. };
  1652. const handleEnd = () => {
  1653. document.removeEventListener('mousemove', handleMove);
  1654. document.removeEventListener('mouseup', handleEnd);
  1655. document.removeEventListener('touchmove', handleMove);
  1656. document.removeEventListener('touchend', handleEnd);
  1657. };
  1658. document.addEventListener('mousemove', handleMove);
  1659. document.addEventListener('mouseup', handleEnd);
  1660. document.addEventListener('touchmove', handleMove);
  1661. document.addEventListener('touchend', handleEnd);
  1662. };
  1663. // 添加搜索开关状态
  1664. const isSearchEnabled = ref(true);
  1665. // 添加切换搜索方法
  1666. const toggleSearch = () => {
  1667. isSearchEnabled.value = !isSearchEnabled.value;
  1668. };
  1669. // 修改 toggleWebSearch 方法
  1670. const toggleWebSearch = () => {
  1671. enableWebSearch.value = !enableWebSearch.value;
  1672. isSearchEnabled.value = enableWebSearch.value;
  1673. // 只有当当前标签是搜索标签时才折叠面板
  1674. if (!enableWebSearch.value && activeTab.value === 'search') {
  1675. isRightMenuCollapsed.value = true;
  1676. currentSearchQuery.value = '';
  1677. if (document.querySelector('.chat')) {
  1678. document.querySelector('.chat').style.right = '0';
  1679. }
  1680. } else if (enableWebSearch.value && inputContent.value.trim()) {
  1681. isRightMenuCollapsed.value = false;
  1682. currentSearchQuery.value = inputContent.value;
  1683. if (document.querySelector('.chat')) {
  1684. document.querySelector('.chat').style.right = `${rightMenuWidth.value}px`;
  1685. }
  1686. }
  1687. };
  1688. // 添加新的响应式变量
  1689. const currentSearchQuery = ref('');
  1690. // 添加输入变化处理方法
  1691. const handleInputChange = () => {
  1692. if (inputContent.value.trim()) {
  1693. currentSearchQuery.value = inputContent.value;
  1694. // 根据当前选中的搜索类型执行相应的搜索
  1695. if (enableWebSearch.value) {
  1696. /* sendMessageS(inputContent.value).catch((error) => {
  1697. console.error("Web search failed:", error);
  1698. }); */
  1699. } else if (enableKnowledgeSearch.value) {
  1700. handleKnowledgeSearch({
  1701. query: inputContent.value,
  1702. page: 1,
  1703. pageSize: 20
  1704. });
  1705. }
  1706. }
  1707. };
  1708. // 添加处理搜索开关的方法
  1709. const handleSearchToggle = (value) => {
  1710. console.log(enableWebSearch);
  1711. enableWebSearch.value = value;
  1712. if (value) {
  1713. // 开启搜索时,确保右侧面板展开
  1714. expandRightMenu();
  1715. activeTab.value = 'search';
  1716. }
  1717. };
  1718. // 添加新的响应式变量
  1719. const activeTab = ref('search'); // 默认显示搜索面板
  1720. // 处理文档选择
  1721. const handleDocumentSelect = (document) => {
  1722. // 处理文档选择逻辑
  1723. console.log('Selected document:', document);
  1724. };
  1725. // 添加收缩状态控制
  1726. const isCollapsed = ref(false);
  1727. // 监听屏幕宽度变化
  1728. const updateCollapsedState = () => {
  1729. isCollapsed.value = window.innerWidth <= 480;
  1730. };
  1731. onUnmounted(() => {
  1732. window.removeEventListener('resize', updateCollapsedState);
  1733. });
  1734. // 添加收缩状态控制
  1735. const isCompact = ref(false);
  1736. // 监听屏幕宽度变化
  1737. const updateCompactState = () => {
  1738. isCompact.value = window.innerWidth <= 768;
  1739. };
  1740. onUnmounted(() => {
  1741. window.removeEventListener('resize', updateCompactState);
  1742. });
  1743. // 添加计算属性
  1744. const isRightMenuVisible = computed(() => {
  1745. // 移除搜索开关对菜单显示的影响
  1746. checkMobile();
  1747. return !isRightMenuCollapsed.value;
  1748. /* return (!isRightMenuCollapsed.value && (
  1749. (enableWebSearch.value && activeTab.value === 'search') ||
  1750. (enableKnowledgeSearch.value && activeTab.value === 'knowledge') ||
  1751. (activeTab.value === 'preview' && attachedFiles.value.length > 0)
  1752. )); */
  1753. });
  1754. /* // 推理模式
  1755. const handleInferenceModel = () => {
  1756. inferenceModel.value = !inferenceModel.value;
  1757. }; */
  1758. watch(inferenceModel, (newValue) => {
  1759. console.log('推理模式状态:', newValue);
  1760. if (newValue) {
  1761. // 打开右侧窗口
  1762. rightMenuWidth.value = 450;
  1763. isRightMenuCollapsed.value = false;
  1764. if (document.querySelector('.chat')) {
  1765. document.querySelector('.chat').style.right = `${rightMenuWidth.value}px`;
  1766. }
  1767. } else {
  1768. // 关闭右侧窗口
  1769. rightMenuWidth.value = 0;
  1770. isRightMenuCollapsed.value = true;
  1771. if (document.querySelector('.chat')) {
  1772. document.querySelector('.chat').style.right = `${rightMenuWidth.value}px`;
  1773. }
  1774. }
  1775. });
  1776. // 修改知识库搜索开关处理方法
  1777. const handleKnowledgeToggle = (event) => {
  1778. if (event.target.checked) {
  1779. rightMenuWidth.value = 450;
  1780. if (document.querySelector('.chat')) {
  1781. document.querySelector('.chat').style.right = `${rightMenuWidth.value}px`;
  1782. }
  1783. // 开启知识库搜索时关闭互联网搜索
  1784. enableWebSearch.value = false;
  1785. // 切换到知识库标签页
  1786. activeTab.value = 'knowledge';
  1787. // 如果有输入内容,执行知识库搜索
  1788. if (inputContent.value.trim()) {
  1789. handleKnowledgeSearch({
  1790. query: inputContent.value,
  1791. page: 1,
  1792. pageSize: 20
  1793. });
  1794. }
  1795. }else{
  1796. rightMenuWidth.value = 0;
  1797. if (document.querySelector('.chat')) {
  1798. document.querySelector('.chat').style.right = `${rightMenuWidth.value}px`;
  1799. }
  1800. }
  1801. };
  1802. // 修改知识库搜索方法
  1803. const handleKnowledgeSearch = async (params) => {
  1804. if (!params.query.trim() || !enableKnowledgeSearch.value) return;
  1805. isKnowledgeLoading.value = true;
  1806. try {
  1807. const response = await axios.post(
  1808. `${import.meta.env.VITE_BASE_AI_API}/chataids/mysql_fuzzy_search/`,
  1809. {
  1810. // collection_names: ["25"], // 固定使用 "25" 作为 collection name
  1811. keywords: params.query, // 使用查询关键词
  1812. documents: [], // 保持空数组
  1813. document_ids: [], // 保持空数组
  1814. limit: 5 // 固定返回5条结果
  1815. },
  1816. {
  1817. headers: {
  1818. 'Authorization': `JWT ${localStorage.getItem('token')}`
  1819. }
  1820. }
  1821. );
  1822. console.log(response);
  1823. if (response.data && response.data.status === 200) {
  1824. knowledgeResults.value = response.data.data.data || [];
  1825. knowledgeTotal.value = response.data.data.data.total || 0;
  1826. }
  1827. } catch (error) {
  1828. console.error('Knowledge search failed:', error);
  1829. knowledgeResults.value = [];
  1830. knowledgeTotal.value = 0;
  1831. } finally {
  1832. isKnowledgeLoading.value = false;
  1833. }
  1834. }
  1835. // 修改处理互联网搜索开关的方法
  1836. const handleWebSearchToggle = (event) => {
  1837. if (event.target.checked) {
  1838. rightMenuWidth.value = 450;
  1839. if (document.querySelector('.chat')) {
  1840. document.querySelector('.chat').style.right = `${rightMenuWidth.value}px`;
  1841. }
  1842. // 开启互联网搜索时关闭知识库搜索
  1843. enableKnowledgeSearch.value = false;
  1844. // 切换到搜索标签页
  1845. activeTab.value = 'search';
  1846. // 如果有输入内容,执行互联网搜索
  1847. if (inputContent.value.trim()) {
  1848. currentSearchQuery.value = inputContent.value;
  1849. /* sendMessageS(inputContent.value).catch((error) => {
  1850. console.error("Web search failed:", error);
  1851. }); */
  1852. }
  1853. }else{
  1854. rightMenuWidth.value = 0;
  1855. if (document.querySelector('.chat')) {
  1856. document.querySelector('.chat').style.right = `${rightMenuWidth.value}px`;
  1857. }
  1858. }
  1859. };
  1860. // 添加新的响应式变量
  1861. const API_BASE_URL = import.meta.env.VITE_BASE_AI_API;
  1862. const models = ref([]);
  1863. const selectedModel = ref(null);
  1864. const isLoadingModels = ref(false);
  1865. // 添加计算属性获取选中的模型信息
  1866. const selectedModelInfo = computed(() => {
  1867. if (!selectedModel.value || !models.value.length) return { type: '', name: '' };
  1868. const model = models.value.find(m => m.id === selectedModel.value);
  1869. return {
  1870. type: model?.type || '',
  1871. name: model?.name || ''
  1872. };
  1873. });
  1874. // 添加获取模型列表的方法
  1875. const fetchModels = async () => {
  1876. isLoadingModels.value = true;
  1877. try {
  1878. const response = await fetch(`${API_BASE_URL}/api/models?merchant_id=${JSON.parse(localStorage.getItem('userInfo')).merchant.merchant_id}`);
  1879. const data = await response.json();
  1880. console.log(data);
  1881. if (data.code === 2000) {
  1882. // 确保每个模型对象都有必要的字段
  1883. models.value = (data.data.models || []).map(model => ({
  1884. id: model.id,
  1885. name: model.name,
  1886. type: model.type || model.model_type || 'default' // 确保有type字段
  1887. }));
  1888. if (models.value.length > 0) {
  1889. selectedModel.value = models.value[0].id; // 默认选择第一个模型
  1890. }
  1891. }
  1892. } catch (error) {
  1893. console.error('Failed to fetch models:', error);
  1894. } finally {
  1895. isLoadingModels.value = false;
  1896. }
  1897. };
  1898. // 添加模型选择改变的处理方法
  1899. const handleModelChange = (modelId) => {
  1900. selectedModel.value = modelId;
  1901. };
  1902. // 用户信息
  1903. const userInfo = ref({
  1904. name: '',
  1905. role: ''
  1906. });
  1907. const getUserInfo = () => {
  1908. const storedUserInfo = localStorage.getItem('userInfo');
  1909. if (storedUserInfo) {
  1910. userInfo.value = JSON.parse(storedUserInfo);
  1911. }
  1912. }
  1913. // 在组件挂载时获取模型列表
  1914. onMounted(() => {
  1915. updateCompactState();
  1916. window.addEventListener('resize', updateCompactState);
  1917. updateCollapsedState();
  1918. window.addEventListener('resize', updateCollapsedState);
  1919. checkMobile();
  1920. window.addEventListener('resize', checkMobile);
  1921. fetchModels();
  1922. fetchDocumentSummary();
  1923. const storedUserInfo = localStorage.getItem('userInfo');
  1924. if (storedUserInfo) {
  1925. try {
  1926. userInfo.value = JSON.parse(storedUserInfo);
  1927. } catch (error) {
  1928. console.error('解析用户信息失败:', error);
  1929. }
  1930. }
  1931. });
  1932. const showPasswordDialog = ref(false)
  1933. const handleModifyPassword = () => {
  1934. showPasswordDialog.value = true
  1935. showDropdown.value = false // 关闭下拉菜单
  1936. }
  1937. const handleLogout = async () => {
  1938. try {
  1939. // 调用退出登录接口
  1940. await axios.post(`${import.meta.env.VITE_BASE_AI_API}/user/logout`, {}, {
  1941. headers: {
  1942. 'Authorization': `JWT ${localStorage.getItem('token')}`
  1943. }
  1944. });
  1945. // 清除本地存储的信息
  1946. localStorage.removeItem('token');
  1947. localStorage.removeItem('userInfo');
  1948. localStorage.removeItem('permissions');
  1949. localStorage.removeItem('username');
  1950. localStorage.removeItem('mobile');
  1951. // 清除 Vuex store 中的用户信息
  1952. store.commit('app/setUser', {
  1953. user: {},
  1954. permission: [],
  1955. auth: {}
  1956. });
  1957. // 关闭下拉菜单
  1958. showDropdown.value = false;
  1959. // 跳转到登录页面
  1960. router.push('/login');
  1961. } catch (error) {
  1962. console.error('退出登录失败:', error);
  1963. // 即使接口调用失败,也清除本地信息并跳转
  1964. localStorage.clear();
  1965. router.push('/login');
  1966. }
  1967. };
  1968. const handleCreateInvitation = async () => {
  1969. try {
  1970. const response = await axios.post(`${import.meta.env.VITE_BASE_AI_API}/user/create_invitation`, {}, {
  1971. headers: {
  1972. 'Authorization': `JWT ${localStorage.getItem('token')}`
  1973. }
  1974. });
  1975. console.log(response);
  1976. if (response.data.code === 200) {
  1977. message.success('创建邀请成功');
  1978. // 从当前窗口location获取基础URL
  1979. const baseUrl = `${window.location.protocol}//${window.location.host}`;
  1980. // 跳转到完整URL
  1981. window.location.href = `${baseUrl}${response.data.data.invitation_link}`;
  1982. } else {
  1983. message.error(response.data.message || '创建邀请失败');
  1984. }
  1985. } catch (error) {
  1986. console.error('创建邀请失败:', error);
  1987. message.error(error.response?.data?.message || '创建邀请失败,请稍后重试');
  1988. }
  1989. showDropdown.value = false; // 关闭下拉菜单
  1990. };
  1991. /* return {
  1992. loginForm,
  1993. formData,
  1994. rules,
  1995. loading,
  1996. rememberMe,
  1997. loginType,
  1998. countdown,
  1999. handleLogin,
  2000. sendVerificationCode,
  2001. switchToMobileLogin,
  2002. switchToAccountLogin,
  2003. switchToWechatLogin,
  2004. wechatLoginModal,
  2005. wechatQrCode,
  2006. getWechatQrCode,
  2007. handleWechatLoginCancel,
  2008. handleCreateInvitation
  2009. }; */
  2010. // These variables are already declared above
  2011. /* const currentAudioSource = ref(null);
  2012. const currentPlayingId = ref(null); */
  2013. // 添加播放音频消息的方法
  2014. const playAudioMessage = async (audioData, messageId) => {
  2015. try {
  2016. // 如果当前有其他音频在播放,先停止
  2017. if (currentPlayingId.value && currentPlayingId.value !== messageId) {
  2018. stopAudio();
  2019. }
  2020. // 如果已经在播放这条消息,直接返回
  2021. if (currentPlayingId.value === messageId) {
  2022. return;
  2023. }
  2024. // 创建新的音频元素
  2025. const audio = new Audio(audioData);
  2026. // 保存当前播放的音频和ID
  2027. currentAudioSource.value = audio;
  2028. currentPlayingId.value = messageId;
  2029. isPlaying.value = true;
  2030. // 播放结束时清理
  2031. audio.onended = () => {
  2032. currentPlayingId.value = null;
  2033. currentAudioSource.value = null;
  2034. isPlaying.value = false;
  2035. };
  2036. // 播放错误时清理
  2037. audio.onerror = (e) => {
  2038. console.error('播放音频失败:', e);
  2039. message.error('播放失败,请稍后重试');
  2040. currentPlayingId.value = null;
  2041. currentAudioSource.value = null;
  2042. isPlaying.value = false;
  2043. };
  2044. // 开始播放
  2045. await audio.play();
  2046. } catch (error) {
  2047. console.error('播放音频失败:', error);
  2048. message.error('播放失败,请稍后重试');
  2049. currentPlayingId.value = null;
  2050. currentAudioSource.value = null;
  2051. isPlaying.value = false;
  2052. }
  2053. };
  2054. // 主题状态
  2055. const isDarkTheme = ref(false);
  2056. // 应用主题
  2057. const applyTheme = (isDark) => {
  2058. document.documentElement.classList.toggle('dark-theme', isDark);
  2059. };
  2060. // 切换主题
  2061. const toggleTheme = () => {
  2062. isDarkTheme.value = !isDarkTheme.value;
  2063. localStorage.setItem('theme', isDarkTheme.value ? 'dark' : 'light');
  2064. applyTheme(isDarkTheme.value);
  2065. };
  2066. // 初始化主题
  2067. onMounted(() => {
  2068. if (document.querySelector('.chat')) {
  2069. document.querySelector('.chat').style.right = `0px`;
  2070. }
  2071. checkMobile();
  2072. // 优先使用存储的主题设置
  2073. const savedTheme = localStorage.getItem('theme');
  2074. if (savedTheme) {
  2075. isDarkTheme.value = savedTheme === 'dark';
  2076. } else {
  2077. // 如果没有存储的主题,则使用系统主题
  2078. isDarkTheme.value = window.matchMedia('(prefers-color-scheme: dark)').matches;
  2079. }
  2080. applyTheme(isDarkTheme.value);
  2081. // 监听系统主题变化
  2082. const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
  2083. mediaQuery.addEventListener('change', (e) => {
  2084. if (!localStorage.getItem('theme')) {
  2085. isDarkTheme.value = e.matches;
  2086. applyTheme(e.matches);
  2087. }
  2088. });
  2089. });
  2090. /* return {
  2091. isDarkTheme,
  2092. toggleTheme,
  2093. // ... existing return values ...
  2094. }; */
  2095. const excelOptions = ref({
  2096. xls: false, // 预览xlsx文件设为false;预览xls文件设为true
  2097. minColLength: 15, // excel最少渲染列数
  2098. minRowLength: 50, // excel最少渲染行数,增加以确保显示更多内容
  2099. widthOffset: 50, // 增加列宽偏移量,使内容显示更完整
  2100. heightOffset: 30, // 增加行高偏移量
  2101. forceRender: true, // 强制渲染
  2102. maxSheetLength: 10, // 最多渲染工作表数量
  2103. showGridLines: true, // 显示网格线
  2104. defaultFontSize: 12, // 默认字体大小
  2105. defaultRowHeight: 24, // 默认行高
  2106. autoFitColumn: true, // 自动适应列宽
  2107. overflowColumns: true, // 允许列内容溢出
  2108. showToolbar: true, // 显示工具栏
  2109. });
  2110. // 添加Excel渲染完成的处理函数
  2111. const handleRendered = () => {
  2112. console.log('Excel文件渲染完成');
  2113. nextTick(() => {
  2114. // 手动触发一次重新计算大小
  2115. window.dispatchEvent(new Event('resize'));
  2116. });
  2117. };
  2118. // 添加Excel错误处理函数的详细信息
  2119. const handleExcelError = (error) => {
  2120. console.error('Excel文件渲染失败:', error);
  2121. message.error('Excel预览失败:' + (error.message || '请尝试下载后查看'));
  2122. };
  2123. const isWordFile = (file) => {
  2124. if (!file) return false;
  2125. const wordTypes = ['doc', 'docx'];
  2126. const ext = file.name?.split('.')?.pop()?.toLowerCase() || '';
  2127. const isWord = wordTypes.includes(ext);
  2128. if (isWord && file.type) {
  2129. return file.type === 'application/msword' ||
  2130. file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
  2131. }
  2132. return isWord;
  2133. };
  2134. const isExcelFile = (file) => {
  2135. if (!file) return false;
  2136. const excelTypes = ['xls', 'xlsx'];
  2137. const ext = file.name?.split('.')?.pop()?.toLowerCase() || '';
  2138. return excelTypes.includes(ext);
  2139. };
  2140. const isPdfFile = (file) => {
  2141. if (!file) return false;
  2142. return file.name?.toLowerCase()?.endsWith('.pdf') || false;
  2143. };
  2144. const currentDoc = ref(null);
  2145. // 监听文档列表变化
  2146. watch(() => attachedFiles.value, (newFiles) => {
  2147. if (newFiles && newFiles.length > 0) {
  2148. currentDoc.value = newFiles[newFiles.length - 1]; // 预览最新上传的文档
  2149. } else {
  2150. currentDoc.value = null;
  2151. }
  2152. }, {
  2153. immediate: true,
  2154. deep: true
  2155. });
  2156. const handleDownload = (doc) => {
  2157. if (doc.url) {
  2158. const link = document.createElement('a');
  2159. link.href = doc.url;
  2160. link.download = doc.name;
  2161. document.body.appendChild(link);
  2162. link.click();
  2163. document.body.removeChild(link);
  2164. }
  2165. };
  2166. const closePreview = () => {
  2167. currentDoc.value = null;
  2168. };
  2169. // 添加文档点击事件处理
  2170. const handleDocClick = (doc) => {
  2171. currentDoc.value = doc;
  2172. };
  2173. /*
  2174. */
  2175. // 添加流式思考步骤
  2176. const currentThinkingSteps = ref([]);
  2177. // 添加流式思考步骤
  2178. const addThinkingStep = (step) => {
  2179. currentThinkingSteps.value.push({
  2180. type: step.type || 'default',
  2181. content: step.content,
  2182. timestamp: Date.now()
  2183. });
  2184. };
  2185. // 清除当前思考步骤
  2186. const clearThinkingSteps = () => {
  2187. currentThinkingSteps.value = [];
  2188. };
  2189. // 在消息发送完成后将思考步骤添加到消息中
  2190. const finalizeThinkingSteps = (messageId) => {
  2191. const message = messages.value.find(m => m.id === messageId);
  2192. if (message) {
  2193. message.thinkingSteps = [...currentThinkingSteps.value];
  2194. clearThinkingSteps();
  2195. }
  2196. };
  2197. </script>
  2198. <style scoped>
  2199. .layout {
  2200. display: flex;
  2201. width: 100%;
  2202. height: 100vh;
  2203. overflow: hidden;
  2204. position: relative;
  2205. }
  2206. .header {
  2207. position: fixed;
  2208. top: 0;
  2209. left: 0;
  2210. width: 100%;
  2211. height: 64px;
  2212. background: #f7f7f8;
  2213. border-bottom: 1px solid rgba(0, 0, 0, 0.06);
  2214. z-index: 999;
  2215. }
  2216. .user-profile-bar {
  2217. margin: 0 auto;
  2218. position: relative;
  2219. display: flex;
  2220. align-items: center;
  2221. justify-content: space-between;
  2222. cursor: pointer;
  2223. }
  2224. .user-info-bar{
  2225. display: flex;
  2226. align-items: center;
  2227. justify-content: flex-end;
  2228. width: 100%;
  2229. height: 100%;
  2230. background-color: #f7f7f8;
  2231. }
  2232. .avatar {
  2233. width: 40px;
  2234. height: 40px;
  2235. border-radius: 50%;
  2236. overflow: hidden;
  2237. margin-right: 12px;
  2238. }
  2239. .avatar img {
  2240. width: 100%;
  2241. height: 100%;
  2242. object-fit: cover;
  2243. }
  2244. .user-info {
  2245. display: flex;
  2246. flex-direction: column;
  2247. margin-right: 15px;
  2248. }
  2249. .user-name {
  2250. font-size: 14px;
  2251. font-weight: 500;
  2252. color: var(--primary-text);
  2253. }
  2254. .dropdown-menu {
  2255. position: absolute;
  2256. top: 100%;
  2257. right: 0;
  2258. background: white;
  2259. border-radius: 8px;
  2260. box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  2261. min-width: 160px;
  2262. z-index: 1000;
  2263. margin-top: 4px;
  2264. }
  2265. .dropdown-item {
  2266. padding: 12px 16px;
  2267. display: flex;
  2268. align-items: center;
  2269. color: var(--primary-text);
  2270. transition: background-color 0.2s;
  2271. }
  2272. .dropdown-item:hover {
  2273. background-color: #f5f5f5;
  2274. }
  2275. .dropdown-item i {
  2276. margin-right: 8px;
  2277. font-size: 14px;
  2278. color: #666;
  2279. }
  2280. .menu {
  2281. margin-top: 64px;
  2282. background: #f7f7f8;
  2283. width: 260px;
  2284. height: 93%;
  2285. display: flex;
  2286. flex-direction: column;
  2287. border-right: 1px solid rgba(0, 0, 0, 0.06);
  2288. position: absolute;
  2289. left: 0;
  2290. /* transform: translateX(0); */
  2291. transition: all 0.3s ease;
  2292. z-index: 2;
  2293. }
  2294. .menu.collapsed {
  2295. display: none;
  2296. transform: translateX(-260px);
  2297. width: 0;
  2298. }
  2299. .conversations {
  2300. padding: 0 12px;
  2301. flex: 1;
  2302. overflow-y: auto;
  2303. }
  2304. .chat {
  2305. flex: 1;
  2306. padding: 24px;
  2307. display: flex;
  2308. flex-direction: column;
  2309. gap: 24px;
  2310. overflow-y: auto;
  2311. overflow-x: hidden;
  2312. margin-left: 0;
  2313. margin-right: 0;
  2314. transition: all 0.3s ease;
  2315. position: absolute;
  2316. left: 0;
  2317. right: 0;
  2318. top: 0;
  2319. bottom: 0;
  2320. justify-content: space-between;
  2321. background-color: var(--primary-bg);
  2322. }
  2323. .menu:not(.collapsed) + .chat {
  2324. left: 260px;
  2325. }
  2326. .chat {
  2327. right: 0;
  2328. transition: all 0.3s ease;
  2329. }
  2330. .layout:has(.right_menu:not(.collapsed)) .chat {
  2331. right: 450px;
  2332. }
  2333. .right_menu:not(.collapsed) ~ .chat {
  2334. right: 450px;
  2335. }
  2336. .messages {
  2337. flex: 1;
  2338. overflow-y: auto;
  2339. padding-right: 16px;
  2340. }
  2341. .sender {
  2342. box-shadow: none;
  2343. border: 1px solid #e5e6eb;
  2344. border-radius: 8px;
  2345. padding: 8px;
  2346. }
  2347. .logo {
  2348. height: 64px;
  2349. display: flex;
  2350. align-items: center;
  2351. padding: 0 20px;
  2352. }
  2353. .logo-img {
  2354. /* width: 24px; */
  2355. height: 60px;
  2356. }
  2357. .logo span {
  2358. margin-left: 12px;
  2359. font-size: 16px;
  2360. font-weight: 600;
  2361. color: rgba(0, 0, 0, 0.88);
  2362. }
  2363. .new-conversation-btn {
  2364. margin: 0 12px 12px;
  2365. height: 40px;
  2366. background: rgba(235, 245, 255, 0.7);
  2367. border: none;
  2368. border-radius: 8px;
  2369. color: #1677ff;
  2370. font-size: 14px;
  2371. display: flex;
  2372. align-items: center;
  2373. justify-content: center;
  2374. gap: 8px;
  2375. cursor: pointer;
  2376. transition: background-color 0.2s;
  2377. }
  2378. .new-conversation-btn:hover {
  2379. background: rgba(235, 245, 255, 0.9);
  2380. }
  2381. .conversation-list {
  2382. background: var(--secondary-bg);
  2383. flex: 1;
  2384. color: var(--primary-text);
  2385. padding: 0 8px;
  2386. overflow-y: auto;
  2387. }
  2388. .conversation-item {
  2389. padding: 12px;
  2390. margin: 4px 0;
  2391. border-radius: 8px;
  2392. color: var(--primary-text);
  2393. font-size: 14px;
  2394. cursor: pointer;
  2395. transition: background-color 0.2s;
  2396. }
  2397. .conversation-item:hover {
  2398. background: rgba(0, 0, 0, 0.04);
  2399. }
  2400. .conversation-item.active {
  2401. background: var(--secondary-bg);
  2402. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
  2403. }
  2404. .conversation-list::-webkit-scrollbar {
  2405. width: 4px;
  2406. }
  2407. .conversation-list::-webkit-scrollbar-thumb {
  2408. background: rgba(0, 0, 0, 0.15);
  2409. border-radius: 2px;
  2410. }
  2411. .conversation-list::-webkit-scrollbar-track {
  2412. background: transparent;
  2413. }
  2414. :deep(.welcome-message) {
  2415. background: #fff;
  2416. border-radius: 8px;
  2417. padding: 16px;
  2418. display: flex;
  2419. align-items: flex-start;
  2420. gap: 16px;
  2421. }
  2422. :deep(.welcome-message .title) {
  2423. font-size: 16px;
  2424. font-weight: 500;
  2425. color: rgba(0, 0, 0, 0.88);
  2426. margin-bottom: 4px;
  2427. }
  2428. :deep(.welcome-message .description) {
  2429. color: rgba(0, 0, 0, 0.45);
  2430. font-size: 14px;
  2431. }
  2432. :deep(.prompt-card) {
  2433. background: #f7f8fa;
  2434. border-radius: 8px;
  2435. padding: 16px;
  2436. margin-bottom: 12px;
  2437. }
  2438. :deep(.prompt-card-title) {
  2439. font-size: 16px;
  2440. font-weight: 500;
  2441. margin-bottom: 8px;
  2442. display: flex;
  2443. align-items: center;
  2444. gap: 8px;
  2445. }
  2446. :deep(.prompt-option) {
  2447. background: #f5f5f5;
  2448. padding: 8px 12px;
  2449. border-radius: 4px;
  2450. margin-top: 8px;
  2451. cursor: pointer;
  2452. transition: background 0.3s;
  2453. }
  2454. :deep(.prompt-option:hover) {
  2455. background: #e8e8e8;
  2456. }
  2457. .input-container {
  2458. width: 61%;
  2459. margin: auto auto 0;
  2460. display: flex;
  2461. flex-direction: column;
  2462. gap: 8px;
  2463. }
  2464. .tabs-wrapper {
  2465. display: flex;
  2466. gap: 8px;
  2467. margin-bottom: 4px;
  2468. }
  2469. .tab-item {
  2470. display: flex;
  2471. align-items: center;
  2472. gap: 6px;
  2473. padding: 4px 12px;
  2474. border-radius: 4px;
  2475. cursor: pointer;
  2476. font-size: 14px;
  2477. color: rgba(0, 0, 0, 0.88);
  2478. background: transparent;
  2479. transition: background-color 0.2s;
  2480. }
  2481. .tab-item:hover {
  2482. background: rgba(0, 0, 0, 0.04);
  2483. }
  2484. .tab-item.active {
  2485. background: #f5f5f5;
  2486. }
  2487. .tab-item .anticon {
  2488. font-size: 14px;
  2489. }
  2490. .input-wrapper {
  2491. display: flex;
  2492. align-items: center;
  2493. padding: 8px 12px;
  2494. border: 1px solid var(--border-color);
  2495. border-radius: 8px;
  2496. background: var(--primary-bg);
  2497. color: var(--primary-text);
  2498. }
  2499. .message-input {
  2500. flex: 1;
  2501. border: none;
  2502. outline: none;
  2503. padding: 8px;
  2504. font-size: 14px;
  2505. background: transparent;
  2506. color: var(--primary-text);
  2507. }
  2508. .message-input::placeholder {
  2509. color: var(--primary-text);
  2510. }
  2511. .attachment-btn,
  2512. .send-btn {
  2513. display: flex;
  2514. align-items: center;
  2515. justify-content: center;
  2516. width: 32px;
  2517. height: 32px;
  2518. border: none;
  2519. background: transparent;
  2520. cursor: pointer;
  2521. color: rgba(0, 0, 0, 0.45);
  2522. border-radius: 4px;
  2523. padding: 0;
  2524. }
  2525. .attachment-btn:hover,
  2526. .send-btn:hover {
  2527. background: rgba(0, 0, 0, 0.04);
  2528. color: var(--primary-text);
  2529. }
  2530. .attachments-panel {
  2531. background: var(--primary-bg);
  2532. border-radius: 12px;
  2533. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
  2534. overflow: hidden;
  2535. }
  2536. .panel-content {
  2537. background: var(--primary-bg);
  2538. }
  2539. .attachments-header {
  2540. padding: 12px 16px;
  2541. border-bottom: 1px solid #f0f0f0;
  2542. display: flex;
  2543. justify-content: space-between;
  2544. align-items: center;
  2545. }
  2546. .attachments-header span {
  2547. font-size: 14px;
  2548. font-weight: 500;
  2549. color: var(--primary-text);
  2550. }
  2551. .close-btn {
  2552. border: none;
  2553. background: transparent;
  2554. cursor: pointer;
  2555. padding: 4px;
  2556. display: flex;
  2557. align-items: center;
  2558. justify-content: center;
  2559. color: rgba(0, 0, 0, 0.45);
  2560. }
  2561. .upload-area {
  2562. padding: 24px;
  2563. border-radius: 8px;
  2564. text-align: center;
  2565. cursor: pointer;
  2566. color: var(--primary-text);
  2567. transition: all 0.3s;
  2568. }
  2569. .upload-area.has-files {
  2570. padding: 12px;
  2571. background: var(--primary-bg);
  2572. }
  2573. .upload-icon {
  2574. width: 32px;
  2575. height: 32px;
  2576. margin-bottom: 8px;
  2577. }
  2578. .upload-text {
  2579. font-size: 14px;
  2580. color: var(--primary-text);
  2581. margin-bottom: 4px;
  2582. }
  2583. .upload-hint {
  2584. font-size: 12px;
  2585. color: var(--primary-text);
  2586. }
  2587. .file-list {
  2588. display: flex;
  2589. flex-wrap: wrap;
  2590. gap: 8px;
  2591. padding: 10px;
  2592. }
  2593. .file-item {
  2594. position: relative;
  2595. width: 260px;
  2596. padding: 8px 12px;
  2597. margin: 4px 0;
  2598. background: var(--secondary-bg);
  2599. border-radius: 8px;
  2600. }
  2601. .file-item.is-image {
  2602. width: 68px;
  2603. padding: 0;
  2604. margin: 4px 0;
  2605. }
  2606. .file-wrapper {
  2607. position: relative;
  2608. width: 100%;
  2609. height: 100%;
  2610. }
  2611. .file-icon {
  2612. width: 68px;
  2613. height: 68px;
  2614. object-fit: cover;
  2615. border-radius: 4px;
  2616. }
  2617. .file-wrapper video.file-icon {
  2618. background: #000;
  2619. }
  2620. .remove-btn {
  2621. position: absolute;
  2622. top: -8px;
  2623. right: -8px;
  2624. width: 16px;
  2625. height: 16px;
  2626. padding: 0;
  2627. border: none;
  2628. background: #545454;
  2629. border-radius: 50%;
  2630. cursor: pointer;
  2631. display: flex;
  2632. align-items: center;
  2633. justify-content: center;
  2634. font-size: 14px;
  2635. line-height: 1;
  2636. color: #fff;
  2637. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  2638. }
  2639. .remove-btn:hover {
  2640. opacity: 0.8;
  2641. }
  2642. .file-info {
  2643. margin-top: 8px;
  2644. }
  2645. .file-name {
  2646. display: block;
  2647. max-width: 150px;
  2648. color: var(--primary-text);
  2649. font-size: 14px;
  2650. white-space: nowrap;
  2651. overflow: hidden;
  2652. text-overflow: ellipsis;
  2653. }
  2654. .file-info .file-name {
  2655. max-width: 150px;
  2656. color: var(--primary-text);
  2657. }
  2658. .file-name:hover {
  2659. cursor: pointer;
  2660. }
  2661. .file-size {
  2662. display: block;
  2663. color: var(--primary-text);
  2664. font-size: 12px;
  2665. margin-top: 4px;
  2666. }
  2667. .add-more-btn {
  2668. display: flex;
  2669. align-items: center;
  2670. justify-content: center;
  2671. width: 68px;
  2672. height: 68px;
  2673. border: 1px dashed #d9d9d9;
  2674. border-radius: 6px;
  2675. background: transparent;
  2676. cursor: pointer;
  2677. transition: all 0.3s;
  2678. margin: 4px 0;
  2679. }
  2680. .add-more-btn:hover {
  2681. border-color: #1677ff;
  2682. background: #f0f7ff;
  2683. }
  2684. /* 欢迎区域样式 */
  2685. .welcome-section {
  2686. max-width: 800px;
  2687. margin: 0 auto;
  2688. width: 50%;
  2689. }
  2690. .welcome-header {
  2691. display: flex;
  2692. align-items: flex-start;
  2693. gap: 16px;
  2694. margin-bottom: 24px;
  2695. }
  2696. .avatar {
  2697. width: 40px;
  2698. height: 40px;
  2699. border-radius: 50%;
  2700. }
  2701. .avatar img{
  2702. width: 100%;
  2703. height: 100%;
  2704. border-radius: 50%;
  2705. }
  2706. .welcome-info {
  2707. flex: 1;
  2708. }
  2709. .welcome-info h2 {
  2710. font-size: 20px;
  2711. font-weight: 500;
  2712. color: rgba(0, 0, 0, 0.88);
  2713. margin: 0 0 4px 0;
  2714. }
  2715. .welcome-info p {
  2716. font-size: 14px;
  2717. color: rgba(0, 0, 0, 0.45);
  2718. margin: 0;
  2719. }
  2720. .action-buttons {
  2721. display: flex;
  2722. gap: 8px;
  2723. }
  2724. .icon-btn {
  2725. padding: 8px;
  2726. border: none;
  2727. background: transparent;
  2728. cursor: pointer;
  2729. color: rgba(0, 0, 0, 0.45);
  2730. }
  2731. /* 建议区域样式 */
  2732. .suggestion-section {
  2733. margin-top: 70px;
  2734. }
  2735. .suggestion-section h3 {
  2736. font-size: 14px;
  2737. color: rgba(0, 0, 0, 0.45);
  2738. margin-bottom: 16px;
  2739. }
  2740. .cards-container {
  2741. display: flex;
  2742. gap: 16px;
  2743. }
  2744. .suggestion-card {
  2745. flex: 1;
  2746. border-radius: 8px;
  2747. padding: 16px;
  2748. background: #ffffff; /* 替换 var(--ant-color-bg-container) */
  2749. border-radius: 8px; /* 替换 var(--ant-border-radius-lg) */
  2750. transition: border 0.3s, background 0.3s; /* 替换 var(--ant-motion-duration-slow) */
  2751. border: 1px solid #f0f0f0; /* 替换合的 border 属性 */
  2752. }
  2753. .loading-btn {
  2754. display: flex;
  2755. align-items: center;
  2756. justify-content: center;
  2757. width: 32px;
  2758. height: 32px;
  2759. background: #4d7cff;
  2760. border-radius: 50%;
  2761. position: relative;
  2762. }
  2763. .loading-square {
  2764. width: 14px;
  2765. height: 14px;
  2766. background: #fff;
  2767. border-radius: 3px;
  2768. display: block;
  2769. animation: loading-square-blink 1s infinite;
  2770. }
  2771. @keyframes loading-square-blink {
  2772. 0%, 100% { opacity: 1; }
  2773. 50% { opacity: 0.5; }
  2774. }
  2775. :root {
  2776. --ant-color-bg-container: #ffffff;
  2777. --ant-border-radius-lg: 8px;
  2778. --ant-motion-duration-slow: 0.3s;
  2779. --ant-line-width: 1px;
  2780. --ant-line-type: solid;
  2781. --ant-color-border-secondary: #f0f0f0;
  2782. }
  2783. .card-header {
  2784. display: flex;
  2785. align-items: center;
  2786. gap: 8px;
  2787. margin-bottom: 8px;
  2788. }
  2789. .card-header h4 {
  2790. font-size: 16px;
  2791. font-weight: 500;
  2792. color: rgba(0, 0, 0, 0.88);
  2793. margin: 0;
  2794. }
  2795. .icon.red {
  2796. color: #ff4d4f;
  2797. }
  2798. .icon.blue {
  2799. color: #1890ff;
  2800. }
  2801. .card-subtitle {
  2802. font-size: 14px;
  2803. color: rgba(0, 0, 0, 0.45);
  2804. margin-bottom: 16px;
  2805. }
  2806. .suggestion-items {
  2807. display: flex;
  2808. flex-direction: column;
  2809. gap: 8px;
  2810. }
  2811. .suggestion-item {
  2812. padding: 8px 12px;
  2813. background: #f5f5f5;
  2814. border-radius: 4px;
  2815. font-size: 12px;
  2816. color: rgba(0, 0, 0, 0.88);
  2817. display: flex;
  2818. align-items: center;
  2819. gap: 8px;
  2820. cursor: pointer;
  2821. transition: opacity 0.3s ease;
  2822. }
  2823. .suggestion-item:hover {
  2824. background: #e8e8e8;
  2825. }
  2826. .tab-icon {
  2827. width: 16px;
  2828. }
  2829. .attached-files {
  2830. padding: 16px;
  2831. }
  2832. .file-item {
  2833. display: flex;
  2834. align-items: center;
  2835. gap: 8px;
  2836. padding: 8px;
  2837. border-radius: 4px;
  2838. }
  2839. .file-item:hover {
  2840. background: var(--secondary-bg);
  2841. }
  2842. .remove-btn {
  2843. margin-left: auto;
  2844. padding: 4px;
  2845. border: none;
  2846. background: transparent;
  2847. cursor: pointer;
  2848. color: var(--primary-text);
  2849. }
  2850. .remove-btn:hover {
  2851. color: var(--primary-text);
  2852. }
  2853. .send-btn:disabled {
  2854. opacity: 0.5;
  2855. cursor: not-allowed;
  2856. }
  2857. .upload-area {
  2858. /* border: 2px dashed #d9d9d9; */
  2859. border-radius: 8px;
  2860. padding: 20px;
  2861. text-align: center;
  2862. cursor: pointer;
  2863. transition: border-color 0.3s;
  2864. }
  2865. /* .upload-area:hover,
  2866. .upload-area.drag-over {
  2867. border-color: #1677ff;
  2868. } */
  2869. .file-item {
  2870. display: flex;
  2871. align-items: center;
  2872. width: 236px;
  2873. height: 68px;
  2874. padding: 0;
  2875. margin: 4px 0;
  2876. background: var(--secondary-bg);
  2877. color: var(--primary-text);
  2878. border-radius: 4px;
  2879. }
  2880. .file-item .remove-btn {
  2881. margin-left: auto;
  2882. padding: 4px 8px;
  2883. background: transparent;
  2884. border: none;
  2885. cursor: pointer;
  2886. }
  2887. /* 聊天内容显示框 */
  2888. .message-list {
  2889. width: 60%;
  2890. overflow-y: auto;
  2891. overflow-x: hidden;
  2892. padding: 20px;
  2893. display: flex;
  2894. flex-direction: column;
  2895. gap: 16px;
  2896. margin: 30px auto 0;
  2897. }
  2898. .message-item {
  2899. display: flex;
  2900. margin-bottom: 12px;
  2901. }
  2902. /* 用户消息靠右显示 */
  2903. .user {
  2904. display: flex;
  2905. justify-content: flex-end;
  2906. }
  2907. .message-bubble {
  2908. max-width: 80%;
  2909. padding: 12px 16px;
  2910. border-radius: 12px;
  2911. background: #fff;
  2912. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
  2913. }
  2914. /* 用户消息的气泡样式 */
  2915. .user .message-bubble {
  2916. background: #fff;
  2917. color: rgba(0, 0, 0, 0.88);
  2918. }
  2919. /* AI消息的气泡样式 */
  2920. .assistant .message-bubble {
  2921. background: #f7f7f8;
  2922. color: rgba(0, 0, 0, 0.88);
  2923. }
  2924. .message-content {
  2925. font-size: 14px;
  2926. line-height: 1.5;
  2927. white-space: pre-wrap;
  2928. word-break: break-word;
  2929. }
  2930. .right_menu {
  2931. /* 修改现有样式 */
  2932. /* position: absolute;
  2933. right: 0; */
  2934. margin-top: 64px;
  2935. height: 93%;
  2936. background: #f7f7f8;
  2937. border-left: 1px solid rgba(0, 0, 0, 0.06);
  2938. transform: translateX(0);
  2939. transition: transform 0.3s ease, width 0.3s ease;
  2940. z-index: 2;
  2941. position: relative;
  2942. }
  2943. .right_menu.collapsed {
  2944. transform: translateX(100%);
  2945. width: 0;
  2946. }
  2947. /* 当右侧菜单展开时,给聊天区域添加右边距 */
  2948. .right_menu:not(.collapsed) + .chat {
  2949. margin-right: 450px;
  2950. }
  2951. .collapse-left-button{
  2952. position: absolute;
  2953. left: 260px;
  2954. top: 50%;
  2955. transform: translateY(-50%);
  2956. width: 20px;
  2957. height: 40px;
  2958. background: var(--secondary-bg);
  2959. border: 1px solid var(--border-color);
  2960. border-right: none;
  2961. border-radius: 4px 0 0 4px;
  2962. display: flex;
  2963. align-items: center;
  2964. justify-content: center;
  2965. cursor: pointer;
  2966. z-index: 2;
  2967. transition: all 0.3s ease;
  2968. }
  2969. .menu-collapsed {
  2970. left: 0px;
  2971. border-right: 1px solid var(--border-color);
  2972. border-left: none;
  2973. border-radius: 0 4px 4px 0;
  2974. }
  2975. /* .collapse-button {
  2976. position: absolute;
  2977. left: -20px;
  2978. top: 50%;
  2979. transform: translateY(-50%);
  2980. width: 20px;
  2981. height: 40px;
  2982. background: #fff;
  2983. border: 1px solid rgba(0, 0, 0, 0.06);
  2984. border-right: none;
  2985. border-radius: 4px 0 0 4px;
  2986. display: flex;
  2987. align-items: center;
  2988. justify-content: center;
  2989. cursor: pointer;
  2990. z-index: 3;
  2991. transition: left 0.3s ease;
  2992. } */
  2993. /* .collapse-button.menu-expanded {
  2994. border-right: 1px solid rgba(0, 0, 0, 0.06);
  2995. border-left: none;
  2996. border-radius: 0 4px 4px 0;
  2997. } */
  2998. /* .collapse-button:hover {
  2999. background: #f5f5f5;
  3000. } */
  3001. /* .collapse-icon {
  3002. width: 12px;
  3003. height: 12px;
  3004. } */
  3005. .right-menu-content {
  3006. height: 100%;
  3007. overflow-y: auto;
  3008. }
  3009. .right_menu.collapsed .right-menu-content {
  3010. display: none;
  3011. }
  3012. /* 添加打字效果的光标样式 */
  3013. /* .assistant .message-content {
  3014. position: relative;
  3015. }
  3016. .assistant .message-content::after {
  3017. content: '|';
  3018. position: absolute;
  3019. margin-left: 2px;
  3020. animation: cursor-blink 1s infinite;
  3021. }
  3022. @keyframes cursor-blink {
  3023. 0%, 100% { opacity: 1; }
  3024. 50% { opacity: 0; }
  3025. }*/
  3026. .file-message {
  3027. background: #f7f7f8;
  3028. border-radius: 8px;
  3029. padding: 12px;
  3030. margin-bottom: 8px;
  3031. }
  3032. .file-info-display {
  3033. display: flex;
  3034. align-items: center;
  3035. gap: 12px;
  3036. margin-bottom: 8px;
  3037. }
  3038. .file-type-icon {
  3039. width: 24px;
  3040. height: 24px;
  3041. object-fit: contain;
  3042. }
  3043. .file-details {
  3044. display: flex;
  3045. flex-direction: column;
  3046. }
  3047. /* .file-name {
  3048. font-size: 14px;
  3049. color: rgba(0, 0, 0, 0.88);
  3050. margin-bottom: 4px;
  3051. } */
  3052. .file-size {
  3053. font-size: 12px;
  3054. color: rgba(0, 0, 0, 0.45);
  3055. }
  3056. .file-action {
  3057. font-size: 12px;
  3058. color: rgba(0, 0, 0, 0.45);
  3059. padding-left: 36px;
  3060. }
  3061. /* 修改用户消息气泡样式,确保文件信息正确显示 */
  3062. .user .message-bubble {
  3063. background: #fff;
  3064. color: rgba(0, 0, 0, 0.88);
  3065. }
  3066. .user .file-message {
  3067. background: #f7f7f8;
  3068. }
  3069. /* 图片预览样式 */
  3070. .image-preview {
  3071. position: relative;
  3072. width: 200px;
  3073. border-radius: 8px;
  3074. overflow: hidden;
  3075. background: #fff;
  3076. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
  3077. }
  3078. .preview-image {
  3079. width: 100%;
  3080. height: auto;
  3081. display: block;
  3082. }
  3083. .image-info {
  3084. position: absolute;
  3085. bottom: 0;
  3086. left: 0;
  3087. right: 0;
  3088. padding: 8px;
  3089. background: rgba(255, 255, 255, 0.9);
  3090. font-size: 12px;
  3091. color: rgba(0, 0, 0, 0.45);
  3092. text-align: center;
  3093. border-top: 1px solid rgba(0, 0, 0, 0.06);
  3094. }
  3095. /* 文档显示样式保持不变 */
  3096. .file-info-display {
  3097. display: flex;
  3098. align-items: center;
  3099. gap: 12px;
  3100. margin-bottom: 8px;
  3101. }
  3102. .file-type-icon {
  3103. width: 24px;
  3104. height: 24px;
  3105. object-fit: contain;
  3106. }
  3107. .file-details {
  3108. display: flex;
  3109. flex-direction: column;
  3110. }
  3111. /* .file-name {
  3112. font-size: 14px;
  3113. color: rgba(0, 0, 0, 0.88);
  3114. margin-bottom: 4px;
  3115. } */
  3116. .file-size {
  3117. font-size: 12px;
  3118. color: rgba(0, 0, 0, 0.45);
  3119. }
  3120. .file-action {
  3121. font-size: 12px;
  3122. color: rgba(0, 0, 0, 0.45);
  3123. padding-left: 36px;
  3124. }
  3125. /* 添加音频控制相关样式 */
  3126. .audio-controls {
  3127. margin-top: 8px;
  3128. }
  3129. .audio-btn {
  3130. background: transparent;
  3131. border: none;
  3132. padding: 4px 8px;
  3133. cursor: pointer;
  3134. display: flex;
  3135. align-items: center;
  3136. justify-content: center;
  3137. border-radius: 4px;
  3138. transition: background-color 0.2s;
  3139. min-width: 32px;
  3140. height: 32px;
  3141. }
  3142. .audio-btn:hover {
  3143. background: rgba(0, 0, 0, 0.04);
  3144. }
  3145. .audio-icon {
  3146. width: 20px;
  3147. height: 20px;
  3148. }
  3149. /* 音频波形动画样式 */
  3150. .wave-animation {
  3151. display: flex;
  3152. align-items: center;
  3153. height: 20px;
  3154. gap: 2px;
  3155. }
  3156. .wave-bar {
  3157. display: inline-block;
  3158. width: 3px;
  3159. height: 100%;
  3160. background: #1677ff;
  3161. border-radius: 1px;
  3162. animation: wave 1s ease-in-out infinite;
  3163. }
  3164. .wave-bar:nth-child(1) {
  3165. animation-delay: 0s;
  3166. }
  3167. .wave-bar:nth-child(2) {
  3168. animation-delay: 0.2s;
  3169. }
  3170. .wave-bar:nth-child(3) {
  3171. animation-delay: 0.4s;
  3172. }
  3173. .wave-bar:nth-child(4) {
  3174. animation-delay: 0.6s;
  3175. }
  3176. @keyframes wave {
  3177. 0%,
  3178. 100% {
  3179. height: 6px;
  3180. }
  3181. 50% {
  3182. height: 18px;
  3183. }
  3184. }
  3185. .audio-btn {
  3186. background: transparent;
  3187. border: none;
  3188. padding: 4px 8px;
  3189. cursor: pointer;
  3190. display: flex;
  3191. align-items: center;
  3192. justify-content: center;
  3193. border-radius: 4px;
  3194. transition: background-color 0.2s;
  3195. min-width: 32px;
  3196. height: 32px;
  3197. }
  3198. .audio-btn:hover {
  3199. background: rgba(0, 0, 0, 0.04);
  3200. }
  3201. .audio-icon {
  3202. width: 20px;
  3203. height: 20px;
  3204. }
  3205. .message-text {
  3206. margin: 0;
  3207. white-space: normal; /* 确保文本会换行 */
  3208. word-wrap: break-word; /* 长单词或 URL 会被断开以适应容器宽度 */
  3209. overflow: auto;
  3210. font-size: 12px;
  3211. line-height: 20px;
  3212. }
  3213. .message-text ::v-deep p {
  3214. font-size: 12px !important;
  3215. margin: 5px 0 10px;
  3216. padding: 0;
  3217. font-size: inherit;
  3218. line-height: 22px;
  3219. font-weight: inherit;
  3220. }
  3221. .message-text ::v-deep ol {
  3222. padding-bottom: 10px;
  3223. }
  3224. .message-text ::v-deep .source-section h3 {
  3225. margin: 20px 0 3px;
  3226. color: #333;
  3227. font-size: 16px;
  3228. }
  3229. .message-text ::v-deep ol li {
  3230. padding-top: 3px;
  3231. }
  3232. .message-text ::v-deep ol li a {
  3233. font-size: 14px;
  3234. color: #1296db;
  3235. }
  3236. .message-text ::v-deep table {
  3237. border-collapse: collapse;
  3238. }
  3239. .message-text ::v-deep table th,
  3240. .message-text ::v-deep table td {
  3241. border: 1px solid #ddd;
  3242. padding: 8px;
  3243. }
  3244. .message-text ::v-deep table th {
  3245. background-color: #f2f2f2;
  3246. }
  3247. .message-text ::v-deep table td {
  3248. background-color: #fff;
  3249. }
  3250. .message-text ::v-deep table tr:nth-child(even) {
  3251. background-color: #f2f2f2;
  3252. }
  3253. .video-preview {
  3254. position: relative;
  3255. width: 300px;
  3256. border-radius: 8px;
  3257. overflow: hidden;
  3258. background: #fff;
  3259. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
  3260. margin-bottom: 8px;
  3261. }
  3262. .preview-video {
  3263. width: 100%;
  3264. height: auto;
  3265. display: block;
  3266. max-height: 300px;
  3267. }
  3268. /* 添加 loading 相关样式 */
  3269. .upload-loading {
  3270. display: flex;
  3271. flex-direction: column;
  3272. align-items: center;
  3273. gap: 12px;
  3274. }
  3275. .loading-spinner {
  3276. width: 24px;
  3277. height: 24px;
  3278. border: 2px solid #f3f3f3;
  3279. border-top: 2px solid #1677ff;
  3280. border-radius: 50%;
  3281. animation: spin 1s linear infinite;
  3282. }
  3283. .file-loading {
  3284. position: absolute;
  3285. top: 0;
  3286. left: 0;
  3287. right: 0;
  3288. bottom: 0;
  3289. background: rgba(255, 255, 255, 0.8);
  3290. display: flex;
  3291. align-items: center;
  3292. justify-content: center;
  3293. z-index: 1;
  3294. }
  3295. @keyframes spin {
  3296. 0% {
  3297. transform: rotate(0deg);
  3298. }
  3299. 100% {
  3300. transform: rotate(360deg);
  3301. }
  3302. }
  3303. /* 上传区域在上传时的样式 */
  3304. .upload-area {
  3305. position: relative;
  3306. cursor: pointer; /* 始终保持指针样式 */
  3307. }
  3308. .upload-area.uploading {
  3309. opacity: 0.7;
  3310. }
  3311. /* 添加配置容器的样式 */
  3312. .config-container {
  3313. margin-top: auto;
  3314. border-top: 1px solid rgba(0, 0, 0, 0.06);
  3315. }
  3316. .web-search-toggle {
  3317. display: flex;
  3318. align-items: center;
  3319. /* margin-left: auto; */
  3320. padding: 0 12px;
  3321. }
  3322. .toggle-input {
  3323. position: relative;
  3324. width: 36px;
  3325. height: 20px;
  3326. margin-right: 8px;
  3327. appearance: none;
  3328. background: #ccc;
  3329. border-radius: 10px;
  3330. cursor: pointer;
  3331. transition: background 0.3s;
  3332. }
  3333. .toggle-input:checked {
  3334. background: #1677ff;
  3335. }
  3336. .toggle-input::before {
  3337. content: "";
  3338. position: absolute;
  3339. top: 2px;
  3340. left: 2px;
  3341. width: 16px;
  3342. height: 16px;
  3343. background: white;
  3344. border-radius: 50%;
  3345. transition: transform 0.3s;
  3346. }
  3347. .toggle-input:checked::before {
  3348. transform: translateX(16px);
  3349. }
  3350. .toggle-label {
  3351. font-size: 14px;
  3352. color: var(--primary-text);
  3353. cursor: pointer;
  3354. }
  3355. .toggle-input:disabled {
  3356. opacity: 0.5;
  3357. cursor: not-allowed;
  3358. }
  3359. .toggle-label.disabled {
  3360. color: var(--primary-text);
  3361. cursor: not-allowed;
  3362. }
  3363. .disabled-hint {
  3364. font-size: 12px;
  3365. color: var(--primary-text);
  3366. margin-left: 4px;
  3367. }
  3368. /* 添加菜单收缩相关样式 */
  3369. .menu {
  3370. /* 现有样式保持不变 */
  3371. transition: width 0.3s ease;
  3372. position: relative;
  3373. }
  3374. .menu.collapsed {
  3375. width: 0;
  3376. padding: 0;
  3377. }
  3378. /* 添加收缩按钮样式 */
  3379. .collapse-button {
  3380. position: absolute;
  3381. top: 50%;
  3382. transform: translateY(-50%);
  3383. width: 20px;
  3384. height: 40px;
  3385. background: var(--secondary-bg);
  3386. border: 1px solid var(--border-color);
  3387. display: flex;
  3388. align-items: center;
  3389. justify-content: center;
  3390. cursor: pointer;
  3391. z-index: 3;
  3392. transition: right 0.3s ease, left 0.3s ease;
  3393. }
  3394. .collapse-button:hover {
  3395. background: var(--secondary-bg);
  3396. }
  3397. .collapse-icon {
  3398. width: 12px;
  3399. height: 12px;
  3400. }
  3401. /* 添加响应式布局样式 */
  3402. @media screen and (max-width: 768px) {
  3403. .layout {
  3404. /* 移动端布局调整 */
  3405. flex-direction: column;
  3406. }
  3407. .menu {
  3408. /* 移动端菜单调整 */
  3409. position: fixed;
  3410. width: 100%;
  3411. height: 93%;
  3412. z-index: 1000;
  3413. transform: translateX(-100%);
  3414. }
  3415. .menu.collapsed {
  3416. transform: translateX(-100%);
  3417. }
  3418. .collapse-left-button {
  3419. z-index: 999;
  3420. /* 移动端收缩按钮调整 */
  3421. /* display: none; */
  3422. }
  3423. .chat {
  3424. /* 移动端聊天区域调整 */
  3425. width: 100%;
  3426. margin: 0;
  3427. padding: 16px;
  3428. left: 0 !important;
  3429. right: 0 !important;
  3430. }
  3431. .message-list {
  3432. /* 移动端消息列表调整 */
  3433. width: 100%;
  3434. padding: 10px;
  3435. }
  3436. .input-container {
  3437. /* 移动端输入区域调整 */
  3438. width: 98%;
  3439. padding: 10px;
  3440. }
  3441. .cards-container {
  3442. /* 移动端卡片容器调整 */
  3443. flex-direction: column;
  3444. }
  3445. .suggestion-card {
  3446. /* 移动端建议卡片调整 */
  3447. width: 100%;
  3448. }
  3449. .welcome-section {
  3450. /* 移动端欢迎区域调整 */
  3451. width: 100%;
  3452. padding: 0;
  3453. }
  3454. .right_menu {
  3455. /* 移动端右侧菜单调整 */
  3456. position: fixed;
  3457. width: 100%;
  3458. height: 100%;
  3459. z-index: 998;
  3460. }
  3461. .right_menu.collapsed {
  3462. transform: translateX(100%);
  3463. }
  3464. /* 移动端文件预览调整 */
  3465. .image-preview,
  3466. .video-preview {
  3467. width: 100%;
  3468. max-width: 300px;
  3469. }
  3470. /* 移动端文件列表调整 */
  3471. .file-list {
  3472. justify-content: center;
  3473. }
  3474. .file-item {
  3475. width: 100%;
  3476. max-width: 300px;
  3477. }
  3478. /* 移动端标签栏调整 */
  3479. .tabs-wrapper {
  3480. flex-wrap: wrap;
  3481. gap: 4px;
  3482. }
  3483. .web-search-toggle {
  3484. width: 100%;
  3485. justify-content: flex-start;
  3486. margin-top: 8px;
  3487. }
  3488. /* 移动端消息气泡调整 */
  3489. .message-bubble {
  3490. max-width: 90%;
  3491. }
  3492. .collapse-button {
  3493. left: -20px !important;
  3494. }
  3495. .collapse-button.menu-expanded {
  3496. left: auto !important;
  3497. right: 0;
  3498. }
  3499. }
  3500. /* 添加移动端手势支持 */
  3501. @media (hover: none) and (pointer: coarse) {
  3502. .menu {
  3503. /* 支持触摸滑动 */
  3504. touch-action: pan-y;
  3505. }
  3506. .message-list {
  3507. /* 支持触摸滚动 */
  3508. -webkit-overflow-scrolling: touch;
  3509. }
  3510. }
  3511. /* 优化移动端字体大小 */
  3512. @media screen and (max-width: 480px) {
  3513. .message-content {
  3514. font-size: 14px;
  3515. }
  3516. .suggestion-item {
  3517. font-size: 13px;
  3518. }
  3519. .file-name {
  3520. font-size: 13px;
  3521. }
  3522. }
  3523. /* 修改现有的响应式样式 */
  3524. @media screen and (max-width: 768px) {
  3525. .layout {
  3526. overflow-x: hidden;
  3527. }
  3528. .chat {
  3529. /* 调整聊天区域以保持一致的宽度 */
  3530. padding: 16px 0 ;
  3531. margin: 0;
  3532. width: 100%;
  3533. }
  3534. .message-list {
  3535. /* 保持消息列表的宽度一致 */
  3536. width: 100%;
  3537. max-width: 800px;
  3538. margin: 60px auto 0;
  3539. padding: 0 10px;
  3540. }
  3541. .input-container {
  3542. /* 保持输入区域的宽度一致 */
  3543. width: 95%;
  3544. max-width: 800px;
  3545. margin: 0 auto;
  3546. padding: 0;
  3547. }
  3548. .welcome-section {
  3549. /* 保持欢迎区域的宽度一致 */
  3550. width: 100%;
  3551. max-width: 800px;
  3552. margin: 0 auto;
  3553. /* padding: 0 16px; */
  3554. }
  3555. .cards-container {
  3556. /* 保持卡片布局 */
  3557. flex-direction: row;
  3558. flex-wrap: wrap;
  3559. gap: 16px;
  3560. }
  3561. .suggestion-card {
  3562. /* 调整卡片大小 */
  3563. flex: 1 1 300px;
  3564. min-width: 300px;
  3565. }
  3566. .message-bubble {
  3567. /* 保持消息气泡的样式 */
  3568. max-width: 80%;
  3569. padding: 12px 16px;
  3570. }
  3571. /* 调整文件预览区域 */
  3572. .image-preview,
  3573. .video-preview {
  3574. width: 200px;
  3575. margin: 0 auto;
  3576. }
  3577. /* 调整文件列表布局 */
  3578. .file-list {
  3579. display: flex;
  3580. flex-wrap: wrap;
  3581. gap: 8px;
  3582. justify-content: flex-start;
  3583. }
  3584. .file-item {
  3585. width: calc(50% - 8px);
  3586. min-width: 200px;
  3587. }
  3588. /* 调整标签栏布局 */
  3589. .tabs-wrapper {
  3590. flex-wrap: nowrap;
  3591. overflow-x: auto;
  3592. -webkit-overflow-scrolling: touch;
  3593. padding-bottom: 8px;
  3594. }
  3595. /* 调整搜索开关位置 */
  3596. .web-search-toggle {
  3597. position: relative;
  3598. width: auto;
  3599. }
  3600. /* 调整菜单样式 */
  3601. .menu {
  3602. position: fixed;
  3603. left: 0;
  3604. top: 0;
  3605. bottom: 0;
  3606. width: 260px;
  3607. transform: translateX(-100%);
  3608. transition: transform 0.3s ease;
  3609. z-index: 1000;
  3610. }
  3611. .menu.collapsed {
  3612. transform: translateX(-100%);
  3613. }
  3614. .menu:not(.collapsed) {
  3615. transform: translateX(0);
  3616. }
  3617. /* 调整右侧菜单样式 */
  3618. /* .right_menu {
  3619. position: fixed;
  3620. right: 0;
  3621. top: 0;
  3622. bottom: 0;
  3623. width: 300px;
  3624. transform: translateX(100%);
  3625. transition: transform 0.3s ease;
  3626. z-index: 998;
  3627. } */
  3628. .right_menu.collapsed {
  3629. transform: translateX(100%);
  3630. }
  3631. .right_menu:not(.collapsed) {
  3632. transform: translateX(0);
  3633. }
  3634. /* 添加遮罩层样式 */
  3635. .menu-overlay {
  3636. position: fixed;
  3637. top: 0;
  3638. left: 0;
  3639. right: 0;
  3640. bottom: 0;
  3641. background: rgba(0, 0, 0, 0.5);
  3642. z-index: 999;
  3643. display: none;
  3644. }
  3645. .menu:not(.collapsed) ~ .menu-overlay,
  3646. .right_menu:not(.collapsed) ~ .menu-overlay {
  3647. display: block;
  3648. }
  3649. }
  3650. /* 添加更细致的断点控制 */
  3651. @media screen and (max-width: 480px) {
  3652. .message-content {
  3653. font-size: 14px;
  3654. line-height: 1.5;
  3655. }
  3656. .suggestion-item {
  3657. font-size: 14px;
  3658. padding: 10px;
  3659. }
  3660. .input-wrapper {
  3661. padding:5px;
  3662. }
  3663. .message-input {
  3664. font-size: 14px;
  3665. }
  3666. }
  3667. /* 确保滚动流畅 */
  3668. * {
  3669. -webkit-overflow-scrolling: touch;
  3670. }
  3671. /* 防止页面横向滚动 */
  3672. body {
  3673. overflow-x: hidden;
  3674. width: 100%;
  3675. }
  3676. /* 优化触摸体验 */
  3677. button,
  3678. .suggestion-item,
  3679. .conversation-item {
  3680. touch-action: manipulation;
  3681. }
  3682. /* 添加拖动条样式 */
  3683. .resize-handle {
  3684. position: absolute;
  3685. left: -5px;
  3686. top: 0;
  3687. width: 10px;
  3688. height: 100%;
  3689. cursor: col-resize;
  3690. background: transparent;
  3691. z-index: 3;
  3692. }
  3693. .resize-handle:hover {
  3694. background: rgba(0, 0, 0, 0.1);
  3695. }
  3696. /* 确保拖动时不会选中文本 */
  3697. .resize-handle:active {
  3698. user-select: none;
  3699. }
  3700. /* 移动端样式调整 */
  3701. @media screen and (max-width: 768px) {
  3702. .resize-handle {
  3703. display: none;
  3704. }
  3705. .right_menu {
  3706. width: 100% !important;
  3707. }
  3708. }
  3709. .search-header {
  3710. display: flex;
  3711. justify-content: space-between;
  3712. align-items: center;
  3713. padding: 16px;
  3714. border-bottom: 1px solid var(--border-color);
  3715. }
  3716. .search-header span {
  3717. font-size: 16px;
  3718. font-weight: bold;
  3719. }
  3720. /* 添加开关样式 */
  3721. .web-search-toggle {
  3722. display: flex;
  3723. align-items: center;
  3724. color: var(--primary-text);
  3725. }
  3726. .toggle-input {
  3727. position: relative;
  3728. width: 36px;
  3729. height: 20px;
  3730. margin-right: 8px;
  3731. appearance: none;
  3732. background: #ccc;
  3733. border-radius: 10px;
  3734. cursor: pointer;
  3735. transition: background 0.3s;
  3736. }
  3737. .toggle-input:checked {
  3738. background: #1677ff;
  3739. }
  3740. .toggle-input::before {
  3741. content: "";
  3742. position: absolute;
  3743. top: 2px;
  3744. left: 2px;
  3745. width: 16px;
  3746. height: 16px;
  3747. background: white;
  3748. border-radius: 50%;
  3749. transition: transform 0.3s;
  3750. }
  3751. .toggle-input:checked::before {
  3752. transform: translateX(16px);
  3753. }
  3754. .toggle-label {
  3755. font-size: 14px;
  3756. color: rgba(0, 0, 0, 0.88);
  3757. cursor: pointer;
  3758. }
  3759. /* 添加 Tab 样式 */
  3760. .right-menu-tabs {
  3761. display: flex;
  3762. border-bottom: 1px solid #f0f0f0;
  3763. background: var(--secondary-bg);
  3764. }
  3765. .tab-item {
  3766. flex: 1;
  3767. padding: 12px;
  3768. display: flex;
  3769. align-items: center;
  3770. justify-content: center;
  3771. gap: 8px;
  3772. cursor: pointer;
  3773. transition: all 0.3s;
  3774. border-bottom: 2px solid transparent;
  3775. color: rgba(0, 0, 0, 0.65);
  3776. }
  3777. .tab-item:hover {
  3778. color: #1677ff;
  3779. }
  3780. .tab-item.active {
  3781. color: #1677ff;
  3782. border-bottom-color: #1677ff;
  3783. background: #f6f9fe;
  3784. }
  3785. .panel-container {
  3786. height: calc(100% - 45px); /* 减去 tab 的高度 */
  3787. overflow-y: auto;
  3788. }
  3789. /* 修改右侧菜单内容样式 */
  3790. .right-menu-content {
  3791. height: 100%;
  3792. display: flex;
  3793. flex-direction: column;
  3794. }
  3795. /* 预览面板样式 */
  3796. .preview-header {
  3797. padding: 16px;
  3798. border-bottom: 1px solid var(--border-color);
  3799. color: var(--primary-text);
  3800. font-size: 16px;
  3801. font-weight: bold;
  3802. }
  3803. .search-toggles {
  3804. display: flex;
  3805. gap: 12px;
  3806. padding: 8px 12px;
  3807. /* background: var(--secondary-bg); */
  3808. border-radius: 8px;
  3809. transition: all 0.3s ease;
  3810. justify-content: space-between;
  3811. &.compact {
  3812. .search-toggle-item {
  3813. padding: 6px;
  3814. min-width: auto;
  3815. width: auto;
  3816. }
  3817. .toggle-content {
  3818. gap: 4px;
  3819. justify-content: center;
  3820. }
  3821. .toggle-text {
  3822. /* display: none; */
  3823. }
  3824. .toggle-switch {
  3825. margin-left: 8px;
  3826. }
  3827. }
  3828. }
  3829. .search-toggle-item {
  3830. position: relative;
  3831. display: flex;
  3832. align-items: center;
  3833. background: var(--primary-bg);
  3834. color: var(--primary-text);
  3835. border-radius: 6px;
  3836. padding: 6px 12px;
  3837. transition: all 0.3s;
  3838. width: 140px;
  3839. @media screen and (max-width: 768px) {
  3840. width: auto;
  3841. padding: 6px 8px;
  3842. }
  3843. &:hover {
  3844. background: rgba(0, 0, 0, 0.02);
  3845. }
  3846. }
  3847. .toggle-content {
  3848. display: flex;
  3849. align-items: center;
  3850. gap: 8px;
  3851. color: rgba(0, 0, 0, 0.65);
  3852. transition: all 0.3s;
  3853. width: 100%;
  3854. .anticon {
  3855. font-size: 16px;
  3856. flex-shrink: 0;
  3857. }
  3858. .toggle-text {
  3859. font-size: 14px;
  3860. transition: all 0.3s;
  3861. white-space: nowrap;
  3862. color: var(--primary-text);
  3863. }
  3864. }
  3865. .toggle-switch {
  3866. position: relative;
  3867. display: flex;
  3868. align-items: center;
  3869. }
  3870. .toggle-input {
  3871. position: absolute;
  3872. opacity: 0;
  3873. width: 0;
  3874. height: 0;
  3875. &:checked + .toggle-label {
  3876. .toggle-slider {
  3877. background: #1677ff;
  3878. &:before {
  3879. transform: translateX(16px);
  3880. }
  3881. }
  3882. }
  3883. &:disabled + .toggle-label {
  3884. cursor: not-allowed;
  3885. .toggle-slider {
  3886. background: #d9d9d9;
  3887. &:before {
  3888. background: #f5f5f5;
  3889. }
  3890. }
  3891. }
  3892. }
  3893. .toggle-label {
  3894. display: flex;
  3895. align-items: center;
  3896. cursor: pointer;
  3897. .toggle-slider {
  3898. position: relative;
  3899. width: 36px;
  3900. height: 20px;
  3901. background: #d9d9d9;
  3902. border-radius: 10px;
  3903. transition: all 0.3s;
  3904. &:before {
  3905. content: '';
  3906. position: absolute;
  3907. top: 2px;
  3908. left: 2px;
  3909. width: 16px;
  3910. height: 16px;
  3911. background: white;
  3912. border-radius: 50%;
  3913. transition: transform 0.3s;
  3914. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  3915. }
  3916. }
  3917. &.disabled {
  3918. cursor: not-allowed;
  3919. }
  3920. }
  3921. /* 移动端适配 */
  3922. @media screen and (max-width: 480px) {
  3923. .search-toggles {
  3924. padding: 6px;
  3925. gap: 6px;
  3926. }
  3927. .search-toggle-item {
  3928. padding: 6px;
  3929. }
  3930. .toggle-content {
  3931. gap: 4px;
  3932. }
  3933. }
  3934. @media screen and (max-width: 768px) {
  3935. .search-toggles {
  3936. &:not(.compact) {
  3937. .search-toggle-item {
  3938. padding: 6px 8px;
  3939. }
  3940. .toggle-content {
  3941. gap: 4px;
  3942. }
  3943. .toggle-text {
  3944. font-size: 12px;
  3945. }
  3946. }
  3947. }
  3948. }
  3949. @media screen and (max-width: 480px) {
  3950. .search-toggles {
  3951. padding: 0px;
  3952. gap: 6px;
  3953. &:not(.compact) {
  3954. .toggle-text {
  3955. /* display: none; */
  3956. }
  3957. .search-toggle-item {
  3958. padding: 6px;
  3959. }
  3960. }
  3961. }
  3962. }
  3963. /* 深色模式支持 */
  3964. @media (prefers-color-scheme: dark) {
  3965. .search-toggle-item {
  3966. background: rgba(255, 255, 255, 0.04);
  3967. &:hover {
  3968. background: rgba(255, 255, 255, 0.08);
  3969. }
  3970. }
  3971. .toggle-content {
  3972. color: rgba(255, 255, 255, 0.65);
  3973. }
  3974. .toggle-label {
  3975. .toggle-slider {
  3976. background: #424242;
  3977. &:before {
  3978. background: #8c8c8c;
  3979. }
  3980. }
  3981. }
  3982. .toggle-input:checked + .toggle-label {
  3983. .toggle-slider {
  3984. background: #1677ff;
  3985. &:before {
  3986. background: white;
  3987. }
  3988. }
  3989. }
  3990. .toggle-input:disabled + .toggle-label {
  3991. .toggle-slider {
  3992. background: #1f1f1f;
  3993. &:before {
  3994. background: #434343;
  3995. }
  3996. }
  3997. }
  3998. }
  3999. /* 修改右侧菜单面板样式 */
  4000. .right_menu {
  4001. position: fixed;
  4002. right: 0;
  4003. top: 0;
  4004. bottom: 0;
  4005. width: 450px;
  4006. background: #fff;
  4007. border-left: 1px solid rgba(0, 0, 0, 0.06);
  4008. display: flex;
  4009. flex-direction: column;
  4010. transition: transform 0.3s ease;
  4011. z-index: 998;
  4012. }
  4013. .right_menu.collapsed {
  4014. transform: translateX(100%);
  4015. }
  4016. .right-menu-content {
  4017. height: 100%;
  4018. display: flex;
  4019. flex-direction: column;
  4020. overflow: hidden;
  4021. }
  4022. /* Tab 样式调整 */
  4023. .right-menu-tabs {
  4024. display: flex;
  4025. background: var(--secondary-bg);
  4026. color: var(--primary-text);
  4027. border-bottom: 1px solid #f0f0f0;
  4028. padding: 0;
  4029. height: 46px;
  4030. flex-shrink: 0;
  4031. }
  4032. .tab-item {
  4033. flex: 1;
  4034. display: flex;
  4035. align-items: center;
  4036. justify-content: center;
  4037. gap: 8px;
  4038. padding: 12px;
  4039. color: var(--primary-text);
  4040. cursor: pointer;
  4041. transition: all 0.3s;
  4042. border-bottom: 2px solid transparent;
  4043. font-size: 14px;
  4044. }
  4045. .tab-item.active {
  4046. color: #1677ff;
  4047. border-bottom-color: #1677ff;
  4048. background: rgba(22, 119, 255, 0.04);
  4049. }
  4050. .tab-item:hover {
  4051. color: #1677ff;
  4052. }
  4053. /* 面板容器样式 */
  4054. .panel-container {
  4055. flex: 1;
  4056. overflow-y: auto;
  4057. background: var(--secondary-bg);
  4058. display: flex;
  4059. flex-direction: column;
  4060. }
  4061. /* 搜索头部样式 */
  4062. .search-header {
  4063. padding: 16px;
  4064. border-bottom: 1px solid var(--border-color);
  4065. display: flex;
  4066. justify-content: space-between;
  4067. align-items: center;
  4068. background: var(--secondary-bg);
  4069. flex-shrink: 0;
  4070. }
  4071. .search-header span {
  4072. font-size: 16px;
  4073. font-weight: 500;
  4074. color: var(--primary-text);
  4075. }
  4076. /* 知识库结果面板样式 */
  4077. .knowledge-results {
  4078. flex: 1;
  4079. padding: 16px 5px;
  4080. overflow-y: auto;
  4081. }
  4082. /* 调整聊天区域的右边距 */
  4083. .chat {
  4084. transition: right 0.3s ease;
  4085. }
  4086. .right_menu:not(.collapsed) ~ .chat {
  4087. right: 450px;
  4088. }
  4089. /* 移动端适配 */
  4090. @media screen and (max-width: 768px) {
  4091. .right_menu {
  4092. width: 93%;
  4093. }
  4094. .right_menu:not(.collapsed) ~ .chat {
  4095. right: 100%;
  4096. }
  4097. .tab-item {
  4098. padding: 8px;
  4099. }
  4100. .tab-item .anticon {
  4101. font-size: 16px;
  4102. }
  4103. .tab-item span {
  4104. font-size: 12px;
  4105. }
  4106. .search-header {
  4107. padding: 12px;
  4108. }
  4109. .search-header span {
  4110. font-size: 14px;
  4111. }
  4112. }
  4113. /* 优化滚动条样式 */
  4114. .panel-container::-webkit-scrollbar {
  4115. width: 4px;
  4116. }
  4117. .panel-container::-webkit-scrollbar-thumb {
  4118. background: rgba(0, 0, 0, 0.1);
  4119. border-radius: 2px;
  4120. }
  4121. .panel-container::-webkit-scrollbar-track {
  4122. background: transparent;
  4123. }
  4124. /* 添加阴影效果 */
  4125. .right_menu {
  4126. box-shadow: -2px 0 8px rgba(0, 0, 0, 0.06);
  4127. }
  4128. /* 优化过渡动画 */
  4129. .right_menu,
  4130. .chat,
  4131. .tab-item,
  4132. .panel-container {
  4133. transition: all 0.3s ease;
  4134. }
  4135. /* 确保内容不会溢出 */
  4136. .right-menu-content {
  4137. max-height: 100%;
  4138. background-color: var(--secondary-bg) !important;
  4139. }
  4140. .panel-container {
  4141. max-height: calc(100% - 46px); /* 减去 tab 的高度 */
  4142. }
  4143. /* 菜单展开时的按钮样式 */
  4144. .collapse-button.menu-expanded {
  4145. left: -20px;
  4146. border-right: none;
  4147. border-radius: 4px 0 0 4px;
  4148. }
  4149. /* 菜单收起时的按钮样式 */
  4150. .collapse-button.menu-collapsed {
  4151. left: -20px;
  4152. border-left: none;
  4153. border-radius: 0 4px 4px 0;
  4154. }
  4155. /* 确保右侧菜单收起时按钮仍然可见 */
  4156. .right_menu.collapsed .collapse-button {
  4157. right: auto;
  4158. left: -20px;
  4159. }
  4160. /* 添加模型选择框样式 */
  4161. .model-select {
  4162. background: transparent;
  4163. border: 1px solid #d9d9d9;
  4164. border-radius: 4px;
  4165. padding: 4px 8px;
  4166. font-size: 14px;
  4167. color: var(--primary-text);
  4168. cursor: pointer;
  4169. transition: all 0.3s;
  4170. outline: none;
  4171. margin-left: auto;
  4172. min-width: 80px;
  4173. &:hover {
  4174. border-color: #1677ff;
  4175. }
  4176. &:focus {
  4177. border-color: #1677ff;
  4178. box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
  4179. }
  4180. &:disabled {
  4181. background: #f5f5f5;
  4182. cursor: not-allowed;
  4183. color: rgba(0, 0, 0, 0.25);
  4184. }
  4185. option {
  4186. background: white;
  4187. color: rgba(0, 0, 0, 0.88);
  4188. }
  4189. }
  4190. @media screen and (max-width: 768px) {
  4191. .model-select {
  4192. width: 80px;
  4193. font-size: 12px;
  4194. padding: 2px 4px;
  4195. }
  4196. }
  4197. /* 全局主题变量 */
  4198. :root {
  4199. /* 浅色主题变量 */
  4200. --primary-bg: #ffffff;
  4201. --secondary-bg: #f5f5f5;
  4202. --primary-text: #333333;
  4203. --secondary-text: #666666;
  4204. --border-color: #e0e0e0;
  4205. --hover-bg: #f0f0f0;
  4206. --card-bg: #ffffff;
  4207. --shadow-color: rgba(0, 0, 0, 0.1);
  4208. --highlight-color: #1890ff;
  4209. /* 动画时间 */
  4210. --theme-transition: 0.3s ease;
  4211. }
  4212. /* 深色主题变量 */
  4213. :root.dark-theme {
  4214. --primary-bg: #1a1a1a;
  4215. --secondary-bg: #2d2d2d;
  4216. --primary-text: #ffffff;
  4217. --secondary-text: #b3b3b3;
  4218. --border-color: #404040;
  4219. --hover-bg: #363636;
  4220. --card-bg: #2d2d2d;
  4221. --shadow-color: rgba(0, 0, 0, 0.3);
  4222. --highlight-color: #40a9ff;
  4223. }
  4224. /* 主题过渡动画 */
  4225. .layout * {
  4226. transition: background-color var(--theme-transition),
  4227. color var(--theme-transition),
  4228. border-color var(--theme-transition),
  4229. box-shadow var(--theme-transition);
  4230. }
  4231. /* 主题切换按钮样式 */
  4232. .theme-switch {
  4233. display: flex;
  4234. align-items: center;
  4235. justify-content: center;
  4236. width: 25px;
  4237. height: 25px;
  4238. margin-right: 16px;
  4239. padding: 8px;
  4240. border-radius: 50%;
  4241. cursor: pointer;
  4242. background-color: var(--hover-bg);
  4243. transition: all var(--theme-transition);
  4244. }
  4245. .theme-switch:hover {
  4246. background-color: var(--border-color);
  4247. }
  4248. .theme-icon {
  4249. width: 20px;
  4250. height: 20px;
  4251. color: var(--primary-text);
  4252. }
  4253. /* 主题图标切换动画 */
  4254. .theme-icon-enter-active,
  4255. .theme-icon-leave-active {
  4256. transition: transform 0.3s ease, opacity 0.3s ease;
  4257. }
  4258. .theme-icon-enter-from,
  4259. .theme-icon-leave-to {
  4260. opacity: 0;
  4261. transform: scale(0.8) rotate(-45deg);
  4262. }
  4263. .theme-icon-enter-to,
  4264. .theme-icon-leave-from {
  4265. opacity: 1;
  4266. transform: scale(1) rotate(0);
  4267. }
  4268. /* 应用主题样式到现有组件 */
  4269. :deep(.layout) {
  4270. background-color: var(--primary-bg);
  4271. color: var(--primary-text);
  4272. }
  4273. :deep(.header) {
  4274. background-color: var(--secondary-bg);
  4275. border-bottom: 1px solid var(--border-color);
  4276. }
  4277. :deep(.menu) {
  4278. background-color: var(--secondary-bg);
  4279. border-right: 1px solid var(--border-color);
  4280. }
  4281. :deep(.user-info-bar) {
  4282. background-color: var(--secondary-bg);
  4283. color: var(--primary-text);
  4284. }
  4285. :deep(.dropdown-menu) {
  4286. background-color: var(--card-bg);
  4287. border: 1px solid var(--border-color);
  4288. box-shadow: 0 2px 8px var(--shadow-color);
  4289. }
  4290. :deep(.dropdown-item) {
  4291. color: var(--primary-text);
  4292. }
  4293. :deep(.dropdown-item:hover) {
  4294. background-color: var(--hover-bg);
  4295. }
  4296. :deep(.suggestion-card) {
  4297. background-color: var(--card-bg);
  4298. border: 1px solid var(--border-color);
  4299. }
  4300. :deep(.suggestion-item) {
  4301. color: var(--primary-text);
  4302. }
  4303. :deep(.suggestion-item:hover) {
  4304. background-color: var(--hover-bg);
  4305. }
  4306. :deep(.message-bubble) {
  4307. background-color: var(--card-bg);
  4308. border: 1px solid var(--border-color);
  4309. }
  4310. /* 确保主题样式应用到聊天消息 */
  4311. :deep(.message-item.user .message-bubble) {
  4312. /* background-color: var(--highlight-color); */
  4313. color: var(--primary-text);
  4314. }
  4315. :deep(.message-item.assistant .message-bubble) {
  4316. background-color: var(--card-bg);
  4317. color: var(--primary-text);
  4318. }
  4319. /* 搜索区域样式 */
  4320. :deep(.search-section) {
  4321. background-color: var(--card-bg);
  4322. border: 1px solid var(--border-color);
  4323. padding: 20px;
  4324. border-radius: 8px;
  4325. }
  4326. :deep(.search-input) {
  4327. background-color: var(--primary-bg);
  4328. border: 1px solid var(--border-color);
  4329. color: var(--primary-text);
  4330. }
  4331. :deep(.search-input:hover),
  4332. :deep(.search-input:focus) {
  4333. border-color: var(--highlight-color);
  4334. box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
  4335. }
  4336. /* 提示问题区域样式 */
  4337. :deep(.suggestion-section) {
  4338. background-color: var(--card-bg);
  4339. border-radius: 8px;
  4340. padding: 20px;
  4341. margin-top: 20px;
  4342. }
  4343. :deep(.suggestion-section h3) {
  4344. color: var(--primary-text);
  4345. margin-bottom: 16px;
  4346. }
  4347. :deep(.cards-container) {
  4348. display: grid;
  4349. gap: 16px;
  4350. grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  4351. }
  4352. :deep(.suggestion-card) {
  4353. background-color: var(--secondary-bg);
  4354. border: 1px solid var(--border-color);
  4355. border-radius: 8px;
  4356. padding: 16px;
  4357. transition: all var(--theme-transition);
  4358. }
  4359. :deep(.suggestion-card:hover) {
  4360. border-color: var(--highlight-color);
  4361. box-shadow: 0 2px 8px var(--shadow-color);
  4362. }
  4363. :deep(.card-header) {
  4364. display: flex;
  4365. align-items: center;
  4366. gap: 8px;
  4367. margin-bottom: 8px;
  4368. }
  4369. :deep(.card-header h4) {
  4370. color: var(--primary-text);
  4371. margin: 0;
  4372. }
  4373. :deep(.card-subtitle) {
  4374. color: var(--secondary-text);
  4375. margin-bottom: 12px;
  4376. }
  4377. :deep(.suggestion-items) {
  4378. display: flex;
  4379. flex-direction: column;
  4380. gap: 8px;
  4381. }
  4382. :deep(.suggestion-item) {
  4383. padding: 8px 12px;
  4384. border-radius: 4px;
  4385. background-color: var(--primary-bg);
  4386. color: var(--primary-text);
  4387. cursor: pointer;
  4388. transition: all var(--theme-transition);
  4389. }
  4390. :deep(.suggestion-item:hover) {
  4391. background-color: var(--hover-bg);
  4392. color: var(--highlight-color);
  4393. }
  4394. /* 保存问题区域样式 */
  4395. :deep(.save-question) {
  4396. background-color: var(--card-bg);
  4397. border: 1px solid var(--border-color);
  4398. border-radius: 8px;
  4399. padding: 20px;
  4400. margin-top: 20px;
  4401. }
  4402. :deep(.save-question-title) {
  4403. color: var(--primary-text);
  4404. margin-bottom: 16px;
  4405. font-weight: bold;
  4406. }
  4407. :deep(.save-question-input) {
  4408. width: 100%;
  4409. padding: 8px 12px;
  4410. border: 1px solid var(--border-color);
  4411. border-radius: 4px;
  4412. background-color: var(--primary-bg);
  4413. color: var(--primary-text);
  4414. margin-bottom: 8px;
  4415. }
  4416. :deep(.save-question-input:focus) {
  4417. border-color: var(--highlight-color);
  4418. outline: none;
  4419. box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
  4420. }
  4421. /* 搜索结果区域样式 */
  4422. :deep(.search-results) {
  4423. background-color: var(--secondary-bg);
  4424. /* border: 1px solid var(--border-color); */
  4425. border-radius: 8px;
  4426. /* padding: 20px; */
  4427. /* margin-top: 20px; */
  4428. }
  4429. :deep(.search-results-title) {
  4430. color: var(--primary-text);
  4431. margin-bottom: 16px;
  4432. }
  4433. :deep(.no-results) {
  4434. color: var(--secondary-text);
  4435. text-align: center;
  4436. padding: 40px 0;
  4437. }
  4438. /* 输入框和按钮样式 */
  4439. :deep(.ant-input),
  4440. :deep(.ant-input-search-button) {
  4441. background-color: var(--primary-bg);
  4442. border-color: var(--border-color);
  4443. color: var(--primary-text);
  4444. }
  4445. :deep(.ant-input:hover),
  4446. :deep(.ant-input:focus) {
  4447. border-color: var(--highlight-color);
  4448. }
  4449. :deep(.ant-input-search-button) {
  4450. background-color: var(--highlight-color);
  4451. border-color: var(--highlight-color);
  4452. color: #ffffff;
  4453. }
  4454. :deep(.ant-input-search-button:hover) {
  4455. background-color: var(--highlight-color);
  4456. opacity: 0.9;
  4457. }
  4458. /* 标签页样式 */
  4459. :deep(.ant-tabs-tab) {
  4460. color: var(--secondary-text);
  4461. }
  4462. :deep(.ant-tabs-tab-active) {
  4463. color: var(--highlight-color) !important;
  4464. }
  4465. :deep(.ant-tabs-ink-bar) {
  4466. background-color: var(--highlight-color);
  4467. }
  4468. /* 图标样式适配 */
  4469. :deep(.tab-icon) {
  4470. filter: var(--icon-filter);
  4471. }
  4472. /* 其他已有的样式保持不变 */
  4473. </style>
  4474. <style>
  4475. /* 添加图标滤镜变量 */
  4476. :root {
  4477. --icon-filter: none;
  4478. }
  4479. :root.dark-theme {
  4480. --icon-filter: brightness(0) invert(1);
  4481. }
  4482. </style>
  4483. <style lang="less" scoped>
  4484. // ... existing code ...
  4485. /* 文档预览样式 */
  4486. .document-preview {
  4487. height: 100%;
  4488. overflow: hidden;
  4489. display: flex;
  4490. flex-direction: column;
  4491. }
  4492. .preview-container {
  4493. display: flex;
  4494. height: 100%;
  4495. gap: 16px;
  4496. }
  4497. .document-list {
  4498. width: 300px;
  4499. flex-shrink: 0;
  4500. overflow-y: auto;
  4501. padding: 16px;
  4502. border-right: 1px solid var(--border-color);
  4503. }
  4504. .empty-state {
  4505. display: flex;
  4506. flex-direction: column;
  4507. align-items: center;
  4508. justify-content: center;
  4509. padding: 48px 0;
  4510. color: var(--primary-text);
  4511. .empty-icon {
  4512. font-size: 48px;
  4513. margin-bottom: 16px;
  4514. }
  4515. p {
  4516. font-size: 14px;
  4517. margin: 0;
  4518. }
  4519. }
  4520. .document-item {
  4521. display: flex;
  4522. align-items: center;
  4523. justify-content: space-between;
  4524. padding: 12px;
  4525. border-radius: 8px;
  4526. margin-bottom: 8px;
  4527. background: var(--secondary-bg);
  4528. transition: all 0.3s;
  4529. cursor: pointer;
  4530. &:hover {
  4531. background: var(--hover-bg);
  4532. }
  4533. &.active {
  4534. background: var(--primary-bg);
  4535. border: 1px solid var(--primary-color);
  4536. }
  4537. }
  4538. .document-info {
  4539. display: flex;
  4540. align-items: center;
  4541. gap: 12px;
  4542. flex: 1;
  4543. }
  4544. .doc-icon {
  4545. width: 40px;
  4546. height: 40px;
  4547. display: flex;
  4548. align-items: center;
  4549. justify-content: center;
  4550. }
  4551. .preview-thumbnail {
  4552. width: 100%;
  4553. height: 100%;
  4554. object-fit: cover;
  4555. border-radius: 4px;
  4556. }
  4557. /* .file-icon {
  4558. width: 24px;
  4559. height: 24px;
  4560. } */
  4561. .info {
  4562. flex: 1;
  4563. min-width: 0;
  4564. .name {
  4565. font-size: 14px;
  4566. color: var(--primary-text);
  4567. margin-bottom: 4px;
  4568. white-space: nowrap;
  4569. overflow: hidden;
  4570. text-overflow: ellipsis;
  4571. }
  4572. .size {
  4573. font-size: 12px;
  4574. color: var(--secondary-text);
  4575. }
  4576. }
  4577. .actions {
  4578. display: flex;
  4579. gap: 8px;
  4580. }
  4581. .action-btn {
  4582. width: 32px;
  4583. height: 32px;
  4584. border: none;
  4585. background: transparent;
  4586. border-radius: 4px;
  4587. cursor: pointer;
  4588. display: flex;
  4589. align-items: center;
  4590. justify-content: center;
  4591. color: var(--primary-text);
  4592. transition: all 0.3s;
  4593. &:hover {
  4594. background: rgba(0, 0, 0, 0.04);
  4595. color: var(--primary-color);
  4596. }
  4597. }
  4598. .direct-preview {
  4599. flex: 1;
  4600. display: flex;
  4601. flex-direction: column;
  4602. overflow: hidden;
  4603. background: var(--secondary-bg);
  4604. border-radius: 8px;
  4605. }
  4606. .preview-header {
  4607. padding: 16px;
  4608. border-bottom: 1px solid var(--border-color);
  4609. display: flex;
  4610. justify-content: space-between;
  4611. align-items: center;
  4612. span {
  4613. font-size: 16px;
  4614. font-weight: 500;
  4615. color: var(--primary-text);
  4616. }
  4617. .close-btn {
  4618. border: none;
  4619. background: transparent;
  4620. cursor: pointer;
  4621. color: var(--primary-text);
  4622. display: flex;
  4623. align-items: center;
  4624. justify-content: center;
  4625. padding: 8px;
  4626. border-radius: 4px;
  4627. transition: all 0.3s;
  4628. &:hover {
  4629. background: rgba(0, 0, 0, 0.04);
  4630. color: var(--primary-color);
  4631. }
  4632. }
  4633. }
  4634. .preview-body {
  4635. flex: 1;
  4636. overflow: auto;
  4637. padding: 24px;
  4638. display: flex;
  4639. align-items: flex-start;
  4640. justify-content: center;
  4641. }
  4642. .preview-image {
  4643. max-width: 100%;
  4644. max-height: 70vh;
  4645. object-fit: contain;
  4646. }
  4647. .preview-video {
  4648. max-width: 100%;
  4649. max-height: 70vh;
  4650. }
  4651. .doc-preview {
  4652. width: 100%;
  4653. height: 100%;
  4654. min-height: 500px;
  4655. }
  4656. :deep(.docx-wrapper) {
  4657. width: 100%;
  4658. height: 100%;
  4659. .docx{
  4660. width: 100% !important;
  4661. }
  4662. }
  4663. :deep(.excel-wrapper) {
  4664. width: 100%;
  4665. height: 100%;
  4666. overflow: auto;
  4667. .excel-container {
  4668. min-width: 100%;
  4669. min-height: 100%;
  4670. }
  4671. .excel-toolbar {
  4672. position: sticky;
  4673. top: 0;
  4674. z-index: 1;
  4675. background: #fff;
  4676. padding: 8px;
  4677. border-bottom: 1px solid #e8e8e8;
  4678. }
  4679. table {
  4680. width: 100%;
  4681. border-collapse: collapse;
  4682. th, td {
  4683. padding: 8px;
  4684. border: 1px solid #e8e8e8;
  4685. white-space: nowrap;
  4686. overflow: hidden;
  4687. text-overflow: ellipsis;
  4688. }
  4689. }
  4690. }
  4691. .file-preview {
  4692. text-align: center;
  4693. color: var(--primary-text);
  4694. .large-icon {
  4695. font-size: 64px;
  4696. margin-bottom: 16px;
  4697. }
  4698. p {
  4699. margin: 0;
  4700. font-size: 14px;
  4701. }
  4702. }
  4703. // 响应式布局
  4704. @media screen and (max-width: 768px) {
  4705. .preview-container {
  4706. flex-direction: column;
  4707. }
  4708. .document-list {
  4709. width: 100%;
  4710. border-right: none;
  4711. border-bottom: 1px solid var(--border-color);
  4712. }
  4713. }
  4714. // ... existing code ...
  4715. </style>
  4716. <style>
  4717. /* 移动端适配样式 */
  4718. @media screen and (max-width: 768px) {
  4719. .suggestion-section{
  4720. padding: 20px 0 !important;
  4721. }
  4722. .layout {
  4723. flex-direction: column;
  4724. }
  4725. .header {
  4726. /* padding: 10px; */
  4727. }
  4728. .user-profile-bar {
  4729. flex-direction: row;
  4730. align-items: center;
  4731. justify-content: space-between;
  4732. width: 100%;
  4733. padding: 0 10px;
  4734. flex-wrap: nowrap;
  4735. }
  4736. .logo {
  4737. flex-shrink: 0;
  4738. max-width: 120px;
  4739. }
  4740. .user-info-bar {
  4741. display: flex;
  4742. align-items: center;
  4743. gap: 8px;
  4744. flex: 1;
  4745. justify-content: flex-end;
  4746. min-width: 0;
  4747. }
  4748. .theme-switch {
  4749. flex-shrink: 0;
  4750. }
  4751. .user-info {
  4752. display: flex;
  4753. flex-direction: column;
  4754. font-size: 12px;
  4755. min-width: 0;
  4756. flex: 1;
  4757. }
  4758. .user-name {
  4759. overflow: hidden;
  4760. text-overflow: ellipsis;
  4761. white-space: nowrap;
  4762. margin: 0;
  4763. line-height: 1.2;
  4764. }
  4765. .avatar {
  4766. flex-shrink: 0;
  4767. width: 32px;
  4768. height: 32px;
  4769. margin-left: 8px;
  4770. }
  4771. .suggestion-item{
  4772. width: 85%;
  4773. margin: 0 auto;
  4774. }
  4775. @media screen and (max-width: 480px) {
  4776. .user-info-bar {
  4777. gap: 4px;
  4778. }
  4779. .logo {
  4780. max-width: 150px;
  4781. }
  4782. .avatar {
  4783. width: 28px;
  4784. height: 28px;
  4785. margin-left: 4px;
  4786. }
  4787. }
  4788. .logo {
  4789. margin-bottom: 10px;
  4790. }
  4791. .logo-img {
  4792. width: 60px;
  4793. }
  4794. .user-info-bar {
  4795. width: 100%;
  4796. justify-content: space-between;
  4797. }
  4798. .menu {
  4799. position: fixed;
  4800. top: 60px;
  4801. left: -100%;
  4802. width: 80%;
  4803. height: calc(100vh - 60px);
  4804. z-index: 1000;
  4805. transition: left 0.3s ease;
  4806. }
  4807. .menu.collapsed {
  4808. left: 0;
  4809. }
  4810. .chat {
  4811. width: 100%;
  4812. margin-left: 0;
  4813. padding: 0;
  4814. }
  4815. .message-list {
  4816. padding: 10px;
  4817. }
  4818. .message-bubble {
  4819. max-width: 90%;
  4820. }
  4821. .config-container {
  4822. padding: 10px;
  4823. }
  4824. .cards-container {
  4825. flex-direction: column;
  4826. }
  4827. .suggestion-card {
  4828. width: 90% !important;
  4829. padding: 16px 2px !important;
  4830. margin: 10px auto !important;
  4831. }
  4832. .collapse-left-button {
  4833. position: fixed;
  4834. top: 70px;
  4835. left: 10px;
  4836. z-index: 1001;
  4837. }
  4838. }
  4839. /* 针对更小屏幕的优化 */
  4840. @media screen and (max-width: 480px) {
  4841. .user-info {
  4842. font-size: 12px;
  4843. }
  4844. .theme-icon {
  4845. width: 20px;
  4846. height: 20px;
  4847. }
  4848. .avatar img {
  4849. width: 30px;
  4850. height: 30px;
  4851. }
  4852. .message-content {
  4853. font-size: 14px;
  4854. }
  4855. .new-conversation-btn {
  4856. padding: 8px;
  4857. font-size: 14px;
  4858. }
  4859. }
  4860. /* 移动端样式调整 */
  4861. @media screen and (max-width: 768px) {
  4862. .layout {
  4863. height: 100vh; /* 使用视口高度 */
  4864. overflow: hidden; /* 防止页面滚动 */
  4865. display: flex;
  4866. flex-direction: column;
  4867. }
  4868. .header {
  4869. flex-shrink: 0; /* 防止header被压缩 */
  4870. }
  4871. .chat {
  4872. flex: 1; /* 占据剩余空间 */
  4873. height: calc(100vh - 60px); /* 减去header高度 */
  4874. overflow-y: auto; /* 内容过多时在chat区域内滚动 */
  4875. -webkit-overflow-scrolling: touch; /* 优化iOS滚动 */
  4876. }
  4877. .message-list {
  4878. height: calc(100% - 120px); /* 减去输入框高度 */
  4879. overflow-y: auto;
  4880. }
  4881. .input-wrapper {
  4882. position: sticky;
  4883. bottom: 0;
  4884. background: var(--primary-bg);
  4885. padding: 10px;
  4886. border-top: 1px solid var(--border-color);
  4887. }
  4888. .resize-handle {
  4889. display: none;
  4890. }
  4891. .right_menu {
  4892. width: 100% !important;
  4893. }
  4894. }
  4895. /* PC端样式保持不变 */
  4896. @media screen and (min-width: 769px) {
  4897. .layout {
  4898. height: 100vh;
  4899. display: flex;
  4900. flex-direction: column;
  4901. }
  4902. .chat {
  4903. flex: 1;
  4904. display: flex;
  4905. flex-direction: column;
  4906. }
  4907. .message-list {
  4908. flex: 1;
  4909. overflow-y: auto;
  4910. }
  4911. }
  4912. .thinking-steps {
  4913. margin-bottom: 10px;
  4914. font-size: 12px;
  4915. color: #666;
  4916. }
  4917. .thinking-step {
  4918. padding: 4px 8px;
  4919. margin-bottom: 4px;
  4920. border-radius: 4px;
  4921. background: #f5f5f5;
  4922. transition: all 0.3s ease;
  4923. }
  4924. .thinking-step.start {
  4925. background: #e8f5e9;
  4926. }
  4927. .thinking-step.text_analysis {
  4928. background: #e3f2fd;
  4929. }
  4930. .thinking-step.planning {
  4931. background: #fff3e0;
  4932. }
  4933. .thinking-step.execution {
  4934. background: #f3e5f5;
  4935. }
  4936. .thinking-step.result {
  4937. background: #e8eaf6;
  4938. }
  4939. .thinking-step.conclusion {
  4940. background: #e0f7fa;
  4941. font-weight: 500;
  4942. }
  4943. </style>
  4944. <style scoped>
  4945. .thinking-steps-stream {
  4946. position: sticky;
  4947. top: 0;
  4948. z-index: 100;
  4949. background: rgba(255, 255, 255, 0.95);
  4950. padding: 10px;
  4951. margin: 10px 0;
  4952. border-radius: 8px;
  4953. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  4954. .thinking-step-stream {
  4955. padding: 8px 12px;
  4956. margin: 4px 0;
  4957. border-radius: 6px;
  4958. background: #f5f5f5;
  4959. font-size: 14px;
  4960. line-height: 1.5;
  4961. opacity: 0;
  4962. transform: translateY(-10px);
  4963. &.fade-in {
  4964. animation: fadeInDown 0.3s ease forwards;
  4965. }
  4966. .typing-indicator {
  4967. display: inline-flex;
  4968. align-items: center;
  4969. margin-left: 8px;
  4970. span {
  4971. width: 4px;
  4972. height: 4px;
  4973. margin: 0 2px;
  4974. background: #666;
  4975. border-radius: 50%;
  4976. animation: typing 1s infinite ease-in-out;
  4977. &:nth-child(1) { animation-delay: 0.2s; }
  4978. &:nth-child(2) { animation-delay: 0.3s; }
  4979. &:nth-child(3) { animation-delay: 0.4s; }
  4980. }
  4981. }
  4982. }
  4983. }
  4984. @keyframes fadeInDown {
  4985. from {
  4986. opacity: 0;
  4987. transform: translateY(-10px);
  4988. }
  4989. to {
  4990. opacity: 1;
  4991. transform: translateY(0);
  4992. }
  4993. }
  4994. @keyframes typing {
  4995. 0%, 60%, 100% { transform: translateY(0); }
  4996. 30% { transform: translateY(-4px); }
  4997. }
  4998. </style>