Chat.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727
  1. <!-- Chat.vue -->
  2. <template>
  3. <div class="chat-container">
  4. <!-- 消息列表 -->
  5. <el-scrollbar class="message-list" ref="scrollbar">
  6. <div class="messages">
  7. <div
  8. v-for="(message, index) in messages"
  9. :key="index"
  10. :class="['message-item', message.isSelf ? 'self' : 'other']"
  11. >
  12. <el-avatar :size="32" :src="message.avatar" />
  13. <div class="message-content">
  14. <div
  15. class="content"
  16. v-if="!message.isSelf"
  17. :class="{'loading-content': message.content === ''}"
  18. >
  19. <div v-html="message.content"></div>
  20. <div class="loading-indicator" v-if="sendLoading && index === messages.length - 1">
  21. <span class="dot"></span>
  22. <span class="dot"></span>
  23. <span class="dot"></span>
  24. </div>
  25. </div>
  26. <div v-else class="content">{{message.content}}</div>
  27. <div class="timestamp ">{{ message.timestamp }}
  28. <!-- <span v-if="message.add" style="cursor: pointer;" @click="handleInput">填充</span> -->
  29. </div>
  30. </div>
  31. </div>
  32. </div>
  33. </el-scrollbar>
  34. <div style="display: flex;gap: 4px;padding: 1rem;">
  35. <!-- <el-check-tag :checked="type === '1'" @change="type = '1'">文档总结</el-check-tag> -->
  36. <el-check-tag :disabled="!taklToHtml" :checked="type === '2'" @change="() => {
  37. if (type !== '2') {
  38. inputMessage = '/智能填表 '
  39. type = '2'
  40. } else {
  41. type = ''
  42. }
  43. }">
  44. <el-tooltip content="选择后,在输入框描述填表流程" placement="top">
  45. 智能填表
  46. </el-tooltip>
  47. </el-check-tag>
  48. </div>
  49. <div class="card-content">
  50. <img :src="pageInfo?.favIconUrl" style="width: 32px;"/>
  51. <div class="title-wrapper">
  52. <div class="title-scroller" :class="{ 'scroll': isHoveringTitle && titleScroll }">
  53. {{ pageInfo?.title }}
  54. </div>
  55. </div>
  56. <el-button :disabled="!taklToHtml" type="primary" size="small" @click="handleCardButtonClick">
  57. 总结
  58. </el-button>
  59. </div>
  60. <!-- 输入区域 -->
  61. <div class="input-area">
  62. <el-input
  63. v-model="inputMessage"
  64. type="textarea"
  65. :rows="2"
  66. placeholder="输入消息..."
  67. @keyup.enter="() => {
  68. addMessage(inputMessage.trim(),true)
  69. inputMessage = ''
  70. }"
  71. />
  72. <div >
  73. <div style="width: 100px;display: flex;justify-content: space-between;">
  74. <el-upload
  75. :before-upload="file => handleUpload(file)"
  76. :multiple="false"
  77. :class="['upload', {'can-hover': taklToHtml}]"
  78. name="file"
  79. :show-file-list="false"
  80. :accept="'.xlsx'"
  81. :disabled="!taklToHtml"
  82. >
  83. <el-icon size="24" :color="taklToHtml ? 'gray' : '#c0c4cc'" style="cursor: pointer;"><Upload /></el-icon>
  84. </el-upload>
  85. <el-button size="small" type="primary" @click="() => {
  86. addMessage(inputMessage.trim(),true)
  87. inputMessage = ''
  88. }" :disabled="!inputMessage.trim() || sendLoading">
  89. 发送
  90. </el-button>
  91. </div>
  92. <el-tooltip content="开启后,将会根据左侧网页中的内容做出回答" placement="top">
  93. <el-switch size="small" v-model="taklToHtml" @change="(_) => {
  94. !_ && (type = '')
  95. }"/>
  96. </el-tooltip>
  97. 与页面对话
  98. </div>
  99. </div>
  100. </div>
  101. </template>
  102. <script setup>
  103. import { ref, onMounted, nextTick ,inject} from 'vue'
  104. import { ElScrollbar, ElAvatar, ElInput, ElButton } from 'element-plus'
  105. import avator from '@/public/icon/32.png'
  106. import moment from 'moment'
  107. import {hepl,getSummaryPrompt,sendMessage ,formatMessage,buildExcelUnderstandingPrompt} from '@/utils/ai-service.js'
  108. import * as XLSX from "xlsx";
  109. import { ElMessage } from 'element-plus';
  110. const pageInfo = ref({})
  111. const sendLoading = ref(false)
  112. // 消息列表数据
  113. const messages = ref([
  114. {
  115. username: '用户1',
  116. content: '你好!有什么我可以帮助你的吗?',
  117. rawContent: '你好!有什么我可以帮助你的吗?',
  118. timestamp: moment().format('YYYY-MM-DD HH:mm:ss'),
  119. isSelf: false,
  120. avatar: avator,
  121. addToHistory: false
  122. }
  123. ])
  124. // 输入框内容
  125. const inputMessage = ref('')
  126. // 滚动条引用
  127. const scrollbar = ref(null)
  128. // 计算标题是否需要滚动
  129. const titleScroll = computed(() => {
  130. return pageInfo.value?.title?.length > 20 // 当标题超过20个字符时触发滚动
  131. })
  132. let xlsxData = {}
  133. let formMap = []
  134. let formInfo = []
  135. const handleInput = () => {
  136. console.log(formMap,1005);
  137. const arr = xlsxData
  138. chrome.runtime.sendMessage({
  139. type: 'FROM_SIDE_PANEL_TO_INPUT_FORM',
  140. data: {
  141. excelData: arr,
  142. formData:formMap
  143. }
  144. });
  145. }
  146. const taklToHtml = ref(false)
  147. const handleUpload = (file) => {
  148. chrome.runtime.sendMessage({
  149. type: 'FROM_SIDE_PANEL_TO_GET_PAGE_FORM',
  150. },async (response) => {
  151. if (chrome.runtime.lastError) {
  152. console.error("消息发送错误:", chrome.runtime.lastError);
  153. } else {
  154. addMessage(`已上传文件:${file.name}`,false)
  155. formInfo = response.data
  156. const reader = new FileReader();
  157. reader.readAsArrayBuffer(file);
  158. reader.onload = async (e) => {
  159. const data = new Uint8Array(e.target.result);
  160. const workbook = XLSX.read(data, {
  161. type: "array",
  162. cellDates: false,
  163. cellNF: true,
  164. cellText: true,
  165. dateNF: 'yyyy-mm-dd'
  166. });
  167. // 修复日期处理
  168. const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
  169. // 转换为JSON数据
  170. const readData = XLSX.utils.sheet_to_json(firstSheet, {
  171. header: 1,
  172. raw: false,
  173. defval: "",
  174. dateNF: 'yyyy-mm-dd'
  175. });
  176. console.log(readData,58);
  177. readData[0].forEach((header, i) => {
  178. if (!xlsxData[header]) xlsxData[header] = []
  179. xlsxData[header].push(readData[1][i])
  180. })
  181. if (type.value === '2') {
  182. await streamRes(buildExcelUnderstandingPrompt(readData,file?.name,response),false)
  183. }
  184. };
  185. }
  186. return true
  187. });
  188. }
  189. const getPageInfo = () => {
  190. return new Promise((res, rej) => {
  191. chrome.runtime.sendMessage({
  192. type: 'FROM_SIDE_PANEL_TO_GET_PAGE_INFO',
  193. },async (response) => {
  194. if (chrome.runtime.lastError) {
  195. console.error("消息发送错误:", chrome.runtime.lastError);
  196. rej(chrome.runtime.lastError)
  197. } else {
  198. pageInfo.value = response.data
  199. res(response.data)
  200. }
  201. });
  202. })
  203. }
  204. const type = ref('')
  205. const handleCardButtonClick = async () => {
  206. addMessage('总结当前页面',false)
  207. await sendRequese('',false)
  208. }
  209. const flag = ref(false) //true调用算法接口
  210. const streamRes = async (msg, addHtml,) => {
  211. sendLoading.value = true
  212. console.log(messages.value);
  213. const obj = reactive({
  214. username: '用户1',
  215. content: '',
  216. rawContent: '', // 存储原始内容
  217. timestamp: moment().format('YYYY-MM-DD HH:mm:ss'),
  218. isSelf: false,
  219. avatar: avator,
  220. addToHistory: !taklToHtml.value
  221. })
  222. let history = []
  223. console.log(messages.value);
  224. if (taklToHtml.value) {
  225. if (addHtml) {
  226. history.push({
  227. role: 'user',
  228. content: `页面主要内容${pageInfo.value.content.mainContent}`
  229. })
  230. }
  231. history.push({
  232. role: 'user',
  233. content: msg
  234. })
  235. } else {
  236. history = messages.value.filter(item => item.addToHistory).slice(-20).map(item => ({
  237. role: item.isSelf ? 'user' : 'system',
  238. content: item.isSelf ? item.content : item.rawContent
  239. }))
  240. // const index = history.findIndex(item => item.content === '总结当前页面')
  241. // if (index !== -1) {
  242. // history.splice(index, 2)
  243. // }
  244. // const index2 = history.findIndex(item => item.has === true)
  245. // if (index2 !== -1) {
  246. // history.splice(index2, 1)
  247. // }
  248. }
  249. messages.value.push(obj)
  250. nextTick(() => {
  251. scrollbar.value?.setScrollTop(99999)
  252. })
  253. const iterator = await sendMessage(history, addHtml)
  254. for await (const chunk of iterator) {
  255. if (chunk) {
  256. const decodedChunk = chunk.choices[0].delta.content;
  257. if (decodedChunk) {
  258. // 保存原始内容
  259. obj.rawContent += decodedChunk
  260. // 实时格式化显示内容
  261. obj.content = formatMessage(obj.rawContent)
  262. }
  263. }
  264. scrollbar.value?.setScrollTop(99999)
  265. }
  266. scrollbar.value?.setScrollTop(99999)
  267. // 处理最终内容
  268. if (type.value === '2') {
  269. try {
  270. formMap = JSON.parse(obj.rawContent.split('json')[1].split('```')[0])
  271. console.log(formMap,100);
  272. handleInput()
  273. } catch (e) {
  274. console.error('解析JSON失败:', e)
  275. }
  276. }
  277. console.log(messages.value);
  278. sendLoading.value = false
  279. nextTick(() => {
  280. scrollbar.value?.setScrollTop(99999)
  281. })
  282. }
  283. const mockData = [
  284. {action:'click', class: "ant-menu-item", tag: "li", innerHTML: "<span class=\"ant-menu-item-icon\"><span role=\"img\" aria-label=\"book\" class=\"anticon anticon-book\"></span></span><span class=\"ant-menu-title-content\"><span>项目建档</span></span>", id: "", text: "项目建档", next: "是" },
  285. {action:'click',class: "ant-menu-item", tag: "button", innerHTML: "<span class=\"ant-menu-item-icon\"><span role=\"img\" aria-label=\"book\" class=\"anticon anticon-book\"></span></span><span class=\"ant-menu-title-content\"><span>项目建档</span></span>", id: "", text: "新增", next: "是"},
  286. {action:'show',class: "ant-menu-item", tag: "button", innerHTML: "<span class=\"ant-menu-item-icon\"><span role=\"img\" aria-label=\"book\" class=\"anticon anticon-book\"></span></span><span class=\"ant-menu-title-content\"><span>项目建档</span></span>", id: "", text: "请上传数据", next: "是"},
  287. ]
  288. let indexTemp = 0
  289. const fetchRes = async (msg) => {
  290. sendLoading.value = true
  291. const obj =reactive({
  292. id: moment(),
  293. username: '用户1',
  294. content: '',
  295. timestamp: moment().format('YYYY-MM-DD HH:mm:ss'),
  296. isSelf: false,
  297. avatar: avator,
  298. addToHistory:!taklToHtml.value
  299. })
  300. messages.value.push(obj)
  301. if (type.value === '2') {
  302. await fetchDataAndProcess(msg,obj)
  303. sendLoading.value = false
  304. // await handleClick(res)
  305. }
  306. // strArr = msg.split(',')
  307. // await fetchDataAndProcess(msg)
  308. // obj.content = '执行操作中'
  309. }
  310. let strArr = []
  311. let index = 0
  312. async function fetchDataAndProcess(input,obj) {
  313. console.log(input);
  314. const pageInfo = await getPageInfo()
  315. console.log(pageInfo);
  316. // 发起请求获取数据
  317. // const res = await hepl({
  318. // input_data: input,
  319. // body:pageInfo.content.mainContent
  320. // })
  321. const res = await new Promise((resolve, reject) => {
  322. setTimeout(() => {
  323. resolve({data:mockData[indexTemp]})
  324. }, 1000)
  325. })
  326. await handleClick(res.data,obj)
  327. }
  328. async function handleClick(res, msgObj) {
  329. await new Promise(resolve => setTimeout(resolve, 1000))
  330. if (res.action === 'click') {
  331. msgObj.content = `点击${res.tag}元素`
  332. chrome.runtime.sendMessage({
  333. type: 'FROM_SIDE_PANEL_TO_ACTION',
  334. data: res
  335. }, async (response) => {
  336. if (chrome.runtime.lastError) {
  337. console.error("消息发送错误:", chrome.runtime.lastError);
  338. rej(chrome.runtime.lastError)
  339. } else {
  340. if (res.next === '是') {
  341. indexTemp++
  342. fetchDataAndProcess(strArr[index],msgObj)
  343. } else {
  344. ElMessage({ message: '操作执行完成', type: 'success', duration: 2 * 1000, grouping: true })
  345. index = 0
  346. }
  347. }
  348. return true
  349. });
  350. }
  351. if (res.action === 'show') {
  352. msgObj.content = `请上传数据`
  353. ElMessage({ message: '请上传数据', type: 'success', duration: 4 * 1000, grouping: true })
  354. }
  355. // chrome.runtime.sendMessage({
  356. // type: 'FROM_SIDE_PANEL_TO_ACTION',
  357. // data:obj
  358. // }, async (response) => {
  359. // if (chrome.runtime.lastError) {
  360. // console.error("消息发送错误:", chrome.runtime.lastError);
  361. // rej(chrome.runtime.lastError)
  362. // } else {
  363. // if (obj.next === '是') {
  364. // console.log(strArr[index]);
  365. // index++
  366. // fetchDataAndProcess(strArr[index])
  367. // } else {
  368. // ElMessage({ message: '操作执行完成', type: 'success', duration: 2 * 1000, grouping: true })
  369. // index = 0
  370. // }
  371. // }
  372. // return true
  373. // });
  374. }
  375. const sendRequese = async (msg, addHtml = false) => {
  376. const a = await getPageInfo()
  377. if (type.value === '2' && msg.startsWith('/')) {
  378. indexTemp = 0
  379. fetchRes(msg, addHtml)
  380. }
  381. else {
  382. if (!addHtml) msg = getSummaryPrompt(a.content)
  383. streamRes(msg, addHtml)
  384. }
  385. // if (msg === '') msg = getSummaryPrompt(a.content)
  386. // streamRes(msg, Summary, format = false, add, copy)
  387. }
  388. // 发送消息
  389. const addMessage = (msg,fetch) => {
  390. if (!msg) return
  391. const newMessage = {
  392. id: messages.value.length + 1,
  393. username: '我',
  394. content: msg,
  395. timestamp: moment().format('YYYY-MM-DD HH:mm:ss'),
  396. isSelf: true,
  397. avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
  398. addToHistory: !taklToHtml.value
  399. }
  400. messages.value.push(newMessage)
  401. // 滚动到底部
  402. nextTick(() => {
  403. scrollbar.value?.setScrollTop(99999)
  404. fetch && sendRequese(msg, taklToHtml.value)
  405. })
  406. }
  407. const isHoveringTitle = ref(false)
  408. // 组件挂载时滚动到底部
  409. onMounted(async () => {
  410. chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  411. if (message.type === 'TO_SIDE_PANEL_PAGE_INFO') {
  412. pageInfo.value = message.data
  413. console.log(pageInfo.value);
  414. // 转发到 content.js
  415. chrome.tabs.sendMessage(sender.tab.id, {
  416. type: 'TO_CONTENT_SCRIPT',
  417. data: message.data
  418. });
  419. }
  420. if (message.type === 'TO_SIDE_PANEL_PAGE_CHANGE') {
  421. pageInfo.value = message.data
  422. }
  423. });
  424. await getPageInfo()
  425. nextTick(() => {
  426. scrollbar.value?.setScrollTop(99999)
  427. })
  428. // 添加标题悬停事件监听
  429. nextTick(() => {
  430. const titleWrapper = document.querySelector('.title-wrapper')
  431. if (titleWrapper) {
  432. titleWrapper.addEventListener('mouseenter', () => {
  433. isHoveringTitle.value = true
  434. })
  435. titleWrapper.addEventListener('mouseleave', () => {
  436. isHoveringTitle.value = false
  437. })
  438. }
  439. scrollbar.value?.setScrollTop(99999)
  440. })
  441. })
  442. </script>
  443. <style scoped>
  444. .chat-container {
  445. height: 100vh;
  446. display: flex;
  447. flex-direction: column;
  448. border: 1px solid #dcdfe6;
  449. border-radius: 4px;
  450. background-color: #fff;
  451. font-family: 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
  452. }
  453. .message-list {
  454. flex: 1;
  455. padding: 16px;
  456. overflow: auto;
  457. }
  458. .messages {
  459. min-height: 100%;
  460. display: flex;
  461. flex-direction: column;
  462. }
  463. .message-item {
  464. display: flex;
  465. margin-bottom: 20px;
  466. gap: 12px;
  467. color: #333;
  468. align-items: flex-start;
  469. animation: fadeIn 0.3s ease-in-out;
  470. }
  471. @keyframes fadeIn {
  472. from { opacity: 0; transform: translateY(10px); }
  473. to { opacity: 1; transform: translateY(0); }
  474. }
  475. .message-item.self {
  476. flex-direction: row-reverse;
  477. }
  478. .message-content {
  479. max-width: 80%;
  480. }
  481. .username {
  482. font-size: 14px;
  483. color: #606266;
  484. margin-bottom: 4px;
  485. }
  486. .content {
  487. padding: 12px;
  488. min-height: 40px;
  489. background-color: #F0F4F8;
  490. border-radius: 12px;
  491. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  492. word-break: break-all;
  493. line-height: 1.5;
  494. font-size: 14px;
  495. }
  496. .content :deep(pre) {
  497. margin: 10px 0;
  498. border-radius: 8px;
  499. }
  500. .content :deep(code) {
  501. font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
  502. }
  503. .content :deep(p) {
  504. margin: 8px 0;
  505. }
  506. .content :deep(ul), .content :deep(ol) {
  507. padding-left: 20px;
  508. margin: 8px 0;
  509. }
  510. .content :deep(blockquote) {
  511. border-left: 4px solid #ddd;
  512. padding-left: 10px;
  513. color: #666;
  514. margin: 8px 0;
  515. }
  516. .self .content {
  517. background: #409eff;
  518. color: white;
  519. border-bottom-right-radius: 4px;
  520. }
  521. .other .content {
  522. border-bottom-left-radius: 4px;
  523. }
  524. .timestamp {
  525. display: flex;
  526. justify-content: space-between;
  527. font-size: 12px;
  528. color: #909399;
  529. margin-top: 6px;
  530. }
  531. .timestamp span {
  532. transition: color 0.2s;
  533. }
  534. .timestamp span:hover {
  535. color: #409eff;
  536. }
  537. .input-area {
  538. padding: 16px;
  539. color: #7F838A;
  540. border-top: 1px solid #eaeaea;
  541. display: flex;
  542. justify-content: space-between;
  543. align-items: center;
  544. gap: 12px;
  545. background-color: #f9f9f9;
  546. }
  547. .input-area :deep(.el-textarea__inner) {
  548. border-radius: 8px;
  549. transition: border-color 0.3s;
  550. resize: none;
  551. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
  552. }
  553. .input-area :deep(.el-textarea__inner:focus) {
  554. border-color: #409eff;
  555. box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
  556. }
  557. .info-card {
  558. margin: 10px;
  559. }
  560. .card-content {
  561. display: flex;
  562. align-items: center;
  563. gap: 12px;
  564. padding: 14px 16px;
  565. margin: 0 16px 12px;
  566. background: #fff;
  567. border-radius: 10px;
  568. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  569. transition: transform 0.2s, box-shadow 0.2s;
  570. }
  571. .card-content:hover {
  572. transform: translateY(-2px);
  573. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
  574. }
  575. .card-icon {
  576. color: #409eff;
  577. }
  578. .title-wrapper {
  579. flex: 1;
  580. overflow: hidden;
  581. position: relative;
  582. height: 24px;
  583. }
  584. .title-scroller {
  585. display: inline-block;
  586. white-space: nowrap;
  587. color: #333;
  588. font-weight: 500;
  589. }
  590. .title-scroller.scroll {
  591. animation: scrollTitle 10s linear infinite;
  592. }
  593. @keyframes scrollTitle {
  594. 0% {
  595. transform: translateX(0);
  596. }
  597. 100% {
  598. transform: translateX(-100%);
  599. }
  600. }
  601. .upload :deep(.el-icon) {
  602. transition: color 0.3s;
  603. }
  604. .can-hover :deep(.el-icon:hover) {
  605. color: #409eff !important;
  606. }
  607. .el-check-tag {
  608. margin-right: 8px;
  609. transition: all 0.3s;
  610. }
  611. .el-check-tag:hover {
  612. transform: scale(1.05);
  613. }
  614. /* 加载中的消息样式 */
  615. .loading-content {
  616. min-height: 40px;
  617. display: flex;
  618. align-items: center;
  619. justify-content: flex-start;
  620. }
  621. .loading-indicator {
  622. display: flex;
  623. align-items: center;
  624. gap: 4px;
  625. }
  626. .dot {
  627. width: 8px;
  628. height: 8px;
  629. background-color: #409eff;
  630. border-radius: 50%;
  631. display: inline-block;
  632. animation: pulse 1.5s infinite ease-in-out;
  633. }
  634. .dot:nth-child(2) {
  635. animation-delay: 0.3s;
  636. }
  637. .dot:nth-child(3) {
  638. animation-delay: 0.6s;
  639. }
  640. @keyframes pulse {
  641. 0%, 100% {
  642. transform: scale(0.8);
  643. opacity: 0.6;
  644. }
  645. 50% {
  646. transform: scale(1.2);
  647. opacity: 1;
  648. }
  649. }
  650. </style>