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