| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763 |
- <!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="videoaccount"
- 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> -->
- </ul>
- </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);
- });
- // 智能体选择(悬停展开)
- document.addEventListener('DOMContentLoaded', function () {
- setTimeout(() => {
- initializeAgentHoverDropdown();
- }, 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 = {
- 'planning': {
- name: '智能策划大师',
- icon: "{{ url_for('static', filename='img/planning.png') }}"
- },
- 'videoaccount': {
- name: '病理分析',
- icon: "{{ url_for('static', filename='img/videoaccount.svg') }}"
- },
- 'diagnosis': {
- name: '投放诊断',
- icon: "{{ url_for('static', filename='img/diagnosis.svg') }}"
- },
- };
- // 兼容历史存储:如果之前存的是已删除的智能体,强制切回智能策划大师
- 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);
- // 显示切换提示
- 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': '智能策划大师',
- 'videoaccount': '病理分析',
- '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': '智能策划大师',
- 'videoaccount': '病理分析',
- 'diagnosis': '投放诊断',
- 'kuaishou': '快手',
- 'toutiao': '头条'
- };
- const currentPlatformName = document.getElementById('current-platform-name');
- if (currentPlatformName) {
- currentPlatformName.textContent = agentNames[agentType] || '智能策划大师';
- }
- }
- function showAgentSwitchNotification(agentType) {
- const agentNames = {
- 'planning': '智能策划大师',
- 'videoaccount': '病理分析',
- '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 localStorage.getItem('current_agent') || 'planning';
- }
- // 动态调整聊天消息区域高度
- 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) {
- chatMessages.innerHTML = `
- <div class="message bot">
- <div class="message-content">请发送你的投放数据。如果是视频诊断,目前仅能诊断文案,请去聚德神转转为文案(最好是带时间戳的字幕-聚德智能视频处理工具可导出)。
- </div>
- </div>
- `;
- }
- // 清除全局状态管理中的当前激活对话
- 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) {
- chatMessages.innerHTML = `
- <div class="message bot">
- <div class="message-content">请发送你的投放数据。如果是视频诊断,目前仅能诊断文案,请去聚德神转转为文案(最好是带时间戳的字幕-聚德智能视频处理工具可导出)。</div>
- </div>
- `;
- } 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>
|