index.html 121 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>AIoT智能终端 - RT-Thread</title>
  7. <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
  8. <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
  9. <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.2/marked.min.js"></script>
  10. <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
  11. <style>
  12. :root {
  13. --primary-color: #10a37f;
  14. --primary-light: rgba(16, 163, 127, 0.1);
  15. --primary-dark: #0d8c6c;
  16. --secondary-color: #6366f1;
  17. --accent-color: #f59e0b;
  18. --sidebar-bg: #1a1b26;
  19. --chat-bg: #24253a;
  20. --message-ai-bg: #313244;
  21. --message-user-bg: #1e1e2e;
  22. --text-primary: #f8f9fa;
  23. --text-secondary: #c9d1d9;
  24. --text-muted: #8b949e;
  25. --border-color: #30363d;
  26. --border-light: rgba(255, 255, 255, 0.1);
  27. --hover-color: rgba(255, 255, 255, 0.08);
  28. --active-color: rgba(16, 163, 127, 0.15);
  29. --shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
  30. --shadow-sm: 0 4px 12px rgba(0, 0, 0, 0.15);
  31. --card-bg: rgba(49, 50, 68, 0.8);
  32. --gradient-primary: linear-gradient(135deg, var(--primary-color), #10b981);
  33. --gradient-secondary: linear-gradient(135deg, var(--secondary-color), #8b5cf6);
  34. }
  35. * {
  36. margin: 0;
  37. padding: 0;
  38. box-sizing: border-box;
  39. }
  40. html {
  41. scroll-behavior: smooth;
  42. }
  43. body {
  44. font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
  45. background-color: var(--chat-bg);
  46. background-image:
  47. radial-gradient(circle at 20% 50%, rgba(16, 163, 127, 0.1) 0%, transparent 50%),
  48. radial-gradient(circle at 80% 80%, rgba(99, 102, 241, 0.1) 0%, transparent 50%);
  49. color: var(--text-primary);
  50. height: 100vh;
  51. display: flex;
  52. overflow: hidden;
  53. line-height: 1.6;
  54. font-weight: 400;
  55. }
  56. /* 侧边栏样式 */
  57. .sidebar {
  58. width: 280px;
  59. background-color: rgba(26, 27, 38, 0.9);
  60. backdrop-filter: blur(20px);
  61. display: flex;
  62. flex-direction: column;
  63. height: 100%;
  64. transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  65. border-right: 1px solid var(--border-light);
  66. box-shadow: var(--shadow);
  67. }
  68. .new-chat-btn {
  69. margin: 15px;
  70. padding: 14px 18px;
  71. border: 1px solid var(--border-light);
  72. border-radius: 12px;
  73. background: var(--gradient-primary);
  74. color: white;
  75. display: flex;
  76. align-items: center;
  77. gap: 12px;
  78. cursor: pointer;
  79. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  80. font-size: 15px;
  81. font-weight: 600;
  82. box-shadow: var(--shadow-sm);
  83. position: relative;
  84. overflow: hidden;
  85. }
  86. .new-chat-btn::before {
  87. content: '';
  88. position: absolute;
  89. top: 0;
  90. left: -100%;
  91. width: 100%;
  92. height: 100%;
  93. background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
  94. transition: left 0.5s;
  95. }
  96. .new-chat-btn:hover::before {
  97. left: 100%;
  98. }
  99. .new-chat-btn:hover {
  100. transform: translateY(-2px);
  101. box-shadow: var(--shadow);
  102. }
  103. .history {
  104. flex: 1;
  105. overflow-y: auto;
  106. padding: 10px;
  107. }
  108. .history-item {
  109. padding: 14px 16px;
  110. border-radius: 10px;
  111. margin-bottom: 6px;
  112. cursor: pointer;
  113. display: flex;
  114. align-items: center;
  115. gap: 12px;
  116. color: var(--text-secondary);
  117. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  118. font-size: 14px;
  119. position: relative;
  120. overflow: hidden;
  121. border: 1px solid transparent;
  122. justify-content: space-between;
  123. }
  124. .history-item-content {
  125. display: flex;
  126. align-items: center;
  127. gap: 12px;
  128. flex: 1;
  129. overflow: hidden;
  130. }
  131. .history-item-title {
  132. white-space: nowrap;
  133. overflow: hidden;
  134. text-overflow: ellipsis;
  135. flex: 1;
  136. }
  137. .delete-chat-btn {
  138. width: 20px;
  139. height: 20px;
  140. border-radius: 4px;
  141. border: none;
  142. background: rgba(255, 255, 255, 0.1);
  143. color: var(--text-muted);
  144. display: flex;
  145. align-items: center;
  146. justify-content: center;
  147. cursor: pointer;
  148. transition: all 0.2s;
  149. font-size: 12px;
  150. opacity: 0;
  151. padding: 0;
  152. }
  153. .history-item:hover .delete-chat-btn {
  154. opacity: 1;
  155. }
  156. .delete-chat-btn:hover {
  157. background: rgba(239, 68, 68, 0.2);
  158. color: #ef4444;
  159. }
  160. .history-item:hover {
  161. background-color: var(--hover-color);
  162. border-color: var(--border-light);
  163. transform: translateX(4px);
  164. }
  165. .history-item.active {
  166. background-color: var(--active-color);
  167. border-color: var(--primary-color);
  168. color: var(--text-primary);
  169. }
  170. .history-item::before {
  171. content: '';
  172. position: absolute;
  173. left: 0;
  174. top: 0;
  175. height: 100%;
  176. width: 3px;
  177. background-color: var(--primary-color);
  178. opacity: 0;
  179. transition: opacity 0.2s;
  180. }
  181. .history-item.active::before {
  182. opacity: 1;
  183. }
  184. .sidebar-footer {
  185. padding: 15px;
  186. border-top: 1px solid var(--border-light);
  187. }
  188. .user-info {
  189. display: flex;
  190. align-items: center;
  191. gap: 10px;
  192. }
  193. .avatar {
  194. width: 32px;
  195. height: 32px;
  196. border-radius: 4px;
  197. background-color: var(--primary-color);
  198. display: flex;
  199. align-items: center;
  200. justify-content: center;
  201. font-size: 14px;
  202. }
  203. /* 主聊天区域样式 */
  204. .main-content {
  205. flex: 1;
  206. display: flex;
  207. flex-direction: column;
  208. height: 100%;
  209. position: relative;
  210. }
  211. .header {
  212. padding: 15px 20px;
  213. border-bottom: 1px solid var(--border-light);
  214. display: flex;
  215. align-items: center;
  216. justify-content: space-between;
  217. background-color: rgba(52, 53, 65, 0.9);
  218. backdrop-filter: blur(10px);
  219. }
  220. .model-selector {
  221. background-color: rgba(255, 255, 255, 0.05);
  222. padding: 8px 16px;
  223. border-radius: 8px;
  224. font-size: 14px;
  225. display: flex;
  226. align-items: center;
  227. gap: 8px;
  228. border: 1px solid var(--border-light);
  229. transition: all 0.2s;
  230. cursor: pointer;
  231. }
  232. .model-selector:hover {
  233. background-color: rgba(255, 255, 255, 0.08);
  234. }
  235. .chat-container {
  236. flex: 1;
  237. overflow-y: auto;
  238. padding: 30px 20px 20px;
  239. display: flex;
  240. flex-direction: column;
  241. gap: 12px; /* 收紧对话��间距 */
  242. }
  243. .message {
  244. display: flex;
  245. gap: 20px;
  246. max-width: 980px;
  247. margin: 0 auto;
  248. width: 100%;
  249. padding: 14px 0 30px; /* 收紧上下内边距,同时保留操作区空间 */
  250. position: relative; /* 使左下角操作按钮可绝对定位 */
  251. }
  252. .message-avatar {
  253. width: 32px;
  254. height: 32px;
  255. border-radius: 4px;
  256. display: flex;
  257. align-items: center;
  258. justify-content: center;
  259. flex-shrink: 0;
  260. font-size: 14px;
  261. }
  262. .ai-avatar {
  263. background-color: var(--primary-color);
  264. }
  265. .user-avatar {
  266. background-color: #8e44ad;
  267. }
  268. .message-content {
  269. flex: 1;
  270. padding: 20px 24px;
  271. line-height: 1.7;
  272. white-space: pre-wrap;
  273. font-size: 15px;
  274. animation: fadeIn 0.5s ease-out;
  275. }
  276. /* 渲染后的Markdown在容器中不保留换行,避免段间距过大 */
  277. .message-content .markdown-body { white-space: normal; }
  278. .ai-message {
  279. background: linear-gradient(135deg, rgba(49, 50, 68, 0.6), rgba(49, 50, 68, 0.4));
  280. border-radius: 16px;
  281. margin: 8px 0;
  282. backdrop-filter: blur(10px);
  283. border: 1px solid var(--border-light);
  284. }
  285. .user-message {
  286. background: linear-gradient(135deg, var(--message-user-bg), rgba(30, 30, 46, 0.8));
  287. border-radius: 16px;
  288. margin: 8px 0;
  289. backdrop-filter: blur(10px);
  290. border: 1px solid var(--border-light);
  291. }
  292. @keyframes fadeIn {
  293. from { opacity: 0; transform: translateY(10px); }
  294. to { opacity: 1; transform: translateY(0); }
  295. }
  296. /* 输入区域样式 */
  297. .input-container {
  298. padding: 24px;
  299. display: flex;
  300. justify-content: center;
  301. background: rgba(36, 37, 58, 0.95);
  302. backdrop-filter: blur(20px);
  303. border-top: 1px solid var(--border-light);
  304. }
  305. .input-wrapper {
  306. max-width: 980px;
  307. width: 100%;
  308. position: relative;
  309. }
  310. .message-input {
  311. width: 100%;
  312. padding: 18px 60px 18px 24px;
  313. border-radius: 16px;
  314. border: 2px solid var(--border-light);
  315. background: rgba(49, 50, 68, 0.9);
  316. color: var(--text-primary);
  317. font-size: 15px;
  318. resize: none;
  319. max-height: 200px;
  320. min-height: 64px;
  321. outline: none;
  322. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  323. box-shadow: var(--shadow-sm);
  324. backdrop-filter: blur(10px);
  325. }
  326. .message-input:focus {
  327. border-color: var(--primary-color);
  328. background: rgba(49, 50, 68, 0.95);
  329. box-shadow: 0 0 0 3px rgba(16, 163, 127, 0.1);
  330. }
  331. .message-input::placeholder {
  332. color: var(--text-muted);
  333. transition: color 0.3s ease;
  334. }
  335. .message-input:focus::placeholder {
  336. color: var(--text-secondary);
  337. }
  338. .send-button {
  339. position: absolute;
  340. right: 16px;
  341. bottom: 16px;
  342. background: var(--gradient-primary);
  343. color: white;
  344. border: none;
  345. border-radius: 12px;
  346. width: 40px;
  347. height: 40px;
  348. display: flex;
  349. align-items: center;
  350. justify-content: center;
  351. cursor: pointer;
  352. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  353. box-shadow: var(--shadow-sm);
  354. }
  355. .send-button:hover:not(:disabled) {
  356. transform: translateY(-2px) scale(1.05);
  357. box-shadow: 0 6px 20px rgba(16, 163, 127, 0.4);
  358. }
  359. .send-button:active:not(:disabled) {
  360. transform: translateY(0) scale(0.98);
  361. }
  362. .send-button:disabled {
  363. background: var(--border-color);
  364. cursor: not-allowed;
  365. box-shadow: none;
  366. transform: none;
  367. opacity: 0.6;
  368. }
  369. /* 加载指示器 */
  370. .typing-indicator {
  371. display: flex;
  372. align-items: center;
  373. gap: 8px;
  374. color: var(--text-secondary);
  375. font-style: italic;
  376. padding: 10px 0;
  377. font-size: 14px;
  378. }
  379. .typing-dots {
  380. display: flex;
  381. gap: 4px;
  382. }
  383. .typing-dot {
  384. width: 6px;
  385. height: 6px;
  386. border-radius: 50%;
  387. background-color: var(--text-secondary);
  388. animation: typing 1.4s infinite ease-in-out;
  389. }
  390. .typing-dot:nth-child(1) { animation-delay: -0.32s; }
  391. .typing-dot:nth-child(2) { animation-delay: -0.16s; }
  392. @keyframes typing {
  393. 0%, 80%, 100% { transform: scale(0.8); opacity: 0.5; }
  394. 40% { transform: scale(1); opacity: 1; }
  395. }
  396. @keyframes fadeInUp {
  397. from {
  398. opacity: 0;
  399. transform: translateY(30px);
  400. }
  401. to {
  402. opacity: 1;
  403. transform: translateY(0);
  404. }
  405. }
  406. @keyframes pulse {
  407. 0%, 100% { opacity: 1; }
  408. 50% { opacity: 0.8; }
  409. }
  410. /* 移动端样式 */
  411. @media (max-width: 768px) {
  412. .sidebar {
  413. transform: translateX(-100%);
  414. position: fixed;
  415. z-index: 100;
  416. height: 100%;
  417. box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
  418. }
  419. .sidebar.open {
  420. transform: translateX(0);
  421. }
  422. .header {
  423. padding: 15px;
  424. }
  425. .chat-container {
  426. padding: 25px 15px 15px;
  427. }
  428. .welcome-screen {
  429. padding: 80px 15px 40px;
  430. }
  431. .input-container {
  432. padding: 15px;
  433. }
  434. .message {
  435. gap: 15px;
  436. padding: 15px 0;
  437. }
  438. .welcome-features {
  439. grid-template-columns: 1fr;
  440. }
  441. .suggestion-cards {
  442. grid-template-columns: 1fr;
  443. }
  444. }
  445. /* 欢迎界面 */
  446. .welcome-screen {
  447. display: flex;
  448. flex-direction: column;
  449. align-items: center;
  450. justify-content: flex-start;
  451. min-height: 100%;
  452. text-align: center;
  453. padding: 60px 20px 40px;
  454. overflow-y: auto;
  455. }
  456. .welcome-title {
  457. font-size: 3.5rem;
  458. margin-bottom: 1.5rem;
  459. background: var(--gradient-primary);
  460. -webkit-background-clip: text;
  461. -webkit-text-fill-color: transparent;
  462. background-clip: text;
  463. font-weight: 800;
  464. letter-spacing: -0.02em;
  465. animation: fadeInUp 0.8s ease-out;
  466. }
  467. .welcome-subtitle {
  468. font-size: 1.25rem;
  469. color: var(--text-secondary);
  470. margin-bottom: 2.5rem;
  471. max-width: 680px;
  472. line-height: 1.7;
  473. animation: fadeInUp 0.8s ease-out 0.2s both;
  474. }
  475. .welcome-features {
  476. display: grid;
  477. grid-template-columns: repeat(3, 1fr);
  478. gap: 2rem;
  479. margin-bottom: 2.5rem;
  480. width: 100%;
  481. max-width: 900px;
  482. margin-left: auto;
  483. margin-right: auto;
  484. }
  485. .feature-card {
  486. background: var(--card-bg);
  487. backdrop-filter: blur(10px);
  488. padding: 2rem;
  489. border-radius: 16px;
  490. border: 1px solid var(--border-light);
  491. text-align: center;
  492. transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  493. animation: fadeInUp 0.8s ease-out 0.4s both;
  494. }
  495. .feature-card:hover {
  496. transform: translateY(-8px) scale(1.02);
  497. box-shadow: var(--shadow);
  498. border-color: var(--primary-color);
  499. background: rgba(49, 50, 68, 0.95);
  500. }
  501. .feature-icon {
  502. font-size: 3rem;
  503. background: var(--gradient-primary);
  504. -webkit-background-clip: text;
  505. -webkit-text-fill-color: transparent;
  506. background-clip: text;
  507. margin-bottom: 1.5rem;
  508. transition: all 0.3s ease;
  509. }
  510. .feature-card:hover .feature-icon {
  511. transform: scale(1.1);
  512. }
  513. .feature-title {
  514. font-size: 1.2rem;
  515. margin-bottom: 0.5rem;
  516. color: var(--text-primary);
  517. font-weight: 600;
  518. }
  519. .feature-desc {
  520. color: var(--text-secondary);
  521. line-height: 1.5;
  522. }
  523. .start-chat-btn {
  524. background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
  525. color: white;
  526. border: none;
  527. padding: 14px 36px;
  528. border-radius: 25px;
  529. font-size: 1.1rem;
  530. cursor: pointer;
  531. transition: all 0.3s;
  532. display: flex;
  533. align-items: center;
  534. gap: 8px;
  535. font-weight: 500;
  536. box-shadow: 0 4px 15px rgba(16, 163, 127, 0.3);
  537. margin-bottom: 20px;
  538. }
  539. .start-chat-btn:hover {
  540. transform: translateY(-2px);
  541. box-shadow: 0 10px 25px rgba(16, 163, 127, 0.4);
  542. }
  543. /* 错误消息样式 */
  544. .error-message {
  545. color: #ef4444;
  546. background-color: rgba(239, 68, 68, 0.1);
  547. padding: 10px 14px;
  548. border-radius: 8px;
  549. border-left: 4px solid #ef4444;
  550. font-size: 14px;
  551. }
  552. /* 滚动条样式 */
  553. ::-webkit-scrollbar {
  554. width: 10px;
  555. }
  556. ::-webkit-scrollbar-track {
  557. background: rgba(255, 255, 255, 0.03);
  558. border-radius: 5px;
  559. }
  560. ::-webkit-scrollbar-thumb {
  561. background: linear-gradient(180deg, var(--primary-color), var(--primary-dark));
  562. border-radius: 5px;
  563. transition: background 0.3s ease;
  564. }
  565. ::-webkit-scrollbar-thumb:hover {
  566. background: linear-gradient(180deg, var(--primary-dark), #0a7c5c);
  567. }
  568. /* 蒙版 */
  569. .overlay {
  570. position: fixed;
  571. top: 0;
  572. left: 0;
  573. right: 0;
  574. bottom: 0;
  575. background-color: rgba(0, 0, 0, 0.5);
  576. z-index: 99;
  577. display: none;
  578. }
  579. .overlay.active {
  580. display: block;
  581. }
  582. /* 新增功能样式 */
  583. .tools-panel {
  584. display: flex;
  585. gap: 10px;
  586. margin-bottom: 15px;
  587. justify-content: center;
  588. flex-wrap: wrap;
  589. max-width: 980px;
  590. margin-left: auto;
  591. margin-right: auto;
  592. }
  593. .tool-btn {
  594. background-color: var(--card-bg);
  595. border: 1px solid var(--border-light);
  596. border-radius: 8px;
  597. padding: 8px 16px;
  598. color: var(--text-secondary);
  599. cursor: pointer;
  600. transition: all 0.2s;
  601. display: flex;
  602. align-items: center;
  603. gap: 8px;
  604. font-size: 14px;
  605. }
  606. .tool-btn:hover {
  607. background-color: var(--hover-color);
  608. color: var(--text-primary);
  609. border-color: var(--primary-color);
  610. }
  611. .suggestion-cards {
  612. display: grid;
  613. grid-template-columns: repeat(3, 1fr);
  614. gap: 18px;
  615. margin-bottom: 2.5rem;
  616. max-width: 900px;
  617. width: 100%;
  618. margin-left: auto;
  619. margin-right: auto;
  620. }
  621. .suggestion-card {
  622. background: var(--card-bg);
  623. backdrop-filter: blur(10px);
  624. border: 1px solid var(--border-light);
  625. border-radius: 14px;
  626. padding: 20px;
  627. cursor: pointer;
  628. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  629. display: flex;
  630. flex-direction: column;
  631. min-height: 120px;
  632. position: relative;
  633. overflow: hidden;
  634. animation: fadeInUp 0.6s ease-out 0.6s both;
  635. }
  636. .suggestion-card::before {
  637. content: '';
  638. position: absolute;
  639. top: 0;
  640. left: 0;
  641. right: 0;
  642. height: 2px;
  643. background: var(--gradient-primary);
  644. transform: scaleX(0);
  645. transition: transform 0.3s ease;
  646. }
  647. .suggestion-card:hover::before {
  648. transform: scaleX(1);
  649. }
  650. .suggestion-card:hover {
  651. background: rgba(49, 50, 68, 0.95);
  652. border-color: var(--primary-color);
  653. transform: translateY(-4px);
  654. box-shadow: var(--shadow-sm);
  655. }
  656. .suggestion-title {
  657. font-weight: 600;
  658. margin-bottom: 8px;
  659. color: var(--text-primary);
  660. }
  661. .suggestion-desc {
  662. font-size: 14px;
  663. color: var(--text-secondary);
  664. }
  665. .message-actions {
  666. position: absolute;
  667. left: 52px; /* 头像(32) + 间距(20) 对齐内容左侧 */
  668. bottom: 4px; /* 随整体收紧,略微下移 */
  669. display: flex;
  670. gap: 8px;
  671. opacity: 0.85;
  672. }
  673. .icon-btn {
  674. width: 28px;
  675. height: 28px;
  676. border-radius: 6px;
  677. border: 1px solid var(--border-light);
  678. background: rgba(255,255,255,0.06);
  679. color: var(--text-secondary);
  680. display: inline-flex;
  681. align-items: center;
  682. justify-content: center;
  683. cursor: pointer;
  684. transition: all 0.2s;
  685. font-size: 13px;
  686. }
  687. .icon-btn:hover { color: var(--text-primary); background: rgba(255,255,255,0.12); }
  688. .action-btn {
  689. background: none;
  690. border: none;
  691. color: var(--text-muted);
  692. cursor: pointer;
  693. font-size: 14px;
  694. padding: 4px 8px;
  695. border-radius: 4px;
  696. transition: all 0.2s;
  697. }
  698. .action-btn:hover {
  699. background-color: var(--hover-color);
  700. color: var(--text-primary);
  701. }
  702. /* 代码块样式 */
  703. .code-block {
  704. background-color: #1e1e1e;
  705. border-radius: 8px;
  706. padding: 16px;
  707. margin: 10px 0;
  708. overflow-x: auto;
  709. font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
  710. font-size: 14px;
  711. border-left: 4px solid var(--primary-color);
  712. position: relative;
  713. }
  714. .code-header {
  715. display: flex;
  716. justify-content: space-between;
  717. align-items: center;
  718. margin-bottom: 10px;
  719. padding-bottom: 8px;
  720. border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  721. }
  722. .code-language {
  723. color: var(--text-muted);
  724. font-size: 12px;
  725. font-weight: 500;
  726. }
  727. .copy-code-btn {
  728. background: rgba(255, 255, 255, 0.1);
  729. border: none;
  730. color: var(--text-secondary);
  731. padding: 4px 8px;
  732. border-radius: 4px;
  733. cursor: pointer;
  734. font-size: 12px;
  735. transition: all 0.2s;
  736. }
  737. .copy-code-btn:hover {
  738. background: rgba(255, 255, 255, 0.2);
  739. color: var(--text-primary);
  740. }
  741. .code-content {
  742. color: #d4d4d4;
  743. line-height: 1.4;
  744. white-space: pre;
  745. }
  746. /* 代码高亮颜色 */
  747. .code-keyword { color: #569cd6; }
  748. .code-string { color: #ce9178; }
  749. .code-comment { color: #6a9955; }
  750. .code-number { color: #b5cea8; }
  751. .code-preprocessor { color: #9b9b9b; }
  752. .code-function { color: #dcdcaa; }
  753. /* Markdown 文本样式 */
  754. .message-content strong {
  755. color: var(--text-primary);
  756. font-weight: 600;
  757. }
  758. .message-content em {
  759. font-style: italic;
  760. color: var(--text-secondary);
  761. }
  762. .message-content ul, .message-content ol {
  763. margin: 10px 0;
  764. padding-left: 20px;
  765. }
  766. .message-content li {
  767. margin: 5px 0;
  768. }
  769. .message-content blockquote {
  770. border-left: 3px solid var(--primary-color);
  771. margin: 10px 0;
  772. padding-left: 15px;
  773. color: var(--text-secondary);
  774. }
  775. .message-content table {
  776. width: 100%;
  777. border-collapse: collapse;
  778. margin: 15px 0;
  779. }
  780. .message-content th, .message-content td {
  781. border: 1px solid var(--border-light);
  782. padding: 8px 12px;
  783. text-align: left;
  784. }
  785. .message-content th {
  786. background-color: rgba(255, 255, 255, 0.05);
  787. font-weight: 600;
  788. }
  789. .message-content hr {
  790. border: none;
  791. height: 1px;
  792. background-color: var(--border-light);
  793. margin: 12px 0;
  794. }
  795. .inline-code {
  796. background: rgba(255, 255, 255, 0.1);
  797. padding: 2px 6px;
  798. border-radius: 4px;
  799. font-family: monospace;
  800. font-size: 0.9em;
  801. }
  802. /* 新的 Markdown 容器,接近 ChatGPT 风格 */
  803. .markdown-body {
  804. font-size: 15px;
  805. line-height: 1.55; /* 更紧凑的行高 */
  806. color: var(--text-primary);
  807. }
  808. .markdown-body p { margin: 0.3em 0 0.6em; color: var(--text-primary); }
  809. .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4 {
  810. margin: 1em 0 0.5em; font-weight: 700; line-height: 1.25;
  811. }
  812. .markdown-body h1 { font-size: 1.6rem; }
  813. .markdown-body h2 { font-size: 1.4rem; }
  814. .markdown-body h3 { font-size: 1.2rem; }
  815. .markdown-body ul, .markdown-body ol { padding-left: 1.2em; margin: 0.3em 0 0.6em; }
  816. .markdown-body li { margin: 0.15em 0; }
  817. .markdown-body blockquote {
  818. padding: 0.6em 0.8em; margin: 0.8em 0; border-left: 3px solid var(--primary-color);
  819. background: rgba(255,255,255,0.04); color: var(--text-secondary); border-radius: 6px;
  820. }
  821. .markdown-body pre { position: relative; margin: 0.6em 0; }
  822. .markdown-body pre code { display: block; padding: 10px 12px; border-radius: 8px; }
  823. .markdown-body code:not(pre code) {
  824. background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 4px;
  825. }
  826. .copy-code-inline {
  827. position: absolute; top: 8px; right: 8px; z-index: 2;
  828. background: rgba(255,255,255,0.08); border: 1px solid var(--border-light);
  829. color: var(--text-secondary); padding: 4px 8px; border-radius: 6px; font-size: 12px; cursor: pointer;
  830. }
  831. /* 设置面板 */
  832. .settings-panel {
  833. position: absolute;
  834. top: 60px;
  835. right: 20px;
  836. background-color: var(--sidebar-bg);
  837. border: 1px solid var(--border-light);
  838. border-radius: 12px;
  839. padding: 16px;
  840. width: 320px;
  841. max-height: 80vh;
  842. overflow-y: auto;
  843. box-shadow: var(--shadow);
  844. z-index: 10;
  845. display: none;
  846. }
  847. .settings-panel.active {
  848. display: block;
  849. }
  850. /* 配置项样式 */
  851. .config-item {
  852. margin-bottom: 15px;
  853. position: relative;
  854. }
  855. .config-label {
  856. display: block;
  857. font-size: 13px;
  858. color: var(--text-secondary);
  859. margin-bottom: 6px;
  860. font-weight: 500;
  861. }
  862. .config-input, .config-select {
  863. width: 100%;
  864. padding: 10px 12px;
  865. border: 1px solid var(--border-light);
  866. border-radius: 8px;
  867. background: rgba(255, 255, 255, 0.05);
  868. color: var(--text-primary);
  869. font-size: 14px;
  870. transition: all 0.2s;
  871. outline: none;
  872. }
  873. .config-input:focus, .config-select:focus {
  874. border-color: var(--primary-color);
  875. background: rgba(255, 255, 255, 0.08);
  876. box-shadow: 0 0 0 3px rgba(16, 163, 127, 0.1);
  877. }
  878. .config-input::placeholder {
  879. color: var(--text-muted);
  880. }
  881. .config-select option {
  882. background: var(--sidebar-bg);
  883. color: var(--text-primary);
  884. }
  885. .config-select optgroup {
  886. background: rgba(255, 255, 255, 0.08);
  887. color: var(--text-secondary);
  888. font-weight: 600;
  889. padding: 4px 0;
  890. font-style: normal;
  891. }
  892. .config-select optgroup option {
  893. background: var(--sidebar-bg);
  894. color: var(--text-primary);
  895. font-weight: normal;
  896. padding-left: 12px;
  897. }
  898. .toggle-password {
  899. position: absolute;
  900. right: 10px;
  901. top: 32px;
  902. background: none;
  903. border: none;
  904. color: var(--text-muted);
  905. cursor: pointer;
  906. padding: 5px;
  907. border-radius: 4px;
  908. transition: all 0.2s;
  909. }
  910. .toggle-password:hover {
  911. color: var(--text-primary);
  912. background: rgba(255, 255, 255, 0.1);
  913. }
  914. .config-save-btn, .config-reset-btn {
  915. flex: 1;
  916. padding: 10px 16px;
  917. border: none;
  918. border-radius: 8px;
  919. font-size: 14px;
  920. font-weight: 500;
  921. cursor: pointer;
  922. transition: all 0.2s;
  923. display: flex;
  924. align-items: center;
  925. justify-content: center;
  926. gap: 6px;
  927. }
  928. .config-save-btn {
  929. background: var(--gradient-primary);
  930. color: white;
  931. }
  932. .config-save-btn:hover:not(:disabled) {
  933. transform: translateY(-1px);
  934. box-shadow: 0 4px 12px rgba(16, 163, 127, 0.3);
  935. }
  936. .config-save-btn:disabled {
  937. background: var(--border-color);
  938. cursor: not-allowed;
  939. transform: none;
  940. }
  941. .config-reset-btn {
  942. background: rgba(255, 255, 255, 0.1);
  943. color: var(--text-secondary);
  944. border: 1px solid var(--border-light);
  945. }
  946. .config-reset-btn:hover {
  947. background: rgba(255, 255, 255, 0.15);
  948. color: var(--text-primary);
  949. border-color: var(--primary-color);
  950. }
  951. .config-status {
  952. margin-top: 12px;
  953. padding: 8px 12px;
  954. border-radius: 6px;
  955. font-size: 13px;
  956. display: flex;
  957. align-items: center;
  958. gap: 6px;
  959. }
  960. .config-status.success {
  961. background: rgba(16, 163, 127, 0.1);
  962. color: var(--primary-color);
  963. border: 1px solid rgba(16, 163, 127, 0.2);
  964. }
  965. .config-status.error {
  966. background: rgba(239, 68, 68, 0.1);
  967. color: #ef4444;
  968. border: 1px solid rgba(239, 68, 68, 0.2);
  969. }
  970. .config-status.loading {
  971. background: rgba(245, 158, 11, 0.1);
  972. color: var(--accent-color);
  973. border: 1px solid rgba(245, 158, 11, 0.2);
  974. }
  975. /* 加载动画 */
  976. .loading-spinner {
  977. width: 14px;
  978. height: 14px;
  979. border: 2px solid transparent;
  980. border-top: 2px solid currentColor;
  981. border-radius: 50%;
  982. animation: spin 1s linear infinite;
  983. }
  984. @keyframes spin {
  985. 0% { transform: rotate(0deg); }
  986. 100% { transform: rotate(360deg); }
  987. }
  988. .setting-item {
  989. display: flex;
  990. justify-content: space-between;
  991. align-items: center;
  992. margin-bottom: 15px;
  993. }
  994. .setting-label {
  995. font-size: 14px;
  996. color: var(--text-secondary);
  997. }
  998. .toggle-switch {
  999. position: relative;
  1000. display: inline-block;
  1001. width: 40px;
  1002. height: 20px;
  1003. }
  1004. .toggle-switch input {
  1005. opacity: 0;
  1006. width: 0;
  1007. height: 0;
  1008. }
  1009. .toggle-slider {
  1010. position: absolute;
  1011. cursor: pointer;
  1012. top: 0;
  1013. left: 0;
  1014. right: 0;
  1015. bottom: 0;
  1016. background-color: var(--border-color);
  1017. transition: .4s;
  1018. border-radius: 20px;
  1019. }
  1020. .toggle-slider:before {
  1021. position: absolute;
  1022. content: "";
  1023. height: 16px;
  1024. width: 16px;
  1025. left: 2px;
  1026. bottom: 2px;
  1027. background-color: white;
  1028. transition: .4s;
  1029. border-radius: 50%;
  1030. }
  1031. input:checked + .toggle-slider {
  1032. background-color: var(--primary-color);
  1033. }
  1034. input:checked + .toggle-slider:before {
  1035. transform: translateX(20px);
  1036. }
  1037. .model-options {
  1038. display: flex;
  1039. flex-direction: column;
  1040. gap: 10px;
  1041. margin-top: 10px;
  1042. }
  1043. .model-option {
  1044. padding: 10px;
  1045. border-radius: 8px;
  1046. cursor: pointer;
  1047. transition: all 0.2s;
  1048. border: 1px solid transparent;
  1049. font-size: 14px;
  1050. }
  1051. .model-option:hover {
  1052. background-color: var(--hover-color);
  1053. }
  1054. .model-option.active {
  1055. background-color: var(--primary-light);
  1056. border-color: var(--primary-color);
  1057. }
  1058. /* 顶部标签切换 */
  1059. .tab-switcher {
  1060. display: flex;
  1061. gap: 8px;
  1062. }
  1063. .tab-btn {
  1064. background: transparent;
  1065. border: 1px solid var(--border-light);
  1066. color: var(--text-secondary);
  1067. padding: 8px 14px;
  1068. border-radius: 8px;
  1069. cursor: pointer;
  1070. transition: all 0.2s;
  1071. font-size: 14px;
  1072. }
  1073. .tab-btn.active {
  1074. color: var(--text-primary);
  1075. border-color: var(--primary-color);
  1076. background: var(--hover-color);
  1077. }
  1078. </style>
  1079. </head>
  1080. <body>
  1081. <!-- 移动端蒙版 -->
  1082. <div class="overlay" id="overlay"></div>
  1083. <!-- 侧边栏 -->
  1084. <div class="sidebar" id="sidebar">
  1085. <button class="new-chat-btn" id="newChatBtn">
  1086. <i class="fas fa-plus"></i>
  1087. 新对话
  1088. </button>
  1089. <div class="history" id="historyList">
  1090. <!-- 对话历史将动态加载 -->
  1091. </div>
  1092. <div class="sidebar-footer">
  1093. <div class="user-info">
  1094. <div class="avatar">
  1095. <i class="fas fa-user"></i>
  1096. </div>
  1097. <div>
  1098. <div>AIoT 用户</div>
  1099. <div style="font-size: 0.8rem; color: var(--text-secondary);">RT-Thread</div>
  1100. </div>
  1101. </div>
  1102. </div>
  1103. </div>
  1104. <!-- 主内容区域 -->
  1105. <div class="main-content">
  1106. <!-- 头部 -->
  1107. <div class="header">
  1108. <div style="display: flex; align-items: center; gap: 15px;">
  1109. <button id="menuToggle" style="background: none; border: none; color: var(--text-secondary); font-size: 1.2rem; cursor: pointer; display: none;">
  1110. <i class="fas fa-bars"></i>
  1111. </button>
  1112. <div class="model-selector" id="modelSelector">
  1113. <i class="fas fa-robot"></i>
  1114. <span>RT-LLM 助手</span>
  1115. <i class="fas fa-chevron-down" style="font-size: 12px;"></i>
  1116. </div>
  1117. </div>
  1118. <div style="display: flex; gap: 10px; align-items: center;">
  1119. <div class="tab-switcher">
  1120. <button class="tab-btn active" id="tabChat"><i class="fas fa-comments"></i> 聊天</button>
  1121. </div>
  1122. <button id="settingsBtn" style="background: none; border: none; color: var(--text-secondary); font-size: 1.2rem; cursor: pointer;">
  1123. <i class="fas fa-cog"></i>
  1124. </button>
  1125. </div>
  1126. </div>
  1127. <!-- 设置面板 -->
  1128. <div class="settings-panel" id="settingsPanel">
  1129. <!-- 通用设置 -->
  1130. <div style="margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid var(--border-light);">
  1131. <div style="font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 12px;">
  1132. <i class="fas fa-cog" style="margin-right: 6px;"></i>通用设置
  1133. </div>
  1134. <div class="setting-item">
  1135. <span class="setting-label">联网搜索</span>
  1136. <label class="toggle-switch">
  1137. <input type="checkbox" id="webSearchToggle">
  1138. <span class="toggle-slider"></span>
  1139. </label>
  1140. </div>
  1141. <div class="setting-item">
  1142. <span class="setting-label">代码高亮</span>
  1143. <label class="toggle-switch">
  1144. <input type="checkbox" id="codeHighlightToggle" checked>
  1145. <span class="toggle-slider"></span>
  1146. </label>
  1147. </div>
  1148. </div>
  1149. <!-- LLM配置 -->
  1150. <div style="margin-bottom: 20px;">
  1151. <div style="font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 12px;">
  1152. <i class="fas fa-robot" style="margin-right: 6px;"></i>大模型配置
  1153. </div>
  1154. <div class="config-item">
  1155. <label class="config-label">API密钥</label>
  1156. <input type="password" id="apiKeyInput" class="config-input" placeholder="请输入API密钥">
  1157. <button class="toggle-password" onclick="togglePasswordVisibility('apiKeyInput')">
  1158. <i class="fas fa-eye"></i>
  1159. </button>
  1160. </div>
  1161. <div class="config-item">
  1162. <label class="config-label">模型名称</label>
  1163. <select id="modelNameSelect" class="config-select">
  1164. <!-- 通义千问模型 -->
  1165. <optgroup label="通义千问" data-provider="qwen">
  1166. <option value="qwen-turbo">通义千问 Turbo</option>
  1167. <option value="qwen-plus">通义千问 Plus</option>
  1168. <option value="qwen-max">通义千问 Max</option>
  1169. <option value="qwen-long">通义千问 Long</option>
  1170. </optgroup>
  1171. <!-- 豆包模型 -->
  1172. <optgroup label="豆包" data-provider="doubao">
  1173. <option value="ep-20241218132920-4q6j9">豆包 Pro-32K</option>
  1174. <option value="ep-20241218132920-gjz3p">豆包 Pro-128K</option>
  1175. </optgroup>
  1176. <!-- DeepSeek模型 -->
  1177. <optgroup label="DeepSeek" data-provider="deepseek">
  1178. <option value="deepseek-chat">DeepSeek Chat</option>
  1179. <option value="deepseek-coder">DeepSeek Coder</option>
  1180. </optgroup>
  1181. </select>
  1182. </div>
  1183. <div class="config-item">
  1184. <label class="config-label">API提供商</label>
  1185. <select id="apiProviderSelect" class="config-select">
  1186. <option value="qwen">通义千问</option>
  1187. <option value="doubao">豆包</option>
  1188. <option value="deepseek">DeepSeek</option>
  1189. </select>
  1190. </div>
  1191. </div>
  1192. <!-- 操作按钮 -->
  1193. <div style="display: flex; gap: 10px; margin-top: 20px;">
  1194. <button id="saveConfigBtn" class="config-save-btn">
  1195. <i class="fas fa-save"></i> 保存配置
  1196. </button>
  1197. <button id="resetConfigBtn" class="config-reset-btn">
  1198. <i class="fas fa-undo"></i> 重置
  1199. </button>
  1200. </div>
  1201. <!-- 状态提示 -->
  1202. <div id="configStatus" class="config-status" style="display: none;"></div>
  1203. </div>
  1204. <!-- 聊天容器 -->
  1205. <div class="chat-container" id="chatContainer">
  1206. <!-- 欢迎界面 -->
  1207. <div class="welcome-screen" id="welcomeScreen">
  1208. <h1 class="welcome-title">AIoT 智能终端</h1>
  1209. <p class="welcome-subtitle">基于 RT-Thread 的智能聊天助手,助力您的物联网开发</p>
  1210. <!-- 工具面板 -->
  1211. <div class="tools-panel">
  1212. <button class="tool-btn" id="codeGenBtn">
  1213. <i class="fas fa-code"></i>
  1214. 代码生成
  1215. </button>
  1216. <button class="tool-btn" id="docSearchBtn">
  1217. <i class="fas fa-book"></i>
  1218. 文档搜索
  1219. </button>
  1220. <button class="tool-btn" id="debugHelpBtn">
  1221. <i class="fas fa-bug"></i>
  1222. 调试帮助
  1223. </button>
  1224. <button class="tool-btn" id="deviceControlBtn">
  1225. <i class="fas fa-microchip"></i>
  1226. 设备控制
  1227. </button>
  1228. </div>
  1229. <!-- 建议卡片 -->
  1230. <div class="suggestion-cards">
  1231. <div class="suggestion-card" data-prompt="如何在RT-Thread中创建一个新线程?">
  1232. <div class="suggestion-title">创建线程</div>
  1233. <div class="suggestion-desc">学习在RT-Thread中创建和管理线程的方法</div>
  1234. </div>
  1235. <div class="suggestion-card" data-prompt="RT-Thread的设备驱动模型是什么?">
  1236. <div class="suggestion-title">设备驱动</div>
  1237. <div class="suggestion-desc">了解RT-Thread的设备驱动框架和实现</div>
  1238. </div>
  1239. <div class="suggestion-card" data-prompt="如何在RT-Thread中使用消息队列?">
  1240. <div class="suggestion-title">消息队列</div>
  1241. <div class="suggestion-desc">掌握RT-Thread中消息队列的使用方法</div>
  1242. </div>
  1243. <div class="suggestion-card" data-prompt="RT-Thread的网络编程示例">
  1244. <div class="suggestion-title">网络编程</div>
  1245. <div class="suggestion-desc">学习RT-Thread中的网络编程和Socket使用</div>
  1246. </div>
  1247. <div class="suggestion-card" data-prompt="如何在RT-Thread中使用定时器(rt_timer)?">
  1248. <div class="suggestion-title">定时器</div>
  1249. <div class="suggestion-desc">掌握一次性/周期性定时器的创建与回调</div>
  1250. </div>
  1251. <div class="suggestion-card" data-prompt="RT-Thread下如何使用互斥量与信号量进行线程同步?">
  1252. <div class="suggestion-title">线程同步</div>
  1253. <div class="suggestion-desc">互斥量/信号量的典型用法与注意事项</div>
  1254. </div>
  1255. </div>
  1256. <div class="welcome-features">
  1257. <div class="feature-card">
  1258. <div class="feature-icon">
  1259. <i class="fas fa-brain"></i>
  1260. </div>
  1261. <div class="feature-title">智能对话</div>
  1262. <div class="feature-desc">与 AI 助手进行自然语言交互,解答技术问题</div>
  1263. </div>
  1264. <div class="feature-card">
  1265. <div class="feature-icon">
  1266. <i class="fas fa-microchip"></i>
  1267. </div>
  1268. <div class="feature-title">实时响应</div>
  1269. <div class="feature-desc">毫秒级响应,支持复杂查询和代码生成</div>
  1270. </div>
  1271. <div class="feature-card">
  1272. <div class="feature-icon">
  1273. <i class="fas fa-shield-alt"></i>
  1274. </div>
  1275. <div class="feature-title">安全可靠</div>
  1276. <div class="feature-desc">端到端加密,本地处理确保数据隐私</div>
  1277. </div>
  1278. </div>
  1279. <button class="start-chat-btn" onclick="document.getElementById('messageInput').focus()">
  1280. <i class="fas fa-comments"></i>
  1281. 开始对话
  1282. </button>
  1283. </div>
  1284. <!-- 已拆分:控制终端/文档/日志 页面改为独立文件 -->
  1285. </div>
  1286. <!-- 输入区域 -->
  1287. <div class="input-container">
  1288. <div class="input-wrapper">
  1289. <textarea
  1290. id="messageInput"
  1291. class="message-input"
  1292. placeholder="输入您的消息,按 Enter 发送..."
  1293. rows="1"
  1294. ></textarea>
  1295. <button class="send-button" id="sendButton" disabled>
  1296. <i class="fas fa-paper-plane"></i>
  1297. </button>
  1298. </div>
  1299. </div>
  1300. </div>
  1301. <script>
  1302. // 对话历史管理类
  1303. class ConversationManager {
  1304. constructor() {
  1305. this.conversations = this.loadConversations();
  1306. this.currentConversationId = null;
  1307. this.storageKey = 'llm_conversations';
  1308. }
  1309. // 从本地存储加载对话
  1310. loadConversations() {
  1311. try {
  1312. const stored = localStorage.getItem(this.storageKey);
  1313. return stored ? JSON.parse(stored) : [];
  1314. } catch (e) {
  1315. console.error('Failed to load conversations:', e);
  1316. return [];
  1317. }
  1318. }
  1319. // 保存对话到本地存储
  1320. saveConversations() {
  1321. try {
  1322. localStorage.setItem(this.storageKey, JSON.stringify(this.conversations));
  1323. } catch (e) {
  1324. console.error('Failed to save conversations:', e);
  1325. }
  1326. }
  1327. // 生成唯一ID
  1328. generateId() {
  1329. return 'conv_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
  1330. }
  1331. // 创建新对话
  1332. createConversation(title = '新对话') {
  1333. const conversation = {
  1334. id: this.generateId(),
  1335. title: title,
  1336. messages: [],
  1337. createdAt: Date.now(),
  1338. updatedAt: Date.now()
  1339. };
  1340. this.conversations.unshift(conversation);
  1341. this.saveConversations();
  1342. return conversation;
  1343. }
  1344. // 获取当前对话
  1345. getCurrentConversation() {
  1346. return this.conversations.find(conv => conv.id === this.currentConversationId);
  1347. }
  1348. // 切换到指定对话
  1349. switchToConversation(conversationId) {
  1350. const conversation = this.conversations.find(conv => conv.id === conversationId);
  1351. if (conversation) {
  1352. this.currentConversationId = conversationId;
  1353. return conversation;
  1354. }
  1355. return null;
  1356. }
  1357. // 添加消息到当前对话
  1358. addMessageToCurrent(content, isUser, isError = false) {
  1359. let conversation = this.getCurrentConversation();
  1360. console.log('添加消息前 - 当前对话ID:', this.currentConversationId, '对话:', conversation ? conversation.title : '无');
  1361. // 如果没有当前对话,创建一个新对话
  1362. if (!conversation) {
  1363. const title = isUser ? this.generateTitle(content) : '新对话';
  1364. conversation = this.createConversation(title);
  1365. this.currentConversationId = conversation.id;
  1366. console.log('创建新对话:', title, 'ID:', conversation.id);
  1367. }
  1368. const message = {
  1369. id: 'msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),
  1370. content,
  1371. isUser,
  1372. isError,
  1373. timestamp: Date.now()
  1374. };
  1375. conversation.messages.push(message);
  1376. conversation.updatedAt = Date.now();
  1377. // 如果是第一条用户消息,更新对话标题
  1378. if (isUser && conversation.messages.filter(m => m.isUser).length === 1) {
  1379. const oldTitle = conversation.title;
  1380. conversation.title = this.generateTitle(content);
  1381. console.log('更新对话标题:', oldTitle, '->', conversation.title);
  1382. }
  1383. console.log('添加消息到对话:', conversation.title, '消息总数:', conversation.messages.length);
  1384. this.saveConversations();
  1385. return message;
  1386. }
  1387. // 获取对话的最后一条消息内容用于显示
  1388. getLastMessageContent(conversation) {
  1389. if (!conversation.messages || conversation.messages.length === 0) {
  1390. return conversation.title || '新对话';
  1391. }
  1392. const lastMessage = conversation.messages[conversation.messages.length - 1];
  1393. let content = lastMessage.content.trim();
  1394. // 如果是错误消息,显示错误提示
  1395. if (lastMessage.isError) {
  1396. return '错误: ' + content.substring(0, 20) + '...';
  1397. }
  1398. // 如果是用户消息,添加"用户:"前缀
  1399. if (lastMessage.isUser) {
  1400. return '用户: ' + content;
  1401. } else {
  1402. // 如果是AI消息,直接显示内容
  1403. return content;
  1404. }
  1405. }
  1406. // 生成对话标题(从用户消息中提取)
  1407. generateTitle(message) {
  1408. const maxLength = 30;
  1409. let title = message.trim();
  1410. // 如果消息很短(比如只有问候语),直接使用作为标题
  1411. if (title.length <= 10) {
  1412. return title;
  1413. }
  1414. // 对于较长的消息,移除常见的问候语开头
  1415. const greetings = /^(你好|hello|hi|您好|Hi|Hello|HI)/i;
  1416. if (greetings.test(title)) {
  1417. title = title.replace(greetings, '').trim();
  1418. if (title.length === 0) {
  1419. // 如果移除问候语后没有内容了,使用原消息的前10个字符
  1420. return message.trim().substring(0, 10);
  1421. }
  1422. }
  1423. if (title.length > maxLength) {
  1424. title = title.substring(0, maxLength) + '...';
  1425. }
  1426. return title || message.trim().substring(0, 10) || '新对话';
  1427. }
  1428. // 删除对话(包含资源清理)
  1429. deleteConversation(conversationId) {
  1430. const index = this.conversations.findIndex(conv => conv.id === conversationId);
  1431. if (index !== -1) {
  1432. const deleted = this.conversations.splice(index, 1)[0];
  1433. // 清理对话数据资源
  1434. this.cleanupConversationResources(deleted);
  1435. // 如果删除的是当前对话,切换到其他对话或创建新对话
  1436. if (deleted.id === this.currentConversationId) {
  1437. if (this.conversations.length > 0) {
  1438. this.currentConversationId = this.conversations[0].id;
  1439. } else {
  1440. this.currentConversationId = null;
  1441. }
  1442. }
  1443. // 强制保存并清理localStorage
  1444. this.saveConversations();
  1445. this.cleanupStorage();
  1446. return deleted;
  1447. }
  1448. return null;
  1449. }
  1450. // 清理对话相关资源(立即清理)
  1451. cleanupConversationResources(conversation) {
  1452. if (!conversation) return;
  1453. console.log('正在清理对话资源:', conversation.title);
  1454. // 1. 立即清空并清理消息数据
  1455. if (conversation.messages && Array.isArray(conversation.messages)) {
  1456. // 遍历并清理每个消息对象
  1457. conversation.messages.forEach((msg, index) => {
  1458. // 清理消息内容
  1459. msg.content = null;
  1460. msg.domElements = null;
  1461. msg.tempData = null;
  1462. msg.renderedContent = null;
  1463. msg.cachedResponse = null;
  1464. });
  1465. // 立即清空数组
  1466. conversation.messages.length = 0;
  1467. conversation.messages = null;
  1468. }
  1469. // 2. 清理对话对象的所有属性
  1470. conversation.title = null;
  1471. conversation.createdAt = null;
  1472. conversation.updatedAt = null;
  1473. conversation.metadata = null;
  1474. conversation.tempCache = null;
  1475. // 3. 强制清理该对话的所有引用
  1476. Object.keys(conversation).forEach(key => {
  1477. if (conversation.hasOwnProperty(key)) {
  1478. conversation[key] = null;
  1479. }
  1480. });
  1481. }
  1482. // 清理localStorage中的无效数据
  1483. cleanupStorage() {
  1484. try {
  1485. const storageKey = this.storageKey;
  1486. const currentData = localStorage.getItem(storageKey);
  1487. if (currentData) {
  1488. const parsed = JSON.parse(currentData);
  1489. // 清理无效对话数据
  1490. if (parsed && Array.isArray(parsed)) {
  1491. const cleaned = parsed.filter(conv => {
  1492. return conv && conv.id && conv.messages && Array.isArray(conv.messages);
  1493. });
  1494. // 如果数据有变化,重新保存
  1495. if (cleaned.length !== parsed.length) {
  1496. localStorage.setItem(storageKey, JSON.stringify(cleaned));
  1497. console.log('清理了', parsed.length - cleaned.length, '个无效对话');
  1498. }
  1499. }
  1500. }
  1501. // 清理其他可能的缓存键
  1502. const cacheKeys = Object.keys(localStorage).filter(key =>
  1503. key.startsWith('llm_cache_') ||
  1504. key.startsWith('temp_') ||
  1505. key.startsWith('chat_cache_')
  1506. );
  1507. cacheKeys.forEach(key => {
  1508. localStorage.removeItem(key);
  1509. console.log('清理缓存键:', key);
  1510. });
  1511. } catch (e) {
  1512. console.warn('清理localStorage时出错:', e);
  1513. }
  1514. }
  1515. // 批量清理所有对话资源
  1516. cleanupAllResources() {
  1517. console.log('开始清理所有对话资源...');
  1518. // 清理所有对话
  1519. this.conversations.forEach(conv => {
  1520. this.cleanupConversationResources(conv);
  1521. });
  1522. // 清空对话数组
  1523. this.conversations.length = 0;
  1524. this.currentConversationId = null;
  1525. // 清理存储
  1526. this.cleanupStorage();
  1527. // 通知垃圾回收
  1528. if (window.gc) {
  1529. window.gc();
  1530. }
  1531. console.log('所有对话资源清理完成');
  1532. }
  1533. // 清空当前对话的消息
  1534. clearCurrentConversation() {
  1535. const conversation = this.getCurrentConversation();
  1536. if (conversation) {
  1537. conversation.messages = [];
  1538. conversation.updatedAt = Date.now();
  1539. this.saveConversations();
  1540. }
  1541. }
  1542. // 获取对话列表(按更新时间排序)
  1543. getConversationList() {
  1544. return [...this.conversations].sort((a, b) => b.updatedAt - a.updatedAt);
  1545. }
  1546. }
  1547. document.addEventListener('DOMContentLoaded', function() {
  1548. // 初始化对话管理器
  1549. const conversationManager = new ConversationManager();
  1550. const chatContainer = document.getElementById('chatContainer');
  1551. const messageInput = document.getElementById('messageInput');
  1552. const sendButton = document.getElementById('sendButton');
  1553. const newChatBtn = document.getElementById('newChatBtn');
  1554. const welcomeScreen = document.getElementById('welcomeScreen');
  1555. const historyList = document.getElementById('historyList');
  1556. // 渲染对话列表
  1557. function renderConversationList() {
  1558. const conversations = conversationManager.getConversationList();
  1559. historyList.innerHTML = '';
  1560. conversations.forEach(conv => {
  1561. const historyItem = document.createElement('div');
  1562. historyItem.className = 'history-item';
  1563. historyItem.setAttribute('data-conversation-id', conv.id);
  1564. if (conv.id === conversationManager.currentConversationId) {
  1565. historyItem.classList.add('active');
  1566. }
  1567. // 获取最后一条消息内容作为显示文本
  1568. const displayContent = conversationManager.getLastMessageContent(conv);
  1569. const truncatedContent = displayContent.length > 40 ? displayContent.substring(0, 40) + '...' : displayContent;
  1570. historyItem.innerHTML = `
  1571. <div class="history-item-content">
  1572. <i class="fas fa-message"></i>
  1573. <span class="history-item-title" title="${displayContent}">${truncatedContent}</span>
  1574. </div>
  1575. <button class="delete-chat-btn" title="删除对话">
  1576. <i class="fas fa-times"></i>
  1577. </button>
  1578. `;
  1579. // 点击切换对话
  1580. const contentDiv = historyItem.querySelector('.history-item-content');
  1581. contentDiv.addEventListener('click', function(e) {
  1582. e.stopPropagation();
  1583. switchToConversation(conv.id);
  1584. });
  1585. // 删除对话
  1586. const deleteBtn = historyItem.querySelector('.delete-chat-btn');
  1587. deleteBtn.addEventListener('click', function(e) {
  1588. e.stopPropagation();
  1589. deleteConversation(conv.id);
  1590. });
  1591. historyList.appendChild(historyItem);
  1592. });
  1593. // 如果没有对话,显示欢迎界面
  1594. if (conversations.length === 0) {
  1595. welcomeScreen.style.display = 'flex';
  1596. }
  1597. }
  1598. // 切换到指定对话
  1599. function switchToConversation(conversationId) {
  1600. const conversation = conversationManager.switchToConversation(conversationId);
  1601. if (conversation) {
  1602. console.log('切换到对话:', conversation.title, '消息数量:', conversation.messages.length);
  1603. // 清空当前聊天内容(保留欢迎界面)
  1604. const messages = chatContainer.querySelectorAll('.message');
  1605. messages.forEach(msg => msg.remove());
  1606. // 隐藏欢迎界面
  1607. welcomeScreen.style.display = 'none';
  1608. // 重新渲染对话消息(直接添加到DOM,不通过addMessage避免重复保存)
  1609. if (conversation.messages && conversation.messages.length > 0) {
  1610. conversation.messages.forEach(msg => {
  1611. console.log('渲染消息:', msg.isUser ? '用户' : 'AI', msg.content.substring(0, 50));
  1612. renderMessageToDOM(msg.content, msg.isUser, msg.isError);
  1613. });
  1614. }
  1615. // 更新历史列表的激活状态
  1616. document.querySelectorAll('.history-item').forEach(item => {
  1617. item.classList.remove('active');
  1618. });
  1619. const activeItem = document.querySelector(`[data-conversation-id="${conversationId}"]`);
  1620. if (activeItem) {
  1621. activeItem.classList.add('active');
  1622. }
  1623. // 重置后端历史记录
  1624. resetBackendHistory();
  1625. }
  1626. }
  1627. // 删除对话(立即释放所有资源)
  1628. function deleteConversation(conversationId) {
  1629. if (confirm('确定要删除这个对话吗?删除后将无法恢复。')) {
  1630. console.log('开始删除对话:', conversationId);
  1631. // 立即执行资源清理
  1632. performImmediateCleanup(conversationId);
  1633. // 删除对话数据(包含内存清理)
  1634. const deleted = conversationManager.deleteConversation(conversationId);
  1635. if (deleted) {
  1636. console.log('对话已删除:', deleted.title);
  1637. // 立即重新渲染列表
  1638. renderConversationList();
  1639. // 如果删除的是当前对话,需要处理界面状态
  1640. if (deleted.id === conversationManager.currentConversationId) {
  1641. // 立即清空聊天界面
  1642. clearChatContainer();
  1643. if (conversationManager.currentConversationId) {
  1644. // 切换到新的当前对话
  1645. switchToConversation(conversationManager.currentConversationId);
  1646. } else {
  1647. // 没有对话了,显示欢迎界面
  1648. welcomeScreen.style.display = 'flex';
  1649. }
  1650. } else {
  1651. // 如果删除的不是当前对话,确保当前对话的激活状态正确
  1652. updateActiveConversationState();
  1653. }
  1654. // 异步通知后端清理(不阻塞UI)
  1655. setTimeout(() => {
  1656. notifyBackendCleanup(conversationId);
  1657. }, 0);
  1658. // 强制垃圾回收
  1659. forceGarbageCollection();
  1660. }
  1661. }
  1662. }
  1663. // 立即执行资源清理
  1664. function performImmediateCleanup(conversationId) {
  1665. console.log('立即执行资源清理:', conversationId);
  1666. // 1. 立即清理DOM资源
  1667. cleanupDOMResourcesImmediate(conversationId);
  1668. // 2. 立即清理事件监听器
  1669. cleanupEventListeners(conversationId);
  1670. // 3. 立即清理内存引用
  1671. cleanupMemoryReferences(conversationId);
  1672. }
  1673. // 立即清理DOM资源
  1674. function cleanupDOMResourcesImmediate(conversationId) {
  1675. // 清理对话列表项
  1676. const historyItem = document.querySelector(`[data-conversation-id="${conversationId}"]`);
  1677. if (historyItem) {
  1678. // 强制移除所有子元素和事件
  1679. while (historyItem.firstChild) {
  1680. const child = historyItem.firstChild;
  1681. if (child.removeEventListener) {
  1682. // 移除所有可能的事件监听器
  1683. child.removeEventListener('click', null);
  1684. child.removeEventListener('mouseover', null);
  1685. child.removeEventListener('mouseout', null);
  1686. }
  1687. historyItem.removeChild(child);
  1688. }
  1689. // 移除父元素
  1690. historyItem.parentNode?.removeChild(historyItem);
  1691. }
  1692. // 清理聊天容器中相关的消息(如果是当前对话)
  1693. const isCurrentConversation = conversationId === conversationManager.currentConversationId;
  1694. if (isCurrentConversation) {
  1695. const messages = chatContainer.querySelectorAll('.message');
  1696. messages.forEach(msg => {
  1697. // 移除所有按钮的事件监听器
  1698. const buttons = msg.querySelectorAll('button');
  1699. buttons.forEach(btn => {
  1700. const newBtn = btn.cloneNode(true);
  1701. btn.parentNode.replaceChild(newBtn, btn);
  1702. });
  1703. // 移除消息元素
  1704. msg.remove();
  1705. });
  1706. }
  1707. }
  1708. // 清理事件监听器
  1709. function cleanupEventListeners(conversationId) {
  1710. // 清理全局事件缓存(如果有的话)
  1711. if (window.conversationEventCache && window.conversationEventCache[conversationId]) {
  1712. const events = window.conversationEventCache[conversationId];
  1713. events.forEach(event => {
  1714. if (event.element && event.type && event.handler) {
  1715. event.element.removeEventListener(event.type, event.handler);
  1716. }
  1717. });
  1718. delete window.conversationEventCache[conversationId];
  1719. }
  1720. }
  1721. // 清理内存引用
  1722. function cleanupMemoryReferences(conversationId) {
  1723. // 清理可能的临时引用
  1724. if (window.conversationTempData && window.conversationTempData[conversationId]) {
  1725. window.conversationTempData[conversationId] = null;
  1726. delete window.conversationTempData[conversationId];
  1727. }
  1728. }
  1729. // 清空聊天容器
  1730. function clearChatContainer() {
  1731. const messages = chatContainer.querySelectorAll('.message');
  1732. messages.forEach(msg => {
  1733. // 移除所有事件监听器
  1734. const buttons = msg.querySelectorAll('button, [onclick], [on*]');
  1735. buttons.forEach(btn => {
  1736. const newBtn = btn.cloneNode(true);
  1737. btn.parentNode.replaceChild(newBtn, btn);
  1738. });
  1739. // 移除消息元素
  1740. msg.remove();
  1741. });
  1742. }
  1743. // 更新当前对话激活状态
  1744. function updateActiveConversationState() {
  1745. document.querySelectorAll('.history-item').forEach(item => {
  1746. item.classList.remove('active');
  1747. });
  1748. const activeItem = document.querySelector(`[data-conversation-id="${conversationManager.currentConversationId}"]`);
  1749. if (activeItem) {
  1750. activeItem.classList.add('active');
  1751. }
  1752. }
  1753. // 强制垃圾回收
  1754. function forceGarbageCollection() {
  1755. if (window.gc) {
  1756. window.gc();
  1757. console.log('已执行垃圾回收');
  1758. } else if (performance.memory) {
  1759. // 浏览器不支持手动gc,记录内存使用情况
  1760. const memory = performance.memory;
  1761. console.log('内存使用情况:', {
  1762. used: Math.round(memory.usedJSHeapSize / 1024 / 1024) + 'MB',
  1763. total: Math.round(memory.totalJSHeapSize / 1024 / 1024) + 'MB'
  1764. });
  1765. }
  1766. }
  1767. // 资源监控工具
  1768. function logResourceUsage() {
  1769. const memory = performance.memory;
  1770. const domNodes = document.querySelectorAll('*').length;
  1771. const eventListeners = document.querySelectorAll('[onclick], [on*]').length;
  1772. console.log('=== 资源使用情况 ===');
  1773. console.log('内存使用:', Math.round(memory.usedJSHeapSize / 1024 / 1024) + 'MB');
  1774. console.log('DOM节点数量:', domNodes);
  1775. console.log('内联事件监听器:', eventListeners);
  1776. console.log('对话数量:', conversationManager.conversations.length);
  1777. console.log('当前对话:', conversationManager.currentConversationId);
  1778. console.log('==================');
  1779. }
  1780. // 全局暴露调试工具
  1781. window.debugResources = logResourceUsage;
  1782. window.forceCleanup = () => {
  1783. conversationManager.cleanupAllResources();
  1784. forceGarbageCollection();
  1785. logResourceUsage();
  1786. };
  1787. // 清理DOM相关资源
  1788. function cleanupDOMResources(conversationId) {
  1789. // 清理对话列表中的DOM元素
  1790. const historyItem = document.querySelector(`[data-conversation-id="${conversationId}"]`);
  1791. if (historyItem) {
  1792. // 移除事件监听器
  1793. const contentDiv = historyItem.querySelector('.history-item-content');
  1794. const deleteBtn = historyItem.querySelector('.delete-chat-btn');
  1795. if (contentDiv) {
  1796. contentDiv.replaceWith(contentDiv.cloneNode(true));
  1797. }
  1798. if (deleteBtn) {
  1799. deleteBtn.replaceWith(deleteBtn.cloneNode(true));
  1800. }
  1801. // 移除DOM元素
  1802. historyItem.remove();
  1803. }
  1804. // 清理可能的消息DOM缓存
  1805. const messageElements = document.querySelectorAll(`[data-conversation-id="${conversationId}"]`);
  1806. messageElements.forEach(element => {
  1807. element.remove();
  1808. });
  1809. }
  1810. // 通知后端清理资源
  1811. async function notifyBackendCleanup(conversationId) {
  1812. const isHttp = /^https?:$/.test(location.protocol);
  1813. if (isHttp) {
  1814. try {
  1815. const cleanupUrl = new URL('/cgi-bin/cleanup', location.origin).toString();
  1816. await fetch(cleanupUrl, {
  1817. method: 'POST',
  1818. headers: { 'Content-Type': 'application/json' },
  1819. body: JSON.stringify({
  1820. conversationId: conversationId,
  1821. action: 'delete_conversation'
  1822. })
  1823. });
  1824. console.log('已通知后端清理对话资源:', conversationId);
  1825. } catch (error) {
  1826. console.warn('通知后端清理失败:', error);
  1827. }
  1828. }
  1829. }
  1830. // 重置后端历史记录
  1831. async function resetBackendHistory() {
  1832. const isHttp = /^https?:$/.test(location.protocol);
  1833. if (isHttp) {
  1834. try {
  1835. const chatUrl = new URL('/cgi-bin/chat', location.origin).toString();
  1836. await fetch(chatUrl, {
  1837. method: 'POST',
  1838. headers: { 'Content-Type': 'application/json' },
  1839. body: JSON.stringify({ reset: true })
  1840. });
  1841. } catch (error) {
  1842. console.warn('Reset backend history failed:', error);
  1843. }
  1844. }
  1845. }
  1846. const menuToggle = document.getElementById('menuToggle');
  1847. const sidebar = document.getElementById('sidebar');
  1848. const overlay = document.getElementById('overlay');
  1849. const settingsBtn = document.getElementById('settingsBtn');
  1850. const settingsPanel = document.getElementById('settingsPanel');
  1851. const modelSelector = document.getElementById('modelSelector');
  1852. const suggestionCards = document.querySelectorAll('.suggestion-card');
  1853. const toolBtns = document.querySelectorAll('.tool-btn');
  1854. const tabChat = document.getElementById('tabChat');
  1855. const inputContainer = document.querySelector('.input-container');
  1856. // 配置管理相关元素
  1857. const apiKeyInput = document.getElementById('apiKeyInput');
  1858. const modelNameSelect = document.getElementById('modelNameSelect');
  1859. const apiProviderSelect = document.getElementById('apiProviderSelect');
  1860. const saveConfigBtn = document.getElementById('saveConfigBtn');
  1861. const resetConfigBtn = document.getElementById('resetConfigBtn');
  1862. const configStatus = document.getElementById('configStatus');
  1863. // API提供商到URL的映射
  1864. const apiProviderUrls = {
  1865. 'qwen': 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
  1866. 'doubao': 'https://ark.cn-beijing.volces.com/api/v3/chat/completions',
  1867. 'deepseek': 'https://api.deepseek.com/chat/completions'
  1868. };
  1869. // Markdown 渲染配置(marked + highlight.js)
  1870. if (window.marked) {
  1871. try {
  1872. marked.setOptions({
  1873. breaks: true,
  1874. gfm: true,
  1875. highlight: function(code, lang) {
  1876. try {
  1877. if (window.hljs && lang && hljs.getLanguage(lang)) {
  1878. return hljs.highlight(code, { language: lang }).value;
  1879. } else if (window.hljs) {
  1880. return hljs.highlightAuto(code).value;
  1881. }
  1882. } catch (e) {}
  1883. return code;
  1884. }
  1885. });
  1886. } catch (e) {}
  1887. }
  1888. // 通用复制:优先使用 Clipboard API,失败回退 textarea 方案
  1889. async function copyText(text) {
  1890. try {
  1891. if (navigator.clipboard && window.isSecureContext) {
  1892. await navigator.clipboard.writeText(text);
  1893. return true;
  1894. }
  1895. } catch (e) {}
  1896. try {
  1897. const ta = document.createElement('textarea');
  1898. ta.value = text;
  1899. ta.style.position = 'fixed';
  1900. ta.style.top = '-1000px';
  1901. document.body.appendChild(ta);
  1902. ta.focus();
  1903. ta.select();
  1904. const ok = document.execCommand('copy');
  1905. document.body.removeChild(ta);
  1906. return ok;
  1907. } catch (e) {
  1908. return false;
  1909. }
  1910. }
  1911. function enhanceCodeBlocks(rootEl) {
  1912. if (!rootEl) return;
  1913. const blocks = rootEl.querySelectorAll('pre');
  1914. blocks.forEach(function(pre){
  1915. if (pre.querySelector('.copy-code-inline')) return;
  1916. const btn = document.createElement('button');
  1917. btn.className = 'copy-code-inline';
  1918. btn.textContent = '复制';
  1919. btn.addEventListener('click', async function(){
  1920. const codeEl = pre.querySelector('code');
  1921. const text = codeEl ? (codeEl.innerText || codeEl.textContent || '') : '';
  1922. const ok = await copyText(text);
  1923. btn.textContent = ok ? '已复制' : '复制失败';
  1924. btn.style.color = ok ? '#10a37f' : '#ef4444';
  1925. setTimeout(function(){ btn.textContent = '复制'; btn.style.color=''; }, 1500);
  1926. });
  1927. pre.appendChild(btn);
  1928. });
  1929. }
  1930. // 调整文本区域高度
  1931. messageInput.addEventListener('input', function() {
  1932. this.style.height = 'auto';
  1933. this.style.height = (this.scrollHeight) + 'px';
  1934. });
  1935. // 发送消息到WebNet服务器
  1936. async function sendMessage() {
  1937. const message = messageInput.value.trim();
  1938. if (!message) return;
  1939. // 隐藏欢迎界面
  1940. if (welcomeScreen.style.display !== 'none') {
  1941. welcomeScreen.style.display = 'none';
  1942. }
  1943. // 添加用户消息
  1944. addMessage(message, true);
  1945. // 记录最近一次用户输入,供“重新生成”使用
  1946. window.__lastUserMessage = message;
  1947. messageInput.value = '';
  1948. messageInput.style.height = 'auto';
  1949. sendButton.disabled = true;
  1950. // 显示AI正在输入
  1951. showTypingIndicator();
  1952. try {
  1953. const isHttp = /^https?:$/.test(location.protocol);
  1954. if (!isHttp) {
  1955. removeTypingIndicator();
  1956. addMessage('当前通过 file:// 打开,浏览器拦截跨域请求。请通过 http/https 访问设备,例如:http://<设备IP>/index.html', false, true);
  1957. sendButton.disabled = false;
  1958. messageInput.focus();
  1959. return;
  1960. }
  1961. const chatUrl = new URL('/cgi-bin/chat', location.origin).toString();
  1962. const payload = { message: message };
  1963. const supportsStream = true;
  1964. let useStream = supportsStream;
  1965. let resp;
  1966. let data;
  1967. if (useStream) {
  1968. payload.stream = true;
  1969. }
  1970. resp = await fetch(chatUrl, {
  1971. method: 'POST',
  1972. headers: { 'Content-Type': 'application/json' },
  1973. body: JSON.stringify(payload)
  1974. });
  1975. if (useStream && resp.ok) {
  1976. const streamResult = await handleStreamedResponse(resp);
  1977. if (streamResult && streamResult.success) {
  1978. data = streamResult;
  1979. }
  1980. else {
  1981. data = streamResult || { success: false, error: 'LLM响应失败' };
  1982. }
  1983. }
  1984. else {
  1985. removeTypingIndicator();
  1986. data = await handleLegacyResponse(resp, message, chatUrl);
  1987. }
  1988. if (useStream && data && data.success) {
  1989. // 将最后一条AI消息标记上一条用户输入,便于重新生成
  1990. const lastMsg = chatContainer.lastElementChild;
  1991. if (lastMsg && lastMsg.classList && lastMsg.classList.contains('message')) {
  1992. lastMsg.setAttribute('data-prev-user', window.__lastUserMessage || '');
  1993. }
  1994. }
  1995. else if (!useStream) {
  1996. if (data && data.success) {
  1997. addMessage(data.response, false);
  1998. const lastMsg = chatContainer.lastElementChild;
  1999. if (lastMsg && lastMsg.classList && lastMsg.classList.contains('message')) {
  2000. lastMsg.setAttribute('data-prev-user', window.__lastUserMessage || '');
  2001. }
  2002. } else {
  2003. addMessage(`错误: ${data && data.error ? data.error : '未知错误'}`, false, true);
  2004. }
  2005. }
  2006. if (data && data.success) {
  2007. // 已经在各自的处理流程中完成渲染
  2008. } else if (useStream) {
  2009. addMessage(`错误: ${data && data.error ? data.error : '未知错误'}`, false, true);
  2010. }
  2011. } catch (error) {
  2012. removeTypingIndicator();
  2013. addMessage('网络错误,请检查服务器连接', false, true);
  2014. } finally {
  2015. sendButton.disabled = false;
  2016. messageInput.focus();
  2017. }
  2018. }
  2019. // 聊天标签切换
  2020. function switchToChat() {
  2021. if (tabChat && tabChat.classList) tabChat.classList.add('active');
  2022. if (inputContainer) inputContainer.style.display = 'flex';
  2023. if (welcomeScreen && chatContainer && chatContainer.querySelectorAll('.message').length === 0) {
  2024. welcomeScreen.style.display = 'flex';
  2025. }
  2026. }
  2027. if (tabChat && typeof switchToChat === 'function') tabChat.addEventListener('click', switchToChat);
  2028. // 简单的代码语言检测
  2029. function detectCodeLanguage(code) {
  2030. if (code.includes('#include') || code.includes('void') || code.includes('int main')) {
  2031. return 'c';
  2032. } else if (code.includes('function') || code.includes('var ') || code.includes('const ')) {
  2033. return 'javascript';
  2034. } else if (code.includes('def ') || code.includes('import ')) {
  2035. return 'python';
  2036. } else if (code.includes('<html') || code.includes('<div')) {
  2037. return 'html';
  2038. } else if (code.includes('{') && code.includes('}')) {
  2039. return 'json';
  2040. }
  2041. return 'text';
  2042. }
  2043. // 简单的C代码高亮
  2044. function highlightCCode(code) {
  2045. return code
  2046. .replace(/\b(int|void|char|float|double|if|else|while|for|return|include|define|struct)\b/g, '<span class="code-keyword">$1</span>')
  2047. .replace(/"([^"]*)"/g, '<span class="code-string">"$1"</span>')
  2048. .replace(/'([^']*)'/g, '<span class="code-string">\'$1\'</span>')
  2049. .replace(/\/\/(.*)/g, '<span class="code-comment">//$1</span>')
  2050. .replace(/\/\*([\s\S]*?)\*\//g, '<span class="code-comment">/*$1*/</span>')
  2051. .replace(/\b(\d+)\b/g, '<span class="code-number">$1</span>')
  2052. .replace(/#(\w+)/g, '<span class="code-preprocessor">#$1</span>')
  2053. .replace(/\b(\w+)\(/g, '<span class="code-function">$1</span>(');
  2054. }
  2055. // 通用代码高亮
  2056. function highlightCode(code, language) {
  2057. if (language === 'c' || language === 'cpp') {
  2058. return highlightCCode(code);
  2059. }
  2060. // 可以添加其他语言的高亮规则
  2061. return code;
  2062. }
  2063. // 解析Markdown文本
  2064. function parseMarkdown(mdText) {
  2065. try {
  2066. var md = String(mdText || '');
  2067. // 1) 修正服务端转义换行:"\n" -> 实际换行
  2068. md = md.replace(/\\n/g, '\n');
  2069. // 2) 将独立语言标识行转为围栏代码块开头,例如: \n"c"\n -> \n```c\n
  2070. md = md.replace(/\n(c|cpp|c\+\+|python|py|javascript|js|ts|typescript|json|html|xml|bash|sh)\n/gi, function(_, lang){
  2071. return '\n```' + (lang.toLowerCase().replace('c++','cpp').replace('py','python').replace('js','javascript')) + '\n';
  2072. });
  2073. // 3) 若围栏数量不平衡,自动补齐结尾的 ```
  2074. var opens = (md.match(/```/g) || []).length;
  2075. if (opens % 2 === 1) md += '\n```';
  2076. if (window.marked) {
  2077. var html = marked.parse(md);
  2078. return '<div class="markdown-body">' + html + '</div>';
  2079. }
  2080. } catch (e) {}
  2081. // 兜底:纯文本
  2082. return '<div class="markdown-body"><pre><code>' + (String(mdText||'')
  2083. .replace(/&/g, '&amp;')
  2084. .replace(/</g, '&lt;')
  2085. .replace(/>/g, '&gt;')) + '</code></pre></div>';
  2086. }
  2087. // 复制代码到剪贴板
  2088. window.copyCodeToClipboard = function(button) {
  2089. const codeBlock = button.closest('.code-block');
  2090. const codeContent = codeBlock.querySelector('.code-content');
  2091. const textToCopy = codeContent.textContent || codeContent.innerText;
  2092. navigator.clipboard.writeText(textToCopy).then(() => {
  2093. const originalText = button.innerHTML;
  2094. button.innerHTML = '<i class="fas fa-check"></i> 已复制';
  2095. button.style.color = '#10a37f';
  2096. setTimeout(() => {
  2097. button.innerHTML = originalText;
  2098. button.style.color = '';
  2099. }, 2000);
  2100. }).catch(err => {
  2101. console.error('复制失败:', err);
  2102. button.innerHTML = '<i class="fas fa-times"></i> 复制失败';
  2103. button.style.color = '#ef4444';
  2104. setTimeout(() => {
  2105. button.innerHTML = '<i class="fas fa-copy"></i> 复制代码';
  2106. button.style.color = '';
  2107. }, 2000);
  2108. });
  2109. };
  2110. // 仅渲染消息到DOM,不保存数据(用于对话切换时加载历史消息)
  2111. function renderMessageToDOM(content, isUser, isError = false) {
  2112. const messageDiv = document.createElement('div');
  2113. messageDiv.className = `message ${isUser ? 'user-message' : 'ai-message'}`;
  2114. const avatarDiv = document.createElement('div');
  2115. avatarDiv.className = `message-avatar ${isUser ? 'user-avatar' : 'ai-avatar'}`;
  2116. avatarDiv.innerHTML = isUser ? '<i class="fas fa-user"></i>' : '<i class="fas fa-robot"></i>';
  2117. const contentDiv = document.createElement('div');
  2118. contentDiv.className = 'message-content';
  2119. if (isError) {
  2120. contentDiv.innerHTML = `<div class="error-message">${content}</div>`;
  2121. } else if (isUser) {
  2122. // 用户消息不解析Markdown,直接显示
  2123. contentDiv.textContent = content;
  2124. } else {
  2125. // AI消息解析Markdown
  2126. contentDiv.innerHTML = parseMarkdown(content);
  2127. try {
  2128. if (window.hljs) {
  2129. contentDiv.querySelectorAll('pre code').forEach(function(block){
  2130. hljs.highlightElement(block);
  2131. });
  2132. }
  2133. } catch (e) {}
  2134. }
  2135. // 左下角小图标:复制、重新生成(仅AI消息)
  2136. const actionsDiv = document.createElement('div');
  2137. actionsDiv.className = 'message-actions';
  2138. if (!isUser) {
  2139. // 复制AI整条回复(Markdown渲染后的纯文本)
  2140. const copyBtn = document.createElement('button');
  2141. copyBtn.className = 'icon-btn';
  2142. copyBtn.innerHTML = '<i class="fas fa-copy"></i>';
  2143. copyBtn.title = '复制';
  2144. copyBtn.addEventListener('click', async function() {
  2145. const htmlEl = contentDiv.querySelector('.markdown-body');
  2146. let textToCopy = content;
  2147. if (htmlEl) textToCopy = htmlEl.innerText || htmlEl.textContent || content;
  2148. const ok = await copyText(textToCopy);
  2149. copyBtn.innerHTML = ok ? '<i class="fas fa-check"></i>' : '<i class="fas fa-times"></i>';
  2150. copyBtn.style.color = ok ? '#10a37f' : '#ef4444';
  2151. setTimeout(()=>{ copyBtn.innerHTML = '<i class="fas fa-copy"></i>'; copyBtn.style.color=''; }, 1500);
  2152. });
  2153. // 重新生成:用上一条用户问题再次询问
  2154. const regenBtn = document.createElement('button');
  2155. regenBtn.className = 'icon-btn';
  2156. regenBtn.innerHTML = '<i class="fas fa-rotate-right"></i>';
  2157. regenBtn.title = '重新生成';
  2158. regenBtn.addEventListener('click', function() {
  2159. const prevUser = messageDiv.getAttribute('data-prev-user');
  2160. const prompt = (prevUser && prevUser.trim()) ? prevUser.trim() : (window.__lastUserMessage || '');
  2161. if (!prompt) return;
  2162. messageInput.value = prompt;
  2163. messageInput.dispatchEvent(new Event('input'));
  2164. sendMessage();
  2165. });
  2166. actionsDiv.appendChild(copyBtn);
  2167. actionsDiv.appendChild(regenBtn);
  2168. }
  2169. messageDiv.appendChild(avatarDiv);
  2170. messageDiv.appendChild(contentDiv);
  2171. if (!isUser) messageDiv.appendChild(actionsDiv);
  2172. chatContainer.appendChild(messageDiv);
  2173. chatContainer.scrollTop = chatContainer.scrollHeight;
  2174. }
  2175. function createStreamingMessageShell() {
  2176. const messageDiv = document.createElement('div');
  2177. messageDiv.className = 'message ai-message streaming';
  2178. const avatarDiv = document.createElement('div');
  2179. avatarDiv.className = 'message-avatar ai-avatar';
  2180. avatarDiv.innerHTML = '<i class="fas fa-robot"></i>';
  2181. const contentDiv = document.createElement('div');
  2182. contentDiv.className = 'message-content';
  2183. contentDiv.textContent = '';
  2184. messageDiv.appendChild(avatarDiv);
  2185. messageDiv.appendChild(contentDiv);
  2186. chatContainer.appendChild(messageDiv);
  2187. chatContainer.scrollTop = chatContainer.scrollHeight;
  2188. return { messageDiv, contentDiv, rawText: '' };
  2189. }
  2190. function updateStreamingMessageShell(state, text) {
  2191. if (!state || !state.contentDiv) return;
  2192. state.rawText = text;
  2193. state.contentDiv.textContent = text;
  2194. chatContainer.scrollTop = chatContainer.scrollHeight;
  2195. }
  2196. function finalizeStreamingMessageShell(state, finalText, isError = false, errorTip = '') {
  2197. if (!state || !state.messageDiv) return;
  2198. const messageDiv = state.messageDiv;
  2199. const contentDiv = state.contentDiv;
  2200. if (isError) {
  2201. const errorMessage = errorTip || 'LLM响应失败';
  2202. contentDiv.innerHTML = `<div class="error-message">${errorMessage}</div>`;
  2203. conversationManager.addMessageToCurrent(errorMessage, false, true);
  2204. renderConversationList();
  2205. messageDiv.setAttribute('data-prev-user', window.__lastUserMessage || '');
  2206. chatContainer.scrollTop = chatContainer.scrollHeight;
  2207. return;
  2208. }
  2209. const text = (finalText || state.rawText || '').toString();
  2210. state.rawText = text;
  2211. contentDiv.innerHTML = parseMarkdown(text);
  2212. try {
  2213. if (window.hljs) {
  2214. contentDiv.querySelectorAll('pre code').forEach(function(block){
  2215. hljs.highlightElement(block);
  2216. });
  2217. }
  2218. } catch (e) {}
  2219. enhanceCodeBlocks(contentDiv);
  2220. const actionsDiv = document.createElement('div');
  2221. actionsDiv.className = 'message-actions';
  2222. const copyBtn = document.createElement('button');
  2223. copyBtn.className = 'icon-btn';
  2224. copyBtn.innerHTML = '<i class="fas fa-copy"></i>';
  2225. copyBtn.title = '复制';
  2226. copyBtn.addEventListener('click', async function() {
  2227. const htmlEl = contentDiv.querySelector('.markdown-body');
  2228. let textToCopy = text;
  2229. if (htmlEl) textToCopy = htmlEl.innerText || htmlEl.textContent || text;
  2230. const ok = await copyText(textToCopy);
  2231. copyBtn.innerHTML = ok ? '<i class="fas fa-check"></i>' : '<i class="fas fa-times"></i>';
  2232. copyBtn.style.color = ok ? '#10a37f' : '#ef4444';
  2233. setTimeout(function(){
  2234. copyBtn.innerHTML = '<i class="fas fa-copy"></i>';
  2235. copyBtn.style.color = '';
  2236. }, 1500);
  2237. });
  2238. const regenBtn = document.createElement('button');
  2239. regenBtn.className = 'icon-btn';
  2240. regenBtn.innerHTML = '<i class="fas fa-rotate-right"></i>';
  2241. regenBtn.title = '重新生成';
  2242. regenBtn.addEventListener('click', function() {
  2243. const prevUser = messageDiv.getAttribute('data-prev-user');
  2244. const prompt = (prevUser && prevUser.trim()) ? prevUser.trim() : (window.__lastUserMessage || '');
  2245. if (!prompt) return;
  2246. messageInput.value = prompt;
  2247. messageInput.dispatchEvent(new Event('input'));
  2248. sendMessage();
  2249. });
  2250. actionsDiv.appendChild(copyBtn);
  2251. actionsDiv.appendChild(regenBtn);
  2252. messageDiv.appendChild(actionsDiv);
  2253. conversationManager.addMessageToCurrent(text, false, false);
  2254. renderConversationList();
  2255. messageDiv.classList.remove('streaming');
  2256. messageDiv.setAttribute('data-prev-user', window.__lastUserMessage || '');
  2257. chatContainer.scrollTop = chatContainer.scrollHeight;
  2258. }
  2259. async function handleStreamedResponse(response) {
  2260. removeTypingIndicator();
  2261. if (!response.body || typeof response.body.getReader !== 'function') {
  2262. finalizeStreamingMessageShell(createStreamingMessageShell(), '', true, '浏览器不支持流式响应');
  2263. return { success: false, error: '浏览器不支持流式响应' };
  2264. }
  2265. const state = createStreamingMessageShell();
  2266. const reader = response.body.getReader();
  2267. const decoder = new TextDecoder('utf-8');
  2268. let buffer = '';
  2269. let aggregatedText = '';
  2270. let finalText = '';
  2271. try {
  2272. while (true) {
  2273. const { value, done } = await reader.read();
  2274. if (done) break;
  2275. buffer += decoder.decode(value, { stream: true });
  2276. const parts = buffer.split('\n\n');
  2277. buffer = parts.pop() || '';
  2278. let shouldStop = false;
  2279. for (const part of parts) {
  2280. if (!part.trim() || part.trim().startsWith(':')) {
  2281. continue;
  2282. }
  2283. const lines = part.split('\n');
  2284. let eventType = 'message';
  2285. const dataLines = [];
  2286. lines.forEach(function(line) {
  2287. if (line.startsWith('event:')) {
  2288. eventType = line.slice(6).trim();
  2289. }
  2290. else if (line.startsWith('data:')) {
  2291. dataLines.push(line.slice(5));
  2292. }
  2293. });
  2294. const payload = dataLines.join('\n');
  2295. if (eventType === 'delta') {
  2296. aggregatedText += payload;
  2297. updateStreamingMessageShell(state, aggregatedText);
  2298. }
  2299. else if (eventType === 'final') {
  2300. finalText = payload || aggregatedText;
  2301. }
  2302. else if (eventType === 'error') {
  2303. finalizeStreamingMessageShell(state, '', true, payload || 'LLM响应失败');
  2304. return { success: false, error: payload || 'LLM响应失败' };
  2305. }
  2306. else if (eventType === 'done') {
  2307. shouldStop = true;
  2308. break;
  2309. }
  2310. }
  2311. if (shouldStop) {
  2312. break;
  2313. }
  2314. }
  2315. }
  2316. catch (err) {
  2317. finalizeStreamingMessageShell(state, '', true, err && err.message ? err.message : '网络错误');
  2318. return { success: false, error: err && err.message ? err.message : '网络错误' };
  2319. }
  2320. finally {
  2321. try { reader.releaseLock(); } catch (e) {}
  2322. }
  2323. if (!finalText) {
  2324. finalText = aggregatedText;
  2325. }
  2326. finalizeStreamingMessageShell(state, finalText || '');
  2327. return { success: true, response: finalText || aggregatedText };
  2328. }
  2329. async function handleLegacyResponse(initialResponse, message, chatUrl) {
  2330. removeTypingIndicator();
  2331. let resp = initialResponse;
  2332. let text = await resp.text();
  2333. let data = null;
  2334. try {
  2335. data = JSON.parse(text);
  2336. } catch (e) {
  2337. data = null;
  2338. }
  2339. if (!resp.ok || !data) {
  2340. try {
  2341. resp = await fetch(chatUrl, {
  2342. method: 'POST',
  2343. headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' },
  2344. body: 'message=' + encodeURIComponent(message)
  2345. });
  2346. text = await resp.text();
  2347. try {
  2348. data = JSON.parse(text);
  2349. } catch (parseErr) {
  2350. data = { success: false, error: text || '服务器返回非JSON' };
  2351. }
  2352. } catch (netErr) {
  2353. data = { success: false, error: '网络错误,请检查服务器连接' };
  2354. }
  2355. }
  2356. if (!data) {
  2357. data = { success: false, error: '未知响应' };
  2358. }
  2359. return data;
  2360. }
  2361. // 添加消息到聊天界面(新消息发送时使用)
  2362. function addMessage(content, isUser, isError = false) {
  2363. // 保存消息到对话管理器
  2364. conversationManager.addMessageToCurrent(content, isUser, isError);
  2365. // 渲染消息到DOM
  2366. renderMessageToDOM(content, isUser, isError);
  2367. // 每次添加消息后都更新对话列表显示,以显示最新的消息内容
  2368. renderConversationList();
  2369. }
  2370. // 显示输入指示器
  2371. function showTypingIndicator() {
  2372. const indicatorDiv = document.createElement('div');
  2373. indicatorDiv.className = 'message ai-message';
  2374. indicatorDiv.id = 'typingIndicator';
  2375. const avatarDiv = document.createElement('div');
  2376. avatarDiv.className = 'message-avatar ai-avatar';
  2377. avatarDiv.innerHTML = '<i class="fas fa-robot"></i>';
  2378. const contentDiv = document.createElement('div');
  2379. contentDiv.className = 'message-content';
  2380. const typingDiv = document.createElement('div');
  2381. typingDiv.className = 'typing-indicator';
  2382. typingDiv.innerHTML = 'AIoT助手正在思考';
  2383. const dotsDiv = document.createElement('div');
  2384. dotsDiv.className = 'typing-dots';
  2385. dotsDiv.innerHTML = '<div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div>';
  2386. typingDiv.appendChild(dotsDiv);
  2387. contentDiv.appendChild(typingDiv);
  2388. indicatorDiv.appendChild(avatarDiv);
  2389. indicatorDiv.appendChild(contentDiv);
  2390. chatContainer.appendChild(indicatorDiv);
  2391. chatContainer.scrollTop = chatContainer.scrollHeight;
  2392. }
  2393. // 移除输入指示器
  2394. function removeTypingIndicator() {
  2395. const indicator = document.getElementById('typingIndicator');
  2396. if (indicator) {
  2397. indicator.remove();
  2398. }
  2399. }
  2400. // 新对话
  2401. async function newChat() {
  2402. // 创建新对话
  2403. const conversation = conversationManager.createConversation();
  2404. conversationManager.currentConversationId = conversation.id;
  2405. // 清空聊天界面
  2406. const messages = chatContainer.querySelectorAll('.message');
  2407. messages.forEach(msg => msg.remove());
  2408. // 隐藏欢迎界面
  2409. welcomeScreen.style.display = 'none';
  2410. // 更新对话列表
  2411. renderConversationList();
  2412. // 清空客户端记忆
  2413. window.__lastUserMessage = '';
  2414. // 重置后端历史记录
  2415. resetBackendHistory();
  2416. }
  2417. // 事件监听
  2418. sendButton.addEventListener('click', sendMessage);
  2419. messageInput.addEventListener('keydown', function(e) {
  2420. if (e.key === 'Enter' && !e.shiftKey) {
  2421. e.preventDefault();
  2422. sendMessage();
  2423. }
  2424. });
  2425. messageInput.addEventListener('input', function() {
  2426. sendButton.disabled = !this.value.trim();
  2427. });
  2428. newChatBtn.addEventListener('click', newChat);
  2429. // 设置面板切换
  2430. settingsBtn.addEventListener('click', function(e) {
  2431. e.stopPropagation();
  2432. settingsPanel.classList.toggle('active');
  2433. });
  2434. // 模型选择器
  2435. modelSelector.addEventListener('click', function(e) {
  2436. e.stopPropagation();
  2437. // 这里可以展开模型选择下拉菜单
  2438. alert('模型选择功能开发中...');
  2439. });
  2440. // 点击建议卡片 - 延迟绑定确保DOM完全加载
  2441. setTimeout(() => {
  2442. const suggestionCards = document.querySelectorAll('.suggestion-card');
  2443. console.log('找到建议卡片数量:', suggestionCards.length); // 调试用
  2444. suggestionCards.forEach(card => {
  2445. card.addEventListener('click', function(e) {
  2446. e.preventDefault();
  2447. e.stopPropagation();
  2448. const prompt = this.getAttribute('data-prompt');
  2449. if (prompt && messageInput) {
  2450. messageInput.value = prompt;
  2451. messageInput.dispatchEvent(new Event('input'));
  2452. messageInput.focus();
  2453. sendButton.disabled = !messageInput.value.trim();
  2454. console.log('直接绑定:点击建议卡片,填充内容:', prompt); // 调试用
  2455. }
  2456. });
  2457. });
  2458. }, 100);
  2459. // 事件委托,确保后续渲染也生效
  2460. chatContainer.addEventListener('click', function(e){
  2461. // 兼容性更好的方式查找父级卡片
  2462. let target = e.target;
  2463. let card = null;
  2464. // 向上查找包含 suggestion-card 类的元素
  2465. while (target && target !== chatContainer) {
  2466. if (target.classList && target.classList.contains('suggestion-card')) {
  2467. card = target;
  2468. break;
  2469. }
  2470. target = target.parentElement;
  2471. }
  2472. if (card) {
  2473. e.preventDefault();
  2474. e.stopPropagation();
  2475. const prompt = card.getAttribute('data-prompt');
  2476. if (prompt && messageInput) {
  2477. messageInput.value = prompt;
  2478. messageInput.dispatchEvent(new Event('input'));
  2479. messageInput.focus();
  2480. sendButton.disabled = !messageInput.value.trim();
  2481. console.log('事件委托:点击建议卡片,填充内容:', prompt); // 调试用
  2482. }
  2483. }
  2484. });
  2485. // 工具按钮点击
  2486. toolBtns.forEach(btn => {
  2487. btn.addEventListener('click', function() {
  2488. const toolId = this.id;
  2489. let prompt = '';
  2490. switch(toolId) {
  2491. case 'codeGenBtn':
  2492. prompt = '请帮我生成一个RT-Thread的示例代码,功能是:';
  2493. break;
  2494. case 'docSearchBtn':
  2495. prompt = '请帮我查找RT-Thread关于以下内容的文档:';
  2496. break;
  2497. case 'debugHelpBtn':
  2498. prompt = '我在RT-Thread开发中遇到了一个问题:';
  2499. break;
  2500. case 'deviceControlBtn':
  2501. prompt = '如何通过RT-Thread控制物联网设备?';
  2502. break;
  2503. }
  2504. messageInput.value = prompt;
  2505. messageInput.focus();
  2506. sendButton.disabled = false;
  2507. });
  2508. });
  2509. // 页面初始化
  2510. function initializeApp() {
  2511. // 加载对话列表
  2512. renderConversationList();
  2513. // 如果有对话,切换到最近的一个
  2514. const conversations = conversationManager.getConversationList();
  2515. if (conversations.length > 0) {
  2516. // 确保设置当前对话ID
  2517. conversationManager.currentConversationId = conversations[0].id;
  2518. switchToConversation(conversations[0].id);
  2519. } else {
  2520. welcomeScreen.style.display = 'flex';
  2521. }
  2522. }
  2523. // 移动端菜单切换
  2524. menuToggle.addEventListener('click', function() {
  2525. sidebar.classList.toggle('open');
  2526. overlay.classList.toggle('active');
  2527. });
  2528. // 点击蒙版关闭移动端菜单
  2529. overlay.addEventListener('click', function() {
  2530. sidebar.classList.remove('open');
  2531. overlay.classList.remove('active');
  2532. });
  2533. // 点击页面其他地方关闭设置面板
  2534. document.addEventListener('click', function() {
  2535. settingsPanel.classList.remove('active');
  2536. });
  2537. // 阻止设置面板内部点击事件冒泡
  2538. settingsPanel.addEventListener('click', function(e) {
  2539. e.stopPropagation();
  2540. });
  2541. // 移动端检测
  2542. function checkMobile() {
  2543. if (window.innerWidth <= 768) {
  2544. menuToggle.style.display = 'block';
  2545. sidebar.style.transform = 'translateX(-100%)';
  2546. } else {
  2547. menuToggle.style.display = 'none';
  2548. sidebar.style.transform = 'translateX(0)';
  2549. overlay.classList.remove('active');
  2550. }
  2551. }
  2552. // 配置管理功能
  2553. // 密码显示/隐藏切换
  2554. window.togglePasswordVisibility = function(inputId) {
  2555. const input = document.getElementById(inputId);
  2556. const button = input.nextElementSibling;
  2557. const icon = button.querySelector('i');
  2558. if (input.type === 'password') {
  2559. input.type = 'text';
  2560. icon.className = 'fas fa-eye-slash';
  2561. } else {
  2562. input.type = 'password';
  2563. icon.className = 'fas fa-eye';
  2564. }
  2565. };
  2566. // 显示配置状态
  2567. function showConfigStatus(message, type = 'success') {
  2568. configStatus.className = `config-status ${type}`;
  2569. configStatus.style.display = 'flex';
  2570. if (type === 'loading') {
  2571. configStatus.innerHTML = `<div class="loading-spinner"></div> ${message}`;
  2572. } else if (type === 'success') {
  2573. configStatus.innerHTML = `<i class="fas fa-check-circle"></i> ${message}`;
  2574. } else if (type === 'error') {
  2575. configStatus.innerHTML = `<i class="fas fa-exclamation-circle"></i> ${message}`;
  2576. }
  2577. if (type !== 'loading') {
  2578. setTimeout(() => {
  2579. configStatus.style.display = 'none';
  2580. }, 3000);
  2581. }
  2582. }
  2583. // 加载配置
  2584. async function loadConfig() {
  2585. // 首先尝试从服务器加载当前配置
  2586. const isHttp = /^https?:$/.test(location.protocol);
  2587. if (isHttp) {
  2588. try {
  2589. const configUrl = new URL('/cgi-bin/get_config', location.origin).toString();
  2590. const response = await fetch(configUrl, {
  2591. method: 'GET',
  2592. headers: { 'Content-Type': 'application/json' }
  2593. });
  2594. if (response.ok) {
  2595. const result = await response.json();
  2596. if (result.success) {
  2597. console.log('Loaded config from server:', result);
  2598. applyConfigToUI(result);
  2599. return;
  2600. }
  2601. }
  2602. } catch (error) {
  2603. console.log('Failed to load config from server, using localStorage:', error);
  2604. }
  2605. }
  2606. // 回退到localStorage
  2607. const savedConfig = localStorage.getItem('llmConfig');
  2608. if (savedConfig) {
  2609. try {
  2610. const config = JSON.parse(savedConfig);
  2611. applyConfigToUI(config);
  2612. } catch (e) {
  2613. console.error('Failed to load config from localStorage:', e);
  2614. }
  2615. }
  2616. }
  2617. // 将配置应用到UI
  2618. function applyConfigToUI(config) {
  2619. apiKeyInput.value = config.apiKey || '';
  2620. modelNameSelect.value = config.modelName || 'qwen-turbo';
  2621. // 根据API URL设置提供商选择
  2622. if (config.apiUrl) {
  2623. let foundProvider = 'qwen'; // 默认值
  2624. for (const [provider, url] of Object.entries(apiProviderUrls)) {
  2625. if (config.apiUrl === url) {
  2626. foundProvider = provider;
  2627. break;
  2628. }
  2629. }
  2630. apiProviderSelect.value = foundProvider;
  2631. filterModelOptions(foundProvider);
  2632. } else {
  2633. apiProviderSelect.value = 'qwen';
  2634. filterModelOptions('qwen');
  2635. }
  2636. }
  2637. // 保存配置
  2638. async function saveConfig() {
  2639. const apiKey = apiKeyInput.value.trim();
  2640. const modelName = modelNameSelect.value;
  2641. const apiProvider = apiProviderSelect.value;
  2642. const apiUrl = apiProviderUrls[apiProvider];
  2643. if (!apiKey) {
  2644. showConfigStatus('请输入API密钥', 'error');
  2645. return;
  2646. }
  2647. if (!apiUrl) {
  2648. showConfigStatus('请选择API提供商', 'error');
  2649. return;
  2650. }
  2651. showConfigStatus('正在保存配置...', 'loading');
  2652. saveConfigBtn.disabled = true;
  2653. try {
  2654. const config = {
  2655. apiKey,
  2656. modelName,
  2657. apiUrl
  2658. };
  2659. // 保存到本地存储
  2660. localStorage.setItem('llmConfig', JSON.stringify(config));
  2661. // 发送到服务器
  2662. const isHttp = /^https?:$/.test(location.protocol);
  2663. if (isHttp) {
  2664. const configUrl = new URL('/cgi-bin/config', location.origin).toString();
  2665. console.log('Sending config to:', configUrl);
  2666. console.log('Config data:', JSON.stringify(config, null, 2));
  2667. try {
  2668. const response = await fetch(configUrl, {
  2669. method: 'POST',
  2670. headers: { 'Content-Type': 'application/json' },
  2671. body: JSON.stringify(config)
  2672. });
  2673. console.log('Response status:', response.status);
  2674. console.log('Response headers:', response.headers);
  2675. if (response.ok) {
  2676. const result = await response.json();
  2677. console.log('Response data:', result);
  2678. if (result.success) {
  2679. showConfigStatus('配置保存成功!', 'success');
  2680. } else {
  2681. showConfigStatus(`保存失败: ${result.error || '未知错误'}`, 'error');
  2682. }
  2683. } else {
  2684. const errorText = await response.text();
  2685. console.error('Server response error:', errorText);
  2686. showConfigStatus(`服务器响应错误 (${response.status}): ${errorText.substring(0, 100)}`, 'error');
  2687. }
  2688. } catch (fetchError) {
  2689. console.error('Fetch error:', fetchError);
  2690. showConfigStatus(`网络错误: ${fetchError.message}`, 'error');
  2691. }
  2692. } else {
  2693. showConfigStatus('配置已保存到本地(注意:需要HTTP环境才能同步到服务器)', 'success');
  2694. }
  2695. } catch (error) {
  2696. console.error('Save config error:', error);
  2697. showConfigStatus('保存失败,请检查网络连接', 'error');
  2698. } finally {
  2699. saveConfigBtn.disabled = false;
  2700. }
  2701. }
  2702. // 重置配置
  2703. function resetConfig() {
  2704. if (confirm('确定要重置所有配置吗?')) {
  2705. localStorage.removeItem('llmConfig');
  2706. apiKeyInput.value = '';
  2707. modelNameSelect.value = 'qwen-turbo';
  2708. apiProviderSelect.value = 'qwen';
  2709. filterModelOptions('qwen');
  2710. showConfigStatus('配置已重置', 'success');
  2711. }
  2712. }
  2713. // 保存和重置按钮事件
  2714. saveConfigBtn.addEventListener('click', saveConfig);
  2715. resetConfigBtn.addEventListener('click', resetConfig);
  2716. // 筛选模型选项
  2717. function filterModelOptions(provider) {
  2718. const optgroups = modelNameSelect.querySelectorAll('optgroup');
  2719. optgroups.forEach(optgroup => {
  2720. const groupProvider = optgroup.getAttribute('data-provider');
  2721. if (groupProvider === provider) {
  2722. optgroup.style.display = 'block';
  2723. } else {
  2724. optgroup.style.display = 'none';
  2725. }
  2726. });
  2727. // 如果当前选中的模型被隐藏了,选择第一个可见的模型
  2728. const selectedOption = modelNameSelect.options[modelNameSelect.selectedIndex];
  2729. if (selectedOption && selectedOption.parentNode.style.display === 'none') {
  2730. const firstVisibleOption = modelNameSelect.querySelector('optgroup[style="display: block;"] option');
  2731. if (firstVisibleOption) {
  2732. modelNameSelect.value = firstVisibleOption.value;
  2733. }
  2734. }
  2735. }
  2736. // API提供商选择变化处理
  2737. apiProviderSelect.addEventListener('change', function() {
  2738. const provider = this.value;
  2739. filterModelOptions(provider);
  2740. });
  2741. // 页面加载时读取配置
  2742. loadConfig();
  2743. // 如果没有保存的配置,根据默认API提供商筛选模型
  2744. if (!localStorage.getItem('llmConfig')) {
  2745. filterModelOptions('qwen');
  2746. }
  2747. checkMobile();
  2748. window.addEventListener('resize', checkMobile);
  2749. // 初始化应用
  2750. initializeApp();
  2751. // 默认进入聊天标签
  2752. switchToChat();
  2753. // 页面卸载时清理资源
  2754. window.addEventListener('beforeunload', function() {
  2755. console.log('页面即将卸载,清理资源...');
  2756. conversationManager.cleanupAllResources();
  2757. });
  2758. // 监听内存压力事件(如果支持)
  2759. if ('memory' in performance && 'onpressure' in window) {
  2760. window.addEventListener('pressure', function() {
  2761. console.log('检测到内存压力,执行清理...');
  2762. conversationManager.cleanupStorage();
  2763. });
  2764. }
  2765. });
  2766. </script>
  2767. </body>
  2768. </html>