index.html 167 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>聚德策划大师-大健康AI文案撰写</title>
  7. <meta name="description" content="聚德AI短视频脚本撰写器 - 专业高效的文案创作平台">
  8. <meta name="theme-color" content="#6366f1" media="(prefers-color-scheme: light)">
  9. <meta name="theme-color" content="#0f172a" media="(prefers-color-scheme: dark)">
  10. <meta name="color-scheme" content="light dark">
  11. <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
  12. <link rel="stylesheet" href="{{ url_for('static', filename='css/modal.css') }}">
  13. <link rel="stylesheet" href="{{ url_for('static', filename='css/components/copy-button.css') }}">
  14. <link rel="icon" href="{{ url_for('static', filename='logo.png') }}">
  15. <link rel="stylesheet" href="{{ url_for('static', filename='css/github.min.css') }}">
  16. <link rel="stylesheet" href="{{ url_for('static', filename='css/highlightjs.min.css') }}">
  17. <link rel="stylesheet" href="{{ url_for('static', filename='css/fonts.css') }}">
  18. <!-- 思考过程样式 -->
  19. <style>
  20. /* AI思考过程样式 */
  21. .thought-container {
  22. background-color: #f3f0fa;
  23. border-left: 3px solid #7952b3;
  24. padding: 10px 15px;
  25. margin: 10px 0 10px 0;
  26. border-radius: 0 4px 4px 0;
  27. font-style: italic;
  28. color: #7952b3;
  29. position: relative;
  30. font-size: 0.98em;
  31. }
  32. .thought-container::before {
  33. content: "思考过程";
  34. display: block;
  35. font-size: 0.75rem;
  36. color: #7952b3;
  37. font-weight: 600;
  38. margin-bottom: 4px;
  39. font-style: normal;
  40. }
  41. .thought-toggle-btn {
  42. position: absolute;
  43. top: 8px;
  44. right: 12px;
  45. background: none;
  46. border: none;
  47. color: #7952b3;
  48. font-size: 1.1em;
  49. cursor: pointer;
  50. z-index: 2;
  51. padding: 2px 6px;
  52. border-radius: 3px;
  53. transition: background 0.2s;
  54. }
  55. .thought-toggle-btn:hover {
  56. background: #e5e1f6;
  57. }
  58. /* 思考过程内容区域样式 - 连续文本块 */
  59. .thought-content {
  60. overflow-y: auto;
  61. max-height: 500px;
  62. transition: max-height 0.35s cubic-bezier(.4, 0, .2, 1), padding 0.3s;
  63. will-change: max-height;
  64. /* 自定义滚动条样式 */
  65. scrollbar-width: thin;
  66. scrollbar-color: #d1c4e9 #f3f0fa;
  67. white-space: normal;
  68. /* 正常换行处理 */
  69. word-wrap: break-word;
  70. /* 长单词换行 */
  71. line-height: 1.4;
  72. /* 适中的行高 */
  73. text-align: left;
  74. /* 左对齐 */
  75. }
  76. /* Webkit浏览器的滚动条样式 */
  77. .thought-content::-webkit-scrollbar {
  78. width: 8px;
  79. }
  80. .thought-content::-webkit-scrollbar-track {
  81. background: #f3f0fa;
  82. border-radius: 4px;
  83. }
  84. .thought-content::-webkit-scrollbar-thumb {
  85. background: linear-gradient(135deg, #d1c4e9, #b39ddb);
  86. border-radius: 4px;
  87. border: 1px solid #f3f0fa;
  88. transition: background 0.2s ease;
  89. }
  90. .thought-content::-webkit-scrollbar-thumb:hover {
  91. background: linear-gradient(135deg, #b39ddb, #9575cd);
  92. }
  93. .thought-content::-webkit-scrollbar-thumb:active {
  94. background: linear-gradient(135deg, #9575cd, #7e57c2);
  95. }
  96. .thought-container.collapsed .thought-content {
  97. max-height: 0;
  98. padding: 0 !important;
  99. }
  100. .thought-container.collapsed::before {
  101. content: "思考过程(已收起)";
  102. }
  103. .thought-container p {
  104. margin: 0;
  105. font-style: italic;
  106. color: #7952b3;
  107. background: none;
  108. border-bottom: none;
  109. padding-bottom: 0;
  110. line-height: 1.6;
  111. }
  112. .thought-container p:last-child {
  113. border-bottom: none;
  114. }
  115. /* 对话项样式优化 */
  116. .conversation-meta {
  117. display: flex;
  118. justify-content: space-between;
  119. align-items: center;
  120. margin-top: 4px;
  121. font-size: 0.75rem;
  122. color: #666;
  123. opacity: 0.8;
  124. }
  125. .message-count {
  126. font-size: 0.7rem;
  127. }
  128. .last-time {
  129. font-size: 0.7rem;
  130. color: #999;
  131. }
  132. .conversation-item.active .conversation-meta {
  133. color: rgba(99, 102, 241, 0.8);
  134. }
  135. .conversation-item.active .last-time {
  136. color: rgba(99, 102, 241, 0.6);
  137. }
  138. /* 对话状态指示器样式 */
  139. .conversation-item.streaming .conversation-title {
  140. color: #10b981;
  141. font-weight: 500;
  142. }
  143. .conversation-item.paused .conversation-title {
  144. color: #f59e0b;
  145. font-weight: 500;
  146. }
  147. .conversation-item.streaming .conversation-meta {
  148. color: #10b981;
  149. }
  150. .conversation-item.paused .conversation-meta {
  151. color: #f59e0b;
  152. }
  153. /* 当前活跃对话指示器 */
  154. .conversation-item.active {
  155. background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(99, 102, 241, 0.05));
  156. border-left: 3px solid #6366f1;
  157. position: relative;
  158. }
  159. /* 对话切换动画 */
  160. .conversation-item {
  161. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  162. cursor: pointer;
  163. }
  164. .conversation-item:hover {
  165. background: rgba(99, 102, 241, 0.05);
  166. transform: translateX(2px);
  167. }
  168. .conversation-item.active:hover {
  169. background: linear-gradient(135deg, rgba(99, 102, 241, 0.15), rgba(99, 102, 241, 0.08));
  170. }
  171. /* 对话状态点指示器 */
  172. .conversation-status-dot {
  173. position: absolute;
  174. top: 12px;
  175. right: 12px;
  176. width: 8px;
  177. height: 8px;
  178. border-radius: 50%;
  179. opacity: 0;
  180. transition: opacity 0.3s ease;
  181. }
  182. .conversation-item.streaming .conversation-status-dot {
  183. background: #10b981;
  184. opacity: 1;
  185. animation: pulse-dot 2s infinite;
  186. }
  187. .conversation-item.paused .conversation-status-dot {
  188. background: #f59e0b;
  189. opacity: 1;
  190. animation: pulse-dot 2s infinite;
  191. }
  192. .conversation-item.active .conversation-status-dot {
  193. background: #6366f1;
  194. opacity: 1;
  195. }
  196. @keyframes pulse-dot {
  197. 0% {
  198. opacity: 1;
  199. transform: scale(1);
  200. }
  201. 50% {
  202. opacity: 0.5;
  203. transform: scale(1.2);
  204. }
  205. 100% {
  206. opacity: 1;
  207. transform: scale(1);
  208. }
  209. }
  210. /* 编辑按钮样式 - 与复制按钮保持一致 */
  211. .edit-button {
  212. position: absolute;
  213. width: 32px;
  214. height: 32px;
  215. background: var(--primary-indigo);
  216. color: white;
  217. border: none;
  218. border-radius: 50%;
  219. display: flex;
  220. align-items: center;
  221. justify-content: center;
  222. cursor: pointer;
  223. z-index: 1001;
  224. box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
  225. transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
  226. opacity: 0;
  227. bottom: 8px;
  228. right: 48px;
  229. /* 位于复制按钮左侧 */
  230. }
  231. /* 悬停时显示编辑按钮 */
  232. .message.user:hover .edit-button {
  233. opacity: 1;
  234. }
  235. .edit-button:hover {
  236. transform: scale(1.1);
  237. box-shadow: 0 6px 10px rgba(0, 0, 0, 0.25);
  238. }
  239. .edit-button svg {
  240. width: 18px;
  241. height: 18px;
  242. }
  243. /* 内联编辑样式 */
  244. .message-editing {
  245. position: relative;
  246. }
  247. .edit-form {
  248. display: flex;
  249. flex-direction: column;
  250. gap: 12px;
  251. padding: 16px;
  252. background: #f8f9fa;
  253. border: 2px solid #6366f1;
  254. border-radius: 12px;
  255. margin: 8px 0;
  256. width: 600px;
  257. }
  258. .edit-textarea {
  259. width: 100%;
  260. min-height: 80px;
  261. padding: 12px;
  262. border: none;
  263. border-radius: 8px;
  264. font-size: 14px;
  265. line-height: 1.5;
  266. resize: vertical;
  267. font-family: inherit;
  268. background: white;
  269. }
  270. .edit-textarea:focus {
  271. outline: none;
  272. box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
  273. }
  274. .edit-actions {
  275. display: flex;
  276. gap: 12px;
  277. justify-content: flex-end;
  278. }
  279. .edit-cancel-btn {
  280. padding: 4px 10px;
  281. background: white;
  282. border: 1px solid #ddd;
  283. border-radius: 6px;
  284. color: #666;
  285. cursor: pointer;
  286. font-size: 14px;
  287. transition: all 0.2s ease;
  288. }
  289. .edit-cancel-btn:hover {
  290. background: #f5f5f5;
  291. border-color: #ccc;
  292. }
  293. .edit-send-btn {
  294. padding: 4px 10px;
  295. background: #6366f1;
  296. border: none;
  297. border-radius: 6px;
  298. color: white;
  299. cursor: pointer;
  300. font-size: 14px;
  301. transition: all 0.2s ease;
  302. }
  303. .edit-send-btn:hover {
  304. background: #5b5cf6;
  305. }
  306. .edit-send-btn:disabled {
  307. background: #ccc;
  308. cursor: not-allowed;
  309. }
  310. </style>
  311. </head>
  312. <body>
  313. <div class="glass-particles"></div>
  314. <!-- 移动端侧边栏切换按钮 -->
  315. <button class="sidebar-toggle" id="sidebar-toggle" style="display: none;">
  316. <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  317. <path d="M3 12h18M3 6h18M3 18h18" stroke="currentColor" stroke-width="2" stroke-linecap="round"
  318. stroke-linejoin="round" />
  319. </svg>
  320. </button>
  321. <!-- 移动端侧边栏遮罩层 -->
  322. <div class="sidebar-overlay" id="sidebar-overlay"></div>
  323. <div class="chat-container">
  324. <!-- 历史记录侧边栏 -->
  325. <div class="history-sidebar" id="history-sidebar">
  326. <div class="sidebar-header">
  327. <button class="new-chat-btn" id="new-chat-btn">
  328. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  329. <path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"
  330. stroke-linejoin="round" />
  331. </svg>
  332. 开启新对话
  333. </button>
  334. </div>
  335. <div class="history-content">
  336. <!-- 历史记录将通过JavaScript动态加载 -->
  337. <div class="history-loading" id="history-loading"
  338. style="display: flex; align-items: center; justify-content: center; padding: 20px; color: #666;">
  339. <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
  340. style="margin-right: 8px; animation: spin 1s linear infinite;">
  341. <path d="M21 12a9 9 0 11-6.219-8.56" stroke="currentColor" stroke-width="2"
  342. stroke-linecap="round" stroke-linejoin="round" />
  343. </svg>
  344. 正在加载历史记录...
  345. </div>
  346. <div class="history-empty" id="history-empty"
  347. style="display: none; text-align: center; padding: 40px 20px; color: #999;">
  348. <svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
  349. style="margin-bottom: 16px; opacity: 0.5;">
  350. <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"
  351. stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
  352. </svg>
  353. <div style="font-size: 16px; margin-bottom: 8px;">暂无历史记录</div>
  354. <div style="font-size: 14px; opacity: 0.7;">开始新的对话吧!</div>
  355. </div>
  356. </div>
  357. </div>
  358. <!-- 主聊天区域 -->
  359. <div class="main-chat-area">
  360. <header class="glass-header">
  361. <div class="logo-title-container">
  362. <img src="{{ url_for('static', filename='logo.png') }}" alt="聚德AI短视频脚本撰写" class="header-logo">
  363. <h1>聚德策划大师-大健康AI文案撰写<span class="version-tag">v 1.6.3</span></h1>
  364. <a class="logo-title-link" href="https://wj.qq.com/s2/23610733/1a16/" target="_blank"
  365. class="jude-link">问题反馈</a>
  366. </div>
  367. <div class="buttons-container">
  368. <!-- 智能体选择(仅智能策划大师) -->
  369. <div id="agent-dropdown" class="glass-btn" style="position: relative;">
  370. <img id="agent-trigger-icon" src="{{ url_for('static', filename='img/planning.png') }}"
  371. alt="平台图标" width="16" height="16" onerror="this.style.display='none'">
  372. <span id="agent-trigger-text">智能策划大师</span>
  373. <svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  374. <path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round"
  375. stroke-linejoin="round" />
  376. </svg>
  377. <div id="agent-menu" class="glass-panel"
  378. style="display:none; position: absolute; top: calc(100% + 6px); left: 0; z-index: 1001; min-width: 220px;">
  379. <ul style="list-style: none; margin: 0; padding: 6px;">
  380. <li class="agent-option" data-agent="planning"
  381. style="display:flex; align-items:center; gap:8px; padding:12px; border-radius:8px; cursor:pointer; transition: all 0.2s ease; user-select: none;"
  382. onmouseover="this.style.background='rgba(99, 102, 241, 0.1)'"
  383. onmouseout="this.style.background=''">
  384. <img src="{{ url_for('static', filename='img/planning.png') }}" alt="智能策划大师"
  385. width="16" height="16" onerror="this.style.display='none'">
  386. <div style="display:flex; flex-direction:column;">
  387. <span style="font-weight: 500;">智能策划大师</span>
  388. </div>
  389. </li>
  390. <li class="agent-option" data-agent="videoaccount"
  391. style="display:flex; align-items:center; gap:8px; padding:12px; border-radius:8px; cursor:pointer; transition: all 0.2s ease; user-select: none;"
  392. onmouseover="this.style.background='rgba(99, 102, 241, 0.1)'"
  393. onmouseout="this.style.background=''">
  394. <img src="{{ url_for('static', filename='img/videoaccount.svg') }}" alt="病理分析"
  395. width="16" height="16" onerror="this.style.display='none'">
  396. <div style="display:flex; flex-direction:column;">
  397. <span style="font-weight: 500;">病理分析</span>
  398. </div>
  399. </li>
  400. <!-- 投放诊断 -->
  401. <li class="agent-option" data-agent="diagnosis"
  402. style="display:flex; align-items:center; gap:8px; padding:12px; border-radius:8px; cursor:pointer; transition: all 0.2s ease; user-select: none;"
  403. onmouseover="this.style.background='rgba(99, 102, 241, 0.1)'"
  404. onmouseout="this.style.background=''">
  405. <img src="{{ url_for('static', filename='img/diagnosis.svg') }}" alt="投放诊断"
  406. width="16" height="16" onerror="this.style.display='none'">
  407. <div style="display:flex; flex-direction:column;">
  408. <span style="font-weight: 500;">投放诊断</span>
  409. </div>
  410. </li>
  411. </ul>
  412. </div>
  413. </div>
  414. <button id="reset-btn" class="btn glass-btn">
  415. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  416. <path
  417. 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"
  418. stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
  419. </svg>
  420. 重置对话
  421. </button>
  422. <!-- 用户信息 -->
  423. <div class="user-info" id="user-info" style="display: none;">
  424. <div class="user-details">
  425. <div class="user-avatar" id="user-avatar">
  426. <img id="avatar-img" src="" alt="用户头像" onerror="this.style.display='none'">
  427. <div id="avatar-fallback" class="avatar-fallback"></div>
  428. </div>
  429. <div class="user-text">
  430. <div class="user-info-row">
  431. <span class="user-name" id="user-name"></span>
  432. <span class="user-type" id="user-type"></span>
  433. </div>
  434. <div class="user-info-row">
  435. <span class="user-title" id="user-title" style="display: none;"></span>
  436. </div>
  437. </div>
  438. </div>
  439. <div class="user-dropdown" id="user-dropdown" style="display: none;">
  440. <div class="user-dropdown-content">
  441. <button id="logout-btn" class="btn glass-btn logout-btn">
  442. <svg width="16" height="16" viewBox="0 0 24 24" fill="none"
  443. xmlns="http://www.w3.org/2000/svg">
  444. <path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4M16 3l4 4-4 4M21 7H9"
  445. stroke="currentColor" stroke-width="2" stroke-linecap="round"
  446. stroke-linejoin="round" />
  447. </svg>
  448. 退出登录
  449. </button>
  450. </div>
  451. </div>
  452. </div>
  453. </div>
  454. </header>
  455. <!-- 移动端菜单切换按钮 -->
  456. <div class="chat-main-content">
  457. <div id="chat-messages" class="chat-messages glass-panel">
  458. <div class="message bot">
  459. <div class="message-content">请发送你的投放数据。 如果是视频诊断,目前仅能诊断文案,请去聚德神转转为文案(最好是带时间戳的字幕-聚德智能视频处理工具可导出)。
  460. <ul>
  461. <!-- <li>请输入想要分析的疾病或者产品名称</li> -->
  462. </ul>
  463. </div>
  464. </div>
  465. </div>
  466. <div class="chat-input-container glass-panel">
  467. <textarea id="user-input" placeholder="请输入需要撰写的品类名和要求..." rows="1"></textarea>
  468. <button id="send-btn" class="glass-btn-primary" aria-label="发送消息">
  469. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  470. <path d="M22 2L11 13M22 2L15 22L11 13M11 13L2 9L22 2" stroke="currentColor" stroke-width="2"
  471. stroke-linecap="round" stroke-linejoin="round" />
  472. </svg>
  473. </button>
  474. </div>
  475. </div>
  476. </div>
  477. </div>
  478. <!-- 页面启动保护:避免脚本异常时出现纯白屏 -->
  479. <script>
  480. (function () {
  481. function showFatalScreen(msg) {
  482. if (document.getElementById('fatal-boot-error')) return;
  483. const box = document.createElement('div');
  484. box.id = 'fatal-boot-error';
  485. box.style.cssText = 'position:fixed;inset:0;z-index:99999;background:#f8fafc;color:#1f2937;display:flex;align-items:center;justify-content:center;padding:24px;';
  486. box.innerHTML = `
  487. <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;">
  488. <h3 style="margin:0 0 10px;font-size:18px;">页面加载失败</h3>
  489. <p style="margin:0 0 12px;line-height:1.6;">${msg || '检测到关键脚本加载异常,已阻止白屏。请先刷新页面,若仍失败请联系管理员清理缓存并检查静态资源发布。'}</p>
  490. <div style="display:flex;gap:10px;">
  491. <button onclick="location.reload()" style="border:0;background:#4f46e5;color:#fff;border-radius:6px;padding:8px 14px;cursor:pointer;">刷新页面</button>
  492. <button onclick="location.href='/login'" style="border:1px solid #d1d5db;background:#fff;color:#111827;border-radius:6px;padding:8px 14px;cursor:pointer;">重新登录</button>
  493. </div>
  494. </div>
  495. `;
  496. document.body.appendChild(box);
  497. }
  498. window.__bootFailedScripts = [];
  499. window.__onScriptLoadError = function (src) {
  500. window.__bootFailedScripts.push(src || 'unknown');
  501. showFatalScreen('关键静态资源加载失败:' + window.__bootFailedScripts.join(', '));
  502. };
  503. window.addEventListener('error', function (e) {
  504. if (e && e.message) {
  505. showFatalScreen('页面初始化异常:' + e.message);
  506. }
  507. });
  508. window.addEventListener('unhandledrejection', function (e) {
  509. const reason = e && e.reason ? (e.reason.message || String(e.reason)) : '未知Promise异常';
  510. showFatalScreen('页面初始化异常:' + reason);
  511. });
  512. window.addEventListener('load', function () {
  513. setTimeout(function () {
  514. if (window.__bootFailedScripts.length > 0) return;
  515. if (typeof window.fetch !== 'function') {
  516. showFatalScreen('浏览器环境异常:fetch不可用。');
  517. }
  518. }, 1200);
  519. });
  520. })();
  521. </script>
  522. <!-- 核心JS库 -->
  523. <script src="{{ url_for('static', filename='js/highlight.min.js') }}" onerror="window.__onScriptLoadError('highlight.min.js')"></script>
  524. <script src="{{ url_for('static', filename='js/marked.min.js') }}" onerror="window.__onScriptLoadError('marked.min.js')"></script>
  525. <script src="{{ url_for('static', filename='js/showdown.min.js') }}" onerror="window.__onScriptLoadError('showdown.min.js')"></script>
  526. <script src="{{ url_for('static', filename='js/copy-buttons.js') }}" onerror="window.__onScriptLoadError('copy-buttons.js')"></script>
  527. <script src="{{ url_for('static', filename='js/chat.js') }}" onerror="window.__onScriptLoadError('chat.js')"></script>
  528. <!-- 认证和用户信息管理 -->
  529. <!-- 自定义弹框 -->
  530. <!-- 删除确认弹框 -->
  531. <div id="delete-modal" class="custom-modal">
  532. <div class="modal-overlay"></div>
  533. <div class="modal-content">
  534. <div class="modal-header">
  535. <h3 class="modal-title">删除对话</h3>
  536. <button class="modal-close" onclick="closeModal('delete-modal')">
  537. <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  538. <path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"
  539. stroke-linejoin="round" />
  540. </svg>
  541. </button>
  542. </div>
  543. <div class="modal-body">
  544. <div class="modal-icon delete-icon">
  545. <svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  546. <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"
  547. stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
  548. <path d="M10 11v6M14 11v6" stroke="currentColor" stroke-width="2" stroke-linecap="round"
  549. stroke-linejoin="round" />
  550. </svg>
  551. </div>
  552. <p class="modal-message">确定要删除这个对话吗?删除后将无法恢复。</p>
  553. </div>
  554. <div class="modal-footer">
  555. <button class="btn btn-secondary" onclick="closeModal('delete-modal')">取消</button>
  556. <button class="btn btn-danger" id="confirm-delete-btn">删除</button>
  557. </div>
  558. </div>
  559. </div>
  560. <!-- 重命名弹框 -->
  561. <div id="rename-modal" class="custom-modal">
  562. <div class="modal-overlay"></div>
  563. <div class="modal-content">
  564. <div class="modal-header">
  565. <h3 class="modal-title">重命名对话</h3>
  566. <button class="modal-close" onclick="closeModal('rename-modal')">
  567. <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  568. <path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"
  569. stroke-linejoin="round" />
  570. </svg>
  571. </button>
  572. </div>
  573. <div class="modal-body">
  574. <div class="form-group">
  575. <input type="text" id="rename-input" class="form-input" placeholder="请输入新的对话标题" maxlength="50">
  576. <div class="form-hint">最多50个字符</div>
  577. </div>
  578. </div>
  579. <div class="modal-footer">
  580. <button class="btn btn-secondary" onclick="closeModal('rename-modal')">取消</button>
  581. <button class="btn btn-primary" id="confirm-rename-btn">保存</button>
  582. </div>
  583. </div>
  584. </div>
  585. <!-- 消息提示弹框 -->
  586. <div id="message-modal" class="custom-modal">
  587. <div class="modal-overlay"></div>
  588. <div class="modal-content message-modal-content">
  589. <div class="modal-body">
  590. <div class="modal-icon message-icon">
  591. <svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  592. <path d="M9 12l2 2 4-4M21 12a9 9 0 11-18 0 9 9 0 0118 0z" stroke="currentColor" stroke-width="2"
  593. stroke-linecap="round" stroke-linejoin="round" />
  594. </svg>
  595. </div>
  596. <p class="modal-message" id="message-text">操作成功</p>
  597. </div>
  598. <div class="modal-footer">
  599. <button class="btn btn-primary" onclick="closeModal('message-modal')">确定</button>
  600. </div>
  601. </div>
  602. </div>
  603. <script>
  604. // 弹框控制函数
  605. function showModal(modalId) {
  606. const modal = document.getElementById(modalId);
  607. if (modal) {
  608. modal.classList.add('show');
  609. document.body.style.overflow = 'hidden'; // 防止背景滚动
  610. }
  611. }
  612. function closeModal(modalId) {
  613. const modal = document.getElementById(modalId);
  614. if (modal) {
  615. modal.classList.remove('show');
  616. document.body.style.overflow = ''; // 恢复滚动
  617. }
  618. }
  619. function showMessage(message, type = 'success') {
  620. const messageText = document.getElementById('message-text');
  621. const messageIcon = document.querySelector('#message-modal .modal-icon');
  622. if (messageText) {
  623. messageText.textContent = message;
  624. }
  625. if (messageIcon) {
  626. // 根据类型设置图标颜色
  627. messageIcon.className = 'modal-icon message-icon';
  628. if (type === 'error') {
  629. messageIcon.style.color = '#ef4444';
  630. } else if (type === 'warning') {
  631. messageIcon.style.color = '#f59e0b';
  632. } else {
  633. messageIcon.style.color = '#10b981';
  634. }
  635. }
  636. showModal('message-modal');
  637. }
  638. // 点击遮罩层关闭弹框
  639. document.addEventListener('click', function (e) {
  640. if (e.target.classList.contains('modal-overlay')) {
  641. const modal = e.target.closest('.custom-modal');
  642. if (modal) {
  643. closeModal(modal.id);
  644. }
  645. }
  646. });
  647. // ESC键关闭弹框
  648. document.addEventListener('keydown', function (e) {
  649. if (e.key === 'Escape') {
  650. const openModal = document.querySelector('.custom-modal.show');
  651. if (openModal) {
  652. closeModal(openModal.id);
  653. }
  654. }
  655. });
  656. // 防重复重定向标志
  657. let isRedirecting = false;
  658. // 页面加载时检查认证状态
  659. document.addEventListener('DOMContentLoaded', function () {
  660. checkAuthStatus();
  661. });
  662. // 思考过程折叠/展开函数
  663. function toggleThought(button) {
  664. const container = button.closest('.thought-container');
  665. const content = container.querySelector('.thought-content');
  666. const icon = button.querySelector('svg path');
  667. if (container.dataset.collapsed === 'true') {
  668. // 展开
  669. container.dataset.collapsed = 'false';
  670. content.style.maxHeight = '500px';
  671. content.style.padding = '10px 0';
  672. icon.setAttribute('d', 'M6 9l6 6 6-6');
  673. } else {
  674. // 折叠
  675. container.dataset.collapsed = 'true';
  676. content.style.maxHeight = '0';
  677. content.style.padding = '0';
  678. icon.setAttribute('d', 'M18 15l-6-6-6 6');
  679. }
  680. }
  681. function checkAuthStatus() {
  682. // 检查URL参数中是否有登录成功信息
  683. const urlParams = new URLSearchParams(window.location.search);
  684. const loginSuccess = urlParams.get('login_success');
  685. const token = urlParams.get('token');
  686. const userInfoJson = urlParams.get('user_info');
  687. if (loginSuccess && token && userInfoJson) {
  688. // 登录成功,处理用户信息
  689. try {
  690. const userInfo = JSON.parse(decodeURIComponent(userInfoJson));
  691. // 保存到localStorage
  692. localStorage.setItem('auth_token', token);
  693. localStorage.setItem('user_info', JSON.stringify(userInfo));
  694. // 显示用户信息
  695. displayUserInfo(userInfo);
  696. showUserSection();
  697. addAuthHeader(token);
  698. // 清理URL参数
  699. window.history.replaceState({}, document.title, window.location.pathname);
  700. // 初始化历史记录侧边栏
  701. initializeHistorySidebar();
  702. // 启动对话状态更新器
  703. if (typeof startConversationStatusUpdater === 'function') {
  704. startConversationStatusUpdater();
  705. }
  706. } catch (e) {
  707. redirectToLogin();
  708. }
  709. } else {
  710. // 正常检查localStorage中的认证信息
  711. const storedToken = localStorage.getItem('auth_token');
  712. const storedUserInfo = localStorage.getItem('user_info');
  713. if (storedToken && storedUserInfo) {
  714. try {
  715. const user = JSON.parse(storedUserInfo);
  716. // 验证用户信息的基本字段
  717. if (!user.name && !user.nickname) {
  718. user.name = '钉钉用户';
  719. user.nickname = '钉钉用户';
  720. }
  721. displayUserInfo(user);
  722. showUserSection();
  723. addAuthHeader(storedToken);
  724. // 初始化历史记录侧边栏
  725. initializeHistorySidebar();
  726. // 启动对话状态更新器
  727. if (typeof startConversationStatusUpdater === 'function') {
  728. startConversationStatusUpdater();
  729. }
  730. } catch (e) {
  731. redirectToLogin();
  732. }
  733. } else {
  734. redirectToLogin();
  735. }
  736. }
  737. }
  738. function displayUserInfo(user) {
  739. // 显示用户头像
  740. const avatarImg = document.getElementById('avatar-img');
  741. const avatarFallback = document.getElementById('avatar-fallback');
  742. if (user.avatar && user.avatar.trim() !== '') {
  743. avatarImg.src = user.avatar;
  744. avatarImg.style.display = 'block';
  745. avatarFallback.style.display = 'none';
  746. } else {
  747. avatarImg.style.display = 'none';
  748. avatarFallback.style.display = 'block';
  749. // 获取用户名的首字母作为头像
  750. const displayName = user.nickname || user.name || 'U';
  751. avatarFallback.textContent = displayName.charAt(0).toUpperCase();
  752. }
  753. // 显示用户信息
  754. const userName = document.getElementById('user-name');
  755. if (userName) {
  756. const displayName = user.nickname || user.name || '钉钉用户';
  757. userName.textContent = displayName;
  758. }
  759. const userType = document.getElementById('user-type');
  760. if (userType) {
  761. // 根据登录类型显示不同的标签样式
  762. if (user.login_type === 'dingtalk') {
  763. userType.textContent = '钉钉';
  764. userType.style.background = 'rgba(24, 144, 255, 0.2)';
  765. userType.style.color = '#1890ff';
  766. userType.style.border = '1px solid rgba(24, 144, 255, 0.3)';
  767. } else {
  768. userType.textContent = user.login_type || '钉钉';
  769. userType.style.background = 'rgba(255, 255, 255, 0.3)';
  770. userType.style.color = '#666';
  771. userType.style.border = '1px solid rgba(255, 255, 255, 0.2)';
  772. }
  773. }
  774. const userTitle = document.getElementById('user-title');
  775. if (userTitle && user.title) {
  776. userTitle.textContent = user.title;
  777. userTitle.style.display = 'block';
  778. }
  779. // 显示其他用户信息
  780. if (user.email) {
  781. const emailItem = document.getElementById('user-email');
  782. if (emailItem) {
  783. emailItem.querySelector('.info-value').textContent = user.email;
  784. emailItem.style.display = 'block';
  785. }
  786. }
  787. if (user.mobile) {
  788. const mobileItem = document.getElementById('user-mobile');
  789. if (mobileItem) {
  790. mobileItem.querySelector('.info-value').textContent = user.mobile;
  791. mobileItem.style.display = 'block';
  792. }
  793. }
  794. if (user.job_number) {
  795. const jobItem = document.getElementById('user-job');
  796. if (jobItem) {
  797. jobItem.querySelector('.info-value').textContent = user.job_number;
  798. jobItem.style.display = 'block';
  799. }
  800. }
  801. if (user.dept_name) {
  802. const deptItem = document.getElementById('user-dept');
  803. if (deptItem) {
  804. deptItem.querySelector('.info-value').textContent = user.dept_name;
  805. deptItem.style.display = 'block';
  806. }
  807. }
  808. }
  809. function showUserSection() {
  810. const userInfo = document.getElementById('user-info');
  811. if (userInfo) userInfo.style.display = 'flex';
  812. }
  813. function addAuthHeader(token) {
  814. // 注意:addAuthHeader函数由chat.js提供,这里不需要重复定义
  815. // 如果需要设置认证头,直接调用chat.js中的函数
  816. if (typeof window.addAuthHeader === 'function') {
  817. window.addAuthHeader(token);
  818. }
  819. }
  820. function redirectToLogin() {
  821. if (isRedirecting) {
  822. return;
  823. }
  824. isRedirecting = true;
  825. localStorage.removeItem('auth_token');
  826. localStorage.removeItem('user_info');
  827. // 添加延迟,避免频繁重定向
  828. setTimeout(() => {
  829. window.location.href = '/login';
  830. }, 100);
  831. }
  832. // 退出登录
  833. document.getElementById('logout-btn').addEventListener('click', function () {
  834. fetch('/api/auth/logout', {
  835. method: 'POST',
  836. headers: {
  837. 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
  838. 'Content-Type': 'application/json'
  839. }
  840. }).then(() => {
  841. redirectToLogin();
  842. }).catch(() => {
  843. redirectToLogin();
  844. });
  845. });
  846. // 用户信息下拉菜单(修复:添加鼠标进入/离开事件,防止移向退出登录按钮时菜单消失)
  847. function initializeUserDropdown() {
  848. const userInfo = document.getElementById('user-info');
  849. const dropdown = document.getElementById('user-dropdown');
  850. if (!userInfo || !dropdown) {
  851. return;
  852. }
  853. // 初始化时确保下拉菜单是隐藏状态(覆盖CSS的:hover效果)
  854. dropdown.style.display = 'none';
  855. dropdown.style.opacity = '0';
  856. dropdown.style.visibility = 'hidden';
  857. dropdown.style.transform = 'translateY(-15px) scale(0.95)';
  858. let isHovering = false;
  859. let hideTimer = null;
  860. function showDropdown() {
  861. if (hideTimer) {
  862. clearTimeout(hideTimer);
  863. hideTimer = null;
  864. }
  865. dropdown.style.display = 'block';
  866. dropdown.style.opacity = '1';
  867. dropdown.style.visibility = 'visible';
  868. dropdown.style.transform = 'translateY(0) scale(1)';
  869. }
  870. function scheduleHide() {
  871. if (hideTimer) {
  872. clearTimeout(hideTimer);
  873. }
  874. hideTimer = setTimeout(() => {
  875. if (!isHovering) {
  876. dropdown.style.display = 'none';
  877. dropdown.style.opacity = '0';
  878. dropdown.style.visibility = 'hidden';
  879. dropdown.style.transform = 'translateY(-15px) scale(0.95)';
  880. }
  881. }, 150); // 150ms延迟,给用户足够时间移动到按钮
  882. }
  883. // 鼠标进入用户信息区域
  884. userInfo.addEventListener('mouseenter', function () {
  885. isHovering = true;
  886. showDropdown();
  887. });
  888. // 鼠标离开用户信息区域
  889. userInfo.addEventListener('mouseleave', function () {
  890. isHovering = false;
  891. scheduleHide();
  892. });
  893. // 鼠标进入下拉菜单区域(重要:防止鼠标移动到按钮时菜单消失)
  894. dropdown.addEventListener('mouseenter', function () {
  895. isHovering = true;
  896. showDropdown();
  897. });
  898. // 鼠标离开下拉菜单区域
  899. dropdown.addEventListener('mouseleave', function () {
  900. isHovering = false;
  901. scheduleHide();
  902. });
  903. // 保留点击事件用于移动端
  904. document.getElementById('user-avatar').addEventListener('click', function (e) {
  905. // 在桌面端,如果已经在悬停显示状态,点击时不处理(避免冲突)
  906. if (window.innerWidth > 768 && dropdown.style.display === 'block') {
  907. return;
  908. }
  909. // 移动端切换显示/隐藏
  910. if (dropdown.style.display === 'none' || dropdown.style.display === '') {
  911. showDropdown();
  912. } else {
  913. dropdown.style.display = 'none';
  914. dropdown.style.opacity = '0';
  915. dropdown.style.visibility = 'hidden';
  916. dropdown.style.transform = 'translateY(-15px) scale(0.95)';
  917. }
  918. });
  919. }
  920. // 初始化用户下拉菜单
  921. document.addEventListener('DOMContentLoaded', function () {
  922. setTimeout(() => {
  923. initializeUserDropdown();
  924. }, 100);
  925. });
  926. // 智能体选择(悬停展开)
  927. document.addEventListener('DOMContentLoaded', function () {
  928. setTimeout(() => {
  929. initializeAgentHoverDropdown();
  930. }, 100);
  931. // 监听窗口大小变化,重新初始化智能体菜单
  932. let resizeTimer;
  933. window.addEventListener('resize', function () {
  934. clearTimeout(resizeTimer);
  935. resizeTimer = setTimeout(() => {
  936. initializeAgentHoverDropdown();
  937. }, 250);
  938. });
  939. });
  940. function initializeAgentHoverDropdown() {
  941. const dropdown = document.getElementById('agent-dropdown');
  942. const menu = document.getElementById('agent-menu');
  943. const triggerText = document.getElementById('agent-trigger-text');
  944. const triggerIcon = document.getElementById('agent-trigger-icon');
  945. if (!dropdown || !menu) return;
  946. const agentMeta = {
  947. 'planning': {
  948. name: '智能策划大师',
  949. icon: "{{ url_for('static', filename='img/planning.png') }}"
  950. },
  951. 'videoaccount': {
  952. name: '病理分析',
  953. icon: "{{ url_for('static', filename='img/videoaccount.svg') }}"
  954. },
  955. 'diagnosis': {
  956. name: '投放诊断',
  957. icon: "{{ url_for('static', filename='img/diagnosis.svg') }}"
  958. },
  959. };
  960. // 兼容历史存储:如果之前存的是已删除的智能体,强制切回智能策划大师
  961. const storedAgent = localStorage.getItem('current_agent');
  962. let currentAgent = 'planning';
  963. if (!storedAgent || !agentMeta[storedAgent]) {
  964. // 没有存过,或存的是已不存在的智能体 -> 统一重置为智能策划大师
  965. localStorage.setItem('current_agent', 'planning');
  966. } else {
  967. currentAgent = storedAgent;
  968. }
  969. // 初始化触发器显示
  970. triggerText.textContent = agentMeta[currentAgent].name;
  971. if (triggerIcon) {
  972. triggerIcon.src = agentMeta[currentAgent].icon;
  973. triggerIcon.style.display = 'block';
  974. }
  975. // 高亮选中项(仅影响下拉菜单项,不改全局样式)
  976. function clearActiveStyles() {
  977. menu.querySelectorAll('.agent-option').forEach(item => {
  978. item.removeAttribute('data-active');
  979. item.style.background = '';
  980. item.style.border = '';
  981. item.style.opacity = '';
  982. item.style.pointerEvents = '';
  983. });
  984. }
  985. function setActiveStyles(agentType) {
  986. clearActiveStyles();
  987. const activeEl = menu.querySelector(`.agent-option[data-agent="${agentType}"]`);
  988. if (activeEl) {
  989. activeEl.setAttribute('data-active', 'true');
  990. activeEl.style.background = 'rgba(99, 102, 241, 0.12)';
  991. activeEl.style.border = '1px solid rgba(99, 102, 241, 0.35)';
  992. activeEl.style.opacity = '1';
  993. // 禁止重复点击当前激活项
  994. activeEl.style.pointerEvents = 'none';
  995. }
  996. }
  997. setActiveStyles(currentAgent);
  998. // 检测是否为移动设备
  999. const isMobile = window.innerWidth <= 768;
  1000. if (isMobile) {
  1001. // 移动端:点击显示/隐藏菜单
  1002. let isMenuOpen = false;
  1003. function showMenu() {
  1004. menu.style.display = 'block';
  1005. isMenuOpen = true;
  1006. // 添加动画类
  1007. setTimeout(() => {
  1008. menu.classList.add('show');
  1009. }, 10);
  1010. }
  1011. function hideMenu() {
  1012. menu.classList.remove('show');
  1013. setTimeout(() => {
  1014. menu.style.display = 'none';
  1015. }, 300);
  1016. isMenuOpen = false;
  1017. }
  1018. // 点击触发器显示菜单
  1019. dropdown.addEventListener('click', function (e) {
  1020. e.preventDefault();
  1021. e.stopPropagation();
  1022. if (isMenuOpen) {
  1023. hideMenu();
  1024. } else {
  1025. showMenu();
  1026. }
  1027. });
  1028. // 移除遮罩层点击事件
  1029. // 点击菜单外部隐藏菜单
  1030. document.addEventListener('click', function (e) {
  1031. if (!dropdown.contains(e.target) && !menu.contains(e.target)) {
  1032. hideMenu();
  1033. }
  1034. });
  1035. } else {
  1036. // 桌面端:悬停显示/隐藏(避免从触发区移动到菜单区时闪烁隐藏)
  1037. let isHovering = false;
  1038. let hideTimer = null;
  1039. function showMenu() {
  1040. if (hideTimer) {
  1041. clearTimeout(hideTimer);
  1042. hideTimer = null;
  1043. }
  1044. menu.style.display = 'block';
  1045. }
  1046. function scheduleHide() {
  1047. if (hideTimer) {
  1048. clearTimeout(hideTimer);
  1049. }
  1050. hideTimer = setTimeout(() => {
  1051. if (!isHovering) {
  1052. menu.style.display = 'none';
  1053. }
  1054. }, 120);
  1055. }
  1056. dropdown.addEventListener('mouseenter', function () {
  1057. isHovering = true;
  1058. showMenu();
  1059. });
  1060. dropdown.addEventListener('mouseleave', function () {
  1061. isHovering = false;
  1062. scheduleHide();
  1063. });
  1064. menu.addEventListener('mouseenter', function () {
  1065. isHovering = true;
  1066. showMenu();
  1067. });
  1068. menu.addEventListener('mouseleave', function () {
  1069. isHovering = false;
  1070. scheduleHide();
  1071. });
  1072. }
  1073. menu.querySelectorAll('.agent-option').forEach((item, index) => {
  1074. item.addEventListener('click', function (e) {
  1075. e.preventDefault();
  1076. e.stopPropagation();
  1077. const agentType = this.getAttribute('data-agent');
  1078. const current = localStorage.getItem('current_agent') || 'planning';
  1079. // 已是当前激活则不处理
  1080. if (agentType === current) {
  1081. return;
  1082. }
  1083. // 检查是否已有对话(不是新对话)且有实际消息内容
  1084. const conversationId = window.currentConversationId;
  1085. const hasExistingConversation = conversationId &&
  1086. conversationId !== 'new' &&
  1087. conversationId !== null &&
  1088. conversationId !== undefined;
  1089. // 检查当前对话是否有实际的消息内容(不是空的新对话)
  1090. const hasActualMessages = checkConversationHasMessages(conversationId);
  1091. if (hasExistingConversation && hasActualMessages) {
  1092. // 先关闭菜单,避免遮挡对话框
  1093. if (isMobile) {
  1094. hideMenu();
  1095. } else {
  1096. isHovering = false;
  1097. scheduleHide();
  1098. }
  1099. // 延迟一下再弹出对话框,确保菜单已关闭
  1100. setTimeout(() => {
  1101. // 弹出确认对话框
  1102. showAgentSwitchConfirmDialog(agentType, current, function (confirmed) {
  1103. if (confirmed) {
  1104. // 用户确认,创建新对话并切换智能体
  1105. switchAgentWithNewConversation(agentType);
  1106. setActiveStyles(agentType);
  1107. // 更新触发器
  1108. triggerText.textContent = agentMeta[agentType].name;
  1109. if (triggerIcon) {
  1110. triggerIcon.src = agentMeta[agentType].icon;
  1111. triggerIcon.style.display = 'block';
  1112. }
  1113. } else {
  1114. // 用户取消,不切换智能体,保持当前智能体
  1115. // 不需要做任何操作
  1116. }
  1117. });
  1118. }, 100);
  1119. } else {
  1120. // 新对话或空对话(没有实际消息),直接切换智能体,无需确认
  1121. switchAgent(agentType);
  1122. setActiveStyles(agentType);
  1123. // 更新触发器
  1124. triggerText.textContent = agentMeta[agentType].name;
  1125. if (triggerIcon) {
  1126. triggerIcon.src = agentMeta[agentType].icon;
  1127. triggerIcon.style.display = 'block';
  1128. }
  1129. // 移动端点击后隐藏菜单
  1130. if (isMobile) {
  1131. hideMenu();
  1132. } else {
  1133. isHovering = false;
  1134. scheduleHide();
  1135. }
  1136. }
  1137. });
  1138. // 添加触摸事件支持(移动端)
  1139. item.addEventListener('touchend', function (e) {
  1140. e.preventDefault();
  1141. this.click(); // 触发点击事件
  1142. });
  1143. });
  1144. }
  1145. // 检查对话是否有实际的消息内容
  1146. function checkConversationHasMessages(conversationId) {
  1147. if (!conversationId || conversationId === 'new') {
  1148. return false;
  1149. }
  1150. // 方法1: 检查聊天消息区域中是否有用户消息
  1151. const chatMessages = document.getElementById('chat-messages');
  1152. if (chatMessages) {
  1153. const userMessages = chatMessages.querySelectorAll('.message.user');
  1154. // 如果有用户消息,说明对话有实际内容
  1155. if (userMessages.length > 0) {
  1156. return true;
  1157. }
  1158. }
  1159. // 方法2: 检查侧边栏中当前对话的消息数量
  1160. const conversationItem = document.querySelector(`[data-conversation-id="${conversationId}"]`);
  1161. if (conversationItem) {
  1162. const messageCountElement = conversationItem.querySelector('.message-count');
  1163. if (messageCountElement) {
  1164. const messageCount = parseInt(messageCountElement.textContent) || 0;
  1165. // 如果消息数量大于0,说明对话有实际内容
  1166. if (messageCount > 0) {
  1167. return true;
  1168. }
  1169. }
  1170. }
  1171. // 方法3: 检查是否有正在进行的流式响应(说明用户正在发送消息)
  1172. if (window.conversationStates && window.conversationStates.has(conversationId)) {
  1173. const state = window.conversationStates.get(conversationId);
  1174. if (state && (state.isStreaming || (state.userMessages && state.userMessages.length > 0))) {
  1175. return true;
  1176. }
  1177. }
  1178. // 如果以上都没有,说明是空的新对话
  1179. return false;
  1180. }
  1181. function switchAgent(agentType) {
  1182. const current = localStorage.getItem('current_agent') || 'planning';
  1183. if (current === agentType) {
  1184. return;
  1185. }
  1186. // 保存到本地存储
  1187. localStorage.setItem('current_agent', agentType);
  1188. // 更新状态提示(如有)
  1189. updateStatusIndicator(agentType);
  1190. // 显示切换提示
  1191. showAgentSwitchNotification(agentType);
  1192. }
  1193. // 切换智能体并创建新对话
  1194. function switchAgentWithNewConversation(agentType) {
  1195. const current = localStorage.getItem('current_agent') || 'planning';
  1196. if (current === agentType) {
  1197. return;
  1198. }
  1199. // 保存到本地存储
  1200. localStorage.setItem('current_agent', agentType);
  1201. // 更新状态提示(如有)
  1202. updateStatusIndicator(agentType);
  1203. // 创建新对话
  1204. if (typeof createNewConversation === 'function') {
  1205. createNewConversation();
  1206. }
  1207. // 显示切换提示
  1208. showAgentSwitchNotification(agentType);
  1209. }
  1210. // 显示智能体切换确认对话框
  1211. function showAgentSwitchConfirmDialog(newAgentType, currentAgentType, callback) {
  1212. // 防止重复弹出对话框
  1213. if (document.querySelector('.agent-switch-dialog-overlay')) {
  1214. return;
  1215. }
  1216. const agentNames = {
  1217. 'planning': '智能策划大师',
  1218. 'videoaccount': '病理分析',
  1219. 'diagnosis': '投放诊断',
  1220. 'kuaishou': '快手',
  1221. 'toutiao': '头条'
  1222. };
  1223. const newAgentName = agentNames[newAgentType] || '新智能体';
  1224. const currentAgentName = agentNames[currentAgentType] || '当前智能体';
  1225. // 创建对话框遮罩层
  1226. const overlay = document.createElement('div');
  1227. overlay.className = 'agent-switch-dialog-overlay';
  1228. overlay.style.cssText = `
  1229. position: fixed;
  1230. top: 0;
  1231. left: 0;
  1232. width: 100%;
  1233. height: 100%;
  1234. background: rgba(0, 0, 0, 0.5);
  1235. display: flex;
  1236. justify-content: center;
  1237. align-items: center;
  1238. z-index: 10000;
  1239. animation: fadeIn 0.2s ease;
  1240. `;
  1241. // 创建对话框
  1242. const dialog = document.createElement('div');
  1243. dialog.className = 'agent-switch-dialog';
  1244. dialog.style.cssText = `
  1245. background: white;
  1246. border-radius: 12px;
  1247. padding: 24px;
  1248. max-width: 400px;
  1249. width: 90%;
  1250. box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
  1251. animation: slideUp 0.3s ease;
  1252. `;
  1253. dialog.innerHTML = `
  1254. <div style="margin-bottom: 20px;">
  1255. <div style="font-size: 18px; font-weight: 600; color: #333; margin-bottom: 12px;">
  1256. 切换智能体
  1257. </div>
  1258. <div style="font-size: 14px; color: #666; line-height: 1.6;">
  1259. 切换将新建对话,是否继续?
  1260. </div>
  1261. </div>
  1262. <div style="display: flex; gap: 12px; justify-content: flex-end;">
  1263. <button class="dialog-btn-cancel" style="
  1264. padding: 10px 20px;
  1265. border: 1px solid #ddd;
  1266. border-radius: 6px;
  1267. background: white;
  1268. color: #666;
  1269. cursor: pointer;
  1270. font-size: 14px;
  1271. transition: all 0.2s;
  1272. ">否</button>
  1273. <button class="dialog-btn-confirm" style="
  1274. padding: 10px 20px;
  1275. border: none;
  1276. border-radius: 6px;
  1277. background: #007bff;
  1278. color: white;
  1279. cursor: pointer;
  1280. font-size: 14px;
  1281. transition: all 0.2s;
  1282. ">继续</button>
  1283. </div>
  1284. `;
  1285. // 添加动画样式
  1286. if (!document.getElementById('agent-switch-dialog-styles')) {
  1287. const style = document.createElement('style');
  1288. style.id = 'agent-switch-dialog-styles';
  1289. style.textContent = `
  1290. @keyframes fadeIn {
  1291. from { opacity: 0; }
  1292. to { opacity: 1; }
  1293. }
  1294. @keyframes fadeOut {
  1295. from { opacity: 1; }
  1296. to { opacity: 0; }
  1297. }
  1298. @keyframes slideUp {
  1299. from {
  1300. opacity: 0;
  1301. transform: translateY(20px);
  1302. }
  1303. to {
  1304. opacity: 1;
  1305. transform: translateY(0);
  1306. }
  1307. }
  1308. @keyframes slideDown {
  1309. from {
  1310. opacity: 1;
  1311. transform: translateY(0);
  1312. }
  1313. to {
  1314. opacity: 0;
  1315. transform: translateY(20px);
  1316. }
  1317. }
  1318. .agent-switch-dialog-overlay {
  1319. backdrop-filter: blur(2px);
  1320. }
  1321. .dialog-btn-cancel:hover {
  1322. background: #f5f5f5 !important;
  1323. border-color: #bbb !important;
  1324. }
  1325. .dialog-btn-confirm:hover {
  1326. background: #0056b3 !important;
  1327. }
  1328. .dialog-btn-cancel:active,
  1329. .dialog-btn-confirm:active {
  1330. transform: scale(0.98);
  1331. }
  1332. `;
  1333. document.head.appendChild(style);
  1334. }
  1335. overlay.appendChild(dialog);
  1336. document.body.appendChild(overlay);
  1337. // 绑定按钮事件
  1338. const cancelBtn = dialog.querySelector('.dialog-btn-cancel');
  1339. const confirmBtn = dialog.querySelector('.dialog-btn-confirm');
  1340. const closeDialog = (result) => {
  1341. overlay.style.animation = 'fadeOut 0.2s ease';
  1342. dialog.style.animation = 'slideDown 0.2s ease';
  1343. setTimeout(() => {
  1344. overlay.remove();
  1345. callback(result);
  1346. }, 200);
  1347. };
  1348. cancelBtn.addEventListener('click', () => closeDialog(false));
  1349. confirmBtn.addEventListener('click', () => closeDialog(true));
  1350. // 点击遮罩层关闭(取消)
  1351. overlay.addEventListener('click', (e) => {
  1352. if (e.target === overlay) {
  1353. closeDialog(false);
  1354. }
  1355. });
  1356. // ESC键关闭(取消)
  1357. const handleEsc = (e) => {
  1358. if (e.key === 'Escape') {
  1359. closeDialog(false);
  1360. document.removeEventListener('keydown', handleEsc);
  1361. }
  1362. };
  1363. document.addEventListener('keydown', handleEsc);
  1364. }
  1365. function updateStatusIndicator(agentType) {
  1366. const agentNames = {
  1367. 'planning': '智能策划大师',
  1368. 'videoaccount': '病理分析',
  1369. 'diagnosis': '投放诊断',
  1370. 'kuaishou': '快手',
  1371. 'toutiao': '头条'
  1372. };
  1373. const currentPlatformName = document.getElementById('current-platform-name');
  1374. if (currentPlatformName) {
  1375. currentPlatformName.textContent = agentNames[agentType] || '智能策划大师';
  1376. }
  1377. }
  1378. function showAgentSwitchNotification(agentType) {
  1379. const agentNames = {
  1380. 'planning': '智能策划大师',
  1381. 'videoaccount': '病理分析',
  1382. 'diagnosis': '投放诊断',
  1383. 'kuaishou': '快手',
  1384. 'toutiao': '头条'
  1385. };
  1386. const agentName = agentNames[agentType] || '智能策划大师';
  1387. // 创建通知元素
  1388. const notification = document.createElement('div');
  1389. notification.className = 'agent-switch-notification';
  1390. notification.setAttribute('data-platform', agentType);
  1391. notification.innerHTML = `
  1392. <div class="notification-content">
  1393. <span class="notification-icon">✨</span>
  1394. <span class="notification-text">已切换到 ${agentName}</span>
  1395. </div>
  1396. `;
  1397. // 添加到页面
  1398. document.body.appendChild(notification);
  1399. // 显示动画
  1400. setTimeout(() => {
  1401. notification.classList.add('show');
  1402. }, 100);
  1403. // 自动隐藏
  1404. setTimeout(() => {
  1405. notification.classList.remove('show');
  1406. setTimeout(() => {
  1407. if (notification.parentNode) {
  1408. notification.parentNode.removeChild(notification);
  1409. }
  1410. }, 300);
  1411. }, 2000);
  1412. }
  1413. // 获取当前平台类型(供其他功能使用)
  1414. function getCurrentAgent() {
  1415. return localStorage.getItem('current_agent') || 'planning';
  1416. }
  1417. // 动态调整聊天消息区域高度
  1418. function adjustChatMessagesHeight() {
  1419. const userInput = document.getElementById('user-input');
  1420. const chatMessages = document.getElementById('chat-messages');
  1421. if (userInput && chatMessages) {
  1422. // 获取输入框的实际高度
  1423. const inputHeight = userInput.scrollHeight;
  1424. const minHeight = 44; // 最小高度
  1425. const maxHeight = 200; // 最大高度
  1426. const actualHeight = Math.max(minHeight, Math.min(inputHeight, maxHeight));
  1427. // 计算聊天消息区域应该的高度
  1428. const containerHeight = document.querySelector('.chat-container').offsetHeight;
  1429. const headerHeight = document.querySelector('header').offsetHeight;
  1430. const inputContainerPadding = 40; // 输入框容器的padding
  1431. const availableHeight = containerHeight - headerHeight - actualHeight - inputContainerPadding;
  1432. // 设置最小高度为100px,确保有足够的显示空间
  1433. const finalHeight = Math.max(availableHeight, 100);
  1434. chatMessages.style.height = `${finalHeight}px`;
  1435. }
  1436. }
  1437. // 滚动到聊天消息区域底部
  1438. function scrollToBottom() {
  1439. const chatMessages = document.getElementById('chat-messages');
  1440. if (chatMessages) {
  1441. chatMessages.scrollTop = chatMessages.scrollHeight;
  1442. }
  1443. }
  1444. // 平滑滚动到聊天消息区域底部
  1445. function smoothScrollToBottom() {
  1446. const chatMessages = document.getElementById('chat-messages');
  1447. if (chatMessages) {
  1448. chatMessages.scrollTo({
  1449. top: chatMessages.scrollHeight,
  1450. behavior: 'smooth'
  1451. });
  1452. }
  1453. }
  1454. // 监听输入框内容变化
  1455. document.addEventListener('DOMContentLoaded', function () {
  1456. const userInput = document.getElementById('user-input');
  1457. if (userInput) {
  1458. // 监听输入事件
  1459. userInput.addEventListener('input', adjustChatMessagesHeight);
  1460. // 监听键盘事件(回车等)
  1461. userInput.addEventListener('keydown', function (e) {
  1462. if (e.key === 'Enter' && !e.shiftKey) {
  1463. // 如果是回车键且没有按Shift,延迟调整高度
  1464. setTimeout(adjustChatMessagesHeight, 100);
  1465. }
  1466. });
  1467. // 监听窗口大小变化
  1468. window.addEventListener('resize', adjustChatMessagesHeight);
  1469. // 初始化时调整一次高度
  1470. setTimeout(adjustChatMessagesHeight, 100);
  1471. }
  1472. // 监听聊天消息区域的内容变化,自动滚动到底部
  1473. const chatMessages = document.getElementById('chat-messages');
  1474. if (chatMessages) {
  1475. // 创建观察器监听内容变化
  1476. const observer = new MutationObserver(function (mutations) {
  1477. // 如果正在编辑消息,不自动滚动
  1478. if (window.isEditingMessage) {
  1479. return;
  1480. }
  1481. const distanceToBottom = chatMessages.scrollHeight - chatMessages.scrollTop - chatMessages.clientHeight;
  1482. const threshold = 50;
  1483. if (distanceToBottom > threshold) {
  1484. return;
  1485. }
  1486. // 延迟滚动,确保内容完全渲染
  1487. setTimeout(() => {
  1488. scrollToBottom();
  1489. }, 100);
  1490. });
  1491. // 监听子节点变化和属性变化
  1492. observer.observe(chatMessages, {
  1493. childList: true,
  1494. subtree: true,
  1495. attributes: false
  1496. });
  1497. // 初始化时滚动到底部
  1498. setTimeout(scrollToBottom, 100);
  1499. // 添加双击快速滚动到底部的功能
  1500. let clickCount = 0;
  1501. let clickTimer = null;
  1502. chatMessages.addEventListener('click', function () {
  1503. clickCount++;
  1504. if (clickCount === 1) {
  1505. clickTimer = setTimeout(() => {
  1506. clickCount = 0;
  1507. }, 300);
  1508. } else if (clickCount === 2) {
  1509. clearTimeout(clickTimer);
  1510. clickCount = 0;
  1511. smoothScrollToBottom();
  1512. }
  1513. });
  1514. }
  1515. });
  1516. // 添加键盘快捷键:Ctrl+End 或 Cmd+End 快速滚动到底部
  1517. document.addEventListener('keydown', function (e) {
  1518. if ((e.ctrlKey || e.metaKey) && e.key === 'End') {
  1519. e.preventDefault();
  1520. smoothScrollToBottom();
  1521. }
  1522. });
  1523. // 历史记录侧边栏功能 - 移除自动初始化,由认证成功后调用
  1524. function initializeHistorySidebar() {
  1525. const sidebar = document.getElementById('history-sidebar');
  1526. const sidebarToggle = document.getElementById('sidebar-toggle');
  1527. const sidebarOverlay = document.getElementById('sidebar-overlay');
  1528. const newChatBtn = document.getElementById('new-chat-btn');
  1529. const conversationItems = document.querySelectorAll('.conversation-item');
  1530. const conversationMenus = document.querySelectorAll('.conversation-menu');
  1531. // 检测是否为移动设备
  1532. const isMobile = window.innerWidth <= 768;
  1533. if (isMobile) {
  1534. // 移动端显示切换按钮
  1535. if (sidebarToggle) {
  1536. sidebarToggle.style.display = 'flex';
  1537. }
  1538. // 移动端侧边栏切换功能
  1539. if (sidebarToggle && sidebar && sidebarOverlay) {
  1540. sidebarToggle.addEventListener('click', function () {
  1541. sidebar.classList.add('show');
  1542. sidebarOverlay.classList.add('show');
  1543. document.body.style.overflow = 'hidden';
  1544. });
  1545. sidebarOverlay.addEventListener('click', function () {
  1546. sidebar.classList.remove('show');
  1547. sidebarOverlay.classList.remove('show');
  1548. document.body.style.overflow = '';
  1549. });
  1550. }
  1551. } else {
  1552. // 桌面端隐藏切换按钮
  1553. if (sidebarToggle) {
  1554. sidebarToggle.style.display = 'none';
  1555. }
  1556. }
  1557. // 监听窗口大小变化
  1558. window.addEventListener('resize', function () {
  1559. const isMobileNow = window.innerWidth <= 768;
  1560. if (isMobileNow && !isMobile) {
  1561. // 切换到移动端
  1562. if (sidebarToggle) {
  1563. sidebarToggle.style.display = 'flex';
  1564. }
  1565. if (sidebar && sidebarOverlay) {
  1566. sidebar.classList.remove('show');
  1567. sidebarOverlay.classList.remove('show');
  1568. document.body.style.overflow = '';
  1569. }
  1570. } else if (!isMobileNow && isMobile) {
  1571. // 切换到桌面端
  1572. if (sidebarToggle) {
  1573. sidebarToggle.style.display = 'none';
  1574. }
  1575. if (sidebar && sidebarOverlay) {
  1576. sidebar.classList.remove('show');
  1577. sidebarOverlay.classList.remove('show');
  1578. document.body.style.overflow = '';
  1579. }
  1580. }
  1581. });
  1582. // 新对话按钮功能
  1583. if (newChatBtn) {
  1584. newChatBtn.addEventListener('click', function () {
  1585. // 立即创建新对话
  1586. createNewConversation();
  1587. });
  1588. }
  1589. // 对话项点击功能
  1590. conversationItems.forEach(item => {
  1591. item.addEventListener('click', function (e) {
  1592. // 如果点击的是菜单按钮,不处理
  1593. if (e.target.closest('.conversation-menu')) {
  1594. return;
  1595. }
  1596. const conversationId = this.getAttribute('data-conversation-id');
  1597. // 设置激活的对话
  1598. if (typeof setActiveConversation === 'function') {
  1599. setActiveConversation(conversationId);
  1600. }
  1601. loadConversation(conversationId);
  1602. updateConversationSelection(conversationId);
  1603. // 移动端关闭侧边栏
  1604. if (isMobile && sidebar && sidebarOverlay) {
  1605. sidebar.classList.remove('show');
  1606. sidebarOverlay.classList.remove('show');
  1607. document.body.style.overflow = '';
  1608. }
  1609. });
  1610. });
  1611. // 对话菜单功能
  1612. conversationMenus.forEach(menu => {
  1613. menu.addEventListener('click', function (e) {
  1614. e.stopPropagation();
  1615. const conversationId = this.getAttribute('data-conversation-id');
  1616. showConversationMenu(conversationId, this);
  1617. });
  1618. });
  1619. // 同步侧边栏用户信息
  1620. syncSidebarUserInfo();
  1621. // 加载历史记录
  1622. loadConversationHistory();
  1623. }
  1624. // 查找现有的空"新对话"
  1625. function findExistingEmptyConversation() {
  1626. const conversationItems = document.querySelectorAll('.conversation-item');
  1627. for (let item of conversationItems) {
  1628. const messageCountElement = item.querySelector('.message-count');
  1629. if (messageCountElement) {
  1630. const messageCount = parseInt(messageCountElement.textContent);
  1631. if (messageCount === 0) {
  1632. return {
  1633. conversationId: item.getAttribute('data-conversation-id'),
  1634. sessionId: item.getAttribute('data-session-id'),
  1635. element: item
  1636. };
  1637. }
  1638. }
  1639. }
  1640. return null;
  1641. }
  1642. // 切换到现有的空"新对话"
  1643. function switchToExistingEmptyConversation(conversation) {
  1644. // 更新当前对话ID和session_id
  1645. window.currentConversationId = conversation.conversationId;
  1646. window.currentSessionId = conversation.sessionId;
  1647. // 清除聊天区域并显示提示语
  1648. clearCurrentConversation();
  1649. // 更新侧边栏选中状态
  1650. updateConversationSelection(conversation.conversationId);
  1651. // 移动端关闭侧边栏
  1652. const isMobile = window.innerWidth <= 768;
  1653. if (isMobile) {
  1654. const sidebar = document.getElementById('history-sidebar');
  1655. const sidebarOverlay = document.getElementById('sidebar-overlay');
  1656. if (sidebar && sidebarOverlay) {
  1657. sidebar.classList.remove('show');
  1658. sidebarOverlay.classList.remove('show');
  1659. document.body.style.overflow = '';
  1660. }
  1661. }
  1662. // 确保输入框状态正确
  1663. const userInput = document.getElementById('user-input');
  1664. const sendButton = document.getElementById('send-btn');
  1665. if (userInput && sendButton) {
  1666. userInput.disabled = false;
  1667. userInput.placeholder = '请输入需要撰写的品类名和要求...';
  1668. userInput.classList.remove('input-loading');
  1669. sendButton.disabled = false;
  1670. sendButton.classList.remove('stopping');
  1671. }
  1672. }
  1673. function createNewConversation() {
  1674. // 检查认证状态
  1675. const token = localStorage.getItem('auth_token');
  1676. if (!token) {
  1677. redirectToLogin();
  1678. return;
  1679. }
  1680. // 先检查是否已经存在一个空的"新对话"
  1681. const existingEmptyConversation = findExistingEmptyConversation();
  1682. if (existingEmptyConversation) {
  1683. // 如果存在空的"新对话",直接切换到该对话
  1684. switchToExistingEmptyConversation(existingEmptyConversation);
  1685. return;
  1686. }
  1687. // 显示加载状态
  1688. const newChatBtn = document.getElementById('new-chat-btn');
  1689. if (newChatBtn) {
  1690. const originalText = newChatBtn.innerHTML;
  1691. newChatBtn.innerHTML = `
  1692. <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;">
  1693. <path d="M21 12a9 9 0 11-6.219-8.56" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
  1694. </svg>
  1695. 创建中...
  1696. `;
  1697. newChatBtn.disabled = true;
  1698. }
  1699. // 调用后端API创建新对话
  1700. fetch('/api/conversations/new', {
  1701. method: 'POST',
  1702. headers: {
  1703. 'Authorization': `Bearer ${token}`,
  1704. 'Content-Type': 'application/json'
  1705. },
  1706. body: JSON.stringify({
  1707. title: '新对话'
  1708. })
  1709. })
  1710. .then(response => {
  1711. if (response.status === 401) {
  1712. redirectToLogin();
  1713. return;
  1714. }
  1715. return response.json();
  1716. })
  1717. .then(data => {
  1718. if (data && data.success) {
  1719. // 更新当前对话ID和session_id
  1720. window.currentConversationId = data.conversation.conversation_id;
  1721. window.currentSessionId = data.conversation.session_id;
  1722. // 清除聊天区域并显示提示语
  1723. clearCurrentConversation();
  1724. // 更新侧边栏选中状态
  1725. updateConversationSelection(data.conversation.conversation_id);
  1726. // 重新加载历史记录以显示新对话,但不自动加载对话内容
  1727. loadConversationHistory(false);
  1728. // 确保输入框状态正确
  1729. const userInput = document.getElementById('user-input');
  1730. const sendButton = document.getElementById('send-btn');
  1731. if (userInput && sendButton) {
  1732. userInput.disabled = false;
  1733. userInput.placeholder = '请输入需要撰写的品类名和要求...';
  1734. userInput.classList.remove('input-loading');
  1735. sendButton.disabled = false;
  1736. sendButton.classList.remove('stopping');
  1737. }
  1738. // 移动端关闭侧边栏
  1739. const isMobile = window.innerWidth <= 768;
  1740. if (isMobile) {
  1741. const sidebar = document.getElementById('history-sidebar');
  1742. const sidebarOverlay = document.getElementById('sidebar-overlay');
  1743. if (sidebar && sidebarOverlay) {
  1744. sidebar.classList.remove('show');
  1745. sidebarOverlay.classList.remove('show');
  1746. document.body.style.overflow = '';
  1747. }
  1748. }
  1749. } else {
  1750. showMessage('创建新对话失败: ' + (data ? data.error : '未知错误'), 'error');
  1751. }
  1752. })
  1753. .catch(error => {
  1754. showMessage('创建新对话时发生错误: ' + error.message, 'error');
  1755. })
  1756. .finally(() => {
  1757. // 恢复按钮状态
  1758. if (newChatBtn) {
  1759. newChatBtn.innerHTML = `
  1760. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  1761. <path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
  1762. </svg>
  1763. 开启新对话
  1764. `;
  1765. newChatBtn.disabled = false;
  1766. }
  1767. });
  1768. }
  1769. function clearCurrentConversation() {
  1770. const chatMessages = document.getElementById('chat-messages');
  1771. if (chatMessages) {
  1772. chatMessages.innerHTML = `
  1773. <div class="message bot">
  1774. <div class="message-content">请发送你的投放数据。如果是视频诊断,目前仅能诊断文案,请去聚德神转转为文案(最好是带时间戳的字幕-聚德智能视频处理工具可导出)。
  1775. </div>
  1776. </div>
  1777. `;
  1778. }
  1779. // 清除全局状态管理中的当前激活对话
  1780. if (typeof currentActiveConversationId !== 'undefined') {
  1781. currentActiveConversationId = null;
  1782. }
  1783. // 清除强制创建新对话的标志(因为已经创建了新对话)
  1784. window.forceNewConversation = false;
  1785. }
  1786. function loadConversation(conversationId) {
  1787. // 检查认证状态
  1788. const token = localStorage.getItem('auth_token');
  1789. if (!token) {
  1790. redirectToLogin();
  1791. return;
  1792. }
  1793. // 设置激活的对话
  1794. if (typeof setActiveConversation === 'function') {
  1795. setActiveConversation(conversationId);
  1796. }
  1797. // 先切换到指定对话,然后加载内容
  1798. switchToConversationAndLoad(conversationId, token);
  1799. }
  1800. function switchToConversationAndLoad(conversationId, token) {
  1801. // 调用后端API切换到指定对话
  1802. fetch(`/api/conversations/${conversationId}/switch`, {
  1803. method: 'POST',
  1804. headers: {
  1805. 'Authorization': `Bearer ${token}`,
  1806. 'Content-Type': 'application/json'
  1807. }
  1808. }).then(response => {
  1809. if (response.status === 401) {
  1810. redirectToLogin();
  1811. return;
  1812. }
  1813. return response.json();
  1814. }).then(data => {
  1815. if (data && data.success) {
  1816. // 更新当前对话ID和session_id
  1817. window.currentConversationId = data.conversation.conversation_id;
  1818. window.currentSessionId = data.conversation.session_id;
  1819. // 加载对话内容
  1820. loadConversationSimple(conversationId, token);
  1821. } else {
  1822. showMessage('切换对话失败: ' + (data ? data.error : '未知错误'), 'error');
  1823. }
  1824. }).catch(error => {
  1825. showMessage('切换对话时发生错误: ' + error.message, 'error');
  1826. });
  1827. }
  1828. function loadConversationSimple(conversationId, token) {
  1829. // 检查是否有正在进行的流式响应
  1830. const hasActiveStream = window.conversationStates && window.conversationStates.has(conversationId);
  1831. const streamState = hasActiveStream ? window.conversationStates.get(conversationId) : null;
  1832. // 检查是否真正在流式响应(只要 isStreaming 为 true,无论是否有 messageElement)
  1833. const isActuallyStreaming = streamState && streamState.isStreaming;
  1834. // 检查是否有可用的消息元素(用于判断是否可以通过 updateExistingMessages 处理)
  1835. const hasMessageElement = streamState && streamState.messageElement && streamState.messageElement.parentNode;
  1836. // 返回 Promise
  1837. return fetch(`/api/conversations/${conversationId}`, {
  1838. method: 'GET',
  1839. headers: {
  1840. 'Authorization': `Bearer ${token}`,
  1841. 'Content-Type': 'application/json'
  1842. }
  1843. })
  1844. .then(response => {
  1845. if (response.status === 401) {
  1846. redirectToLogin();
  1847. return;
  1848. }
  1849. return response.json();
  1850. })
  1851. .then(data => {
  1852. if (data && data.success) {
  1853. const chatMessages = document.getElementById('chat-messages');
  1854. // 如果有正在进行的流式响应且有可用的消息元素,保留流式响应元素
  1855. if (isActuallyStreaming && hasMessageElement) {
  1856. // 保留流式响应的消息元素,只更新历史消息
  1857. updateExistingMessages(data.messages, streamState.messageElement);
  1858. } else {
  1859. // 清空聊天区域并重新渲染所有消息
  1860. chatMessages.innerHTML = '';
  1861. // 如果没有消息,显示提示语
  1862. if (!data.messages || data.messages.length === 0) {
  1863. chatMessages.innerHTML = `
  1864. <div class="message bot">
  1865. <div class="message-content">请发送你的投放数据。如果是视频诊断,目前仅能诊断文案,请去聚德神转转为文案(最好是带时间戳的字幕-聚德智能视频处理工具可导出)。</div>
  1866. </div>
  1867. `;
  1868. } else {
  1869. renderAllMessages(data.messages, chatMessages);
  1870. }
  1871. // 确保用户消息在切换对话后仍然可见
  1872. // 检查是否有未保存的用户消息
  1873. if (streamState && streamState.userMessages) {
  1874. streamState.userMessages.forEach(userMsg => {
  1875. // 检查用户消息是否已经在DOM中
  1876. const existingUserMsg = Array.from(chatMessages.children).find(msg =>
  1877. msg.classList.contains('user') &&
  1878. msg.querySelector('.message-content')?.textContent === userMsg.content
  1879. );
  1880. if (!existingUserMsg) {
  1881. // 如果用户消息不在DOM中,重新添加
  1882. chatMessages.appendChild(userMsg.element);
  1883. }
  1884. });
  1885. }
  1886. // ===== 恢复流式响应内容(无论是正在进行还是已完成但未保存到数据库) =====
  1887. // 检查数据库中是否已经包含了流式响应的内容
  1888. let shouldRestoreStreamContent = false;
  1889. // 如果流式响应正在进行,必须恢复显示
  1890. if (streamState && isActuallyStreaming) {
  1891. shouldRestoreStreamContent = true;
  1892. }
  1893. // 如果流式响应已完成,检查是否需要恢复(内容未保存到数据库)
  1894. else if (streamState && !isActuallyStreaming && (streamState.currentMessage || (streamState.currentThoughts && streamState.currentThoughts.length > 0))) {
  1895. // 检查数据库返回的最后一条 assistant 消息是否与流式响应的内容匹配
  1896. const dbMessages = data.messages || [];
  1897. const lastAssistantMsg = dbMessages.filter(msg => msg.role === 'assistant').pop();
  1898. if (lastAssistantMsg) {
  1899. // 如果最后一条 assistant 消息的内容与流式响应的内容匹配,说明内容已经保存到数据库
  1900. // 使用更精确的匹配:检查消息内容是否相同或包含流式响应的主要内容
  1901. let contentMatches = false;
  1902. if (streamState.currentMessage) {
  1903. const streamContent = streamState.currentMessage.trim();
  1904. const dbContent = lastAssistantMsg.content.trim();
  1905. // 精确匹配或检查前100个字符是否匹配(处理可能的截断)
  1906. contentMatches = (dbContent === streamContent) ||
  1907. (dbContent.length > 50 && streamContent.length > 50 &&
  1908. dbContent.substring(0, 100) === streamContent.substring(0, 100));
  1909. } else {
  1910. // 如果没有流式响应内容,认为匹配
  1911. contentMatches = true;
  1912. }
  1913. let thoughtsMatch = false;
  1914. if (streamState.currentThoughts && streamState.currentThoughts.length > 0) {
  1915. const streamThoughts = streamState.currentThoughts.join('').trim();
  1916. const dbThoughts = (lastAssistantMsg.thinking_content || '').trim();
  1917. thoughtsMatch = (dbThoughts === streamThoughts) ||
  1918. (dbThoughts.length > 50 && streamThoughts.length > 50 &&
  1919. dbThoughts.substring(0, 100) === streamThoughts.substring(0, 100));
  1920. } else {
  1921. // 如果流式响应没有思考过程,检查数据库是否有思考过程
  1922. thoughtsMatch = !lastAssistantMsg.thinking_content || lastAssistantMsg.thinking_content.trim() === '';
  1923. }
  1924. // 只有当内容不匹配时,才需要恢复流式响应的内容
  1925. shouldRestoreStreamContent = !contentMatches || !thoughtsMatch;
  1926. } else {
  1927. // 如果数据库中没有 assistant 消息,说明确实需要恢复流式响应的内容
  1928. shouldRestoreStreamContent = true;
  1929. }
  1930. // 如果内容已保存在数据库中,清除流式响应状态以避免下次重复检查
  1931. if (!shouldRestoreStreamContent) {
  1932. streamState.currentMessage = '';
  1933. streamState.currentThoughts = [];
  1934. }
  1935. }
  1936. if (shouldRestoreStreamContent && streamState) {
  1937. // 如果流式响应正在进行或已完成,但消息元素不在DOM中,需要重新创建或恢复
  1938. let restoredMessageElement = streamState.messageElement;
  1939. // 如果流式响应正在进行,确保消息元素存在
  1940. const isStreamingNow = streamState.isStreaming;
  1941. // 如果消息元素不存在或不在DOM中
  1942. if (!restoredMessageElement) {
  1943. // 消息元素不存在,需要创建
  1944. restoredMessageElement = document.createElement('div');
  1945. restoredMessageElement.className = 'message bot';
  1946. // 创建思考过程容器(如果有)
  1947. if (streamState.currentThoughts && streamState.currentThoughts.length > 0) {
  1948. const thoughtContainer = document.createElement('div');
  1949. thoughtContainer.className = 'thought-container';
  1950. const thoughtToggleBtn = document.createElement('button');
  1951. thoughtToggleBtn.className = 'thought-toggle-btn';
  1952. 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>';
  1953. thoughtToggleBtn.title = '收起/展开思考过程';
  1954. thoughtToggleBtn.onclick = function (e) {
  1955. e.stopPropagation();
  1956. thoughtContainer.classList.toggle('collapsed');
  1957. };
  1958. const thoughtContent = document.createElement('div');
  1959. thoughtContent.className = 'thought-content';
  1960. if (typeof window.renderThoughts === 'function') {
  1961. thoughtContent.innerHTML = window.renderThoughts(streamState.currentThoughts.join(''));
  1962. } else {
  1963. thoughtContent.textContent = streamState.currentThoughts.join('');
  1964. }
  1965. thoughtContainer.appendChild(thoughtToggleBtn);
  1966. thoughtContainer.appendChild(thoughtContent);
  1967. restoredMessageElement.appendChild(thoughtContainer);
  1968. // 更新状态中的引用
  1969. streamState.thoughtContainer = thoughtContainer;
  1970. }
  1971. // 创建消息内容容器
  1972. const contentContainer = document.createElement('div');
  1973. // 如果流式响应正在进行,添加 typing 类名
  1974. contentContainer.className = isStreamingNow ? 'message-content typing' : 'message-content';
  1975. if (streamState.currentMessage) {
  1976. if (typeof window.renderMarkdown === 'function') {
  1977. contentContainer.innerHTML = window.renderMarkdown(streamState.currentMessage);
  1978. if (typeof window.applyCodeHighlighting === 'function') {
  1979. window.applyCodeHighlighting(contentContainer);
  1980. }
  1981. } else {
  1982. contentContainer.textContent = streamState.currentMessage;
  1983. }
  1984. }
  1985. restoredMessageElement.appendChild(contentContainer);
  1986. // 保存 contentContainer 引用到状态中,以便流式响应继续更新
  1987. if (isStreamingNow && contentContainer) {
  1988. // 确保状态中的消息元素引用正确
  1989. streamState.messageElement = restoredMessageElement;
  1990. }
  1991. // 添加到聊天区域
  1992. chatMessages.appendChild(restoredMessageElement);
  1993. // 添加对话ID标识,便于后续管理
  1994. restoredMessageElement.setAttribute('data-conversation-id', conversationId);
  1995. // 更新状态中的引用(确保流式响应能够继续更新)
  1996. streamState.messageElement = restoredMessageElement;
  1997. // 如果流式响应正在进行,确保恢复后能够继续更新
  1998. if (isStreamingNow) {
  1999. // 确保消息元素可见
  2000. restoredMessageElement.style.opacity = '1';
  2001. restoredMessageElement.style.transform = 'translateY(0)';
  2002. // 滚动到底部以看到实时更新
  2003. setTimeout(() => {
  2004. chatMessages.scrollTop = chatMessages.scrollHeight;
  2005. }, 100);
  2006. }
  2007. } else if (!restoredMessageElement.parentNode) {
  2008. // 消息元素存在但不在DOM中,需要重新添加到DOM
  2009. chatMessages.appendChild(restoredMessageElement);
  2010. // 确保消息元素可见
  2011. restoredMessageElement.style.opacity = '1';
  2012. restoredMessageElement.style.transform = 'translateY(0)';
  2013. // 如果流式响应正在进行,滚动到底部
  2014. if (isStreamingNow) {
  2015. setTimeout(() => {
  2016. chatMessages.scrollTop = chatMessages.scrollHeight;
  2017. }, 100);
  2018. }
  2019. } else {
  2020. // 如果消息元素还在DOM中,只需要确保内容是最新的
  2021. const contentContainer = restoredMessageElement.querySelector('.message-content');
  2022. if (contentContainer) {
  2023. // 如果流式响应正在进行,确保有 typing 类名
  2024. if (isStreamingNow && !contentContainer.classList.contains('typing')) {
  2025. contentContainer.classList.add('typing');
  2026. } else if (!isStreamingNow && contentContainer.classList.contains('typing')) {
  2027. contentContainer.classList.remove('typing');
  2028. }
  2029. // 更新内容
  2030. if (streamState.currentMessage) {
  2031. if (typeof window.renderMarkdown === 'function') {
  2032. contentContainer.innerHTML = window.renderMarkdown(streamState.currentMessage);
  2033. if (typeof window.applyCodeHighlighting === 'function') {
  2034. window.applyCodeHighlighting(contentContainer);
  2035. }
  2036. } else {
  2037. contentContainer.textContent = streamState.currentMessage;
  2038. }
  2039. }
  2040. }
  2041. // 恢复思考过程
  2042. if (streamState.thoughtContainer && streamState.currentThoughts && streamState.currentThoughts.length > 0) {
  2043. const thoughtContent = streamState.thoughtContainer.querySelector('.thought-content');
  2044. if (thoughtContent) {
  2045. if (typeof window.renderThoughts === 'function') {
  2046. thoughtContent.innerHTML = window.renderThoughts(streamState.currentThoughts.join(''));
  2047. } else {
  2048. thoughtContent.textContent = streamState.currentThoughts.join('');
  2049. }
  2050. }
  2051. }
  2052. }
  2053. // 确保消息元素可见
  2054. restoredMessageElement.style.opacity = '1';
  2055. restoredMessageElement.style.transform = 'translateY(0)';
  2056. // 为新恢复的消息添加复制按钮
  2057. if (typeof window.setupCopyButtons === 'function') {
  2058. window.setupCopyButtons();
  2059. }
  2060. }
  2061. }
  2062. // 更新当前对话ID和session_id
  2063. window.currentConversationId = conversationId;
  2064. if (data.conversation && data.conversation.session_id) {
  2065. window.currentSessionId = data.conversation.session_id;
  2066. }
  2067. // 更新输入框状态 - 确保切换对话时输入框状态正确
  2068. if (typeof updateInputUIForConversation === 'function') {
  2069. updateInputUIForConversation(conversationId);
  2070. } else if (typeof resetUIState === 'function') {
  2071. resetUIState();
  2072. }
  2073. // 滚动到底部
  2074. setTimeout(() => {
  2075. chatMessages.scrollTop = chatMessages.scrollHeight;
  2076. }, 100);
  2077. } else {
  2078. showMessage('加载对话失败: ' + (data ? data.error : '未知错误'), 'error');
  2079. }
  2080. })
  2081. .catch(error => {
  2082. showMessage('加载对话时发生错误: ' + error.message, 'error');
  2083. });
  2084. }
  2085. function updateExistingMessages(messages, streamElement) {
  2086. const chatMessages = document.getElementById('chat-messages');
  2087. const existingMessages = Array.from(chatMessages.children);
  2088. // 找到流式响应元素的位置
  2089. const streamIndex = existingMessages.indexOf(streamElement);
  2090. // 移除流式响应元素之前的所有消息(这些是历史消息)
  2091. for (let i = 0; i < streamIndex; i++) {
  2092. if (existingMessages[i] && existingMessages[i].parentNode) {
  2093. existingMessages[i].remove();
  2094. }
  2095. }
  2096. // 在流式响应元素之前插入所有历史消息
  2097. messages.forEach((msg, index) => {
  2098. // 显示所有历史消息,包括用户消息和AI回复
  2099. const messageDiv = createMessageElement(msg);
  2100. chatMessages.insertBefore(messageDiv, streamElement);
  2101. });
  2102. }
  2103. function renderAllMessages(messages, chatMessages) {
  2104. messages.forEach((msg, index) => {
  2105. const messageDiv = createMessageElement(msg);
  2106. chatMessages.appendChild(messageDiv);
  2107. });
  2108. }
  2109. function createMessageElement(msg) {
  2110. const messageDiv = document.createElement('div');
  2111. messageDiv.className = `message ${msg.role}`;
  2112. let content = msg.content;
  2113. // 如果是AI消息且有思考过程,添加思考容器
  2114. if (msg.role === 'assistant' && msg.thinking_content) {
  2115. // 使用思考过程专用的渲染函数
  2116. let thoughtHtml = '';
  2117. if (typeof window.renderThoughts === 'function') {
  2118. thoughtHtml = window.renderThoughts(msg.thinking_content);
  2119. } else if (typeof window.renderMarkdown === 'function') {
  2120. thoughtHtml = window.renderMarkdown(msg.thinking_content);
  2121. } else if (typeof showdown !== 'undefined') {
  2122. const converter = new showdown.Converter({
  2123. strikethrough: true,
  2124. tables: true,
  2125. tasklists: true,
  2126. ghCodeBlocks: true,
  2127. openLinksInNewWindow: true
  2128. });
  2129. thoughtHtml = converter.makeHtml(msg.thinking_content);
  2130. } else {
  2131. thoughtHtml = msg.thinking_content;
  2132. }
  2133. const thoughtContainerHtml = `
  2134. <div class="thought-container" data-collapsed="false">
  2135. <button class="thought-toggle-btn" onclick="toggleThought(this)">
  2136. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  2137. <path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
  2138. </svg>
  2139. </button>
  2140. <div class="thought-content">
  2141. ${thoughtHtml}
  2142. </div>
  2143. </div>
  2144. `;
  2145. content = thoughtContainerHtml + content;
  2146. }
  2147. // 使用统一的Markdown渲染函数渲染消息内容
  2148. let renderedContent = content;
  2149. if (msg.role === 'assistant') {
  2150. if (typeof window.renderMarkdown === 'function') {
  2151. // 只渲染消息内容部分,思考过程已经渲染过了
  2152. const messageContentOnly = msg.content;
  2153. renderedContent = msg.thinking_content ?
  2154. content.replace(msg.content, window.renderMarkdown(messageContentOnly)) :
  2155. window.renderMarkdown(messageContentOnly);
  2156. } else if (typeof showdown !== 'undefined') {
  2157. const converter = new showdown.Converter({
  2158. strikethrough: true,
  2159. tables: true,
  2160. tasklists: true,
  2161. ghCodeBlocks: true,
  2162. openLinksInNewWindow: true
  2163. });
  2164. // 只渲染消息内容部分,思考过程已经渲染过了
  2165. const messageContentOnly = msg.content;
  2166. renderedContent = msg.thinking_content ?
  2167. content.replace(msg.content, converter.makeHtml(messageContentOnly)) :
  2168. converter.makeHtml(messageContentOnly);
  2169. }
  2170. }
  2171. // 为用户消息添加编辑按钮
  2172. let messageContentHtml = `<div class="message-content">${renderedContent}</div>`;
  2173. if (msg.role === 'user' && msg.message_id) {
  2174. // 转义特殊字符,防止XSS攻击
  2175. const escapedContent = msg.content.replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  2176. messageContentHtml = `
  2177. <div class="message-content">${renderedContent}</div>
  2178. <button class="edit-button" onclick="editMessage(${msg.message_id}, '${escapedContent}')" title="编辑消息">
  2179. <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  2180. <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"/>
  2181. <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"/>
  2182. </svg>
  2183. </button>
  2184. `;
  2185. }
  2186. messageDiv.innerHTML = messageContentHtml;
  2187. // 为用户消息添加data-message-id属性
  2188. if (msg.role === 'user' && msg.message_id) {
  2189. messageDiv.setAttribute('data-message-id', msg.message_id);
  2190. }
  2191. // 确保消息立即可见
  2192. messageDiv.style.opacity = '1';
  2193. messageDiv.style.transform = 'translateY(0)';
  2194. // 应用代码高亮和表格处理
  2195. if (typeof applyCodeHighlighting === 'function') {
  2196. applyCodeHighlighting(messageDiv);
  2197. }
  2198. if (typeof processAuditResultTables === 'function') {
  2199. processAuditResultTables();
  2200. }
  2201. // 为历史消息添加复制按钮
  2202. if (typeof window.setupCopyButtons === 'function') {
  2203. window.setupCopyButtons();
  2204. }
  2205. return messageDiv;
  2206. }
  2207. function updateConversationSelection(conversationId) {
  2208. const conversationItems = document.querySelectorAll('.conversation-item');
  2209. conversationItems.forEach(item => {
  2210. const itemId = item.getAttribute('data-conversation-id');
  2211. if (itemId === conversationId) {
  2212. item.classList.add('active');
  2213. // 确保状态点显示
  2214. const statusDot = item.querySelector('.conversation-status-dot');
  2215. if (statusDot) {
  2216. statusDot.style.opacity = '1';
  2217. }
  2218. } else {
  2219. item.classList.remove('active');
  2220. // 移除非活跃对话的状态类
  2221. item.classList.remove('streaming', 'paused');
  2222. const statusDot = item.querySelector('.conversation-status-dot');
  2223. if (statusDot) {
  2224. statusDot.style.opacity = '0';
  2225. }
  2226. }
  2227. });
  2228. }
  2229. function updateConversationStatus(conversationId, status) {
  2230. const conversationItem = document.querySelector(`[data-conversation-id="${conversationId}"]`);
  2231. if (conversationItem) {
  2232. const statusDot = conversationItem.querySelector('.conversation-status-dot');
  2233. // 移除所有状态类
  2234. conversationItem.classList.remove('streaming', 'paused');
  2235. // 添加新的状态类
  2236. if (status === 'streaming') {
  2237. conversationItem.classList.add('streaming');
  2238. if (statusDot) {
  2239. statusDot.style.opacity = '1';
  2240. statusDot.style.background = '#10b981';
  2241. }
  2242. } else if (status === 'paused') {
  2243. conversationItem.classList.add('paused');
  2244. if (statusDot) {
  2245. statusDot.style.opacity = '1';
  2246. statusDot.style.background = '#f59e0b';
  2247. }
  2248. } else if (status === 'stopped') {
  2249. if (statusDot) {
  2250. statusDot.style.opacity = '0';
  2251. }
  2252. }
  2253. }
  2254. }
  2255. function showConversationMenu(conversationId, button) {
  2256. // 创建菜单
  2257. const menu = document.createElement('div');
  2258. menu.className = 'conversation-context-menu';
  2259. menu.innerHTML = `
  2260. <div class="menu-item" data-action="rename">
  2261. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  2262. <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"/>
  2263. <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"/>
  2264. </svg>
  2265. 重命名
  2266. </div>
  2267. <div class="menu-item" data-action="delete">
  2268. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  2269. <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"/>
  2270. </svg>
  2271. 删除
  2272. </div>
  2273. `;
  2274. // 添加样式
  2275. menu.style.cssText = `
  2276. position: absolute;
  2277. background: rgba(255, 255, 255, 0.95);
  2278. backdrop-filter: blur(20px);
  2279. border: 1px solid rgba(255, 255, 255, 0.3);
  2280. border-radius: 8px;
  2281. box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
  2282. padding: 8px 0;
  2283. min-width: 120px;
  2284. z-index: 1000;
  2285. opacity: 0;
  2286. transform: scale(0.9);
  2287. transition: all 0.2s ease;
  2288. `;
  2289. // 定位菜单
  2290. const rect = button.getBoundingClientRect();
  2291. menu.style.left = (rect.left - 100) + 'px';
  2292. menu.style.top = (rect.bottom + 5) + 'px';
  2293. // 添加到页面
  2294. document.body.appendChild(menu);
  2295. // 显示动画
  2296. setTimeout(() => {
  2297. menu.style.opacity = '1';
  2298. menu.style.transform = 'scale(1)';
  2299. }, 10);
  2300. // 菜单项点击事件
  2301. menu.querySelectorAll('.menu-item').forEach(item => {
  2302. item.addEventListener('click', function () {
  2303. const action = this.getAttribute('data-action');
  2304. handleConversationAction(action, conversationId);
  2305. closeMenu();
  2306. });
  2307. });
  2308. // 点击外部关闭菜单
  2309. function closeMenu() {
  2310. menu.style.opacity = '0';
  2311. menu.style.transform = 'scale(0.9)';
  2312. setTimeout(() => {
  2313. if (menu.parentNode) {
  2314. menu.parentNode.removeChild(menu);
  2315. }
  2316. }, 200);
  2317. }
  2318. // 点击外部关闭
  2319. document.addEventListener('click', function closeOnOutsideClick(e) {
  2320. if (!menu.contains(e.target) && e.target !== button) {
  2321. closeMenu();
  2322. document.removeEventListener('click', closeOnOutsideClick);
  2323. }
  2324. });
  2325. }
  2326. function handleConversationAction(action, conversationId) {
  2327. switch (action) {
  2328. case 'rename':
  2329. renameConversation(conversationId);
  2330. break;
  2331. case 'delete':
  2332. deleteConversation(conversationId);
  2333. break;
  2334. }
  2335. }
  2336. function renameConversation(conversationId) {
  2337. // 检查认证状态
  2338. const token = localStorage.getItem('auth_token');
  2339. if (!token) {
  2340. redirectToLogin();
  2341. return;
  2342. }
  2343. // 获取当前对话标题
  2344. const conversationItem = document.querySelector(`[data-conversation-id="${conversationId}"]`);
  2345. const currentTitle = conversationItem ? conversationItem.querySelector('.conversation-title').textContent : '';
  2346. // 设置输入框的值
  2347. const renameInput = document.getElementById('rename-input');
  2348. if (renameInput) {
  2349. renameInput.value = currentTitle;
  2350. renameInput.focus();
  2351. renameInput.select();
  2352. }
  2353. // 显示重命名弹框
  2354. showModal('rename-modal');
  2355. // 绑定确认按钮事件
  2356. const confirmBtn = document.getElementById('confirm-rename-btn');
  2357. if (confirmBtn) {
  2358. confirmBtn.onclick = function () {
  2359. const newTitle = renameInput.value.trim();
  2360. if (newTitle && newTitle !== currentTitle) {
  2361. performRename(conversationId, newTitle, token);
  2362. } else if (!newTitle) {
  2363. showMessage('请输入新的对话标题', 'warning');
  2364. } else {
  2365. closeModal('rename-modal');
  2366. }
  2367. };
  2368. }
  2369. // 绑定回车键事件
  2370. if (renameInput) {
  2371. renameInput.onkeydown = function (e) {
  2372. if (e.key === 'Enter') {
  2373. confirmBtn.click();
  2374. } else if (e.key === 'Escape') {
  2375. closeModal('rename-modal');
  2376. }
  2377. };
  2378. }
  2379. }
  2380. function performRename(conversationId, newTitle, token) {
  2381. fetch(`/api/conversations/${conversationId}/rename`, {
  2382. method: 'POST',
  2383. headers: {
  2384. 'Authorization': `Bearer ${token}`,
  2385. 'Content-Type': 'application/json'
  2386. },
  2387. body: JSON.stringify({
  2388. title: newTitle
  2389. })
  2390. })
  2391. .then(response => {
  2392. if (response.status === 401) {
  2393. redirectToLogin();
  2394. return;
  2395. }
  2396. return response.json();
  2397. })
  2398. .then(data => {
  2399. if (data && data.success) {
  2400. const conversationItem = document.querySelector(`[data-conversation-id="${conversationId}"]`);
  2401. if (conversationItem) {
  2402. const titleElement = conversationItem.querySelector('.conversation-title');
  2403. if (titleElement) {
  2404. titleElement.textContent = newTitle;
  2405. }
  2406. }
  2407. closeModal('rename-modal');
  2408. showMessage('对话标题已更新');
  2409. } else {
  2410. showMessage('重命名失败: ' + (data ? data.error : '未知错误'), 'error');
  2411. }
  2412. })
  2413. .catch(error => {
  2414. showMessage('重命名对话时发生错误: ' + error.message, 'error');
  2415. });
  2416. }
  2417. function deleteConversation(conversationId) {
  2418. // 检查认证状态
  2419. const token = localStorage.getItem('auth_token');
  2420. if (!token) {
  2421. redirectToLogin();
  2422. return;
  2423. }
  2424. // 显示删除确认弹框
  2425. showModal('delete-modal');
  2426. // 绑定确认删除按钮事件
  2427. const confirmBtn = document.getElementById('confirm-delete-btn');
  2428. if (confirmBtn) {
  2429. confirmBtn.onclick = function () {
  2430. performDelete(conversationId, token);
  2431. };
  2432. }
  2433. }
  2434. function performDelete(conversationId, token) {
  2435. fetch(`/api/conversations/${conversationId}`, {
  2436. method: 'DELETE',
  2437. headers: {
  2438. 'Authorization': `Bearer ${token}`,
  2439. 'Content-Type': 'application/json'
  2440. }
  2441. })
  2442. .then(response => {
  2443. if (response.status === 401) {
  2444. redirectToLogin();
  2445. return;
  2446. }
  2447. return response.json();
  2448. })
  2449. .then(data => {
  2450. if (data && data.success) {
  2451. const conversationItem = document.querySelector(`[data-conversation-id="${conversationId}"]`);
  2452. if (conversationItem) {
  2453. conversationItem.remove();
  2454. }
  2455. closeModal('delete-modal');
  2456. showMessage('对话已删除');
  2457. // 如果删除的是当前对话,清除当前对话ID
  2458. if (window.currentConversationId === conversationId) {
  2459. window.currentConversationId = null;
  2460. // 清空聊天区域
  2461. const chatMessages = document.getElementById('chat-messages');
  2462. if (chatMessages) {
  2463. chatMessages.innerHTML = '';
  2464. }
  2465. }
  2466. } else {
  2467. showMessage('删除失败: ' + (data ? data.error : '未知错误'), 'error');
  2468. }
  2469. })
  2470. .catch(error => {
  2471. showMessage('删除对话时发生错误: ' + error.message, 'error');
  2472. });
  2473. }
  2474. function loadConversationHistory(autoSelectLatest = false) {
  2475. // 检查认证状态
  2476. const token = localStorage.getItem('auth_token');
  2477. if (!token) {
  2478. redirectToLogin();
  2479. return;
  2480. }
  2481. // 从服务器加载历史记录
  2482. fetch('/api/conversations', {
  2483. method: 'GET',
  2484. headers: {
  2485. 'Authorization': `Bearer ${token}`,
  2486. 'Content-Type': 'application/json'
  2487. }
  2488. })
  2489. .then(response => {
  2490. if (response.status === 401) {
  2491. // 认证失败,清除本地存储并重定向到登录页面
  2492. redirectToLogin();
  2493. return;
  2494. }
  2495. return response.json();
  2496. })
  2497. .then(data => {
  2498. if (data && data.success) {
  2499. updateHistorySidebar(data.conversations, autoSelectLatest);
  2500. } else {
  2501. // 显示空状态
  2502. const historyLoading = document.getElementById('history-loading');
  2503. const historyEmpty = document.getElementById('history-empty');
  2504. if (historyLoading) historyLoading.style.display = 'none';
  2505. if (historyEmpty) historyEmpty.style.display = 'block';
  2506. }
  2507. })
  2508. .catch(error => {
  2509. // 显示空状态
  2510. const historyLoading = document.getElementById('history-loading');
  2511. const historyEmpty = document.getElementById('history-empty');
  2512. if (historyLoading) historyLoading.style.display = 'none';
  2513. if (historyEmpty) historyEmpty.style.display = 'block';
  2514. });
  2515. }
  2516. // 将loadConversationHistory函数暴露到全局作用域,供chat.js调用
  2517. window.loadConversationHistory = loadConversationHistory;
  2518. function updateHistorySidebar(conversations, autoSelectLatest = false) {
  2519. // 更新侧边栏的历史记录
  2520. const historyContent = document.querySelector('.history-content');
  2521. const historyLoading = document.getElementById('history-loading');
  2522. const historyEmpty = document.getElementById('history-empty');
  2523. if (!historyContent) {
  2524. return;
  2525. }
  2526. // 隐藏加载状态
  2527. if (historyLoading) {
  2528. historyLoading.style.display = 'none';
  2529. }
  2530. // 清空现有内容
  2531. historyContent.innerHTML = '';
  2532. // 检查是否有任何对话
  2533. const hasAnyConversations = Object.values(conversations).some(group => group && group.length > 0);
  2534. if (!hasAnyConversations) {
  2535. // 显示空状态
  2536. if (historyEmpty) {
  2537. historyEmpty.style.display = 'block';
  2538. }
  2539. return;
  2540. }
  2541. // 隐藏空状态
  2542. if (historyEmpty) {
  2543. historyEmpty.style.display = 'none';
  2544. }
  2545. // 按时间分组渲染
  2546. const timeGroups = [
  2547. { key: 'today', label: '今天' },
  2548. { key: 'yesterday', label: '昨天' },
  2549. { key: 'week', label: '7天内' },
  2550. { key: 'month', label: '30天内' }
  2551. ];
  2552. timeGroups.forEach(group => {
  2553. const groupConversations = conversations[group.key] || [];
  2554. if (groupConversations.length === 0) return;
  2555. // 创建分组容器
  2556. const groupDiv = document.createElement('div');
  2557. groupDiv.className = 'history-group';
  2558. // 分组标题
  2559. const headerDiv = document.createElement('div');
  2560. headerDiv.className = 'group-header';
  2561. headerDiv.textContent = group.label;
  2562. // 对话列表
  2563. const listDiv = document.createElement('div');
  2564. listDiv.className = 'conversation-list';
  2565. groupConversations.forEach(conv => {
  2566. const itemDiv = document.createElement('div');
  2567. itemDiv.className = 'conversation-item';
  2568. itemDiv.setAttribute('data-conversation-id', conv.conversation_id);
  2569. itemDiv.setAttribute('data-session-id', conv.session_id || '');
  2570. // 格式化时间显示
  2571. const lastMessageTime = conv.last_message_at ? new Date(conv.last_message_at).toLocaleTimeString('zh-CN', {
  2572. hour: '2-digit',
  2573. minute: '2-digit'
  2574. }) : '';
  2575. itemDiv.innerHTML = `
  2576. <div class="conversation-content">
  2577. <div class="conversation-title">${conv.title}</div>
  2578. <div class="conversation-meta">
  2579. <span class="message-count">${conv.message_count}条消息</span>
  2580. ${lastMessageTime ? `<span class="last-time">${lastMessageTime}</span>` : ''}
  2581. </div>
  2582. </div>
  2583. <div class="conversation-status-dot"></div>
  2584. <button class="conversation-menu" data-conversation-id="${conv.conversation_id}">
  2585. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  2586. <circle cx="12" cy="12" r="1" fill="currentColor"/>
  2587. <circle cx="19" cy="12" r="1" fill="currentColor"/>
  2588. <circle cx="5" cy="12" r="1" fill="currentColor"/>
  2589. </svg>
  2590. </button>
  2591. `;
  2592. listDiv.appendChild(itemDiv);
  2593. });
  2594. groupDiv.appendChild(headerDiv);
  2595. groupDiv.appendChild(listDiv);
  2596. historyContent.appendChild(groupDiv);
  2597. });
  2598. // 重新绑定事件
  2599. bindConversationEvents();
  2600. // 自动选中对话
  2601. const firstConversationItem = document.querySelector('.conversation-item');
  2602. if (firstConversationItem) {
  2603. const conversationId = firstConversationItem.getAttribute('data-conversation-id');
  2604. if (autoSelectLatest) {
  2605. // 新对话完成后,选中最新的对话,但不加载内容(保持提示语显示)
  2606. updateConversationSelection(conversationId);
  2607. // 不调用 loadConversation,保持提示语显示
  2608. } else if (!window.currentConversationId) {
  2609. // 页面初始加载时,选中第一个对话
  2610. updateConversationSelection(conversationId);
  2611. loadConversation(conversationId);
  2612. }
  2613. }
  2614. }
  2615. function bindConversationEvents() {
  2616. // 重新绑定对话项点击事件
  2617. const conversationItems = document.querySelectorAll('.conversation-item');
  2618. conversationItems.forEach(item => {
  2619. item.addEventListener('click', function (e) {
  2620. if (e.target.closest('.conversation-menu')) {
  2621. return;
  2622. }
  2623. const conversationId = this.getAttribute('data-conversation-id');
  2624. const sessionId = this.getAttribute('data-session-id');
  2625. // 设置当前对话的session_id
  2626. if (sessionId) {
  2627. window.currentSessionId = sessionId;
  2628. }
  2629. // 设置激活的对话
  2630. if (typeof setActiveConversation === 'function') {
  2631. setActiveConversation(conversationId);
  2632. }
  2633. loadConversation(conversationId);
  2634. updateConversationSelection(conversationId);
  2635. // 移动端关闭侧边栏
  2636. const isMobile = window.innerWidth <= 768;
  2637. if (isMobile) {
  2638. const sidebar = document.getElementById('history-sidebar');
  2639. const sidebarOverlay = document.getElementById('sidebar-overlay');
  2640. if (sidebar && sidebarOverlay) {
  2641. sidebar.classList.remove('show');
  2642. sidebarOverlay.classList.remove('show');
  2643. document.body.style.overflow = '';
  2644. }
  2645. }
  2646. });
  2647. });
  2648. // 重新绑定对话菜单事件
  2649. const conversationMenus = document.querySelectorAll('.conversation-menu');
  2650. conversationMenus.forEach(menu => {
  2651. menu.addEventListener('click', function (e) {
  2652. e.stopPropagation();
  2653. const conversationId = this.getAttribute('data-conversation-id');
  2654. showConversationMenu(conversationId, this);
  2655. });
  2656. });
  2657. }
  2658. function syncSidebarUserInfo() {
  2659. const userInfo = localStorage.getItem('user_info');
  2660. if (userInfo) {
  2661. try {
  2662. const user = JSON.parse(userInfo);
  2663. const sidebarUserName = document.getElementById('sidebar-user-name');
  2664. const sidebarAvatarImg = document.getElementById('sidebar-avatar-img');
  2665. const sidebarAvatarFallback = document.getElementById('sidebar-avatar-fallback');
  2666. if (sidebarUserName) {
  2667. const displayName = user.nickname || user.name || '用户';
  2668. sidebarUserName.textContent = displayName;
  2669. }
  2670. if (sidebarAvatarImg && user.avatar && user.avatar.trim() !== '') {
  2671. sidebarAvatarImg.src = user.avatar;
  2672. sidebarAvatarImg.style.display = 'block';
  2673. sidebarAvatarFallback.style.display = 'none';
  2674. } else if (sidebarAvatarFallback) {
  2675. sidebarAvatarImg.style.display = 'none';
  2676. sidebarAvatarFallback.style.display = 'flex';
  2677. const displayName = user.nickname || user.name || 'U';
  2678. sidebarAvatarFallback.textContent = displayName.charAt(0).toUpperCase();
  2679. }
  2680. } catch (e) {
  2681. }
  2682. }
  2683. }
  2684. // 编辑消息功能
  2685. let currentEditMessageId = null;
  2686. let currentEditMessageElement = null;
  2687. window.isEditingMessage = false; // 设置为全局变量
  2688. window.editMessage = function (messageId, originalContent) {
  2689. // 如果已经在编辑其他消息,先取消
  2690. if (currentEditMessageElement) {
  2691. cancelEdit();
  2692. }
  2693. currentEditMessageId = messageId;
  2694. // 找到对应的消息元素
  2695. const messageElement = document.querySelector(`[data-message-id="${messageId}"]`);
  2696. if (!messageElement) {
  2697. console.error('找不到消息元素');
  2698. return;
  2699. }
  2700. currentEditMessageElement = messageElement;
  2701. window.isEditingMessage = true; // 设置编辑状态
  2702. // 保存当前滚动位置
  2703. const chatMessages = document.getElementById('chat-messages');
  2704. const currentScrollTop = chatMessages.scrollTop;
  2705. // 解码HTML实体
  2706. const decodedContent = originalContent
  2707. .replace(/&quot;/g, '"')
  2708. .replace(/&#39;/g, "'")
  2709. .replace(/&lt;/g, '<')
  2710. .replace(/&gt;/g, '>')
  2711. .replace(/&amp;/g, '&');
  2712. // 创建编辑表单
  2713. const editForm = document.createElement('div');
  2714. editForm.className = 'edit-form';
  2715. editForm.innerHTML = `
  2716. <textarea class="edit-textarea" placeholder="请输入消息内容...">${decodedContent}</textarea>
  2717. <div class="edit-actions">
  2718. <button class="edit-cancel-btn" onclick="cancelEdit()">取消</button>
  2719. <button class="edit-send-btn" onclick="confirmEdit()">发送</button>
  2720. </div>
  2721. `;
  2722. // 隐藏原始消息内容,显示编辑表单
  2723. const messageContent = messageElement.querySelector('.message-content');
  2724. if (messageContent) {
  2725. messageContent.style.display = 'none';
  2726. }
  2727. // 隐藏编辑按钮
  2728. const editButton = messageElement.querySelector('.edit-button');
  2729. if (editButton) {
  2730. editButton.style.display = 'none';
  2731. }
  2732. // 添加编辑表单到消息元素
  2733. messageElement.appendChild(editForm);
  2734. messageElement.classList.add('message-editing');
  2735. // 恢复滚动位置
  2736. chatMessages.scrollTop = currentScrollTop;
  2737. // 聚焦到文本域
  2738. const textarea = editForm.querySelector('.edit-textarea');
  2739. if (textarea) {
  2740. textarea.focus();
  2741. textarea.select();
  2742. // 绑定键盘事件:Enter 发送(Shift+Enter 换行),Esc 取消
  2743. textarea.onkeydown = function (e) {
  2744. if (e.key === 'Enter') {
  2745. if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey || e.isComposing) {
  2746. return; // 保留换行/组合键
  2747. }
  2748. e.preventDefault();
  2749. confirmEdit();
  2750. } else if (e.key === 'Escape') {
  2751. cancelEdit();
  2752. }
  2753. };
  2754. }
  2755. }
  2756. function cancelEdit() {
  2757. if (currentEditMessageElement) {
  2758. // 保存当前滚动位置
  2759. const chatMessages = document.getElementById('chat-messages');
  2760. const currentScrollTop = chatMessages.scrollTop;
  2761. // 恢复原始消息内容
  2762. const messageContent = currentEditMessageElement.querySelector('.message-content');
  2763. if (messageContent) {
  2764. messageContent.style.display = 'block';
  2765. }
  2766. // 显示编辑按钮
  2767. const editButton = currentEditMessageElement.querySelector('.edit-button');
  2768. if (editButton) {
  2769. editButton.style.display = 'flex';
  2770. }
  2771. // 移除编辑表单
  2772. const editForm = currentEditMessageElement.querySelector('.edit-form');
  2773. if (editForm) {
  2774. editForm.remove();
  2775. }
  2776. currentEditMessageElement.classList.remove('message-editing');
  2777. // 先恢复滚动位置,再清除编辑状态
  2778. chatMessages.scrollTop = currentScrollTop;
  2779. // 延迟清除编辑状态,确保DOM变化不会触发滚动
  2780. setTimeout(() => {
  2781. currentEditMessageElement = null;
  2782. currentEditMessageId = null;
  2783. window.isEditingMessage = false;
  2784. }, 150);
  2785. }
  2786. }
  2787. function confirmEdit() {
  2788. if (!currentEditMessageElement || !currentEditMessageId) {
  2789. return;
  2790. }
  2791. const editForm = currentEditMessageElement.querySelector('.edit-form');
  2792. const textarea = editForm.querySelector('.edit-textarea');
  2793. const sendBtn = editForm.querySelector('.edit-send-btn');
  2794. if (!textarea || !sendBtn) {
  2795. return;
  2796. }
  2797. const newContent = textarea.value.trim();
  2798. if (!newContent) {
  2799. showMessage('消息内容不能为空', 'warning');
  2800. return;
  2801. }
  2802. // 显示加载状态
  2803. const originalText = sendBtn.textContent;
  2804. sendBtn.textContent = '发送中...';
  2805. sendBtn.disabled = true;
  2806. // 调用后端API编辑消息
  2807. performEditMessage(currentEditMessageId, newContent);
  2808. }
  2809. function performEditMessage(messageId, newContent) {
  2810. // 检查认证状态
  2811. const token = localStorage.getItem('auth_token');
  2812. if (!token) {
  2813. redirectToLogin();
  2814. return;
  2815. }
  2816. // 设置输入框为加载状态
  2817. const userInput = document.getElementById('user-input');
  2818. const sendButton = document.getElementById('send-btn');
  2819. if (userInput && sendButton) {
  2820. userInput.classList.add('input-loading');
  2821. userInput.placeholder = '正在重新生成回复...';
  2822. userInput.disabled = true;
  2823. sendButton.innerHTML = `
  2824. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  2825. <rect x="6" y="6" width="12" height="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
  2826. </svg>`;
  2827. sendButton.classList.add('stopping');
  2828. }
  2829. // 隐藏编辑表单
  2830. if (currentEditMessageElement) {
  2831. const editForm = currentEditMessageElement.querySelector('.edit-form');
  2832. if (editForm) {
  2833. editForm.style.display = 'none';
  2834. }
  2835. }
  2836. // ===== 立即隐藏将被删除的消息(编辑的消息及其之后的所有消息) =====
  2837. const editMessageElement = currentEditMessageElement;
  2838. const chatMessages = document.getElementById('chat-messages');
  2839. let messagesToHide = [];
  2840. if (editMessageElement && chatMessages) {
  2841. let foundEditMessage = false;
  2842. const allMessages = Array.from(chatMessages.querySelectorAll('.message'));
  2843. allMessages.forEach(msg => {
  2844. if (msg === editMessageElement) {
  2845. foundEditMessage = true;
  2846. messagesToHide.push(msg);
  2847. // 立即隐藏消息(但保留DOM元素,后续再删除)
  2848. msg.style.display = 'none';
  2849. } else if (foundEditMessage) {
  2850. messagesToHide.push(msg);
  2851. // 立即隐藏消息
  2852. msg.style.display = 'none';
  2853. }
  2854. });
  2855. }
  2856. // 创建新的用户消息显示(立即显示编辑后的内容)
  2857. function createNewUserMessageImmediately() {
  2858. const chatMessages = document.getElementById('chat-messages');
  2859. if (!chatMessages) return null;
  2860. const newUserMessageElement = document.createElement('div');
  2861. newUserMessageElement.className = 'message user';
  2862. const contentElement = document.createElement('div');
  2863. contentElement.className = 'message-content';
  2864. contentElement.textContent = newContent;
  2865. newUserMessageElement.appendChild(contentElement);
  2866. // 重新对话添加编辑按钮
  2867. if (typeof messageId !== 'undefined' && messageId !== null) {
  2868. const editButton = document.createElement('button');
  2869. editButton.className = 'edit-button';
  2870. editButton.setAttribute('title', '编辑消息');
  2871. 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>`;
  2872. editButton.onclick = function () {
  2873. if (typeof window.editMessage === 'function') window.editMessage(messageId, newContent);
  2874. };
  2875. newUserMessageElement.appendChild(editButton);
  2876. newUserMessageElement.setAttribute('data-message-id', messageId);
  2877. }
  2878. chatMessages.appendChild(newUserMessageElement);
  2879. // 确保消息立即可见
  2880. newUserMessageElement.style.opacity = '1';
  2881. newUserMessageElement.style.transform = 'translateY(0)';
  2882. return newUserMessageElement;
  2883. }
  2884. // 立即创建并显示新的用户消息
  2885. const newUserMessageElement = createNewUserMessageImmediately();
  2886. // ===== 初始化对话状态(参考正常发送消息的逻辑) =====
  2887. const conversationId = window.currentConversationId || 'new';
  2888. // 初始化对话状态(如果不存在)
  2889. if (typeof window.initializeConversationState === 'function') {
  2890. window.initializeConversationState(conversationId);
  2891. }
  2892. // 获取或创建对话状态
  2893. const conversationStates = window.conversationStates || new Map();
  2894. const state = conversationStates.get(conversationId) || {
  2895. isStreaming: false,
  2896. isPaused: false,
  2897. currentMessage: '',
  2898. currentThoughts: [],
  2899. messageElement: null,
  2900. thoughtContainer: null,
  2901. abortController: null,
  2902. lastUpdateTime: Date.now(),
  2903. isVisible: false,
  2904. userMessages: []
  2905. };
  2906. // 重置流式响应状态
  2907. state.isStreaming = true;
  2908. state.isPaused = false;
  2909. state.currentMessage = '';
  2910. state.currentThoughts = [];
  2911. state.lastUpdateTime = Date.now();
  2912. state.isVisible = true;
  2913. // 保存用户消息到状态
  2914. if (!state.userMessages) {
  2915. state.userMessages = [];
  2916. }
  2917. state.userMessages.push({
  2918. content: newContent,
  2919. timestamp: new Date(),
  2920. element: newUserMessageElement,
  2921. messageId: messageId
  2922. });
  2923. // 更新状态映射
  2924. conversationStates.set(conversationId, state);
  2925. window.conversationStates = conversationStates;
  2926. // 更新侧边栏状态
  2927. if (typeof window.updateConversationItemStatus === 'function') {
  2928. window.updateConversationItemStatus(conversationId, 'streaming');
  2929. }
  2930. // ===== 创建AbortController用于取消请求(必须在fetch之前) =====
  2931. state.abortController = new AbortController();
  2932. conversationStates.set(conversationId, state);
  2933. // 调用后端API编辑消息(流式响应)
  2934. fetch(`/api/messages/${messageId}/edit`, {
  2935. method: 'POST',
  2936. headers: {
  2937. 'Authorization': `Bearer ${token}`,
  2938. 'Content-Type': 'application/json'
  2939. },
  2940. body: JSON.stringify({
  2941. content: newContent,
  2942. conversation_id: window.currentConversationId,
  2943. agent: localStorage.getItem('current_agent') || 'planning'
  2944. }),
  2945. signal: state.abortController.signal
  2946. })
  2947. .then(response => {
  2948. if (response.status === 401) {
  2949. // 恢复UI状态
  2950. if (userInput && sendButton) {
  2951. userInput.classList.remove('input-loading');
  2952. userInput.placeholder = '请输入需要撰写的品类名和要求...';
  2953. userInput.disabled = false;
  2954. sendButton.innerHTML = `
  2955. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  2956. <path d="M22 2L11 13M22 2L15 22L11 13M11 13L2 9L22 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
  2957. </svg>`;
  2958. sendButton.classList.remove('stopping');
  2959. }
  2960. currentEditMessageElement = null;
  2961. currentEditMessageId = null;
  2962. window.isEditingMessage = false;
  2963. redirectToLogin();
  2964. return;
  2965. }
  2966. if (!response.ok) {
  2967. throw new Error(`HTTP error! Status: ${response.status}`);
  2968. }
  2969. // 使用流式读取器处理SSE响应
  2970. const reader = response.body.getReader();
  2971. const decoder = new TextDecoder();
  2972. let buffer = '';
  2973. // 用于实时渲染的变量(保存到state中,便于切换对话时恢复)
  2974. let fullMessage = '';
  2975. let allThoughts = [];
  2976. let thoughtContainer = null;
  2977. let thoughtContent = null;
  2978. let contentContainer = null;
  2979. let messageDiv = null;
  2980. // newUserMessageElement 已经在前面创建了
  2981. let lastRenderTime = 0;
  2982. const RENDER_INTERVAL = 16;
  2983. // 创建AI回复消息容器
  2984. function createBotMessageContainer() {
  2985. if (messageDiv) return messageDiv;
  2986. messageDiv = document.createElement('div');
  2987. messageDiv.className = 'message bot';
  2988. // 添加对话ID标识,便于后续管理
  2989. messageDiv.setAttribute('data-conversation-id', conversationId);
  2990. // 创建思考过程容器
  2991. thoughtContainer = document.createElement('div');
  2992. thoughtContainer.className = 'thought-container';
  2993. const thoughtToggleBtn = document.createElement('button');
  2994. thoughtToggleBtn.className = 'thought-toggle-btn';
  2995. 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>';
  2996. thoughtToggleBtn.title = '收起/展开思考过程';
  2997. thoughtToggleBtn.onclick = function (e) {
  2998. e.stopPropagation();
  2999. thoughtContainer.classList.toggle('collapsed');
  3000. const icon = thoughtToggleBtn.querySelector('.toggle-icon');
  3001. if (thoughtContainer.classList.contains('collapsed')) {
  3002. 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>';
  3003. } else {
  3004. 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>';
  3005. }
  3006. };
  3007. thoughtContainer.appendChild(thoughtToggleBtn);
  3008. thoughtContent = document.createElement('div');
  3009. thoughtContent.className = 'thought-content';
  3010. thoughtContainer.appendChild(thoughtContent);
  3011. messageDiv.appendChild(thoughtContainer);
  3012. const chatMessages = document.getElementById('chat-messages');
  3013. if (chatMessages) {
  3014. chatMessages.appendChild(messageDiv);
  3015. }
  3016. // ===== 保存到对话状态(关键!) =====
  3017. state.messageElement = messageDiv;
  3018. state.thoughtContainer = thoughtContainer;
  3019. conversationStates.set(conversationId, state);
  3020. // 确保消息元素可见
  3021. messageDiv.style.opacity = '1';
  3022. messageDiv.style.transform = 'translateY(0)';
  3023. return messageDiv;
  3024. }
  3025. function readStream() {
  3026. reader.read().then(({ done, value }) => {
  3027. if (done) {
  3028. // 流式响应完成
  3029. state.isStreaming = false;
  3030. if (typeof window.updateConversationItemStatus === 'function') {
  3031. window.updateConversationItemStatus(conversationId, 'stopped');
  3032. }
  3033. conversationStates.set(conversationId, state);
  3034. updateUIOnComplete();
  3035. return;
  3036. }
  3037. const chunk = decoder.decode(value, { stream: true });
  3038. buffer += chunk;
  3039. const lines = buffer.split('\n\n');
  3040. buffer = lines.pop() || '';
  3041. lines.forEach(line => {
  3042. if (line.startsWith('data: ')) {
  3043. try {
  3044. const eventData = JSON.parse(line.substring(6));
  3045. handleEditEventData(eventData);
  3046. } catch (error) {
  3047. console.error('解析SSE数据失败:', error);
  3048. }
  3049. }
  3050. });
  3051. // 无论是否暂停,都继续读取流数据
  3052. readStream();
  3053. }).catch(error => {
  3054. if (!state.isPaused) {
  3055. console.error('读取流数据失败:', error);
  3056. showMessage('编辑消息时发生错误: ' + error.message, 'error');
  3057. }
  3058. state.isStreaming = false;
  3059. if (typeof window.updateConversationItemStatus === 'function') {
  3060. window.updateConversationItemStatus(conversationId, 'stopped');
  3061. }
  3062. conversationStates.set(conversationId, state);
  3063. updateUIOnError();
  3064. });
  3065. }
  3066. function handleEditEventData(data) {
  3067. // 更新最后更新时间
  3068. state.lastUpdateTime = Date.now();
  3069. if (!data.success) {
  3070. showMessage('编辑消息失败: ' + (data.error || '未知错误'), 'error');
  3071. state.isStreaming = false;
  3072. if (typeof window.updateConversationItemStatus === 'function') {
  3073. window.updateConversationItemStatus(conversationId, 'stopped');
  3074. }
  3075. updateUIOnError();
  3076. return;
  3077. }
  3078. if (data.thought) {
  3079. // AI回复容器已存在或需要创建
  3080. allThoughts.push(data.thought);
  3081. // ===== 保存思考过程到状态 =====
  3082. state.currentThoughts = allThoughts;
  3083. createBotMessageContainer();
  3084. if (thoughtContent) {
  3085. const combinedThoughts = allThoughts.join('');
  3086. if (typeof window.renderThoughts === 'function') {
  3087. thoughtContent.innerHTML = window.renderThoughts(combinedThoughts);
  3088. } else {
  3089. thoughtContent.textContent = combinedThoughts;
  3090. }
  3091. // 更新状态
  3092. conversationStates.set(conversationId, state);
  3093. forceScrollToBottom();
  3094. }
  3095. return;
  3096. }
  3097. if (data.chunk) {
  3098. // AI回复容器已存在或需要创建
  3099. fullMessage += data.chunk;
  3100. // ===== 保存消息内容到状态 =====
  3101. state.currentMessage = fullMessage;
  3102. createBotMessageContainer();
  3103. if (!contentContainer) {
  3104. contentContainer = document.createElement('div');
  3105. contentContainer.className = 'message-content typing';
  3106. messageDiv.appendChild(contentContainer);
  3107. }
  3108. const now = performance.now();
  3109. if (now - lastRenderTime >= RENDER_INTERVAL) {
  3110. requestAnimationFrame(() => {
  3111. if (contentContainer) {
  3112. if (typeof window.renderMarkdown === 'function') {
  3113. contentContainer.innerHTML = window.renderMarkdown(fullMessage);
  3114. if (typeof window.applyCodeHighlighting === 'function') {
  3115. window.applyCodeHighlighting(contentContainer);
  3116. }
  3117. } else {
  3118. contentContainer.textContent = fullMessage;
  3119. }
  3120. // 更新状态
  3121. conversationStates.set(conversationId, state);
  3122. if (isUserNearBottom()) {
  3123. forceScrollToBottom();
  3124. }
  3125. }
  3126. });
  3127. lastRenderTime = now;
  3128. }
  3129. }
  3130. if (data.session_id) {
  3131. window.currentSessionId = data.session_id;
  3132. }
  3133. if (data.done) {
  3134. if (contentContainer) {
  3135. contentContainer.classList.remove('typing');
  3136. if (typeof window.renderMarkdown === 'function') {
  3137. contentContainer.innerHTML = window.renderMarkdown(fullMessage);
  3138. if (typeof window.applyCodeHighlighting === 'function') {
  3139. window.applyCodeHighlighting(contentContainer);
  3140. }
  3141. } else {
  3142. contentContainer.textContent = fullMessage;
  3143. }
  3144. }
  3145. // ===== 标记流式响应完成,但保留状态 =====
  3146. state.isStreaming = false;
  3147. state.currentMessage = fullMessage;
  3148. state.currentThoughts = allThoughts;
  3149. conversationStates.set(conversationId, state);
  3150. if (typeof window.updateConversationItemStatus === 'function') {
  3151. window.updateConversationItemStatus(conversationId, 'stopped');
  3152. }
  3153. updateUIOnComplete();
  3154. }
  3155. }
  3156. function updateUIOnComplete() {
  3157. // 删除旧的消息元素(被编辑的消息及其后的所有消息)
  3158. // 使用chat-messages容器来查找并删除所有相关消息
  3159. const chatMessages = document.getElementById('chat-messages');
  3160. if (chatMessages && editMessageElement) {
  3161. // 获取所有消息元素
  3162. const allMessages = Array.from(chatMessages.querySelectorAll('.message'));
  3163. let foundEditMessage = false;
  3164. allMessages.forEach(msg => {
  3165. // 找到被编辑的消息
  3166. if (msg === editMessageElement) {
  3167. foundEditMessage = true;
  3168. // 删除编辑表单
  3169. const editForm = msg.querySelector('.edit-form');
  3170. if (editForm) {
  3171. editForm.remove();
  3172. }
  3173. msg.classList.remove('message-editing');
  3174. // 删除整个消息元素(除了新创建的用户消息和AI回复)
  3175. if (msg && msg.parentNode && msg !== messageDiv && msg !== newUserMessageElement) {
  3176. msg.remove();
  3177. }
  3178. } else if (foundEditMessage) {
  3179. // 删除被编辑消息之后的所有消息(除了新创建的用户消息和AI回复)
  3180. if (msg && msg.parentNode && msg !== messageDiv && msg !== newUserMessageElement) {
  3181. msg.remove();
  3182. }
  3183. }
  3184. });
  3185. }
  3186. // 如果editMessageElement还存在但没有被删除,手动删除它
  3187. if (editMessageElement && editMessageElement.parentNode) {
  3188. editMessageElement.remove();
  3189. }
  3190. currentEditMessageElement = null;
  3191. currentEditMessageId = null;
  3192. window.isEditingMessage = false;
  3193. if (userInput && sendButton) {
  3194. userInput.classList.remove('input-loading');
  3195. userInput.placeholder = '请输入需要撰写的品类名和要求...';
  3196. userInput.disabled = false;
  3197. sendButton.innerHTML = `
  3198. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  3199. <path d="M22 2L11 13M22 2L15 22L11 13M11 13L2 9L22 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
  3200. </svg>`;
  3201. sendButton.classList.remove('stopping');
  3202. }
  3203. // 只刷新侧边栏的对话列表,不重新加载消息(避免旧消息重新出现)
  3204. if (window.currentConversationId && typeof loadConversationHistory === 'function') {
  3205. setTimeout(() => {
  3206. loadConversationHistory(false); // 只更新侧边栏,不重新加载消息
  3207. }, 300);
  3208. }
  3209. // 为新创建的消息添加复制按钮
  3210. if (typeof window.setupCopyButtons === 'function') {
  3211. window.setupCopyButtons();
  3212. }
  3213. forceScrollToBottom();
  3214. }
  3215. function updateUIOnError() {
  3216. if (currentEditMessageElement) {
  3217. const sendBtn = currentEditMessageElement.querySelector('.edit-send-btn');
  3218. if (sendBtn) {
  3219. sendBtn.textContent = '发送';
  3220. sendBtn.disabled = false;
  3221. }
  3222. }
  3223. if (userInput && sendButton) {
  3224. userInput.classList.remove('input-loading');
  3225. userInput.placeholder = '请输入需要撰写的品类名和要求...';
  3226. userInput.disabled = false;
  3227. sendButton.innerHTML = `
  3228. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  3229. <path d="M22 2L11 13M22 2L15 22L11 13M11 13L2 9L22 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
  3230. </svg>`;
  3231. sendButton.classList.remove('stopping');
  3232. }
  3233. currentEditMessageElement = null;
  3234. currentEditMessageId = null;
  3235. window.isEditingMessage = false;
  3236. }
  3237. function forceScrollToBottom() {
  3238. const chatMessages = document.getElementById('chat-messages');
  3239. if (chatMessages) {
  3240. chatMessages.scrollTop = chatMessages.scrollHeight;
  3241. }
  3242. }
  3243. function isUserNearBottom() {
  3244. const chatMessages = document.getElementById('chat-messages');
  3245. if (!chatMessages) return true;
  3246. const scrollThreshold = 50;
  3247. return chatMessages.scrollHeight - chatMessages.scrollTop - chatMessages.clientHeight < scrollThreshold;
  3248. }
  3249. readStream();
  3250. })
  3251. .catch(error => {
  3252. // 确保在错误时也更新状态
  3253. if (error.name !== 'AbortError') {
  3254. state.isStreaming = false;
  3255. if (typeof window.updateConversationItemStatus === 'function') {
  3256. window.updateConversationItemStatus(conversationId, 'stopped');
  3257. }
  3258. conversationStates.set(conversationId, state);
  3259. }
  3260. console.error('编辑消息时发生错误:', error);
  3261. showMessage('编辑消息时发生错误: ' + error.message, 'error');
  3262. // 恢复UI状态
  3263. if (currentEditMessageElement) {
  3264. const sendBtn = currentEditMessageElement.querySelector('.edit-send-btn');
  3265. if (sendBtn) {
  3266. sendBtn.textContent = '发送';
  3267. sendBtn.disabled = false;
  3268. }
  3269. }
  3270. if (userInput && sendButton) {
  3271. userInput.classList.remove('input-loading');
  3272. userInput.placeholder = '请输入需要撰写的品类名和要求...';
  3273. userInput.disabled = false;
  3274. sendButton.innerHTML = `
  3275. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  3276. <path d="M22 2L11 13M22 2L15 22L11 13M11 13L2 9L22 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
  3277. </svg>`;
  3278. sendButton.classList.remove('stopping');
  3279. }
  3280. currentEditMessageElement = null;
  3281. currentEditMessageId = null;
  3282. window.isEditingMessage = false;
  3283. });
  3284. }
  3285. </script>
  3286. </body>
  3287. </html>