index.vue 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091
  1. <script setup lang="ts">
  2. import type { Ref } from "vue";
  3. import { computed, h, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
  4. import { useRoute, useRouter } from "vue-router";
  5. import { storeToRefs } from "pinia";
  6. import {
  7. NAutoComplete,
  8. NAvatar,
  9. NButton,
  10. NImage,
  11. NInput,
  12. NModal,
  13. useDialog,
  14. useMessage,
  15. } from "naive-ui";
  16. import html2canvas from "html2canvas";
  17. import to from "await-to-js";
  18. import { AiEditor } from "aieditor";
  19. import drawListVue from "../mj/drawList.vue";
  20. import aiGPT from "../mj/aiGpt.vue";
  21. import AiSiderInput from "../mj/aiSiderInput.vue";
  22. import aiGptInput from "../mj/aiGptInput.vue";
  23. import { Message } from "./components";
  24. import { useScroll } from "./hooks/useScroll";
  25. import { useChat } from "./hooks/useChat";
  26. import { useUsingContext } from "./hooks/useUsingContext";
  27. import { getGpts } from "@/api/chatmsg";
  28. import { SvgIcon } from "@/components/common";
  29. import { useBasicLayout } from "@/hooks/useBasicLayout";
  30. import {
  31. gptConfigStore,
  32. gptsUlistStore,
  33. homeStore,
  34. useAppStore,
  35. useChatStore,
  36. usePromptStore,
  37. } from "@/store";
  38. import type { gptsType } from "@/api";
  39. import { chatSetting, fetchChatAPIProcess, mlog } from "@/api";
  40. import { t } from "@/locales";
  41. import { getInform, getNotice, readNotice } from "@/api/notice";
  42. import "aieditor/dist/style.css";
  43. import extendIcon from "@/assets/icons/extend.svg?raw";
  44. import abbrIcon from "@/assets/icons/abbr.svg?raw";
  45. import { getToken } from "@/store/modules/auth/helper";
  46. import { copyToClip } from "@/utils/copy";
  47. import { exportDoc } from "./components/index";
  48. let controller = new AbortController();
  49. const openLongReply = import.meta.env.VITE_GLOB_OPEN_LONG_REPLY === "true";
  50. const route = useRoute();
  51. const dialog = useDialog();
  52. const ms = useMessage();
  53. const router = useRouter();
  54. const chatStore = useChatStore();
  55. // const href = window.location.href.split('#')[0]
  56. const uuid1 = chatStore.active;
  57. const chatSet = new chatSetting(uuid1 == null ? 1002 : uuid1);
  58. const nGptStore = ref(chatSet.getGptConfig());
  59. console.log("nGptStore", nGptStore);
  60. const href = window.location.hostname;
  61. const { isMobile } = useBasicLayout();
  62. const { addChat, updateChat, updateChatSome, getChatByUuidAndIndex } =
  63. useChat();
  64. const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll();
  65. const { usingContext, toggleUsingContext } = useUsingContext();
  66. const { uuid } = route.params as { uuid: string };
  67. const dataSources = computed(() => chatStore.getChatByUuid(+uuid));
  68. const conversationList = computed(() =>
  69. dataSources.value.filter(
  70. (item) => !item.inversion && !!item.conversationOptions
  71. )
  72. );
  73. const prompt = ref<string>("");
  74. const loading = ref<boolean>(false);
  75. const inputRef = ref<Ref | null>(null);
  76. const divRef = ref();
  77. let aiEditor: AiEditor | null = null;
  78. const editedMessage = ref();
  79. const editedMessageIndex = ref();
  80. const showDialog = ref(false);
  81. const aiPanel = ref(false);
  82. // 添加PromptStore
  83. const promptStore = usePromptStore();
  84. // 使用storeToRefs,保证store修改后,联想部分能够重新渲染
  85. const { promptList: promptTemplate } = storeToRefs<any>(promptStore);
  86. const appStore = useAppStore();
  87. const isChat = computed(() => appStore.isChat);
  88. // 未知原因刷新页面,loading 状态不会重置,手动重置
  89. dataSources.value.forEach((item, index) => {
  90. if (item.loading) updateChatSome(+uuid, index, { loading: false });
  91. });
  92. function handleSubmit() {
  93. // onConversation() //把这个放到aiGpt
  94. const message = prompt.value;
  95. if (!message || message.trim() === "") return;
  96. if (loading.value) return;
  97. loading.value = true;
  98. homeStore.setMyData({
  99. act: "gpt.submit",
  100. actData: { prompt: prompt.value, uuid },
  101. });
  102. prompt.value = "";
  103. }
  104. async function onConversation() {
  105. let message = prompt.value;
  106. if (loading.value) return;
  107. if (!message || message.trim() === "") return;
  108. controller = new AbortController();
  109. addChat(+uuid, {
  110. dateTime: new Date().toLocaleString(),
  111. text: message,
  112. inversion: true,
  113. error: false,
  114. conversationOptions: null,
  115. requestOptions: { prompt: message, options: null },
  116. });
  117. scrollToBottom();
  118. loading.value = true;
  119. prompt.value = "";
  120. let options: Chat.ConversationRequest = {};
  121. const lastContext =
  122. conversationList.value[conversationList.value.length - 1]
  123. ?.conversationOptions;
  124. if (lastContext && usingContext.value) options = { ...lastContext };
  125. addChat(+uuid, {
  126. dateTime: new Date().toLocaleString(),
  127. text: "思考中",
  128. loading: true,
  129. inversion: false,
  130. error: false,
  131. conversationOptions: null,
  132. requestOptions: { prompt: message, options: { ...options } },
  133. });
  134. scrollToBottom();
  135. try {
  136. let lastText = "";
  137. const fetchChatAPIOnce = async () => {
  138. await fetchChatAPIProcess<Chat.ConversationResponse>({
  139. prompt: message,
  140. options,
  141. signal: controller.signal,
  142. onDownloadProgress: ({ event }) => {
  143. const xhr = event.target;
  144. const { responseText } = xhr;
  145. // Always process the final line
  146. const lastIndex = responseText.lastIndexOf(
  147. "\n",
  148. responseText.length - 2
  149. );
  150. let chunk = responseText;
  151. if (lastIndex !== -1) chunk = responseText.substring(lastIndex);
  152. try {
  153. const data = JSON.parse(chunk);
  154. updateChat(+uuid, dataSources.value.length - 1, {
  155. dateTime: new Date().toLocaleString(),
  156. text: lastText + (data.text ?? ""),
  157. inversion: false,
  158. error: false,
  159. loading: true,
  160. conversationOptions: {
  161. conversationId: data.conversationId,
  162. parentMessageId: data.id,
  163. },
  164. requestOptions: { prompt: message, options: { ...options } },
  165. });
  166. if (
  167. openLongReply &&
  168. data.detail.choices[0].finish_reason === "length"
  169. ) {
  170. options.parentMessageId = data.id;
  171. lastText = data.text;
  172. message = "";
  173. return fetchChatAPIOnce();
  174. }
  175. scrollToBottomIfAtBottom();
  176. } catch (error) {
  177. //
  178. }
  179. },
  180. });
  181. updateChatSome(+uuid, dataSources.value.length - 1, { loading: false });
  182. };
  183. await fetchChatAPIOnce();
  184. } catch (error: any) {
  185. const errorMessage = error?.message ?? t("common.wrong");
  186. if (error.message === "canceled") {
  187. updateChatSome(+uuid, dataSources.value.length - 1, {
  188. loading: false,
  189. });
  190. scrollToBottomIfAtBottom();
  191. return;
  192. }
  193. const currentChat = getChatByUuidAndIndex(
  194. +uuid,
  195. dataSources.value.length - 1
  196. );
  197. if (currentChat?.text && currentChat.text !== "") {
  198. updateChatSome(+uuid, dataSources.value.length - 1, {
  199. text: `${currentChat.text}\n[${errorMessage}]`,
  200. error: false,
  201. loading: false,
  202. });
  203. return;
  204. }
  205. updateChat(+uuid, dataSources.value.length - 1, {
  206. dateTime: new Date().toLocaleString(),
  207. text: errorMessage,
  208. inversion: false,
  209. error: true,
  210. loading: false,
  211. conversationOptions: null,
  212. requestOptions: { prompt: message, options: { ...options } },
  213. });
  214. scrollToBottomIfAtBottom();
  215. } finally {
  216. loading.value = false;
  217. }
  218. }
  219. async function onRegenerate(index: number) {
  220. if (loading.value) return;
  221. controller = new AbortController();
  222. const { requestOptions } = dataSources.value[index];
  223. let message = requestOptions?.prompt ?? "";
  224. let options: Chat.ConversationRequest = {};
  225. if (requestOptions.options) options = { ...requestOptions.options };
  226. loading.value = true;
  227. updateChat(+uuid, index, {
  228. dateTime: new Date().toLocaleString(),
  229. text: "",
  230. inversion: false,
  231. error: false,
  232. loading: true,
  233. conversationOptions: null,
  234. requestOptions: { prompt: message, options: { ...options } },
  235. });
  236. try {
  237. let lastText = "";
  238. const fetchChatAPIOnce = async () => {
  239. await fetchChatAPIProcess<Chat.ConversationResponse>({
  240. prompt: message,
  241. options,
  242. signal: controller.signal,
  243. onDownloadProgress: ({ event }) => {
  244. const xhr = event.target;
  245. const { responseText } = xhr;
  246. // Always process the final line
  247. const lastIndex = responseText.lastIndexOf(
  248. "\n",
  249. responseText.length - 2
  250. );
  251. let chunk = responseText;
  252. if (lastIndex !== -1) chunk = responseText.substring(lastIndex);
  253. try {
  254. const data = JSON.parse(chunk);
  255. updateChat(+uuid, index, {
  256. dateTime: new Date().toLocaleString(),
  257. text: lastText + (data.text ?? ""),
  258. inversion: false,
  259. error: false,
  260. loading: true,
  261. conversationOptions: {
  262. conversationId: data.conversationId,
  263. parentMessageId: data.id,
  264. },
  265. requestOptions: { prompt: message, options: { ...options } },
  266. });
  267. if (
  268. openLongReply &&
  269. data.detail.choices[0].finish_reason === "length"
  270. ) {
  271. options.parentMessageId = data.id;
  272. lastText = data.text;
  273. message = "";
  274. return fetchChatAPIOnce();
  275. }
  276. } catch (error) {
  277. //
  278. }
  279. },
  280. });
  281. updateChatSome(+uuid, index, { loading: false });
  282. };
  283. await fetchChatAPIOnce();
  284. } catch (error: any) {
  285. if (error.message === "canceled") {
  286. updateChatSome(+uuid, index, {
  287. loading: false,
  288. });
  289. return;
  290. }
  291. const errorMessage = error?.message ?? t("common.wrong");
  292. updateChat(+uuid, index, {
  293. dateTime: new Date().toLocaleString(),
  294. text: errorMessage,
  295. inversion: false,
  296. error: true,
  297. loading: false,
  298. conversationOptions: null,
  299. requestOptions: { prompt: message, options: { ...options } },
  300. });
  301. } finally {
  302. loading.value = false;
  303. }
  304. }
  305. function handleExport() {
  306. if (loading.value) return;
  307. const d = dialog.warning({
  308. title: t("chat.exportImage"),
  309. content: t("chat.exportImageConfirm"),
  310. positiveText: t("common.yes"),
  311. negativeText: t("common.no"),
  312. onPositiveClick: async () => {
  313. try {
  314. d.loading = true;
  315. const ele = document.getElementById("image-wrapper");
  316. const canvas = await html2canvas(ele as HTMLDivElement, {
  317. useCORS: true,
  318. });
  319. const imgUrl = canvas.toDataURL("image/png");
  320. const tempLink = document.createElement("a");
  321. tempLink.style.display = "none";
  322. tempLink.href = imgUrl;
  323. tempLink.setAttribute("download", "chat-shot.png");
  324. if (typeof tempLink.download === "undefined")
  325. tempLink.setAttribute("target", "_blank");
  326. document.body.appendChild(tempLink);
  327. tempLink.click();
  328. document.body.removeChild(tempLink);
  329. window.URL.revokeObjectURL(imgUrl);
  330. d.loading = false;
  331. ms.success(t("chat.exportSuccess"));
  332. Promise.resolve();
  333. } catch (error: any) {
  334. ms.error(t("chat.exportFailed"));
  335. } finally {
  336. d.loading = false;
  337. }
  338. },
  339. });
  340. }
  341. function handleDelete(index: number) {
  342. if (loading.value) return;
  343. dialog.warning({
  344. title: t("chat.deleteMessage"),
  345. content: t("chat.deleteMessageConfirm"),
  346. positiveText: t("common.yes"),
  347. negativeText: t("common.no"),
  348. onPositiveClick: () => {
  349. chatStore.deleteChatByUuid(+uuid, index);
  350. },
  351. });
  352. }
  353. function handleClear() {
  354. if (loading.value) return;
  355. dialog.warning({
  356. title: t("chat.clearChat"),
  357. content: t("chat.clearChatConfirm"),
  358. positiveText: t("common.yes"),
  359. negativeText: t("common.no"),
  360. onPositiveClick: () => {
  361. chatStore.clearChatByUuid(+uuid);
  362. },
  363. });
  364. }
  365. function handleEnter(event: KeyboardEvent) {
  366. if (!isMobile.value) {
  367. if (event.key === "Enter" && !event.shiftKey) {
  368. event.preventDefault();
  369. handleSubmit();
  370. }
  371. } else {
  372. if (event.key === "Enter" && event.ctrlKey) {
  373. event.preventDefault();
  374. handleSubmit();
  375. }
  376. }
  377. }
  378. function handleStop() {
  379. if (loading.value) {
  380. homeStore.setMyData({ act: "abort" });
  381. controller.abort();
  382. loading.value = false;
  383. }
  384. }
  385. function handleEdit(index: number) {
  386. if (loading.value) return;
  387. editedMessageIndex.value = index;
  388. editedMessage.value = dataSources.value[index].text.slice();
  389. showDialog.value = true;
  390. nextTick(() => {
  391. aiEditor = new AiEditor({
  392. element: divRef.value as Element,
  393. toolbarKeys: [],
  394. placeholder: "点击输入内容...",
  395. content: editedMessage.value,
  396. contentIsMarkdown: true,
  397. textSelectionBubbleMenu: {
  398. enable: true,
  399. items: ["ai", "Bold", "Italic", "Underline", "Strike"],
  400. },
  401. ai: {
  402. models: {
  403. custom: {
  404. url: "http://127.0.0.1:6039/chat/send",
  405. headers: () => {
  406. return {
  407. "Content-Type": "application/json;charset=UTF-8",
  408. Authorization: "Bearer " + getToken(),
  409. Accept: "text/event-stream ",
  410. };
  411. },
  412. wrapPayload: (message: string) => {
  413. return JSON.stringify({
  414. messages: [
  415. {
  416. role: "user",
  417. content: message,
  418. },
  419. ],
  420. model: nGptStore.value.modelLabel,
  421. });
  422. },
  423. parseMessage: (message: any) => {
  424. const data = JSON.parse(message);
  425. const msg =
  426. data.choices[0].delta?.content ??
  427. data.choices[0].delta?.reasoning_content ??
  428. "";
  429. return {
  430. role: "assistant",
  431. content: msg,
  432. // index: number,
  433. // //0 代表首个文本结果;1 代表中间文本结果;2 代表最后一个文本结果。
  434. // status: 0|1|2,
  435. };
  436. },
  437. protocol: "sse",
  438. },
  439. },
  440. bubblePanelMenus: [
  441. {
  442. prompt: `<content>{content}</content>\n请帮我优化一下这段内容,并直接返回优化后的结果。\n注意:你应该先判断一下这句话是中文还是英文,如果是中文,请给我返回中文的内容,如果是英文,请给我返回英文内容,只需要返回内容即可,不需要告知我是中文还是英文。`,
  443. icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15.1986 9.94447C14.7649 9.5337 14.4859 8.98613 14.4085 8.39384L14.0056 5.31138L11.275 6.79724C10.7503 7.08274 10.1433 7.17888 9.55608 7.06948L6.49998 6.50015L7.06931 9.55625C7.17871 10.1435 7.08257 10.7505 6.79707 11.2751L5.31121 14.0057L8.39367 14.4086C8.98596 14.4861 9.53353 14.7651 9.94431 15.1987L12.0821 17.4557L13.4178 14.6486C13.6745 14.1092 14.109 13.6747 14.6484 13.418L17.4555 12.0823L15.1986 9.94447ZM15.2238 15.5079L13.0111 20.1581C12.8687 20.4573 12.5107 20.5844 12.2115 20.442C12.1448 20.4103 12.0845 20.3665 12.0337 20.3129L8.49229 16.5741C8.39749 16.474 8.27113 16.4096 8.13445 16.3918L3.02816 15.7243C2.69958 15.6814 2.46804 15.3802 2.51099 15.0516C2.52056 14.9784 2.54359 14.9075 2.5789 14.8426L5.04031 10.3192C5.1062 10.1981 5.12839 10.058 5.10314 9.92253L4.16 4.85991C4.09931 4.53414 4.3142 4.22086 4.63997 4.16017C4.7126 4.14664 4.78711 4.14664 4.85974 4.16017L9.92237 5.10331C10.0579 5.12855 10.198 5.10637 10.319 5.04048L14.8424 2.57907C15.1335 2.42068 15.4979 2.52825 15.6562 2.81931C15.6916 2.88421 15.7146 2.95507 15.7241 3.02833L16.3916 8.13462C16.4095 8.2713 16.4739 8.39766 16.5739 8.49245L20.3127 12.0338C20.5533 12.2617 20.5636 12.6415 20.3357 12.8821C20.2849 12.9357 20.2246 12.9795 20.1579 13.0112L15.5078 15.224C15.3833 15.2832 15.283 15.3835 15.2238 15.5079ZM16.0206 17.435L17.4348 16.0208L21.6775 20.2634L20.2633 21.6776L16.0206 17.435Z"></path></svg>`,
  444. title: "润色",
  445. },
  446. {
  447. prompt: `<content>{content}</content>\n请帮我扩写一下这段内容,并直接返回扩写后的结果。\n注意:你应该先判断一下这句话是中文还是英文,如果是中文,请给我返回中文的内容,如果是英文,请给我返回英文内容,只需要返回内容即可,不需要告知我是中文还是英文。`,
  448. icon: extendIcon,
  449. title: "扩写",
  450. },
  451. {
  452. prompt: `<content>{content}</content>\n请帮我缩写一下这段内容,并直接返回缩写后的结果。\n注意:你应该先判断一下这句话是中文还是英文,如果是中文,请给我返回中文的内容,如果是英文,请给我返回英文内容,只需要返回内容即可,不需要告知我是中文还是英文。`,
  453. icon: abbrIcon,
  454. title: "缩写",
  455. },
  456. ],
  457. },
  458. });
  459. });
  460. }
  461. const submitCallback = () => {
  462. const markdownData = aiEditor?.getMarkdown();
  463. updateChatSome(+uuid, editedMessageIndex.value, {
  464. text: markdownData,
  465. });
  466. showDialog.value = false;
  467. };
  468. // 可优化部分
  469. // 搜索选项计算,这里使用value作为索引项,所以当出现重复value时渲染异常(多项同时出现选中效果)
  470. // 理想状态下其实应该是key作为索引项,但官方的renderOption会出现问题,所以就需要value反renderLabel实现
  471. const searchOptions = computed(() => {
  472. if (prompt.value.startsWith("/")) {
  473. const abc = promptTemplate.value
  474. .filter((item: { key: string }) =>
  475. item.key.toLowerCase().includes(prompt.value.substring(1).toLowerCase())
  476. )
  477. .map((obj: { value: any }) => {
  478. return {
  479. label: obj.value,
  480. value: obj.value,
  481. };
  482. });
  483. mlog("搜索选项", abc);
  484. return abc;
  485. } else if (prompt.value === "@") {
  486. const abc = gptsUlistStore.myData.slice(0, 10).map((v: gptsType) => {
  487. return {
  488. label: v.info,
  489. gpts: v,
  490. value: v.gid,
  491. };
  492. });
  493. return abc;
  494. } else {
  495. return [];
  496. }
  497. });
  498. const goUseGpts = async (item: gptsType) => {
  499. const saveObj = { model: `${item.modelName}`, gpts: item };
  500. gptConfigStore.setMyData(saveObj);
  501. if (chatStore.active) {
  502. // 保存到对话框
  503. const chatSet = new chatSetting(chatStore.active);
  504. if (chatSet.findIndex() > -1) chatSet.save(saveObj);
  505. }
  506. ms.success(t("mjchat.success2"));
  507. // const gptUrl= `https://gpts.ddaiai.com/open/gptsapi/use`;
  508. // myFetch(gptUrl,item );
  509. mlog("go local ", homeStore.myData.local);
  510. if (homeStore.myData.local !== "Chat")
  511. router.replace({ name: "Chat", params: { uuid: chatStore.active } });
  512. gptsUlistStore.setMyData(item);
  513. };
  514. // value反渲染key
  515. const renderOption = (option: { label: string; gpts?: gptsType }) => {
  516. if (prompt.value == "@") {
  517. // return [ h( NAvatar,{src:'https://cos.aitutu.cc/gpts/gpt4all.jpg',size:"small",round:true}),option.label ]
  518. return [
  519. h(
  520. "div",
  521. {
  522. class: "flex justify-start items-center",
  523. onclick: () => {
  524. if (option.gpts) goUseGpts(option.gpts);
  525. prompt.value = "";
  526. setTimeout(() => (prompt.value = ""), 80);
  527. },
  528. },
  529. [
  530. h(NAvatar, {
  531. src: option.gpts?.logo,
  532. "fallback-src": "https://cos.aitutu.cc/gpts/3.5net.png",
  533. size: "small",
  534. round: true,
  535. class: "w-8 h-8",
  536. }),
  537. h("span", { class: "pl-1" }, option.gpts?.name),
  538. h(
  539. "span",
  540. { class: "line-clamp-1 flex-1 pl-1 opacity-50" },
  541. option.label
  542. ),
  543. ]
  544. ),
  545. ];
  546. }
  547. for (const i of promptTemplate.value) {
  548. if (i.value === option.label) return [i.key];
  549. }
  550. return [];
  551. };
  552. const placeholder = computed(() => {
  553. if (isMobile.value) return t("chat.placeholderMobile");
  554. return t("chat.placeholder");
  555. });
  556. const buttonDisabled = computed(() => {
  557. return loading.value || !prompt.value || prompt.value.trim() === "";
  558. });
  559. const footerClass = computed(() => {
  560. let classes = ["p-4"];
  561. if (isMobile.value)
  562. classes = ["sticky", "left-0", "bottom-0", "right-0", "p-2", "pr-3"]; // , 'overflow-hidden'
  563. return classes;
  564. });
  565. onMounted(() => {
  566. scrollToBottom();
  567. if (inputRef.value && !isMobile.value) inputRef.value?.focus();
  568. // 查询公告信息
  569. selectNotice();
  570. // 查询通知信息
  571. selectInform();
  572. });
  573. onUnmounted(() => {
  574. aiEditor && aiEditor.destroy();
  575. if (loading.value) controller.abort();
  576. homeStore.setMyData({ isLoader: false });
  577. });
  578. const local = computed(() => homeStore.myData.local);
  579. watch(
  580. () => homeStore.myData.act,
  581. (n) => {
  582. if (n === "draw") scrollToBottom();
  583. if (n === "scrollToBottom") scrollToBottom();
  584. if (n === "scrollToBottomIfAtBottom") scrollToBottomIfAtBottom();
  585. if (n === "gpt.submit" || n === "gpt.resubmit") loading.value = true;
  586. if (n === "stopLoading") loading.value = false;
  587. }
  588. );
  589. const st = ref({ inputme: true });
  590. watch(
  591. () => loading.value,
  592. (n) => homeStore.setMyData({ isLoader: n })
  593. );
  594. const ychat = computed(() => {
  595. let text = prompt.value;
  596. if (loading.value) text = "";
  597. else scrollToBottomIfAtBottom();
  598. return { text, dateTime: t("chat.preview") } as Chat.Chat;
  599. });
  600. const showModal = ref(false);
  601. const modalContent = ref("<h2>暂无内容</h2>");
  602. const informContent = ref([]);
  603. const noticeId = ref("");
  604. async function selectNotice() {
  605. const [err, result] = await to(getNotice());
  606. console.log("result?.data", result?.data);
  607. if (result?.data) {
  608. showModal.value = true;
  609. noticeId.value = result.data.noticeId;
  610. modalContent.value = result.data.noticeContent;
  611. }
  612. }
  613. async function selectInform() {
  614. const [err, result] = await to(getInform());
  615. if (result?.rows) informContent.value = result.rows.length ? result.rows : [];
  616. }
  617. async function handleClose() {
  618. await to(readNotice(noticeId.value));
  619. }
  620. const gptsList = ref<gptsType[]>([]);
  621. const gptsFilterList = ref<gptsType[]>([]);
  622. const getRandowNum = (Min: number, Max: number): number => {
  623. const Range = Max - Min + 1;
  624. const Rand = Math.random();
  625. return Min + Math.floor(Rand * Range);
  626. };
  627. const load = async () => {
  628. // const gptUrl= homeStore.myData.session.gptUrl? homeStore.myData.session.gptUrl :'';
  629. // mlog('load',gptUrl );
  630. // let d;
  631. // if( homeStore.myData.session.gptUrl ){
  632. // d = await my2Fetch( homeStore.myData.session.gptUrl );
  633. // }else {
  634. // d = await myFetch('https://gpts.ddaiai.com/open/gpts');
  635. // }
  636. const params = { pageNum: 1, pageSize: 20 };
  637. const [err, result] = await to(getGpts(params));
  638. if (err) console.log("err===", err);
  639. else gptsList.value = result.rows as unknown as gptsType[];
  640. // gptsList.value = d.gpts as gptsType[];
  641. if (gptsList.value.length && gptsList.value.length > 3)
  642. gptsFilterList.value = gptsList.value.slice(0, 4);
  643. };
  644. const refresh = () => {
  645. gptsFilterList.value = [];
  646. const num = gptsList.value[getRandowNum(0, gptsList.value.length - 1)];
  647. const num1 = gptsList.value[getRandowNum(0, gptsList.value.length - 1)];
  648. const num2 = gptsList.value[getRandowNum(0, gptsList.value.length - 1)];
  649. const num3 = gptsList.value[getRandowNum(0, gptsList.value.length - 1)];
  650. const arr = [num, num1, num2, num3];
  651. if (Array.from(new Set(arr)).length != 4) {
  652. refresh();
  653. return;
  654. }
  655. gptsFilterList.value = [num, num1, num2, num3];
  656. };
  657. load();
  658. function closeModel() {
  659. submitCallback();
  660. showDialog.value = false;
  661. }
  662. async function handleCopy(txt?: string) {
  663. const markdownData = aiEditor?.getMarkdown();
  664. console.log("txt", txt);
  665. console.log("markdownData", markdownData);
  666. try {
  667. await copyToClip(txt || markdownData || "");
  668. ms.success(t("chat.copied"));
  669. } catch {
  670. ms.error(t("mj.copyFail"));
  671. }
  672. }
  673. function exportDocHandel() {
  674. const markdownData = aiEditor?.getMarkdown();
  675. exportDoc(markdownData);
  676. }
  677. </script>
  678. <template>
  679. <NModal
  680. v-model:show="showModal"
  681. closable
  682. :mask-closable="false"
  683. preset="dialog"
  684. title="公告详情"
  685. positive-text="我已知晓"
  686. @on-after-leave=""
  687. @positive-click="handleClose"
  688. >
  689. <div v-html="modalContent" />
  690. </NModal>
  691. <NModal
  692. class="editModel"
  693. v-model:show="showDialog"
  694. style="width: 100vw; height: 100vh; max-width: 100vw"
  695. >
  696. <div class="flex flex-row w-[100%] h-[100%]">
  697. <div class="leftContent basis-1/3">111</div>
  698. <div class="rightContent basis-2/3">
  699. <div class="top">
  700. <div class="flex flex-row float-right">
  701. <div class="tool" @click="handleCopy()">
  702. <SvgIcon icon="ri-file-copy-line" class="text-xl" />
  703. <span>复制</span>
  704. </div>
  705. <div class="tool ml-5 mr-5" @click="exportDocHandel()">
  706. <SvgIcon icon="ri-download-line" class="text-xl" />
  707. <span>下载</span>
  708. </div>
  709. |
  710. <div class="tool">
  711. <SvgIcon
  712. icon="ri-close-large-line"
  713. class="text-xl"
  714. @click="closeModel"
  715. />
  716. </div>
  717. </div>
  718. </div>
  719. <div ref="divRef" style="height: calc(100vh - 50px)"></div>
  720. </div>
  721. </div>
  722. </NModal>
  723. <div
  724. class="flex flex-col w-full h-full chat-content"
  725. :class="[isMobile ? '' : 'chat-content-noMobile']"
  726. >
  727. <!-- v-if="isMobile" -->
  728. <!-- <HeaderComponent
  729. :haveData="!!dataSources.length"
  730. :using-context="usingContext"
  731. @export="handleExport"
  732. @handle-clear="handleClear"
  733. /> -->
  734. <main class="flex-1 overflow-hidden">
  735. <div
  736. id="scrollRef"
  737. ref="scrollRef"
  738. class="h-full overflow-hidden overflow-y-auto"
  739. >
  740. <div
  741. id="image-wrapper"
  742. class="w-full max-w-screen-xl m-auto dark:bg-[#101014]"
  743. :class="[isMobile ? 'p-2' : 'p-4']"
  744. >
  745. <template v-if="!dataSources.length">
  746. <div
  747. v-if="homeStore.myData.session.notify"
  748. class="text-neutral-300 mt-4"
  749. v-html="homeStore.myData.session.notify"
  750. />
  751. <div v-else class="gpts-box">
  752. <br />
  753. <br />
  754. <div v-if="local !== 'draw'">
  755. <!-- <h1>{{ href }}</h1> -->
  756. <div
  757. v-if="informContent.length"
  758. class="annou"
  759. :style="{ 'margin-bottom': isMobile ? '15px' : '30px' }"
  760. >
  761. <div class="ai-icon">
  762. <IconSvg
  763. icon="chatGPT"
  764. :width="isMobile ? '32px' : '64px'"
  765. :height="isMobile ? '32px' : '64px'"
  766. />
  767. </div>
  768. <div
  769. class="text"
  770. :style="{ padding: isMobile ? '22px 10px' : '22px 68px' }"
  771. >
  772. <p class="title">
  773. {{ t("chat.annouce") }}
  774. </p>
  775. <!-- <p v-for="(item,index) in t('chat.annouceContent').split(';')" :key="index">{{ item }}</p> -->
  776. <div
  777. v-for="(item, index) in informContent.slice(0, 1)"
  778. :key="index"
  779. >
  780. <!-- <p style="margin-top: 10px; font-size: 18px">{{ item.noticeTitle }}</p> -->
  781. <div v-html="item.noticeContent" />
  782. </div>
  783. </div>
  784. </div>
  785. <div
  786. v-if="gptsFilterList && gptsFilterList.length"
  787. class="help"
  788. >
  789. <div class="ai-icon">
  790. <IconSvg
  791. icon="chatGPT"
  792. :width="isMobile ? '32px' : '64px'"
  793. :height="isMobile ? '32px' : '64px'"
  794. />
  795. </div>
  796. <div
  797. class="text"
  798. :style="{
  799. padding: isMobile ? '22px 10px' : '22px 68px',
  800. 'font-size': isMobile ? '14px' : '16px',
  801. 'line-height': isMobile ? '20px' : '28px',
  802. }"
  803. >
  804. <p class="title">
  805. {{ t("chat.helpTitle") }}
  806. </p>
  807. <p
  808. v-for="(item, index) in t('chat.helpcontent').split(';')"
  809. :key="index"
  810. >
  811. {{ item }}
  812. </p>
  813. <div class="gpts-list">
  814. <div class="refresh" @click="refresh">
  815. <IconSvg icon="refresh" />&nbsp;{{ t("chat.refresh") }}
  816. </div>
  817. <div
  818. v-for="v in gptsFilterList"
  819. :key="v.name"
  820. class="gpts-item"
  821. :style="{
  822. width: isMobile ? '100%' : 'calc(50% - 20px)',
  823. marginRight: '20px',
  824. padding: isMobile ? '5px 8px' : '14px 10px',
  825. 'margin-bottom': isMobile ? '8px' : '20px',
  826. }"
  827. >
  828. <NImage
  829. :src="v.logo"
  830. :preview-disabled="true"
  831. lazy
  832. class="group-hover:scale-[130%] duration-300 shrink-0 overflow-hidden bg-base object-cover rounded-full bc-avatar w-[80px] h-[80px]"
  833. :style="{
  834. width: isMobile ? '23px' : '46px',
  835. height: isMobile ? '23px' : '46px',
  836. }"
  837. >
  838. <template #placeholder>
  839. <div
  840. class="w-full h-full justify-center items-center flex"
  841. >
  842. <SvgIcon
  843. icon="line-md:downloading-loop"
  844. class="text-[60px] text-green-300"
  845. />
  846. </div>
  847. </template>
  848. </NImage>
  849. <div
  850. :style="{
  851. width: `calc(100% - ${isMobile ? '43px' : '66px'})`,
  852. float: 'left',
  853. marginLeft: '10px',
  854. }"
  855. >
  856. <p class="info" :title="v.info">
  857. {{ v.info }}
  858. </p>
  859. <p class="name" @click="goUseGpts(v)">
  860. {{ t("chat.used") }} {{ v.name }}
  861. </p>
  862. </div>
  863. </div>
  864. </div>
  865. </div>
  866. </div>
  867. </div>
  868. </div>
  869. <!-- <div class="flex items-center justify-center mt-4 text-center text-neutral-300" v-else>
  870. <SvgIcon icon="ri:bubble-chart-fill" class="mr-2 text-3xl" />
  871. <span>Aha~</span>
  872. </div> -->
  873. </template>
  874. <template v-else>
  875. <div>
  876. <Message
  877. v-for="(item, index) of dataSources"
  878. :key="index"
  879. :date-time="item.dateTime"
  880. :text="item.text"
  881. :inversion="item.inversion"
  882. :error="item.error"
  883. :loading="item.loading"
  884. :chat="item"
  885. :index="index"
  886. @regenerate="onRegenerate(index)"
  887. @delete="handleDelete(index)"
  888. @edit="handleEdit(index)"
  889. />
  890. <Message
  891. v-if="ychat.text && !homeStore.myData.session.isCloseMdPreview"
  892. :key="dataSources.length"
  893. :inversion="true"
  894. :date-time="$t('mj.typing')"
  895. :chat="ychat"
  896. :text="ychat.text"
  897. :index="dataSources.length"
  898. />
  899. <div class="sticky bottom-0 left-0 flex justify-center">
  900. <NButton v-if="loading" type="warning" @click="handleStop">
  901. <template #icon>
  902. <SvgIcon icon="ri:stop-circle-line" />
  903. </template>
  904. {{ t("common.stopResponding") }}
  905. </NButton>
  906. </div>
  907. </div>
  908. </template>
  909. </div>
  910. </div>
  911. </main>
  912. <footer v-if="local !== 'draw'" :class="footerClass" class="footer-content">
  913. <div class="w-full max-w-screen-xl m-auto">
  914. <aiGptInput
  915. v-if="
  916. ['gpt-4o-mini', 'gpt-3.5-turbo-16k'].indexOf(
  917. gptConfigStore.myData.model
  918. ) > -1 || st.inputme
  919. "
  920. v-model:modelValue="prompt"
  921. :disabled="buttonDisabled"
  922. :search-options="searchOptions"
  923. :render-option="renderOption"
  924. @handle-clear="handleClear"
  925. @export="handleExport"
  926. />
  927. <div v-else class="flex items-center justify-between space-x-2">
  928. <!--
  929. <HoverButton v-if="!isMobile" @click="handleClear">
  930. <span class="text-xl text-[#4f555e] dark:text-white">
  931. <SvgIcon icon="ri:delete-bin-line" />
  932. </span>
  933. </HoverButton>
  934. <HoverButton v-if="!isMobile" @click="handleExport">
  935. <span class="text-xl text-[#4f555e] dark:text-white">
  936. <SvgIcon icon="ri:download-2-line" />
  937. </span>
  938. </HoverButton>
  939. <HoverButton @click="toggleUsingContext">
  940. <span class="text-xl" :class="{ 'text-[#4b9e5f]': usingContext, 'text-[#a8071a]': !usingContext }">
  941. <SvgIcon icon="ri:chat-history-line" />
  942. </span>
  943. </HoverButton>
  944. -->
  945. <NAutoComplete
  946. v-model:value="prompt"
  947. :options="searchOptions"
  948. :render-label="renderOption"
  949. >
  950. <template #default="{ handleInput, handleBlur, handleFocus }">
  951. <NInput
  952. ref="inputRef"
  953. v-model:value="prompt"
  954. type="textarea"
  955. :placeholder="placeholder"
  956. :autosize="{ minRows: 1, maxRows: isMobile ? 4 : 8 }"
  957. @input="handleInput"
  958. @focus="handleFocus"
  959. @blur="handleBlur"
  960. @keypress="handleEnter"
  961. />
  962. </template>
  963. </NAutoComplete>
  964. <NButton
  965. type="primary"
  966. :disabled="buttonDisabled"
  967. @click="handleSubmit"
  968. >
  969. <template #icon>
  970. <span class="dark:text-black">
  971. <SvgIcon icon="ri:send-plane-fill" />
  972. </span>
  973. </template>
  974. </NButton>
  975. </div>
  976. </div>
  977. </footer>
  978. </div>
  979. <drawListVue />
  980. <aiGPT @finished="loading = false" />
  981. <AiSiderInput v-if="isMobile" :button-disabled="false" />
  982. </template>
  983. <style lang="less" scoped>
  984. .leftContent {
  985. background-color: #f9fafb;
  986. box-shadow: inset 0px -4px 15px 0px #e9eced;
  987. }
  988. .rightContent {
  989. .top {
  990. height: 50px;
  991. line-height: 50px;
  992. background-color: #fff;
  993. .tool {
  994. display: flex;
  995. align-items: center;
  996. justify-content: space-around;
  997. width: 60px;
  998. cursor: pointer;
  999. }
  1000. }
  1001. }
  1002. </style>