| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>聚德策划大师-大健康AI文案撰写</title>
- <meta name="description" content="聚德AI短视频脚本撰写器 - 专业高效的文案创作平台">
- <meta name="theme-color" content="#6366f1" media="(prefers-color-scheme: light)">
- <meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)">
- <meta name="color-scheme" content="light dark">
- <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
- <link rel="stylesheet" href="{{ url_for('static', filename='css/modal.css') }}">
- <link rel="stylesheet" href="{{ url_for('static', filename='css/components/copy-button.css') }}">
- <link rel="icon" href="{{ url_for('static', filename='logo.png') }}">
- <link rel="stylesheet" href="{{ url_for('static', filename='css/github.min.css') }}">
- <link rel="stylesheet" href="{{ url_for('static', filename='css/highlightjs.min.css') }}">
- <link rel="stylesheet" href="{{ url_for('static', filename='css/fonts.css') }}">
- <!-- 思考过程样式 -->
- <style>
- /* AI思考过程样式 */
- .thought-container {
- background-color: #f3f0fa;
- border-left: 3px solid #7952b3;
- padding: 10px 15px;
- margin: 10px 0 10px 0;
- border-radius: 0 4px 4px 0;
- font-style: italic;
- color: #7952b3;
- position: relative;
- font-size: 0.98em;
- }
- .thought-container::before {
- content: "思考过程";
- display: block;
- font-size: 0.75rem;
- color: #7952b3;
- font-weight: 600;
- margin-bottom: 4px;
- font-style: normal;
- }
- .thought-toggle-btn {
- position: absolute;
- top: 8px;
- right: 12px;
- background: none;
- border: none;
- color: #7952b3;
- font-size: 1.1em;
- cursor: pointer;
- z-index: 2;
- padding: 2px 6px;
- border-radius: 3px;
- transition: background 0.2s;
- }
- .thought-toggle-btn:hover {
- background: #e5e1f6;
- }
- /* 思考过程内容区域样式 - 连续文本块 */
- .thought-content {
- overflow-y: auto;
- max-height: 500px;
- transition: max-height 0.35s cubic-bezier(.4, 0, .2, 1), padding 0.3s;
- will-change: max-height;
- /* 自定义滚动条样式 */
- scrollbar-width: thin;
- scrollbar-color: #d1c4e9 #f3f0fa;
- white-space: normal;
- /* 正常换行处理 */
- word-wrap: break-word;
- /* 长单词换行 */
- line-height: 1.4;
- /* 适中的行高 */
- text-align: left;
- /* 左对齐 */
- }
- /* Webkit浏览器的滚动条样式 */
- .thought-content::-webkit-scrollbar {
- width: 8px;
- }
- .thought-content::-webkit-scrollbar-track {
- background: #f3f0fa;
- border-radius: 4px;
- }
- .thought-content::-webkit-scrollbar-thumb {
- background: linear-gradient(135deg, #d1c4e9, #b39ddb);
- border-radius: 4px;
- border: 1px solid #f3f0fa;
- transition: background 0.2s ease;
- }
- .thought-content::-webkit-scrollbar-thumb:hover {
- background: linear-gradient(135deg, #b39ddb, #9575cd);
- }
- .thought-content::-webkit-scrollbar-thumb:active {
- background: linear-gradient(135deg, #9575cd, #7e57c2);
- }
- .thought-container.collapsed .thought-content {
- max-height: 0;
- padding: 0 !important;
- }
- .thought-container.collapsed::before {
- content: "思考过程(已收起)";
- }
- .thought-container p {
- margin: 0;
- font-style: italic;
- color: #7952b3;
- background: none;
- border-bottom: none;
- padding-bottom: 0;
- line-height: 1.6;
- }
- .thought-container p:last-child {
- border-bottom: none;
- }
- /* 对话项样式优化 */
- .conversation-meta {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-top: 4px;
- font-size: 0.75rem;
- color: #666;
- opacity: 0.8;
- }
- .message-count {
- font-size: 0.7rem;
- }
- .last-time {
- font-size: 0.7rem;
- color: #999;
- }
- .conversation-item.active .conversation-meta {
- color: rgba(99, 102, 241, 0.8);
- }
- .conversation-item.active .last-time {
- color: rgba(99, 102, 241, 0.6);
- }
- /* 对话状态指示器样式 */
- .conversation-item.streaming .conversation-title {
- color: #10b981;
- font-weight: 500;
- }
- .conversation-item.paused .conversation-title {
- color: #f59e0b;
- font-weight: 500;
- }
- .conversation-item.streaming .conversation-meta {
- color: #10b981;
- }
- .conversation-item.paused .conversation-meta {
- color: #f59e0b;
- }
- /* 当前活跃对话指示器 */
- .conversation-item.active {
- background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(99, 102, 241, 0.05));
- border-left: 3px solid #6366f1;
- position: relative;
- }
- /* 对话切换动画 */
- .conversation-item {
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- cursor: pointer;
- }
- .conversation-item:hover {
- background: rgba(99, 102, 241, 0.05);
- transform: translateX(2px);
- }
- .conversation-item.active:hover {
- background: linear-gradient(135deg, rgba(99, 102, 241, 0.15), rgba(99, 102, 241, 0.08));
- }
- /* 对话状态点指示器 */
- .conversation-status-dot {
- position: absolute;
- top: 12px;
- right: 12px;
- width: 8px;
- height: 8px;
- border-radius: 50%;
- opacity: 0;
- transition: opacity 0.3s ease;
- }
- .conversation-item.streaming .conversation-status-dot {
- background: #10b981;
- opacity: 1;
- animation: pulse-dot 2s infinite;
- }
- .conversation-item.paused .conversation-status-dot {
- background: #f59e0b;
- opacity: 1;
- animation: pulse-dot 2s infinite;
- }
- .conversation-item.active .conversation-status-dot {
- background: #6366f1;
- opacity: 1;
- }
- @keyframes pulse-dot {
- 0% {
- opacity: 1;
- transform: scale(1);
- }
- 50% {
- opacity: 0.5;
- transform: scale(1.2);
- }
- 100% {
- opacity: 1;
- transform: scale(1);
- }
- }
- /* 编辑按钮样式 - 与复制按钮保持一致 */
- .edit-button {
- position: absolute;
- width: 32px;
- height: 32px;
- background: var(--primary-indigo);
- color: white;
- border: none;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- z-index: 1001;
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
- transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
- opacity: 0;
- bottom: 8px;
- right: 48px;
- /* 位于复制按钮左侧 */
- }
- /* 悬停时显示编辑按钮 */
- .message.user:hover .edit-button {
- opacity: 1;
- }
- .edit-button:hover {
- transform: scale(1.1);
- box-shadow: 0 6px 10px rgba(0, 0, 0, 0.25);
- }
- .edit-button svg {
- width: 18px;
- height: 18px;
- }
- /* 内联编辑样式 */
- .message-editing {
- position: relative;
- }
- .edit-form {
- display: flex;
- flex-direction: column;
- gap: 12px;
- padding: 16px;
- background: #f8f9fa;
- border: 2px solid #6366f1;
- border-radius: 12px;
- margin: 8px 0;
- width: 600px;
- }
- .edit-textarea {
- width: 100%;
- min-height: 80px;
- padding: 12px;
- border: none;
- border-radius: 8px;
- font-size: 14px;
- line-height: 1.5;
- resize: vertical;
- font-family: inherit;
- background: white;
- }
- .edit-textarea:focus {
- outline: none;
- box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
- }
- .edit-actions {
- display: flex;
- gap: 12px;
- justify-content: flex-end;
- }
- .edit-cancel-btn {
- padding: 4px 10px;
- background: white;
- border: 1px solid #ddd;
- border-radius: 6px;
- color: #666;
- cursor: pointer;
- font-size: 14px;
- transition: all 0.2s ease;
- }
- .edit-cancel-btn:hover {
- background: #f5f5f5;
- border-color: #ccc;
- }
- .edit-send-btn {
- padding: 4px 10px;
- background: #6366f1;
- border: none;
- border-radius: 6px;
- color: white;
- cursor: pointer;
- font-size: 14px;
- transition: all 0.2s ease;
- }
- .edit-send-btn:hover {
- background: #5b5cf6;
- }
- .edit-send-btn:disabled {
- background: #ccc;
- cursor: not-allowed;
- }
- </style>
- </head>
- <body>
- <div class="glass-particles"></div>
- <!-- 移动端侧边栏切换按钮 -->
- <button class="sidebar-toggle" id="sidebar-toggle" style="display: none;">
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M3 12h18M3 6h18M3 18h18" stroke="currentColor" stroke-width="2" stroke-linecap="round"
- stroke-linejoin="round" />
- </svg>
- </button>
- <!-- 移动端侧边栏遮罩层 -->
- <div class="sidebar-overlay" id="sidebar-overlay"></div>
- <div class="chat-container">
- <!-- 历史记录侧边栏 -->
- <div class="history-sidebar" id="history-sidebar">
- <div class="sidebar-header">
- <button class="new-chat-btn" id="new-chat-btn">
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"
- stroke-linejoin="round" />
- </svg>
- 开启新对话
- </button>
- </div>
- <div class="history-content">
- <!-- 历史记录将通过JavaScript动态加载 -->
- <div class="history-loading" id="history-loading"
- style="display: flex; align-items: center; justify-content: center; padding: 20px; color: #666;">
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
- style="margin-right: 8px; animation: spin 1s linear infinite;">
- <path d="M21 12a9 9 0 11-6.219-8.56" stroke="currentColor" stroke-width="2"
- stroke-linecap="round" stroke-linejoin="round" />
- </svg>
- 正在加载历史记录...
- </div>
- <div class="history-empty" id="history-empty"
- style="display: none; text-align: center; padding: 40px 20px; color: #999;">
- <svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
- style="margin-bottom: 16px; opacity: 0.5;">
- <path d="M8 2v4M16 2v4M3 10h18M5 4h14a2 2 0 012 2v14a2 2 0 01-2 2H5a2 2 0 01-2-2V6a2 2 0 012-2z"
- stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
- </svg>
- <div style="font-size: 16px; margin-bottom: 8px;">暂无历史记录</div>
- <div style="font-size: 14px; opacity: 0.7;">开始新的对话吧!</div>
- </div>
- </div>
- </div>
- <!-- 主聊天区域 -->
- <div class="main-chat-area">
- <header class="glass-header">
- <div class="logo-title-container">
- <img src="{{ url_for('static', filename='logo.png') }}" alt="聚德AI短视频脚本撰写" class="header-logo">
- <h1>聚德策划大师-大健康AI文案撰写<span class="version-tag">v 1.6.3</span></h1>
- <a class="logo-title-link" href="https://wj.qq.com/s2/23610733/1a16/" target="_blank"
- class="jude-link">问题反馈</a>
- </div>
- <div class="buttons-container">
- <!-- 智能体选择(仅智能策划大师) -->
- <div id="agent-dropdown" class="glass-btn" style="position: relative;">
- <img id="agent-trigger-icon" src="{{ url_for('static', filename='img/planning.png') }}"
- alt="平台图标" width="16" height="16" onerror="this.style.display='none'">
- <span id="agent-trigger-text">智能策划大师</span>
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round"
- stroke-linejoin="round" />
- </svg>
- <div id="agent-menu" class="glass-panel"
- style="display:none; position: absolute; top: calc(100% + 6px); left: 0; z-index: 1001; min-width: 220px;">
- <ul style="list-style: none; margin: 0; padding: 6px;">
- <li class="agent-option" data-agent="planning"
- style="display:flex; align-items:center; gap:8px; padding:12px; border-radius:8px; cursor:pointer; transition: all 0.2s ease; user-select: none;"
- onmouseover="this.style.background='rgba(99, 102, 241, 0.1)'"
- onmouseout="this.style.background=''">
- <img src="{{ url_for('static', filename='img/planning.png') }}" alt="智能策划大师"
- width="16" height="16" onerror="this.style.display='none'">
- <div style="display:flex; flex-direction:column;">
- <span style="font-weight: 500;">智能策划大师</span>
- </div>
- </li>
- <li class="agent-option" data-agent="pathology"
- style="display:flex; align-items:center; gap:8px; padding:12px; border-radius:8px; cursor:pointer; transition: all 0.2s ease; user-select: none;"
- onmouseover="this.style.background='rgba(99, 102, 241, 0.1)'"
- onmouseout="this.style.background=''">
- <img src="{{ url_for('static', filename='img/videoaccount.svg') }}" alt="病理分析"
- width="16" height="16" onerror="this.style.display='none'">
- <div style="display:flex; flex-direction:column;">
- <span style="font-weight: 500;">病理分析</span>
- </div>
- </li>
- <!-- 投放诊断 -->
- <li class="agent-option" data-agent="diagnosis"
- style="display:flex; align-items:center; gap:8px; padding:12px; border-radius:8px; cursor:pointer; transition: all 0.2s ease; user-select: none;"
- onmouseover="this.style.background='rgba(99, 102, 241, 0.1)'"
- onmouseout="this.style.background=''">
- <img src="{{ url_for('static', filename='img/diagnosis.svg') }}" alt="投放诊断"
- width="16" height="16" onerror="this.style.display='none'">
- <div style="display:flex; flex-direction:column;">
- <span style="font-weight: 500;">投放诊断</span>
- </div>
- </li>
- </ul>
- </div>
- </div>
- <button id="reset-btn" class="btn glass-btn">
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path
- d="M4 4V9H4.58152M19.9381 11C19.446 7.05369 16.0796 4 12 4C8.64262 4 5.76829 6.06817 4.58152 9M4.58152 9H9M20 20V15H19.4185M19.4185 15C18.2317 17.9318 15.3574 20 12 20C7.92038 20 4.55399 16.9463 4.06189 13M19.4185 15H15"
- stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
- </svg>
- 重置对话
- </button>
- <!-- 用户信息 -->
- <div class="user-info" id="user-info" style="display: none;">
- <div class="user-details">
- <div class="user-avatar" id="user-avatar">
- <img id="avatar-img" src="" alt="用户头像" onerror="this.style.display='none'">
- <div id="avatar-fallback" class="avatar-fallback"></div>
- </div>
- <div class="user-text">
- <div class="user-info-row">
- <span class="user-name" id="user-name"></span>
- <span class="user-type" id="user-type"></span>
- </div>
- <div class="user-info-row">
- <span class="user-title" id="user-title" style="display: none;"></span>
- </div>
- </div>
- </div>
- <div class="user-dropdown" id="user-dropdown" style="display: none;">
- <div class="user-dropdown-content">
- <button id="logout-btn" class="btn glass-btn logout-btn">
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none"
- xmlns="http://www.w3.org/2000/svg">
- <path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4M16 3l4 4-4 4M21 7H9"
- stroke="currentColor" stroke-width="2" stroke-linecap="round"
- stroke-linejoin="round" />
- </svg>
- 退出登录
- </button>
- </div>
- </div>
- </div>
- </div>
- </header>
- <!-- 移动端菜单切换按钮 -->
- <div class="chat-main-content">
- <div id="chat-messages" class="chat-messages glass-panel">
- <div class="message bot">
- <div class="message-content">您好,我是聚德策划大师。请输入您需要的内容,例如:
- <ul>
- <li>请输入需要撰写的产品或疾病名称</li>
- <li>补充目标人群、投放场景、文案风格</li>
- </ul>
- </div>
- <div class="message-content">我将为您输出可直接使用的营销文案、脚本方向和优化建议。</div>
- </div>
- </div>
- <div class="chat-input-container glass-panel">
- <textarea id="user-input" placeholder="请输入需要撰写的品类名和要求..." rows="1"></textarea>
- <button id="send-btn" class="glass-btn-primary" aria-label="发送消息">
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M22 2L11 13M22 2L15 22L11 13M11 13L2 9L22 2" stroke="currentColor" stroke-width="2"
- stroke-linecap="round" stroke-linejoin="round" />
- </svg>
- </button>
- </div>
- </div>
- </div>
- </div>
- <!-- 页面启动保护:避免脚本异常时出现纯白屏 -->
- <script>
- (function () {
- function showFatalScreen (msg) {
- if (document.getElementById('fatal-boot-error')) return;
- const box = document.createElement('div');
- box.id = 'fatal-boot-error';
- box.style.cssText = 'position:fixed;inset:0;z-index:99999;background:#f8fafc;color:#1f2937;display:flex;align-items:center;justify-content:center;padding:24px;';
- box.innerHTML = `
- <div style="max-width:680px;background:#fff;border:1px solid #e5e7eb;border-radius:10px;box-shadow:0 10px 20px rgba(0,0,0,.08);padding:20px;">
- <h3 style="margin:0 0 10px;font-size:18px;">页面加载失败</h3>
- <p style="margin:0 0 12px;line-height:1.6;">${msg || '检测到关键脚本加载异常,已阻止白屏。请先刷新页面,若仍失败请联系管理员清理缓存并检查静态资源发布。'}</p>
- <div style="display:flex;gap:10px;">
- <button onclick="location.reload()" style="border:0;background:#4f46e5;color:#fff;border-radius:6px;padding:8px 14px;cursor:pointer;">刷新页面</button>
- <button onclick="location.href='/login'" style="border:1px solid #d1d5db;background:#fff;color:#111827;border-radius:6px;padding:8px 14px;cursor:pointer;">重新登录</button>
- </div>
- </div>
- `;
- document.body.appendChild(box);
- }
- window.__bootFailedScripts = [];
- window.__onScriptLoadError = function (src) {
- window.__bootFailedScripts.push(src || 'unknown');
- showFatalScreen('关键静态资源加载失败:' + window.__bootFailedScripts.join(', '));
- };
- window.addEventListener('error', function (e) {
- if (e && e.message) {
- showFatalScreen('页面初始化异常:' + e.message);
- }
- });
- window.addEventListener('unhandledrejection', function (e) {
- const reason = e && e.reason ? (e.reason.message || String(e.reason)) : '未知Promise异常';
- showFatalScreen('页面初始化异常:' + reason);
- });
- window.addEventListener('load', function () {
- setTimeout(function () {
- if (window.__bootFailedScripts.length > 0) return;
- if (typeof window.fetch !== 'function') {
- showFatalScreen('浏览器环境异常:fetch不可用。');
- }
- }, 1200);
- });
- })();
- </script>
- <!-- 核心JS库 -->
- <script src="{{ url_for('static', filename='js/highlight.min.js') }}"
- onerror="window.__onScriptLoadError('highlight.min.js')"></script>
- <script src="{{ url_for('static', filename='js/marked.min.js') }}"
- onerror="window.__onScriptLoadError('marked.min.js')"></script>
- <script src="{{ url_for('static', filename='js/showdown.min.js') }}"
- onerror="window.__onScriptLoadError('showdown.min.js')"></script>
- <script src="{{ url_for('static', filename='js/copy-buttons.js') }}"
- onerror="window.__onScriptLoadError('copy-buttons.js')"></script>
- <script src="{{ url_for('static', filename='js/chat.js') }}"
- onerror="window.__onScriptLoadError('chat.js')"></script>
- <!-- 认证和用户信息管理 -->
- <!-- 自定义弹框 -->
- <!-- 删除确认弹框 -->
- <div id="delete-modal" class="custom-modal">
- <div class="modal-overlay"></div>
- <div class="modal-content">
- <div class="modal-header">
- <h3 class="modal-title">删除对话</h3>
- <button class="modal-close" onclick="closeModal('delete-modal')">
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"
- stroke-linejoin="round" />
- </svg>
- </button>
- </div>
- <div class="modal-body">
- <div class="modal-icon delete-icon">
- <svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"
- stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
- <path d="M10 11v6M14 11v6" stroke="currentColor" stroke-width="2" stroke-linecap="round"
- stroke-linejoin="round" />
- </svg>
- </div>
- <p class="modal-message">确定要删除这个对话吗?删除后将无法恢复。</p>
- </div>
- <div class="modal-footer">
- <button class="btn btn-secondary" onclick="closeModal('delete-modal')">取消</button>
- <button class="btn btn-danger" id="confirm-delete-btn">删除</button>
- </div>
- </div>
- </div>
- <!-- 重命名弹框 -->
- <div id="rename-modal" class="custom-modal">
- <div class="modal-overlay"></div>
- <div class="modal-content">
- <div class="modal-header">
- <h3 class="modal-title">重命名对话</h3>
- <button class="modal-close" onclick="closeModal('rename-modal')">
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"
- stroke-linejoin="round" />
- </svg>
- </button>
- </div>
- <div class="modal-body">
- <div class="form-group">
- <input type="text" id="rename-input" class="form-input" placeholder="请输入新的对话标题" maxlength="50">
- <div class="form-hint">最多50个字符</div>
- </div>
- </div>
- <div class="modal-footer">
- <button class="btn btn-secondary" onclick="closeModal('rename-modal')">取消</button>
- <button class="btn btn-primary" id="confirm-rename-btn">保存</button>
- </div>
- </div>
- </div>
- <!-- 消息提示弹框 -->
- <div id="message-modal" class="custom-modal">
- <div class="modal-overlay"></div>
- <div class="modal-content message-modal-content">
- <div class="modal-body">
- <div class="modal-icon message-icon">
- <svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M9 12l2 2 4-4M21 12a9 9 0 11-18 0 9 9 0 0118 0z" stroke="currentColor" stroke-width="2"
- stroke-linecap="round" stroke-linejoin="round" />
- </svg>
- </div>
- <p class="modal-message" id="message-text">操作成功</p>
- </div>
- <div class="modal-footer">
- <button class="btn btn-primary" onclick="closeModal('message-modal')">确定</button>
- </div>
- </div>
- </div>
- <script>
- // 弹框控制函数
- function showModal (modalId) {
- const modal = document.getElementById(modalId);
- if (modal) {
- modal.classList.add('show');
- document.body.style.overflow = 'hidden'; // 防止背景滚动
- }
- }
- function closeModal (modalId) {
- const modal = document.getElementById(modalId);
- if (modal) {
- modal.classList.remove('show');
- document.body.style.overflow = ''; // 恢复滚动
- }
- }
- function showMessage (message, type = 'success') {
- const messageText = document.getElementById('message-text');
- const messageIcon = document.querySelector('#message-modal .modal-icon');
- if (messageText) {
- messageText.textContent = message;
- }
- if (messageIcon) {
- // 根据类型设置图标颜色
- messageIcon.className = 'modal-icon message-icon';
- if (type === 'error') {
- messageIcon.style.color = '#ef4444';
- } else if (type === 'warning') {
- messageIcon.style.color = '#f59e0b';
- } else {
- messageIcon.style.color = '#10b981';
- }
- }
- showModal('message-modal');
- }
- // 点击遮罩层关闭弹框
- document.addEventListener('click', function (e) {
- if (e.target.classList.contains('modal-overlay')) {
- const modal = e.target.closest('.custom-modal');
- if (modal) {
- closeModal(modal.id);
- }
- }
- });
- // ESC键关闭弹框
- document.addEventListener('keydown', function (e) {
- if (e.key === 'Escape') {
- const openModal = document.querySelector('.custom-modal.show');
- if (openModal) {
- closeModal(openModal.id);
- }
- }
- });
- // 防重复重定向标志
- let isRedirecting = false;
- // 页面加载时检查认证状态
- document.addEventListener('DOMContentLoaded', function () {
- checkAuthStatus();
- });
- // 思考过程折叠/展开函数
- function toggleThought (button) {
- const container = button.closest('.thought-container');
- const content = container.querySelector('.thought-content');
- const icon = button.querySelector('svg path');
- if (container.dataset.collapsed === 'true') {
- // 展开
- container.dataset.collapsed = 'false';
- content.style.maxHeight = '500px';
- content.style.padding = '10px 0';
- icon.setAttribute('d', 'M6 9l6 6 6-6');
- } else {
- // 折叠
- container.dataset.collapsed = 'true';
- content.style.maxHeight = '0';
- content.style.padding = '0';
- icon.setAttribute('d', 'M18 15l-6-6-6 6');
- }
- }
- function checkAuthStatus () {
- // 检查URL参数中是否有登录成功信息
- const urlParams = new URLSearchParams(window.location.search);
- const loginSuccess = urlParams.get('login_success');
- const token = urlParams.get('token');
- const userInfoJson = urlParams.get('user_info');
- if (loginSuccess && token && userInfoJson) {
- // 登录成功,处理用户信息
- try {
- const userInfo = JSON.parse(decodeURIComponent(userInfoJson));
- // 保存到localStorage
- localStorage.setItem('auth_token', token);
- localStorage.setItem('user_info', JSON.stringify(userInfo));
- // 显示用户信息
- displayUserInfo(userInfo);
- showUserSection();
- addAuthHeader(token);
- // 清理URL参数
- window.history.replaceState({}, document.title, window.location.pathname);
- // 初始化历史记录侧边栏
- initializeHistorySidebar();
- // 启动对话状态更新器
- if (typeof startConversationStatusUpdater === 'function') {
- startConversationStatusUpdater();
- }
- } catch (e) {
- redirectToLogin();
- }
- } else {
- // 正常检查localStorage中的认证信息
- const storedToken = localStorage.getItem('auth_token');
- const storedUserInfo = localStorage.getItem('user_info');
- if (storedToken && storedUserInfo) {
- try {
- const user = JSON.parse(storedUserInfo);
- // 验证用户信息的基本字段
- if (!user.name && !user.nickname) {
- user.name = '钉钉用户';
- user.nickname = '钉钉用户';
- }
- displayUserInfo(user);
- showUserSection();
- addAuthHeader(storedToken);
- // 初始化历史记录侧边栏
- initializeHistorySidebar();
- // 启动对话状态更新器
- if (typeof startConversationStatusUpdater === 'function') {
- startConversationStatusUpdater();
- }
- } catch (e) {
- redirectToLogin();
- }
- } else {
- redirectToLogin();
- }
- }
- }
- function displayUserInfo (user) {
- // 显示用户头像
- const avatarImg = document.getElementById('avatar-img');
- const avatarFallback = document.getElementById('avatar-fallback');
- if (user.avatar && user.avatar.trim() !== '') {
- avatarImg.src = user.avatar;
- avatarImg.style.display = 'block';
- avatarFallback.style.display = 'none';
- } else {
- avatarImg.style.display = 'none';
- avatarFallback.style.display = 'block';
- // 获取用户名的首字母作为头像
- const displayName = user.nickname || user.name || 'U';
- avatarFallback.textContent = displayName.charAt(0).toUpperCase();
- }
- // 显示用户信息
- const userName = document.getElementById('user-name');
- if (userName) {
- const displayName = user.nickname || user.name || '钉钉用户';
- userName.textContent = displayName;
- }
- const userType = document.getElementById('user-type');
- if (userType) {
- // 根据登录类型显示不同的标签样式
- if (user.login_type === 'dingtalk') {
- userType.textContent = '钉钉';
- userType.style.background = 'rgba(24, 144, 255, 0.2)';
- userType.style.color = '#1890ff';
- userType.style.border = '1px solid rgba(24, 144, 255, 0.3)';
- } else {
- userType.textContent = user.login_type || '钉钉';
- userType.style.background = 'rgba(255, 255, 255, 0.3)';
- userType.style.color = '#666';
- userType.style.border = '1px solid rgba(255, 255, 255, 0.2)';
- }
- }
- const userTitle = document.getElementById('user-title');
- if (userTitle && user.title) {
- userTitle.textContent = user.title;
- userTitle.style.display = 'block';
- }
- // 显示其他用户信息
- if (user.email) {
- const emailItem = document.getElementById('user-email');
- if (emailItem) {
- emailItem.querySelector('.info-value').textContent = user.email;
- emailItem.style.display = 'block';
- }
- }
- if (user.mobile) {
- const mobileItem = document.getElementById('user-mobile');
- if (mobileItem) {
- mobileItem.querySelector('.info-value').textContent = user.mobile;
- mobileItem.style.display = 'block';
- }
- }
- if (user.job_number) {
- const jobItem = document.getElementById('user-job');
- if (jobItem) {
- jobItem.querySelector('.info-value').textContent = user.job_number;
- jobItem.style.display = 'block';
- }
- }
- if (user.dept_name) {
- const deptItem = document.getElementById('user-dept');
- if (deptItem) {
- deptItem.querySelector('.info-value').textContent = user.dept_name;
- deptItem.style.display = 'block';
- }
- }
- }
- function showUserSection () {
- const userInfo = document.getElementById('user-info');
- if (userInfo) userInfo.style.display = 'flex';
- }
- function addAuthHeader (token) {
- // 注意:addAuthHeader函数由chat.js提供,这里不需要重复定义
- // 如果需要设置认证头,直接调用chat.js中的函数
- if (typeof window.addAuthHeader === 'function') {
- window.addAuthHeader(token);
- }
- }
- function redirectToLogin () {
- if (isRedirecting) {
- return;
- }
- isRedirecting = true;
- localStorage.removeItem('auth_token');
- localStorage.removeItem('user_info');
- // 添加延迟,避免频繁重定向
- setTimeout(() => {
- window.location.href = '/login';
- }, 100);
- }
- // 退出登录
- document.getElementById('logout-btn').addEventListener('click', function () {
- fetch('/api/auth/logout', {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
- 'Content-Type': 'application/json'
- }
- }).then(() => {
- redirectToLogin();
- }).catch(() => {
- redirectToLogin();
- });
- });
- // 用户信息下拉菜单(修复:添加鼠标进入/离开事件,防止移向退出登录按钮时菜单消失)
- function initializeUserDropdown () {
- const userInfo = document.getElementById('user-info');
- const dropdown = document.getElementById('user-dropdown');
- if (!userInfo || !dropdown) {
- return;
- }
- // 初始化时确保下拉菜单是隐藏状态(覆盖CSS的:hover效果)
- dropdown.style.display = 'none';
- dropdown.style.opacity = '0';
- dropdown.style.visibility = 'hidden';
- dropdown.style.transform = 'translateY(-15px) scale(0.95)';
- let isHovering = false;
- let hideTimer = null;
- function showDropdown () {
- if (hideTimer) {
- clearTimeout(hideTimer);
- hideTimer = null;
- }
- dropdown.style.display = 'block';
- dropdown.style.opacity = '1';
- dropdown.style.visibility = 'visible';
- dropdown.style.transform = 'translateY(0) scale(1)';
- }
- function scheduleHide () {
- if (hideTimer) {
- clearTimeout(hideTimer);
- }
- hideTimer = setTimeout(() => {
- if (!isHovering) {
- dropdown.style.display = 'none';
- dropdown.style.opacity = '0';
- dropdown.style.visibility = 'hidden';
- dropdown.style.transform = 'translateY(-15px) scale(0.95)';
- }
- }, 150); // 150ms延迟,给用户足够时间移动到按钮
- }
- // 鼠标进入用户信息区域
- userInfo.addEventListener('mouseenter', function () {
- isHovering = true;
- showDropdown();
- });
- // 鼠标离开用户信息区域
- userInfo.addEventListener('mouseleave', function () {
- isHovering = false;
- scheduleHide();
- });
- // 鼠标进入下拉菜单区域(重要:防止鼠标移动到按钮时菜单消失)
- dropdown.addEventListener('mouseenter', function () {
- isHovering = true;
- showDropdown();
- });
- // 鼠标离开下拉菜单区域
- dropdown.addEventListener('mouseleave', function () {
- isHovering = false;
- scheduleHide();
- });
- // 保留点击事件用于移动端
- document.getElementById('user-avatar').addEventListener('click', function (e) {
- // 在桌面端,如果已经在悬停显示状态,点击时不处理(避免冲突)
- if (window.innerWidth > 768 && dropdown.style.display === 'block') {
- return;
- }
- // 移动端切换显示/隐藏
- if (dropdown.style.display === 'none' || dropdown.style.display === '') {
- showDropdown();
- } else {
- dropdown.style.display = 'none';
- dropdown.style.opacity = '0';
- dropdown.style.visibility = 'hidden';
- dropdown.style.transform = 'translateY(-15px) scale(0.95)';
- }
- });
- }
- // 初始化用户下拉菜单
- document.addEventListener('DOMContentLoaded', function () {
- setTimeout(() => {
- initializeUserDropdown();
- }, 100);
- });
- const AGENT_META = {
- 'planning': {
- name: '智能策划大师',
- icon: "{{ url_for('static', filename='img/planning.png') }}"
- },
- 'pathology': {
- name: '病理分析',
- icon: "{{ url_for('static', filename='img/videoaccount.svg') }}"
- },
- 'diagnosis': {
- name: '投放诊断',
- icon: "{{ url_for('static', filename='img/diagnosis.svg') }}"
- },
- };
- const AGENT_WELCOME_CONTENT = {
- 'planning': [
- `您好,我是聚德策划大师。请输入您需要的内容,例如:
- <ul>
- <li>请输入需要撰写的产品或疾病名称</li>
- <li>补充目标人群、投放场景、文案风格</li>
- </ul>`,
- '我将为您输出可直接使用的营销文案、脚本方向和优化建议。'
- ],
- 'pathology': [
- `您好,我是病理分析助手。为了更准确分析,请尽量提供:
- <ul>
- <li>疾病或产品名称</li>
- </ul>`,
- '我会给出病理逻辑拆解、用户关注点与内容表达建议。'
- ],
- 'diagnosis': [
- `您好,我是投放诊断助手。请发送您的投放数据,例如:
- <ul>
- <li>消耗、曝光、点击、转化、成本等核心指标</li>
- </ul>`,
- '如果是视频诊断,目前仅支持文案诊断,建议先转为文案(最好附时间戳字幕)。'
- ]
- };
- function getCurrentAgentType () {
- return localStorage.getItem('current_agent') || 'planning';
- }
- function getAgentWelcomeHtml (agentType) {
- const welcomeBlocks = AGENT_WELCOME_CONTENT[agentType] || AGENT_WELCOME_CONTENT['planning'];
- return `
- <div class="message bot">
- ${welcomeBlocks.map(content => `<div class="message-content">${content}</div>`).join('')}
- </div>
- `;
- }
- function renderAgentWelcomeMessage (agentType) {
- const chatMessages = document.getElementById('chat-messages');
- if (!chatMessages) return;
- chatMessages.innerHTML = getAgentWelcomeHtml(agentType || getCurrentAgentType());
- }
- function syncAgentWelcomeMessageForEmptyConversation () {
- const chatMessages = document.getElementById('chat-messages');
- if (!chatMessages) return;
- const hasUserMessages = chatMessages.querySelectorAll('.message.user').length > 0;
- if (hasUserMessages) return;
- const hasOnlyOneBotMessage = chatMessages.children.length === 1 &&
- chatMessages.querySelectorAll('.message.bot').length === 1;
- const hasNoMessages = chatMessages.children.length === 0;
- if (hasOnlyOneBotMessage || hasNoMessages) {
- renderAgentWelcomeMessage(getCurrentAgentType());
- }
- }
- // 智能体选择(悬停展开)
- document.addEventListener('DOMContentLoaded', function () {
- setTimeout(() => {
- initializeAgentHoverDropdown();
- syncAgentWelcomeMessageForEmptyConversation();
- }, 100);
- // 监听窗口大小变化,重新初始化智能体菜单
- let resizeTimer;
- window.addEventListener('resize', function () {
- clearTimeout(resizeTimer);
- resizeTimer = setTimeout(() => {
- initializeAgentHoverDropdown();
- }, 250);
- });
- });
- function initializeAgentHoverDropdown () {
- const dropdown = document.getElementById('agent-dropdown');
- const menu = document.getElementById('agent-menu');
- const triggerText = document.getElementById('agent-trigger-text');
- const triggerIcon = document.getElementById('agent-trigger-icon');
- if (!dropdown || !menu) return;
- const agentMeta = AGENT_META;
- // 兼容历史存储:如果之前存的是已删除的智能体,强制切回智能策划大师
- const storedAgent = localStorage.getItem('current_agent');
- let currentAgent = 'planning';
- if (!storedAgent || !agentMeta[storedAgent]) {
- // 没有存过,或存的是已不存在的智能体 -> 统一重置为智能策划大师
- localStorage.setItem('current_agent', 'planning');
- } else {
- currentAgent = storedAgent;
- }
- // 初始化触发器显示
- triggerText.textContent = agentMeta[currentAgent].name;
- if (triggerIcon) {
- triggerIcon.src = agentMeta[currentAgent].icon;
- triggerIcon.style.display = 'block';
- }
- // 高亮选中项(仅影响下拉菜单项,不改全局样式)
- function clearActiveStyles () {
- menu.querySelectorAll('.agent-option').forEach(item => {
- item.removeAttribute('data-active');
- item.style.background = '';
- item.style.border = '';
- item.style.opacity = '';
- item.style.pointerEvents = '';
- });
- }
- function setActiveStyles (agentType) {
- clearActiveStyles();
- const activeEl = menu.querySelector(`.agent-option[data-agent="${agentType}"]`);
- if (activeEl) {
- activeEl.setAttribute('data-active', 'true');
- activeEl.style.background = 'rgba(99, 102, 241, 0.12)';
- activeEl.style.border = '1px solid rgba(99, 102, 241, 0.35)';
- activeEl.style.opacity = '1';
- // 禁止重复点击当前激活项
- activeEl.style.pointerEvents = 'none';
- }
- }
- setActiveStyles(currentAgent);
- // 检测是否为移动设备
- const isMobile = window.innerWidth <= 768;
- if (isMobile) {
- // 移动端:点击显示/隐藏菜单
- let isMenuOpen = false;
- function showMenu () {
- menu.style.display = 'block';
- isMenuOpen = true;
- // 添加动画类
- setTimeout(() => {
- menu.classList.add('show');
- }, 10);
- }
- function hideMenu () {
- menu.classList.remove('show');
- setTimeout(() => {
- menu.style.display = 'none';
- }, 300);
- isMenuOpen = false;
- }
- // 点击触发器显示菜单
- dropdown.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
- if (isMenuOpen) {
- hideMenu();
- } else {
- showMenu();
- }
- });
- // 移除遮罩层点击事件
- // 点击菜单外部隐藏菜单
- document.addEventListener('click', function (e) {
- if (!dropdown.contains(e.target) && !menu.contains(e.target)) {
- hideMenu();
- }
- });
- } else {
- // 桌面端:悬停显示/隐藏(避免从触发区移动到菜单区时闪烁隐藏)
- let isHovering = false;
- let hideTimer = null;
- function showMenu () {
- if (hideTimer) {
- clearTimeout(hideTimer);
- hideTimer = null;
- }
- menu.style.display = 'block';
- }
- function scheduleHide () {
- if (hideTimer) {
- clearTimeout(hideTimer);
- }
- hideTimer = setTimeout(() => {
- if (!isHovering) {
- menu.style.display = 'none';
- }
- }, 120);
- }
- dropdown.addEventListener('mouseenter', function () {
- isHovering = true;
- showMenu();
- });
- dropdown.addEventListener('mouseleave', function () {
- isHovering = false;
- scheduleHide();
- });
- menu.addEventListener('mouseenter', function () {
- isHovering = true;
- showMenu();
- });
- menu.addEventListener('mouseleave', function () {
- isHovering = false;
- scheduleHide();
- });
- }
- menu.querySelectorAll('.agent-option').forEach((item, index) => {
- item.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
- const agentType = this.getAttribute('data-agent');
- const current = localStorage.getItem('current_agent') || 'planning';
- // 已是当前激活则不处理
- if (agentType === current) {
- return;
- }
- // 检查是否已有对话(不是新对话)且有实际消息内容
- const conversationId = window.currentConversationId;
- const hasExistingConversation = conversationId &&
- conversationId !== 'new' &&
- conversationId !== null &&
- conversationId !== undefined;
- // 检查当前对话是否有实际的消息内容(不是空的新对话)
- const hasActualMessages = checkConversationHasMessages(conversationId);
- if (hasExistingConversation && hasActualMessages) {
- // 先关闭菜单,避免遮挡对话框
- if (isMobile) {
- hideMenu();
- } else {
- isHovering = false;
- scheduleHide();
- }
- // 延迟一下再弹出对话框,确保菜单已关闭
- setTimeout(() => {
- // 弹出确认对话框
- showAgentSwitchConfirmDialog(agentType, current, function (confirmed) {
- if (confirmed) {
- // 用户确认,创建新对话并切换智能体
- switchAgentWithNewConversation(agentType);
- setActiveStyles(agentType);
- // 更新触发器
- triggerText.textContent = agentMeta[agentType].name;
- if (triggerIcon) {
- triggerIcon.src = agentMeta[agentType].icon;
- triggerIcon.style.display = 'block';
- }
- } else {
- // 用户取消,不切换智能体,保持当前智能体
- // 不需要做任何操作
- }
- });
- }, 100);
- } else {
- // 新对话或空对话(没有实际消息),直接切换智能体,无需确认
- switchAgent(agentType);
- setActiveStyles(agentType);
- // 更新触发器
- triggerText.textContent = agentMeta[agentType].name;
- if (triggerIcon) {
- triggerIcon.src = agentMeta[agentType].icon;
- triggerIcon.style.display = 'block';
- }
- // 移动端点击后隐藏菜单
- if (isMobile) {
- hideMenu();
- } else {
- isHovering = false;
- scheduleHide();
- }
- }
- });
- // 添加触摸事件支持(移动端)
- item.addEventListener('touchend', function (e) {
- e.preventDefault();
- this.click(); // 触发点击事件
- });
- });
- }
- // 检查对话是否有实际的消息内容
- function checkConversationHasMessages (conversationId) {
- if (!conversationId || conversationId === 'new') {
- return false;
- }
- // 方法1: 检查聊天消息区域中是否有用户消息
- const chatMessages = document.getElementById('chat-messages');
- if (chatMessages) {
- const userMessages = chatMessages.querySelectorAll('.message.user');
- // 如果有用户消息,说明对话有实际内容
- if (userMessages.length > 0) {
- return true;
- }
- }
- // 方法2: 检查侧边栏中当前对话的消息数量
- const conversationItem = document.querySelector(`[data-conversation-id="${conversationId}"]`);
- if (conversationItem) {
- const messageCountElement = conversationItem.querySelector('.message-count');
- if (messageCountElement) {
- const messageCount = parseInt(messageCountElement.textContent) || 0;
- // 如果消息数量大于0,说明对话有实际内容
- if (messageCount > 0) {
- return true;
- }
- }
- }
- // 方法3: 检查是否有正在进行的流式响应(说明用户正在发送消息)
- if (window.conversationStates && window.conversationStates.has(conversationId)) {
- const state = window.conversationStates.get(conversationId);
- if (state && (state.isStreaming || (state.userMessages && state.userMessages.length > 0))) {
- return true;
- }
- }
- // 如果以上都没有,说明是空的新对话
- return false;
- }
- function switchAgent (agentType) {
- const current = localStorage.getItem('current_agent') || 'planning';
- if (current === agentType) {
- return;
- }
- // 保存到本地存储
- localStorage.setItem('current_agent', agentType);
- // 更新状态提示(如有)
- updateStatusIndicator(agentType);
- // 空对话下切换智能体时,同步更新说明文案
- syncAgentWelcomeMessageForEmptyConversation();
- // 显示切换提示
- showAgentSwitchNotification(agentType);
- }
- // 切换智能体并创建新对话
- function switchAgentWithNewConversation (agentType) {
- const current = localStorage.getItem('current_agent') || 'planning';
- if (current === agentType) {
- return;
- }
- // 保存到本地存储
- localStorage.setItem('current_agent', agentType);
- // 更新状态提示(如有)
- updateStatusIndicator(agentType);
- // 创建新对话
- if (typeof createNewConversation === 'function') {
- createNewConversation();
- }
- // 显示切换提示
- showAgentSwitchNotification(agentType);
- }
- // 显示智能体切换确认对话框
- function showAgentSwitchConfirmDialog (newAgentType, currentAgentType, callback) {
- // 防止重复弹出对话框
- if (document.querySelector('.agent-switch-dialog-overlay')) {
- return;
- }
- const agentNames = {
- 'planning': '智能策划大师',
- 'pathology': '病理分析',
- 'diagnosis': '投放诊断',
- 'kuaishou': '快手',
- 'toutiao': '头条'
- };
- const newAgentName = agentNames[newAgentType] || '新智能体';
- const currentAgentName = agentNames[currentAgentType] || '当前智能体';
- // 创建对话框遮罩层
- const overlay = document.createElement('div');
- overlay.className = 'agent-switch-dialog-overlay';
- overlay.style.cssText = `
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.5);
- display: flex;
- justify-content: center;
- align-items: center;
- z-index: 10000;
- animation: fadeIn 0.2s ease;
- `;
- // 创建对话框
- const dialog = document.createElement('div');
- dialog.className = 'agent-switch-dialog';
- dialog.style.cssText = `
- background: white;
- border-radius: 12px;
- padding: 24px;
- max-width: 400px;
- width: 90%;
- box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
- animation: slideUp 0.3s ease;
- `;
- dialog.innerHTML = `
- <div style="margin-bottom: 20px;">
- <div style="font-size: 18px; font-weight: 600; color: #333; margin-bottom: 12px;">
- 切换智能体
- </div>
- <div style="font-size: 14px; color: #666; line-height: 1.6;">
- 切换将新建对话,是否继续?
- </div>
- </div>
- <div style="display: flex; gap: 12px; justify-content: flex-end;">
- <button class="dialog-btn-cancel" style="
- padding: 10px 20px;
- border: 1px solid #ddd;
- border-radius: 6px;
- background: white;
- color: #666;
- cursor: pointer;
- font-size: 14px;
- transition: all 0.2s;
- ">否</button>
- <button class="dialog-btn-confirm" style="
- padding: 10px 20px;
- border: none;
- border-radius: 6px;
- background: #007bff;
- color: white;
- cursor: pointer;
- font-size: 14px;
- transition: all 0.2s;
- ">继续</button>
- </div>
- `;
- // 添加动画样式
- if (!document.getElementById('agent-switch-dialog-styles')) {
- const style = document.createElement('style');
- style.id = 'agent-switch-dialog-styles';
- style.textContent = `
- @keyframes fadeIn {
- from { opacity: 0; }
- to { opacity: 1; }
- }
- @keyframes fadeOut {
- from { opacity: 1; }
- to { opacity: 0; }
- }
- @keyframes slideUp {
- from {
- opacity: 0;
- transform: translateY(20px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
- @keyframes slideDown {
- from {
- opacity: 1;
- transform: translateY(0);
- }
- to {
- opacity: 0;
- transform: translateY(20px);
- }
- }
- .agent-switch-dialog-overlay {
- backdrop-filter: blur(2px);
- }
- .dialog-btn-cancel:hover {
- background: #f5f5f5 !important;
- border-color: #bbb !important;
- }
- .dialog-btn-confirm:hover {
- background: #0056b3 !important;
- }
- .dialog-btn-cancel:active,
- .dialog-btn-confirm:active {
- transform: scale(0.98);
- }
- `;
- document.head.appendChild(style);
- }
- overlay.appendChild(dialog);
- document.body.appendChild(overlay);
- // 绑定按钮事件
- const cancelBtn = dialog.querySelector('.dialog-btn-cancel');
- const confirmBtn = dialog.querySelector('.dialog-btn-confirm');
- const closeDialog = (result) => {
- overlay.style.animation = 'fadeOut 0.2s ease';
- dialog.style.animation = 'slideDown 0.2s ease';
- setTimeout(() => {
- overlay.remove();
- callback(result);
- }, 200);
- };
- cancelBtn.addEventListener('click', () => closeDialog(false));
- confirmBtn.addEventListener('click', () => closeDialog(true));
- // 点击遮罩层关闭(取消)
- overlay.addEventListener('click', (e) => {
- if (e.target === overlay) {
- closeDialog(false);
- }
- });
- // ESC键关闭(取消)
- const handleEsc = (e) => {
- if (e.key === 'Escape') {
- closeDialog(false);
- document.removeEventListener('keydown', handleEsc);
- }
- };
- document.addEventListener('keydown', handleEsc);
- }
- function updateStatusIndicator (agentType) {
- const agentNames = {
- 'planning': '智能策划大师',
- 'pathology': '病理分析',
- 'diagnosis': '投放诊断',
- 'kuaishou': '快手',
- 'toutiao': '头条'
- };
- const currentPlatformName = document.getElementById('current-platform-name');
- if (currentPlatformName) {
- currentPlatformName.textContent = agentNames[agentType] || '智能策划大师';
- }
- }
- function showAgentSwitchNotification (agentType) {
- const agentNames = {
- 'planning': '智能策划大师',
- 'pathology': '病理分析',
- 'diagnosis': '投放诊断',
- 'kuaishou': '快手',
- 'toutiao': '头条'
- };
- const agentName = agentNames[agentType] || '智能策划大师';
- // 创建通知元素
- const notification = document.createElement('div');
- notification.className = 'agent-switch-notification';
- notification.setAttribute('data-platform', agentType);
- notification.innerHTML = `
- <div class="notification-content">
- <span class="notification-icon">✨</span>
- <span class="notification-text">已切换到 ${agentName}</span>
- </div>
- `;
- // 添加到页面
- document.body.appendChild(notification);
- // 显示动画
- setTimeout(() => {
- notification.classList.add('show');
- }, 100);
- // 自动隐藏
- setTimeout(() => {
- notification.classList.remove('show');
- setTimeout(() => {
- if (notification.parentNode) {
- notification.parentNode.removeChild(notification);
- }
- }, 300);
- }, 2000);
- }
- // 获取当前平台类型(供其他功能使用)
- function getCurrentAgent () {
- return getCurrentAgentType();
- }
- // 动态调整聊天消息区域高度
- function adjustChatMessagesHeight () {
- const userInput = document.getElementById('user-input');
- const chatMessages = document.getElementById('chat-messages');
- if (userInput && chatMessages) {
- // 获取输入框的实际高度
- const inputHeight = userInput.scrollHeight;
- const minHeight = 44; // 最小高度
- const maxHeight = 200; // 最大高度
- const actualHeight = Math.max(minHeight, Math.min(inputHeight, maxHeight));
- // 计算聊天消息区域应该的高度
- const containerHeight = document.querySelector('.chat-container').offsetHeight;
- const headerHeight = document.querySelector('header').offsetHeight;
- const inputContainerPadding = 40; // 输入框容器的padding
- const availableHeight = containerHeight - headerHeight - actualHeight - inputContainerPadding;
- // 设置最小高度为100px,确保有足够的显示空间
- const finalHeight = Math.max(availableHeight, 100);
- chatMessages.style.height = `${finalHeight}px`;
- }
- }
- // 滚动到聊天消息区域底部
- function scrollToBottom () {
- const chatMessages = document.getElementById('chat-messages');
- if (chatMessages) {
- chatMessages.scrollTop = chatMessages.scrollHeight;
- }
- }
- // 平滑滚动到聊天消息区域底部
- function smoothScrollToBottom () {
- const chatMessages = document.getElementById('chat-messages');
- if (chatMessages) {
- chatMessages.scrollTo({
- top: chatMessages.scrollHeight,
- behavior: 'smooth'
- });
- }
- }
- // 监听输入框内容变化
- document.addEventListener('DOMContentLoaded', function () {
- const userInput = document.getElementById('user-input');
- if (userInput) {
- // 监听输入事件
- userInput.addEventListener('input', adjustChatMessagesHeight);
- // 监听键盘事件(回车等)
- userInput.addEventListener('keydown', function (e) {
- if (e.key === 'Enter' && !e.shiftKey) {
- // 如果是回车键且没有按Shift,延迟调整高度
- setTimeout(adjustChatMessagesHeight, 100);
- }
- });
- // 监听窗口大小变化
- window.addEventListener('resize', adjustChatMessagesHeight);
- // 初始化时调整一次高度
- setTimeout(adjustChatMessagesHeight, 100);
- }
- // 监听聊天消息区域的内容变化,自动滚动到底部
- const chatMessages = document.getElementById('chat-messages');
- if (chatMessages) {
- // 创建观察器监听内容变化
- const observer = new MutationObserver(function (mutations) {
- // 如果正在编辑消息,不自动滚动
- if (window.isEditingMessage) {
- return;
- }
- const distanceToBottom = chatMessages.scrollHeight - chatMessages.scrollTop - chatMessages.clientHeight;
- const threshold = 50;
- if (distanceToBottom > threshold) {
- return;
- }
- // 延迟滚动,确保内容完全渲染
- setTimeout(() => {
- scrollToBottom();
- }, 100);
- });
- // 监听子节点变化和属性变化
- observer.observe(chatMessages, {
- childList: true,
- subtree: true,
- attributes: false
- });
- // 初始化时滚动到底部
- setTimeout(scrollToBottom, 100);
- // 添加双击快速滚动到底部的功能
- let clickCount = 0;
- let clickTimer = null;
- chatMessages.addEventListener('click', function () {
- clickCount++;
- if (clickCount === 1) {
- clickTimer = setTimeout(() => {
- clickCount = 0;
- }, 300);
- } else if (clickCount === 2) {
- clearTimeout(clickTimer);
- clickCount = 0;
- smoothScrollToBottom();
- }
- });
- }
- });
- // 添加键盘快捷键:Ctrl+End 或 Cmd+End 快速滚动到底部
- document.addEventListener('keydown', function (e) {
- if ((e.ctrlKey || e.metaKey) && e.key === 'End') {
- e.preventDefault();
- smoothScrollToBottom();
- }
- });
- // 历史记录侧边栏功能 - 移除自动初始化,由认证成功后调用
- function initializeHistorySidebar () {
- const sidebar = document.getElementById('history-sidebar');
- const sidebarToggle = document.getElementById('sidebar-toggle');
- const sidebarOverlay = document.getElementById('sidebar-overlay');
- const newChatBtn = document.getElementById('new-chat-btn');
- const conversationItems = document.querySelectorAll('.conversation-item');
- const conversationMenus = document.querySelectorAll('.conversation-menu');
- // 检测是否为移动设备
- const isMobile = window.innerWidth <= 768;
- if (isMobile) {
- // 移动端显示切换按钮
- if (sidebarToggle) {
- sidebarToggle.style.display = 'flex';
- }
- // 移动端侧边栏切换功能
- if (sidebarToggle && sidebar && sidebarOverlay) {
- sidebarToggle.addEventListener('click', function () {
- sidebar.classList.add('show');
- sidebarOverlay.classList.add('show');
- document.body.style.overflow = 'hidden';
- });
- sidebarOverlay.addEventListener('click', function () {
- sidebar.classList.remove('show');
- sidebarOverlay.classList.remove('show');
- document.body.style.overflow = '';
- });
- }
- } else {
- // 桌面端隐藏切换按钮
- if (sidebarToggle) {
- sidebarToggle.style.display = 'none';
- }
- }
- // 监听窗口大小变化
- window.addEventListener('resize', function () {
- const isMobileNow = window.innerWidth <= 768;
- if (isMobileNow && !isMobile) {
- // 切换到移动端
- if (sidebarToggle) {
- sidebarToggle.style.display = 'flex';
- }
- if (sidebar && sidebarOverlay) {
- sidebar.classList.remove('show');
- sidebarOverlay.classList.remove('show');
- document.body.style.overflow = '';
- }
- } else if (!isMobileNow && isMobile) {
- // 切换到桌面端
- if (sidebarToggle) {
- sidebarToggle.style.display = 'none';
- }
- if (sidebar && sidebarOverlay) {
- sidebar.classList.remove('show');
- sidebarOverlay.classList.remove('show');
- document.body.style.overflow = '';
- }
- }
- });
- // 新对话按钮功能
- if (newChatBtn) {
- newChatBtn.addEventListener('click', function () {
- // 立即创建新对话
- createNewConversation();
- });
- }
- // 对话项点击功能
- conversationItems.forEach(item => {
- item.addEventListener('click', function (e) {
- // 如果点击的是菜单按钮,不处理
- if (e.target.closest('.conversation-menu')) {
- return;
- }
- const conversationId = this.getAttribute('data-conversation-id');
- // 设置激活的对话
- if (typeof setActiveConversation === 'function') {
- setActiveConversation(conversationId);
- }
- loadConversation(conversationId);
- updateConversationSelection(conversationId);
- // 移动端关闭侧边栏
- if (isMobile && sidebar && sidebarOverlay) {
- sidebar.classList.remove('show');
- sidebarOverlay.classList.remove('show');
- document.body.style.overflow = '';
- }
- });
- });
- // 对话菜单功能
- conversationMenus.forEach(menu => {
- menu.addEventListener('click', function (e) {
- e.stopPropagation();
- const conversationId = this.getAttribute('data-conversation-id');
- showConversationMenu(conversationId, this);
- });
- });
- // 同步侧边栏用户信息
- syncSidebarUserInfo();
- // 加载历史记录
- loadConversationHistory();
- }
- // 查找现有的空"新对话"
- function findExistingEmptyConversation () {
- const conversationItems = document.querySelectorAll('.conversation-item');
- for (let item of conversationItems) {
- const messageCountElement = item.querySelector('.message-count');
- if (messageCountElement) {
- const messageCount = parseInt(messageCountElement.textContent);
- if (messageCount === 0) {
- return {
- conversationId: item.getAttribute('data-conversation-id'),
- sessionId: item.getAttribute('data-session-id'),
- element: item
- };
- }
- }
- }
- return null;
- }
- // 切换到现有的空"新对话"
- function switchToExistingEmptyConversation (conversation) {
- // 更新当前对话ID和session_id
- window.currentConversationId = conversation.conversationId;
- window.currentSessionId = conversation.sessionId;
- // 清除聊天区域并显示提示语
- clearCurrentConversation();
- // 更新侧边栏选中状态
- updateConversationSelection(conversation.conversationId);
- // 移动端关闭侧边栏
- const isMobile = window.innerWidth <= 768;
- if (isMobile) {
- const sidebar = document.getElementById('history-sidebar');
- const sidebarOverlay = document.getElementById('sidebar-overlay');
- if (sidebar && sidebarOverlay) {
- sidebar.classList.remove('show');
- sidebarOverlay.classList.remove('show');
- document.body.style.overflow = '';
- }
- }
- // 确保输入框状态正确
- const userInput = document.getElementById('user-input');
- const sendButton = document.getElementById('send-btn');
- if (userInput && sendButton) {
- userInput.disabled = false;
- userInput.placeholder = '请输入需要撰写的品类名和要求...';
- userInput.classList.remove('input-loading');
- sendButton.disabled = false;
- sendButton.classList.remove('stopping');
- }
- }
- function createNewConversation () {
- // 检查认证状态
- const token = localStorage.getItem('auth_token');
- if (!token) {
- redirectToLogin();
- return;
- }
- // 先检查是否已经存在一个空的"新对话"
- const existingEmptyConversation = findExistingEmptyConversation();
- if (existingEmptyConversation) {
- // 如果存在空的"新对话",直接切换到该对话
- switchToExistingEmptyConversation(existingEmptyConversation);
- return;
- }
- // 显示加载状态
- const newChatBtn = document.getElementById('new-chat-btn');
- if (newChatBtn) {
- const originalText = newChatBtn.innerHTML;
- newChatBtn.innerHTML = `
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="animation: spin 1s linear infinite;">
- <path d="M21 12a9 9 0 11-6.219-8.56" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
- </svg>
- 创建中...
- `;
- newChatBtn.disabled = true;
- }
- // 调用后端API创建新对话
- fetch('/api/conversations/new', {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${token}`,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- title: '新对话'
- })
- })
- .then(response => {
- if (response.status === 401) {
- redirectToLogin();
- return;
- }
- return response.json();
- })
- .then(data => {
- if (data && data.success) {
- // 更新当前对话ID和session_id
- window.currentConversationId = data.conversation.conversation_id;
- window.currentSessionId = data.conversation.session_id;
- // 清除聊天区域并显示提示语
- clearCurrentConversation();
- // 更新侧边栏选中状态
- updateConversationSelection(data.conversation.conversation_id);
- // 重新加载历史记录以显示新对话,但不自动加载对话内容
- loadConversationHistory(false);
- // 确保输入框状态正确
- const userInput = document.getElementById('user-input');
- const sendButton = document.getElementById('send-btn');
- if (userInput && sendButton) {
- userInput.disabled = false;
- userInput.placeholder = '请输入需要撰写的品类名和要求...';
- userInput.classList.remove('input-loading');
- sendButton.disabled = false;
- sendButton.classList.remove('stopping');
- }
- // 移动端关闭侧边栏
- const isMobile = window.innerWidth <= 768;
- if (isMobile) {
- const sidebar = document.getElementById('history-sidebar');
- const sidebarOverlay = document.getElementById('sidebar-overlay');
- if (sidebar && sidebarOverlay) {
- sidebar.classList.remove('show');
- sidebarOverlay.classList.remove('show');
- document.body.style.overflow = '';
- }
- }
- } else {
- showMessage('创建新对话失败: ' + (data ? data.error : '未知错误'), 'error');
- }
- })
- .catch(error => {
- showMessage('创建新对话时发生错误: ' + error.message, 'error');
- })
- .finally(() => {
- // 恢复按钮状态
- if (newChatBtn) {
- newChatBtn.innerHTML = `
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
- </svg>
- 开启新对话
- `;
- newChatBtn.disabled = false;
- }
- });
- }
- function clearCurrentConversation () {
- const chatMessages = document.getElementById('chat-messages');
- if (chatMessages) {
- renderAgentWelcomeMessage(getCurrentAgentType());
- }
- // 清除全局状态管理中的当前激活对话
- if (typeof currentActiveConversationId !== 'undefined') {
- currentActiveConversationId = null;
- }
- // 清除强制创建新对话的标志(因为已经创建了新对话)
- window.forceNewConversation = false;
- }
- function loadConversation (conversationId) {
- // 检查认证状态
- const token = localStorage.getItem('auth_token');
- if (!token) {
- redirectToLogin();
- return;
- }
- // 设置激活的对话
- if (typeof setActiveConversation === 'function') {
- setActiveConversation(conversationId);
- }
- // 先切换到指定对话,然后加载内容
- switchToConversationAndLoad(conversationId, token);
- }
- function switchToConversationAndLoad (conversationId, token) {
- // 调用后端API切换到指定对话
- fetch(`/api/conversations/${conversationId}/switch`, {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${token}`,
- 'Content-Type': 'application/json'
- }
- }).then(response => {
- if (response.status === 401) {
- redirectToLogin();
- return;
- }
- return response.json();
- }).then(data => {
- if (data && data.success) {
- // 更新当前对话ID和session_id
- window.currentConversationId = data.conversation.conversation_id;
- window.currentSessionId = data.conversation.session_id;
- // 加载对话内容
- loadConversationSimple(conversationId, token);
- } else {
- showMessage('切换对话失败: ' + (data ? data.error : '未知错误'), 'error');
- }
- }).catch(error => {
- showMessage('切换对话时发生错误: ' + error.message, 'error');
- });
- }
- function loadConversationSimple (conversationId, token) {
- // 检查是否有正在进行的流式响应
- const hasActiveStream = window.conversationStates && window.conversationStates.has(conversationId);
- const streamState = hasActiveStream ? window.conversationStates.get(conversationId) : null;
- // 检查是否真正在流式响应(只要 isStreaming 为 true,无论是否有 messageElement)
- const isActuallyStreaming = streamState && streamState.isStreaming;
- // 检查是否有可用的消息元素(用于判断是否可以通过 updateExistingMessages 处理)
- const hasMessageElement = streamState && streamState.messageElement && streamState.messageElement.parentNode;
- // 返回 Promise
- return fetch(`/api/conversations/${conversationId}`, {
- method: 'GET',
- headers: {
- 'Authorization': `Bearer ${token}`,
- 'Content-Type': 'application/json'
- }
- })
- .then(response => {
- if (response.status === 401) {
- redirectToLogin();
- return;
- }
- return response.json();
- })
- .then(data => {
- if (data && data.success) {
- const chatMessages = document.getElementById('chat-messages');
- // 如果有正在进行的流式响应且有可用的消息元素,保留流式响应元素
- if (isActuallyStreaming && hasMessageElement) {
- // 保留流式响应的消息元素,只更新历史消息
- updateExistingMessages(data.messages, streamState.messageElement);
- } else {
- // 清空聊天区域并重新渲染所有消息
- chatMessages.innerHTML = '';
- // 如果没有消息,显示提示语
- if (!data.messages || data.messages.length === 0) {
- renderAgentWelcomeMessage(getCurrentAgentType());
- } else {
- renderAllMessages(data.messages, chatMessages);
- }
- // 确保用户消息在切换对话后仍然可见
- // 检查是否有未保存的用户消息
- if (streamState && streamState.userMessages) {
- streamState.userMessages.forEach(userMsg => {
- // 检查用户消息是否已经在DOM中
- const existingUserMsg = Array.from(chatMessages.children).find(msg =>
- msg.classList.contains('user') &&
- msg.querySelector('.message-content')?.textContent === userMsg.content
- );
- if (!existingUserMsg) {
- // 如果用户消息不在DOM中,重新添加
- chatMessages.appendChild(userMsg.element);
- }
- });
- }
- // ===== 恢复流式响应内容(无论是正在进行还是已完成但未保存到数据库) =====
- // 检查数据库中是否已经包含了流式响应的内容
- let shouldRestoreStreamContent = false;
- // 如果流式响应正在进行,必须恢复显示
- if (streamState && isActuallyStreaming) {
- shouldRestoreStreamContent = true;
- }
- // 如果流式响应已完成,检查是否需要恢复(内容未保存到数据库)
- else if (streamState && !isActuallyStreaming && (streamState.currentMessage || (streamState.currentThoughts && streamState.currentThoughts.length > 0))) {
- // 检查数据库返回的最后一条 assistant 消息是否与流式响应的内容匹配
- const dbMessages = data.messages || [];
- const lastAssistantMsg = dbMessages.filter(msg => msg.role === 'assistant').pop();
- if (lastAssistantMsg) {
- // 如果最后一条 assistant 消息的内容与流式响应的内容匹配,说明内容已经保存到数据库
- // 使用更精确的匹配:检查消息内容是否相同或包含流式响应的主要内容
- let contentMatches = false;
- if (streamState.currentMessage) {
- const streamContent = streamState.currentMessage.trim();
- const dbContent = lastAssistantMsg.content.trim();
- // 精确匹配或检查前100个字符是否匹配(处理可能的截断)
- contentMatches = (dbContent === streamContent) ||
- (dbContent.length > 50 && streamContent.length > 50 &&
- dbContent.substring(0, 100) === streamContent.substring(0, 100));
- } else {
- // 如果没有流式响应内容,认为匹配
- contentMatches = true;
- }
- let thoughtsMatch = false;
- if (streamState.currentThoughts && streamState.currentThoughts.length > 0) {
- const streamThoughts = streamState.currentThoughts.join('').trim();
- const dbThoughts = (lastAssistantMsg.thinking_content || '').trim();
- thoughtsMatch = (dbThoughts === streamThoughts) ||
- (dbThoughts.length > 50 && streamThoughts.length > 50 &&
- dbThoughts.substring(0, 100) === streamThoughts.substring(0, 100));
- } else {
- // 如果流式响应没有思考过程,检查数据库是否有思考过程
- thoughtsMatch = !lastAssistantMsg.thinking_content || lastAssistantMsg.thinking_content.trim() === '';
- }
- // 只有当内容不匹配时,才需要恢复流式响应的内容
- shouldRestoreStreamContent = !contentMatches || !thoughtsMatch;
- } else {
- // 如果数据库中没有 assistant 消息,说明确实需要恢复流式响应的内容
- shouldRestoreStreamContent = true;
- }
- // 如果内容已保存在数据库中,清除流式响应状态以避免下次重复检查
- if (!shouldRestoreStreamContent) {
- streamState.currentMessage = '';
- streamState.currentThoughts = [];
- }
- }
- if (shouldRestoreStreamContent && streamState) {
- // 如果流式响应正在进行或已完成,但消息元素不在DOM中,需要重新创建或恢复
- let restoredMessageElement = streamState.messageElement;
- // 如果流式响应正在进行,确保消息元素存在
- const isStreamingNow = streamState.isStreaming;
- // 如果消息元素不存在或不在DOM中
- if (!restoredMessageElement) {
- // 消息元素不存在,需要创建
- restoredMessageElement = document.createElement('div');
- restoredMessageElement.className = 'message bot';
- // 创建思考过程容器(如果有)
- if (streamState.currentThoughts && streamState.currentThoughts.length > 0) {
- const thoughtContainer = document.createElement('div');
- thoughtContainer.className = 'thought-container';
- const thoughtToggleBtn = document.createElement('button');
- thoughtToggleBtn.className = 'thought-toggle-btn';
- thoughtToggleBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
- thoughtToggleBtn.title = '收起/展开思考过程';
- thoughtToggleBtn.onclick = function (e) {
- e.stopPropagation();
- thoughtContainer.classList.toggle('collapsed');
- };
- const thoughtContent = document.createElement('div');
- thoughtContent.className = 'thought-content';
- if (typeof window.renderThoughts === 'function') {
- thoughtContent.innerHTML = window.renderThoughts(streamState.currentThoughts.join(''));
- } else {
- thoughtContent.textContent = streamState.currentThoughts.join('');
- }
- thoughtContainer.appendChild(thoughtToggleBtn);
- thoughtContainer.appendChild(thoughtContent);
- restoredMessageElement.appendChild(thoughtContainer);
- // 更新状态中的引用
- streamState.thoughtContainer = thoughtContainer;
- }
- // 创建消息内容容器
- const contentContainer = document.createElement('div');
- // 如果流式响应正在进行,添加 typing 类名
- contentContainer.className = isStreamingNow ? 'message-content typing' : 'message-content';
- if (streamState.currentMessage) {
- if (typeof window.renderMarkdown === 'function') {
- contentContainer.innerHTML = window.renderMarkdown(streamState.currentMessage);
- if (typeof window.applyCodeHighlighting === 'function') {
- window.applyCodeHighlighting(contentContainer);
- }
- } else {
- contentContainer.textContent = streamState.currentMessage;
- }
- }
- restoredMessageElement.appendChild(contentContainer);
- // 保存 contentContainer 引用到状态中,以便流式响应继续更新
- if (isStreamingNow && contentContainer) {
- // 确保状态中的消息元素引用正确
- streamState.messageElement = restoredMessageElement;
- }
- // 添加到聊天区域
- chatMessages.appendChild(restoredMessageElement);
- // 添加对话ID标识,便于后续管理
- restoredMessageElement.setAttribute('data-conversation-id', conversationId);
- // 更新状态中的引用(确保流式响应能够继续更新)
- streamState.messageElement = restoredMessageElement;
- // 如果流式响应正在进行,确保恢复后能够继续更新
- if (isStreamingNow) {
- // 确保消息元素可见
- restoredMessageElement.style.opacity = '1';
- restoredMessageElement.style.transform = 'translateY(0)';
- // 滚动到底部以看到实时更新
- setTimeout(() => {
- chatMessages.scrollTop = chatMessages.scrollHeight;
- }, 100);
- }
- } else if (!restoredMessageElement.parentNode) {
- // 消息元素存在但不在DOM中,需要重新添加到DOM
- chatMessages.appendChild(restoredMessageElement);
- // 确保消息元素可见
- restoredMessageElement.style.opacity = '1';
- restoredMessageElement.style.transform = 'translateY(0)';
- // 如果流式响应正在进行,滚动到底部
- if (isStreamingNow) {
- setTimeout(() => {
- chatMessages.scrollTop = chatMessages.scrollHeight;
- }, 100);
- }
- } else {
- // 如果消息元素还在DOM中,只需要确保内容是最新的
- const contentContainer = restoredMessageElement.querySelector('.message-content');
- if (contentContainer) {
- // 如果流式响应正在进行,确保有 typing 类名
- if (isStreamingNow && !contentContainer.classList.contains('typing')) {
- contentContainer.classList.add('typing');
- } else if (!isStreamingNow && contentContainer.classList.contains('typing')) {
- contentContainer.classList.remove('typing');
- }
- // 更新内容
- if (streamState.currentMessage) {
- if (typeof window.renderMarkdown === 'function') {
- contentContainer.innerHTML = window.renderMarkdown(streamState.currentMessage);
- if (typeof window.applyCodeHighlighting === 'function') {
- window.applyCodeHighlighting(contentContainer);
- }
- } else {
- contentContainer.textContent = streamState.currentMessage;
- }
- }
- }
- // 恢复思考过程
- if (streamState.thoughtContainer && streamState.currentThoughts && streamState.currentThoughts.length > 0) {
- const thoughtContent = streamState.thoughtContainer.querySelector('.thought-content');
- if (thoughtContent) {
- if (typeof window.renderThoughts === 'function') {
- thoughtContent.innerHTML = window.renderThoughts(streamState.currentThoughts.join(''));
- } else {
- thoughtContent.textContent = streamState.currentThoughts.join('');
- }
- }
- }
- }
- // 确保消息元素可见
- restoredMessageElement.style.opacity = '1';
- restoredMessageElement.style.transform = 'translateY(0)';
- // 为新恢复的消息添加复制按钮
- if (typeof window.setupCopyButtons === 'function') {
- window.setupCopyButtons();
- }
- }
- }
- // 更新当前对话ID和session_id
- window.currentConversationId = conversationId;
- if (data.conversation && data.conversation.session_id) {
- window.currentSessionId = data.conversation.session_id;
- }
- // 更新输入框状态 - 确保切换对话时输入框状态正确
- if (typeof updateInputUIForConversation === 'function') {
- updateInputUIForConversation(conversationId);
- } else if (typeof resetUIState === 'function') {
- resetUIState();
- }
- // 滚动到底部
- setTimeout(() => {
- chatMessages.scrollTop = chatMessages.scrollHeight;
- }, 100);
- } else {
- showMessage('加载对话失败: ' + (data ? data.error : '未知错误'), 'error');
- }
- })
- .catch(error => {
- showMessage('加载对话时发生错误: ' + error.message, 'error');
- });
- }
- function updateExistingMessages (messages, streamElement) {
- const chatMessages = document.getElementById('chat-messages');
- const existingMessages = Array.from(chatMessages.children);
- // 找到流式响应元素的位置
- const streamIndex = existingMessages.indexOf(streamElement);
- // 移除流式响应元素之前的所有消息(这些是历史消息)
- for (let i = 0; i < streamIndex; i++) {
- if (existingMessages[i] && existingMessages[i].parentNode) {
- existingMessages[i].remove();
- }
- }
- // 在流式响应元素之前插入所有历史消息
- messages.forEach((msg, index) => {
- // 显示所有历史消息,包括用户消息和AI回复
- const messageDiv = createMessageElement(msg);
- chatMessages.insertBefore(messageDiv, streamElement);
- });
- }
- function renderAllMessages (messages, chatMessages) {
- messages.forEach((msg, index) => {
- const messageDiv = createMessageElement(msg);
- chatMessages.appendChild(messageDiv);
- });
- }
- function createMessageElement (msg) {
- const messageDiv = document.createElement('div');
- messageDiv.className = `message ${msg.role}`;
- let content = msg.content;
- // 如果是AI消息且有思考过程,添加思考容器
- if (msg.role === 'assistant' && msg.thinking_content) {
- // 使用思考过程专用的渲染函数
- let thoughtHtml = '';
- if (typeof window.renderThoughts === 'function') {
- thoughtHtml = window.renderThoughts(msg.thinking_content);
- } else if (typeof window.renderMarkdown === 'function') {
- thoughtHtml = window.renderMarkdown(msg.thinking_content);
- } else if (typeof showdown !== 'undefined') {
- const converter = new showdown.Converter({
- strikethrough: true,
- tables: true,
- tasklists: true,
- ghCodeBlocks: true,
- openLinksInNewWindow: true
- });
- thoughtHtml = converter.makeHtml(msg.thinking_content);
- } else {
- thoughtHtml = msg.thinking_content;
- }
- const thoughtContainerHtml = `
- <div class="thought-container" data-collapsed="false">
- <button class="thought-toggle-btn" onclick="toggleThought(this)">
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>
- </button>
- <div class="thought-content">
- ${thoughtHtml}
- </div>
- </div>
- `;
- content = thoughtContainerHtml + content;
- }
- // 使用统一的Markdown渲染函数渲染消息内容
- let renderedContent = content;
- if (msg.role === 'assistant') {
- if (typeof window.renderMarkdown === 'function') {
- // 只渲染消息内容部分,思考过程已经渲染过了
- const messageContentOnly = msg.content;
- renderedContent = msg.thinking_content ?
- content.replace(msg.content, window.renderMarkdown(messageContentOnly)) :
- window.renderMarkdown(messageContentOnly);
- } else if (typeof showdown !== 'undefined') {
- const converter = new showdown.Converter({
- strikethrough: true,
- tables: true,
- tasklists: true,
- ghCodeBlocks: true,
- openLinksInNewWindow: true
- });
- // 只渲染消息内容部分,思考过程已经渲染过了
- const messageContentOnly = msg.content;
- renderedContent = msg.thinking_content ?
- content.replace(msg.content, converter.makeHtml(messageContentOnly)) :
- converter.makeHtml(messageContentOnly);
- }
- }
- // 为用户消息添加编辑按钮
- let messageContentHtml = `<div class="message-content">${renderedContent}</div>`;
- if (msg.role === 'user' && msg.message_id) {
- // 转义特殊字符,防止XSS攻击
- const escapedContent = msg.content.replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>');
- messageContentHtml = `
- <div class="message-content">${renderedContent}</div>
- <button class="edit-button" onclick="editMessage(${msg.message_id}, '${escapedContent}')" title="编辑消息">
- <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>
- </button>
- `;
- }
- messageDiv.innerHTML = messageContentHtml;
- // 为用户消息添加data-message-id属性
- if (msg.role === 'user' && msg.message_id) {
- messageDiv.setAttribute('data-message-id', msg.message_id);
- }
- // 确保消息立即可见
- messageDiv.style.opacity = '1';
- messageDiv.style.transform = 'translateY(0)';
- // 应用代码高亮和表格处理
- if (typeof applyCodeHighlighting === 'function') {
- applyCodeHighlighting(messageDiv);
- }
- if (typeof processAuditResultTables === 'function') {
- processAuditResultTables();
- }
- // 为历史消息添加复制按钮
- if (typeof window.setupCopyButtons === 'function') {
- window.setupCopyButtons();
- }
- return messageDiv;
- }
- function updateConversationSelection (conversationId) {
- const conversationItems = document.querySelectorAll('.conversation-item');
- conversationItems.forEach(item => {
- const itemId = item.getAttribute('data-conversation-id');
- if (itemId === conversationId) {
- item.classList.add('active');
- // 确保状态点显示
- const statusDot = item.querySelector('.conversation-status-dot');
- if (statusDot) {
- statusDot.style.opacity = '1';
- }
- } else {
- item.classList.remove('active');
- // 移除非活跃对话的状态类
- item.classList.remove('streaming', 'paused');
- const statusDot = item.querySelector('.conversation-status-dot');
- if (statusDot) {
- statusDot.style.opacity = '0';
- }
- }
- });
- }
- function updateConversationStatus (conversationId, status) {
- const conversationItem = document.querySelector(`[data-conversation-id="${conversationId}"]`);
- if (conversationItem) {
- const statusDot = conversationItem.querySelector('.conversation-status-dot');
- // 移除所有状态类
- conversationItem.classList.remove('streaming', 'paused');
- // 添加新的状态类
- if (status === 'streaming') {
- conversationItem.classList.add('streaming');
- if (statusDot) {
- statusDot.style.opacity = '1';
- statusDot.style.background = '#10b981';
- }
- } else if (status === 'paused') {
- conversationItem.classList.add('paused');
- if (statusDot) {
- statusDot.style.opacity = '1';
- statusDot.style.background = '#f59e0b';
- }
- } else if (status === 'stopped') {
- if (statusDot) {
- statusDot.style.opacity = '0';
- }
- }
- }
- }
- function showConversationMenu (conversationId, button) {
- // 创建菜单
- const menu = document.createElement('div');
- menu.className = 'conversation-context-menu';
- menu.innerHTML = `
- <div class="menu-item" data-action="rename">
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>
- 重命名
- </div>
- <div class="menu-item" data-action="delete">
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>
- 删除
- </div>
- `;
- // 添加样式
- menu.style.cssText = `
- position: absolute;
- background: rgba(255, 255, 255, 0.95);
- backdrop-filter: blur(20px);
- border: 1px solid rgba(255, 255, 255, 0.3);
- border-radius: 8px;
- box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
- padding: 8px 0;
- min-width: 120px;
- z-index: 1000;
- opacity: 0;
- transform: scale(0.9);
- transition: all 0.2s ease;
- `;
- // 定位菜单
- const rect = button.getBoundingClientRect();
- menu.style.left = (rect.left - 100) + 'px';
- menu.style.top = (rect.bottom + 5) + 'px';
- // 添加到页面
- document.body.appendChild(menu);
- // 显示动画
- setTimeout(() => {
- menu.style.opacity = '1';
- menu.style.transform = 'scale(1)';
- }, 10);
- // 菜单项点击事件
- menu.querySelectorAll('.menu-item').forEach(item => {
- item.addEventListener('click', function () {
- const action = this.getAttribute('data-action');
- handleConversationAction(action, conversationId);
- closeMenu();
- });
- });
- // 点击外部关闭菜单
- function closeMenu () {
- menu.style.opacity = '0';
- menu.style.transform = 'scale(0.9)';
- setTimeout(() => {
- if (menu.parentNode) {
- menu.parentNode.removeChild(menu);
- }
- }, 200);
- }
- // 点击外部关闭
- document.addEventListener('click', function closeOnOutsideClick (e) {
- if (!menu.contains(e.target) && e.target !== button) {
- closeMenu();
- document.removeEventListener('click', closeOnOutsideClick);
- }
- });
- }
- function handleConversationAction (action, conversationId) {
- switch (action) {
- case 'rename':
- renameConversation(conversationId);
- break;
- case 'delete':
- deleteConversation(conversationId);
- break;
- }
- }
- function renameConversation (conversationId) {
- // 检查认证状态
- const token = localStorage.getItem('auth_token');
- if (!token) {
- redirectToLogin();
- return;
- }
- // 获取当前对话标题
- const conversationItem = document.querySelector(`[data-conversation-id="${conversationId}"]`);
- const currentTitle = conversationItem ? conversationItem.querySelector('.conversation-title').textContent : '';
- // 设置输入框的值
- const renameInput = document.getElementById('rename-input');
- if (renameInput) {
- renameInput.value = currentTitle;
- renameInput.focus();
- renameInput.select();
- }
- // 显示重命名弹框
- showModal('rename-modal');
- // 绑定确认按钮事件
- const confirmBtn = document.getElementById('confirm-rename-btn');
- if (confirmBtn) {
- confirmBtn.onclick = function () {
- const newTitle = renameInput.value.trim();
- if (newTitle && newTitle !== currentTitle) {
- performRename(conversationId, newTitle, token);
- } else if (!newTitle) {
- showMessage('请输入新的对话标题', 'warning');
- } else {
- closeModal('rename-modal');
- }
- };
- }
- // 绑定回车键事件
- if (renameInput) {
- renameInput.onkeydown = function (e) {
- if (e.key === 'Enter') {
- confirmBtn.click();
- } else if (e.key === 'Escape') {
- closeModal('rename-modal');
- }
- };
- }
- }
- function performRename (conversationId, newTitle, token) {
- fetch(`/api/conversations/${conversationId}/rename`, {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${token}`,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- title: newTitle
- })
- })
- .then(response => {
- if (response.status === 401) {
- redirectToLogin();
- return;
- }
- return response.json();
- })
- .then(data => {
- if (data && data.success) {
- const conversationItem = document.querySelector(`[data-conversation-id="${conversationId}"]`);
- if (conversationItem) {
- const titleElement = conversationItem.querySelector('.conversation-title');
- if (titleElement) {
- titleElement.textContent = newTitle;
- }
- }
- closeModal('rename-modal');
- showMessage('对话标题已更新');
- } else {
- showMessage('重命名失败: ' + (data ? data.error : '未知错误'), 'error');
- }
- })
- .catch(error => {
- showMessage('重命名对话时发生错误: ' + error.message, 'error');
- });
- }
- function deleteConversation (conversationId) {
- // 检查认证状态
- const token = localStorage.getItem('auth_token');
- if (!token) {
- redirectToLogin();
- return;
- }
- // 显示删除确认弹框
- showModal('delete-modal');
- // 绑定确认删除按钮事件
- const confirmBtn = document.getElementById('confirm-delete-btn');
- if (confirmBtn) {
- confirmBtn.onclick = function () {
- performDelete(conversationId, token);
- };
- }
- }
- function performDelete (conversationId, token) {
- fetch(`/api/conversations/${conversationId}`, {
- method: 'DELETE',
- headers: {
- 'Authorization': `Bearer ${token}`,
- 'Content-Type': 'application/json'
- }
- })
- .then(response => {
- if (response.status === 401) {
- redirectToLogin();
- return;
- }
- return response.json();
- })
- .then(data => {
- if (data && data.success) {
- const conversationItem = document.querySelector(`[data-conversation-id="${conversationId}"]`);
- if (conversationItem) {
- conversationItem.remove();
- }
- closeModal('delete-modal');
- showMessage('对话已删除');
- // 如果删除的是当前对话,清除当前对话ID
- if (window.currentConversationId === conversationId) {
- window.currentConversationId = null;
- // 清空聊天区域
- const chatMessages = document.getElementById('chat-messages');
- if (chatMessages) {
- chatMessages.innerHTML = '';
- }
- }
- } else {
- showMessage('删除失败: ' + (data ? data.error : '未知错误'), 'error');
- }
- })
- .catch(error => {
- showMessage('删除对话时发生错误: ' + error.message, 'error');
- });
- }
- function loadConversationHistory (autoSelectLatest = false) {
- // 检查认证状态
- const token = localStorage.getItem('auth_token');
- if (!token) {
- redirectToLogin();
- return;
- }
- // 从服务器加载历史记录
- fetch('/api/conversations', {
- method: 'GET',
- headers: {
- 'Authorization': `Bearer ${token}`,
- 'Content-Type': 'application/json'
- }
- })
- .then(response => {
- if (response.status === 401) {
- // 认证失败,清除本地存储并重定向到登录页面
- redirectToLogin();
- return;
- }
- return response.json();
- })
- .then(data => {
- if (data && data.success) {
- updateHistorySidebar(data.conversations, autoSelectLatest);
- } else {
- // 显示空状态
- const historyLoading = document.getElementById('history-loading');
- const historyEmpty = document.getElementById('history-empty');
- if (historyLoading) historyLoading.style.display = 'none';
- if (historyEmpty) historyEmpty.style.display = 'block';
- }
- })
- .catch(error => {
- // 显示空状态
- const historyLoading = document.getElementById('history-loading');
- const historyEmpty = document.getElementById('history-empty');
- if (historyLoading) historyLoading.style.display = 'none';
- if (historyEmpty) historyEmpty.style.display = 'block';
- });
- }
- // 将loadConversationHistory函数暴露到全局作用域,供chat.js调用
- window.loadConversationHistory = loadConversationHistory;
- function updateHistorySidebar (conversations, autoSelectLatest = false) {
- // 更新侧边栏的历史记录
- const historyContent = document.querySelector('.history-content');
- const historyLoading = document.getElementById('history-loading');
- const historyEmpty = document.getElementById('history-empty');
- if (!historyContent) {
- return;
- }
- // 隐藏加载状态
- if (historyLoading) {
- historyLoading.style.display = 'none';
- }
- // 清空现有内容
- historyContent.innerHTML = '';
- // 检查是否有任何对话
- const hasAnyConversations = Object.values(conversations).some(group => group && group.length > 0);
- if (!hasAnyConversations) {
- // 显示空状态
- if (historyEmpty) {
- historyEmpty.style.display = 'block';
- }
- return;
- }
- // 隐藏空状态
- if (historyEmpty) {
- historyEmpty.style.display = 'none';
- }
- // 按时间分组渲染
- const timeGroups = [
- { key: 'today', label: '今天' },
- { key: 'yesterday', label: '昨天' },
- { key: 'week', label: '7天内' },
- { key: 'month', label: '30天内' }
- ];
- timeGroups.forEach(group => {
- const groupConversations = conversations[group.key] || [];
- if (groupConversations.length === 0) return;
- // 创建分组容器
- const groupDiv = document.createElement('div');
- groupDiv.className = 'history-group';
- // 分组标题
- const headerDiv = document.createElement('div');
- headerDiv.className = 'group-header';
- headerDiv.textContent = group.label;
- // 对话列表
- const listDiv = document.createElement('div');
- listDiv.className = 'conversation-list';
- groupConversations.forEach(conv => {
- const itemDiv = document.createElement('div');
- itemDiv.className = 'conversation-item';
- itemDiv.setAttribute('data-conversation-id', conv.conversation_id);
- itemDiv.setAttribute('data-session-id', conv.session_id || '');
- // 格式化时间显示
- const lastMessageTime = conv.last_message_at ? new Date(conv.last_message_at).toLocaleTimeString('zh-CN', {
- hour: '2-digit',
- minute: '2-digit'
- }) : '';
- itemDiv.innerHTML = `
- <div class="conversation-content">
- <div class="conversation-title">${conv.title}</div>
- <div class="conversation-meta">
- <span class="message-count">${conv.message_count}条消息</span>
- ${lastMessageTime ? `<span class="last-time">${lastMessageTime}</span>` : ''}
- </div>
- </div>
- <div class="conversation-status-dot"></div>
- <button class="conversation-menu" data-conversation-id="${conv.conversation_id}">
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
- <circle cx="12" cy="12" r="1" fill="currentColor"/>
- <circle cx="19" cy="12" r="1" fill="currentColor"/>
- <circle cx="5" cy="12" r="1" fill="currentColor"/>
- </svg>
- </button>
- `;
- listDiv.appendChild(itemDiv);
- });
- groupDiv.appendChild(headerDiv);
- groupDiv.appendChild(listDiv);
- historyContent.appendChild(groupDiv);
- });
- // 重新绑定事件
- bindConversationEvents();
- // 自动选中对话
- const firstConversationItem = document.querySelector('.conversation-item');
- if (firstConversationItem) {
- const conversationId = firstConversationItem.getAttribute('data-conversation-id');
- if (autoSelectLatest) {
- // 新对话完成后,选中最新的对话,但不加载内容(保持提示语显示)
- updateConversationSelection(conversationId);
- // 不调用 loadConversation,保持提示语显示
- } else if (!window.currentConversationId) {
- // 页面初始加载时,选中第一个对话
- updateConversationSelection(conversationId);
- loadConversation(conversationId);
- }
- }
- }
- function bindConversationEvents () {
- // 重新绑定对话项点击事件
- const conversationItems = document.querySelectorAll('.conversation-item');
- conversationItems.forEach(item => {
- item.addEventListener('click', function (e) {
- if (e.target.closest('.conversation-menu')) {
- return;
- }
- const conversationId = this.getAttribute('data-conversation-id');
- const sessionId = this.getAttribute('data-session-id');
- // 设置当前对话的session_id
- if (sessionId) {
- window.currentSessionId = sessionId;
- }
- // 设置激活的对话
- if (typeof setActiveConversation === 'function') {
- setActiveConversation(conversationId);
- }
- loadConversation(conversationId);
- updateConversationSelection(conversationId);
- // 移动端关闭侧边栏
- const isMobile = window.innerWidth <= 768;
- if (isMobile) {
- const sidebar = document.getElementById('history-sidebar');
- const sidebarOverlay = document.getElementById('sidebar-overlay');
- if (sidebar && sidebarOverlay) {
- sidebar.classList.remove('show');
- sidebarOverlay.classList.remove('show');
- document.body.style.overflow = '';
- }
- }
- });
- });
- // 重新绑定对话菜单事件
- const conversationMenus = document.querySelectorAll('.conversation-menu');
- conversationMenus.forEach(menu => {
- menu.addEventListener('click', function (e) {
- e.stopPropagation();
- const conversationId = this.getAttribute('data-conversation-id');
- showConversationMenu(conversationId, this);
- });
- });
- }
- function syncSidebarUserInfo () {
- const userInfo = localStorage.getItem('user_info');
- if (userInfo) {
- try {
- const user = JSON.parse(userInfo);
- const sidebarUserName = document.getElementById('sidebar-user-name');
- const sidebarAvatarImg = document.getElementById('sidebar-avatar-img');
- const sidebarAvatarFallback = document.getElementById('sidebar-avatar-fallback');
- if (sidebarUserName) {
- const displayName = user.nickname || user.name || '用户';
- sidebarUserName.textContent = displayName;
- }
- if (sidebarAvatarImg && user.avatar && user.avatar.trim() !== '') {
- sidebarAvatarImg.src = user.avatar;
- sidebarAvatarImg.style.display = 'block';
- sidebarAvatarFallback.style.display = 'none';
- } else if (sidebarAvatarFallback) {
- sidebarAvatarImg.style.display = 'none';
- sidebarAvatarFallback.style.display = 'flex';
- const displayName = user.nickname || user.name || 'U';
- sidebarAvatarFallback.textContent = displayName.charAt(0).toUpperCase();
- }
- } catch (e) {
- }
- }
- }
- // 编辑消息功能
- let currentEditMessageId = null;
- let currentEditMessageElement = null;
- window.isEditingMessage = false; // 设置为全局变量
- window.editMessage = function (messageId, originalContent) {
- // 如果已经在编辑其他消息,先取消
- if (currentEditMessageElement) {
- cancelEdit();
- }
- currentEditMessageId = messageId;
- // 找到对应的消息元素
- const messageElement = document.querySelector(`[data-message-id="${messageId}"]`);
- if (!messageElement) {
- console.error('找不到消息元素');
- return;
- }
- currentEditMessageElement = messageElement;
- window.isEditingMessage = true; // 设置编辑状态
- // 保存当前滚动位置
- const chatMessages = document.getElementById('chat-messages');
- const currentScrollTop = chatMessages.scrollTop;
- // 解码HTML实体
- const decodedContent = originalContent
- .replace(/"/g, '"')
- .replace(/'/g, "'")
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/&/g, '&');
- // 创建编辑表单
- const editForm = document.createElement('div');
- editForm.className = 'edit-form';
- editForm.innerHTML = `
- <textarea class="edit-textarea" placeholder="请输入消息内容...">${decodedContent}</textarea>
- <div class="edit-actions">
- <button class="edit-cancel-btn" onclick="cancelEdit()">取消</button>
- <button class="edit-send-btn" onclick="confirmEdit()">发送</button>
- </div>
- `;
- // 隐藏原始消息内容,显示编辑表单
- const messageContent = messageElement.querySelector('.message-content');
- if (messageContent) {
- messageContent.style.display = 'none';
- }
- // 隐藏编辑按钮
- const editButton = messageElement.querySelector('.edit-button');
- if (editButton) {
- editButton.style.display = 'none';
- }
- // 添加编辑表单到消息元素
- messageElement.appendChild(editForm);
- messageElement.classList.add('message-editing');
- // 恢复滚动位置
- chatMessages.scrollTop = currentScrollTop;
- // 聚焦到文本域
- const textarea = editForm.querySelector('.edit-textarea');
- if (textarea) {
- textarea.focus();
- textarea.select();
- // 绑定键盘事件:Enter 发送(Shift+Enter 换行),Esc 取消
- textarea.onkeydown = function (e) {
- if (e.key === 'Enter') {
- if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey || e.isComposing) {
- return; // 保留换行/组合键
- }
- e.preventDefault();
- confirmEdit();
- } else if (e.key === 'Escape') {
- cancelEdit();
- }
- };
- }
- }
- function cancelEdit () {
- if (currentEditMessageElement) {
- // 保存当前滚动位置
- const chatMessages = document.getElementById('chat-messages');
- const currentScrollTop = chatMessages.scrollTop;
- // 恢复原始消息内容
- const messageContent = currentEditMessageElement.querySelector('.message-content');
- if (messageContent) {
- messageContent.style.display = 'block';
- }
- // 显示编辑按钮
- const editButton = currentEditMessageElement.querySelector('.edit-button');
- if (editButton) {
- editButton.style.display = 'flex';
- }
- // 移除编辑表单
- const editForm = currentEditMessageElement.querySelector('.edit-form');
- if (editForm) {
- editForm.remove();
- }
- currentEditMessageElement.classList.remove('message-editing');
- // 先恢复滚动位置,再清除编辑状态
- chatMessages.scrollTop = currentScrollTop;
- // 延迟清除编辑状态,确保DOM变化不会触发滚动
- setTimeout(() => {
- currentEditMessageElement = null;
- currentEditMessageId = null;
- window.isEditingMessage = false;
- }, 150);
- }
- }
- function confirmEdit () {
- if (!currentEditMessageElement || !currentEditMessageId) {
- return;
- }
- const editForm = currentEditMessageElement.querySelector('.edit-form');
- const textarea = editForm.querySelector('.edit-textarea');
- const sendBtn = editForm.querySelector('.edit-send-btn');
- if (!textarea || !sendBtn) {
- return;
- }
- const newContent = textarea.value.trim();
- if (!newContent) {
- showMessage('消息内容不能为空', 'warning');
- return;
- }
- // 显示加载状态
- const originalText = sendBtn.textContent;
- sendBtn.textContent = '发送中...';
- sendBtn.disabled = true;
- // 调用后端API编辑消息
- performEditMessage(currentEditMessageId, newContent);
- }
- function performEditMessage (messageId, newContent) {
- // 检查认证状态
- const token = localStorage.getItem('auth_token');
- if (!token) {
- redirectToLogin();
- return;
- }
- // 设置输入框为加载状态
- const userInput = document.getElementById('user-input');
- const sendButton = document.getElementById('send-btn');
- if (userInput && sendButton) {
- userInput.classList.add('input-loading');
- userInput.placeholder = '正在重新生成回复...';
- userInput.disabled = true;
- sendButton.innerHTML = `
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
- <rect x="6" y="6" width="12" height="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>`;
- sendButton.classList.add('stopping');
- }
- // 隐藏编辑表单
- if (currentEditMessageElement) {
- const editForm = currentEditMessageElement.querySelector('.edit-form');
- if (editForm) {
- editForm.style.display = 'none';
- }
- }
- // ===== 立即隐藏将被删除的消息(编辑的消息及其之后的所有消息) =====
- const editMessageElement = currentEditMessageElement;
- const chatMessages = document.getElementById('chat-messages');
- let messagesToHide = [];
- if (editMessageElement && chatMessages) {
- let foundEditMessage = false;
- const allMessages = Array.from(chatMessages.querySelectorAll('.message'));
- allMessages.forEach(msg => {
- if (msg === editMessageElement) {
- foundEditMessage = true;
- messagesToHide.push(msg);
- // 立即隐藏消息(但保留DOM元素,后续再删除)
- msg.style.display = 'none';
- } else if (foundEditMessage) {
- messagesToHide.push(msg);
- // 立即隐藏消息
- msg.style.display = 'none';
- }
- });
- }
- // 创建新的用户消息显示(立即显示编辑后的内容)
- function createNewUserMessageImmediately () {
- const chatMessages = document.getElementById('chat-messages');
- if (!chatMessages) return null;
- const newUserMessageElement = document.createElement('div');
- newUserMessageElement.className = 'message user';
- const contentElement = document.createElement('div');
- contentElement.className = 'message-content';
- contentElement.textContent = newContent;
- newUserMessageElement.appendChild(contentElement);
- // 重新对话添加编辑按钮
- if (typeof messageId !== 'undefined' && messageId !== null) {
- const editButton = document.createElement('button');
- editButton.className = 'edit-button';
- editButton.setAttribute('title', '编辑消息');
- editButton.innerHTML = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
- editButton.onclick = function () {
- if (typeof window.editMessage === 'function') window.editMessage(messageId, newContent);
- };
- newUserMessageElement.appendChild(editButton);
- newUserMessageElement.setAttribute('data-message-id', messageId);
- }
- chatMessages.appendChild(newUserMessageElement);
- // 确保消息立即可见
- newUserMessageElement.style.opacity = '1';
- newUserMessageElement.style.transform = 'translateY(0)';
- return newUserMessageElement;
- }
- // 立即创建并显示新的用户消息
- const newUserMessageElement = createNewUserMessageImmediately();
- // ===== 初始化对话状态(参考正常发送消息的逻辑) =====
- const conversationId = window.currentConversationId || 'new';
- // 初始化对话状态(如果不存在)
- if (typeof window.initializeConversationState === 'function') {
- window.initializeConversationState(conversationId);
- }
- // 获取或创建对话状态
- const conversationStates = window.conversationStates || new Map();
- const state = conversationStates.get(conversationId) || {
- isStreaming: false,
- isPaused: false,
- currentMessage: '',
- currentThoughts: [],
- messageElement: null,
- thoughtContainer: null,
- abortController: null,
- lastUpdateTime: Date.now(),
- isVisible: false,
- userMessages: []
- };
- // 重置流式响应状态
- state.isStreaming = true;
- state.isPaused = false;
- state.currentMessage = '';
- state.currentThoughts = [];
- state.lastUpdateTime = Date.now();
- state.isVisible = true;
- // 保存用户消息到状态
- if (!state.userMessages) {
- state.userMessages = [];
- }
- state.userMessages.push({
- content: newContent,
- timestamp: new Date(),
- element: newUserMessageElement,
- messageId: messageId
- });
- // 更新状态映射
- conversationStates.set(conversationId, state);
- window.conversationStates = conversationStates;
- // 更新侧边栏状态
- if (typeof window.updateConversationItemStatus === 'function') {
- window.updateConversationItemStatus(conversationId, 'streaming');
- }
- // ===== 创建AbortController用于取消请求(必须在fetch之前) =====
- state.abortController = new AbortController();
- conversationStates.set(conversationId, state);
- // 调用后端API编辑消息(流式响应)
- fetch(`/api/messages/${messageId}/edit`, {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${token}`,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- content: newContent,
- conversation_id: window.currentConversationId,
- agent: localStorage.getItem('current_agent') || 'planning'
- }),
- signal: state.abortController.signal
- })
- .then(response => {
- if (response.status === 401) {
- // 恢复UI状态
- if (userInput && sendButton) {
- userInput.classList.remove('input-loading');
- userInput.placeholder = '请输入需要撰写的品类名和要求...';
- userInput.disabled = false;
- sendButton.innerHTML = `
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M22 2L11 13M22 2L15 22L11 13M11 13L2 9L22 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>`;
- sendButton.classList.remove('stopping');
- }
- currentEditMessageElement = null;
- currentEditMessageId = null;
- window.isEditingMessage = false;
- redirectToLogin();
- return;
- }
- if (!response.ok) {
- throw new Error(`HTTP error! Status: ${response.status}`);
- }
- // 使用流式读取器处理SSE响应
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
- let buffer = '';
- // 用于实时渲染的变量(保存到state中,便于切换对话时恢复)
- let fullMessage = '';
- let allThoughts = [];
- let thoughtContainer = null;
- let thoughtContent = null;
- let contentContainer = null;
- let messageDiv = null;
- // newUserMessageElement 已经在前面创建了
- let lastRenderTime = 0;
- const RENDER_INTERVAL = 16;
- // 创建AI回复消息容器
- function createBotMessageContainer () {
- if (messageDiv) return messageDiv;
- messageDiv = document.createElement('div');
- messageDiv.className = 'message bot';
- // 添加对话ID标识,便于后续管理
- messageDiv.setAttribute('data-conversation-id', conversationId);
- // 创建思考过程容器
- thoughtContainer = document.createElement('div');
- thoughtContainer.className = 'thought-container';
- const thoughtToggleBtn = document.createElement('button');
- thoughtToggleBtn.className = 'thought-toggle-btn';
- thoughtToggleBtn.innerHTML = '<span class="toggle-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 15L12 9L6 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>';
- thoughtToggleBtn.title = '收起/展开思考过程';
- thoughtToggleBtn.onclick = function (e) {
- e.stopPropagation();
- thoughtContainer.classList.toggle('collapsed');
- const icon = thoughtToggleBtn.querySelector('.toggle-icon');
- if (thoughtContainer.classList.contains('collapsed')) {
- icon.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M6 9L12 15L18 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
- } else {
- icon.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 15L12 9L6 15" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
- }
- };
- thoughtContainer.appendChild(thoughtToggleBtn);
- thoughtContent = document.createElement('div');
- thoughtContent.className = 'thought-content';
- thoughtContainer.appendChild(thoughtContent);
- messageDiv.appendChild(thoughtContainer);
- const chatMessages = document.getElementById('chat-messages');
- if (chatMessages) {
- chatMessages.appendChild(messageDiv);
- }
- // ===== 保存到对话状态(关键!) =====
- state.messageElement = messageDiv;
- state.thoughtContainer = thoughtContainer;
- conversationStates.set(conversationId, state);
- // 确保消息元素可见
- messageDiv.style.opacity = '1';
- messageDiv.style.transform = 'translateY(0)';
- return messageDiv;
- }
- function readStream () {
- reader.read().then(({ done, value }) => {
- if (done) {
- // 流式响应完成
- state.isStreaming = false;
- if (typeof window.updateConversationItemStatus === 'function') {
- window.updateConversationItemStatus(conversationId, 'stopped');
- }
- conversationStates.set(conversationId, state);
- updateUIOnComplete();
- return;
- }
- const chunk = decoder.decode(value, { stream: true });
- buffer += chunk;
- const lines = buffer.split('\n\n');
- buffer = lines.pop() || '';
- lines.forEach(line => {
- if (line.startsWith('data: ')) {
- try {
- const eventData = JSON.parse(line.substring(6));
- handleEditEventData(eventData);
- } catch (error) {
- console.error('解析SSE数据失败:', error);
- }
- }
- });
- // 无论是否暂停,都继续读取流数据
- readStream();
- }).catch(error => {
- if (!state.isPaused) {
- console.error('读取流数据失败:', error);
- showMessage('编辑消息时发生错误: ' + error.message, 'error');
- }
- state.isStreaming = false;
- if (typeof window.updateConversationItemStatus === 'function') {
- window.updateConversationItemStatus(conversationId, 'stopped');
- }
- conversationStates.set(conversationId, state);
- updateUIOnError();
- });
- }
- function handleEditEventData (data) {
- // 更新最后更新时间
- state.lastUpdateTime = Date.now();
- if (!data.success) {
- showMessage('编辑消息失败: ' + (data.error || '未知错误'), 'error');
- state.isStreaming = false;
- if (typeof window.updateConversationItemStatus === 'function') {
- window.updateConversationItemStatus(conversationId, 'stopped');
- }
- updateUIOnError();
- return;
- }
- if (data.thought) {
- // AI回复容器已存在或需要创建
- allThoughts.push(data.thought);
- // ===== 保存思考过程到状态 =====
- state.currentThoughts = allThoughts;
- createBotMessageContainer();
- if (thoughtContent) {
- const combinedThoughts = allThoughts.join('');
- if (typeof window.renderThoughts === 'function') {
- thoughtContent.innerHTML = window.renderThoughts(combinedThoughts);
- } else {
- thoughtContent.textContent = combinedThoughts;
- }
- // 更新状态
- conversationStates.set(conversationId, state);
- forceScrollToBottom();
- }
- return;
- }
- if (data.chunk) {
- // AI回复容器已存在或需要创建
- fullMessage += data.chunk;
- // ===== 保存消息内容到状态 =====
- state.currentMessage = fullMessage;
- createBotMessageContainer();
- if (!contentContainer) {
- contentContainer = document.createElement('div');
- contentContainer.className = 'message-content typing';
- messageDiv.appendChild(contentContainer);
- }
- const now = performance.now();
- if (now - lastRenderTime >= RENDER_INTERVAL) {
- requestAnimationFrame(() => {
- if (contentContainer) {
- if (typeof window.renderMarkdown === 'function') {
- contentContainer.innerHTML = window.renderMarkdown(fullMessage);
- if (typeof window.applyCodeHighlighting === 'function') {
- window.applyCodeHighlighting(contentContainer);
- }
- } else {
- contentContainer.textContent = fullMessage;
- }
- // 更新状态
- conversationStates.set(conversationId, state);
- if (isUserNearBottom()) {
- forceScrollToBottom();
- }
- }
- });
- lastRenderTime = now;
- }
- }
- if (data.session_id) {
- window.currentSessionId = data.session_id;
- }
- if (data.done) {
- if (contentContainer) {
- contentContainer.classList.remove('typing');
- if (typeof window.renderMarkdown === 'function') {
- contentContainer.innerHTML = window.renderMarkdown(fullMessage);
- if (typeof window.applyCodeHighlighting === 'function') {
- window.applyCodeHighlighting(contentContainer);
- }
- } else {
- contentContainer.textContent = fullMessage;
- }
- }
- // ===== 标记流式响应完成,但保留状态 =====
- state.isStreaming = false;
- state.currentMessage = fullMessage;
- state.currentThoughts = allThoughts;
- conversationStates.set(conversationId, state);
- if (typeof window.updateConversationItemStatus === 'function') {
- window.updateConversationItemStatus(conversationId, 'stopped');
- }
- updateUIOnComplete();
- }
- }
- function updateUIOnComplete () {
- // 删除旧的消息元素(被编辑的消息及其后的所有消息)
- // 使用chat-messages容器来查找并删除所有相关消息
- const chatMessages = document.getElementById('chat-messages');
- if (chatMessages && editMessageElement) {
- // 获取所有消息元素
- const allMessages = Array.from(chatMessages.querySelectorAll('.message'));
- let foundEditMessage = false;
- allMessages.forEach(msg => {
- // 找到被编辑的消息
- if (msg === editMessageElement) {
- foundEditMessage = true;
- // 删除编辑表单
- const editForm = msg.querySelector('.edit-form');
- if (editForm) {
- editForm.remove();
- }
- msg.classList.remove('message-editing');
- // 删除整个消息元素(除了新创建的用户消息和AI回复)
- if (msg && msg.parentNode && msg !== messageDiv && msg !== newUserMessageElement) {
- msg.remove();
- }
- } else if (foundEditMessage) {
- // 删除被编辑消息之后的所有消息(除了新创建的用户消息和AI回复)
- if (msg && msg.parentNode && msg !== messageDiv && msg !== newUserMessageElement) {
- msg.remove();
- }
- }
- });
- }
- // 如果editMessageElement还存在但没有被删除,手动删除它
- if (editMessageElement && editMessageElement.parentNode) {
- editMessageElement.remove();
- }
- currentEditMessageElement = null;
- currentEditMessageId = null;
- window.isEditingMessage = false;
- if (userInput && sendButton) {
- userInput.classList.remove('input-loading');
- userInput.placeholder = '请输入需要撰写的品类名和要求...';
- userInput.disabled = false;
- sendButton.innerHTML = `
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M22 2L11 13M22 2L15 22L11 13M11 13L2 9L22 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>`;
- sendButton.classList.remove('stopping');
- }
- // 只刷新侧边栏的对话列表,不重新加载消息(避免旧消息重新出现)
- if (window.currentConversationId && typeof loadConversationHistory === 'function') {
- setTimeout(() => {
- loadConversationHistory(false); // 只更新侧边栏,不重新加载消息
- }, 300);
- }
- // 为新创建的消息添加复制按钮
- if (typeof window.setupCopyButtons === 'function') {
- window.setupCopyButtons();
- }
- forceScrollToBottom();
- }
- function updateUIOnError () {
- if (currentEditMessageElement) {
- const sendBtn = currentEditMessageElement.querySelector('.edit-send-btn');
- if (sendBtn) {
- sendBtn.textContent = '发送';
- sendBtn.disabled = false;
- }
- }
- if (userInput && sendButton) {
- userInput.classList.remove('input-loading');
- userInput.placeholder = '请输入需要撰写的品类名和要求...';
- userInput.disabled = false;
- sendButton.innerHTML = `
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M22 2L11 13M22 2L15 22L11 13M11 13L2 9L22 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>`;
- sendButton.classList.remove('stopping');
- }
- currentEditMessageElement = null;
- currentEditMessageId = null;
- window.isEditingMessage = false;
- }
- function forceScrollToBottom () {
- const chatMessages = document.getElementById('chat-messages');
- if (chatMessages) {
- chatMessages.scrollTop = chatMessages.scrollHeight;
- }
- }
- function isUserNearBottom () {
- const chatMessages = document.getElementById('chat-messages');
- if (!chatMessages) return true;
- const scrollThreshold = 50;
- return chatMessages.scrollHeight - chatMessages.scrollTop - chatMessages.clientHeight < scrollThreshold;
- }
- readStream();
- })
- .catch(error => {
- // 确保在错误时也更新状态
- if (error.name !== 'AbortError') {
- state.isStreaming = false;
- if (typeof window.updateConversationItemStatus === 'function') {
- window.updateConversationItemStatus(conversationId, 'stopped');
- }
- conversationStates.set(conversationId, state);
- }
- console.error('编辑消息时发生错误:', error);
- showMessage('编辑消息时发生错误: ' + error.message, 'error');
- // 恢复UI状态
- if (currentEditMessageElement) {
- const sendBtn = currentEditMessageElement.querySelector('.edit-send-btn');
- if (sendBtn) {
- sendBtn.textContent = '发送';
- sendBtn.disabled = false;
- }
- }
- if (userInput && sendButton) {
- userInput.classList.remove('input-loading');
- userInput.placeholder = '请输入需要撰写的品类名和要求...';
- userInput.disabled = false;
- sendButton.innerHTML = `
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
- <path d="M22 2L11 13M22 2L15 22L11 13M11 13L2 9L22 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
- </svg>`;
- sendButton.classList.remove('stopping');
- }
- currentEditMessageElement = null;
- currentEditMessageId = null;
- window.isEditingMessage = false;
- });
- }
- </script>
- </body>
- </html>
|