Merge branch 'master' of http://47.103.154.90:83/r/WI/Web.V1.0
| | |
| | | { |
| | | "name": "phm", |
| | | "version": "2.4.31", |
| | | "description": "vue3 vite next admin template", |
| | | "author": "lyt_20201208", |
| | | "license": "MIT", |
| | | "scripts": { |
| | | "use": "node ./scripts/use.js", |
| | | "dev": "node ./scripts/dev.js", |
| | | "deploy": "node ./scripts/deploy.js", |
| | | "publish": "node ./scripts/publish.js", |
| | | "preview": "vite preview", |
| | | "dev:yw": "vite --mode yw", |
| | | "serve": "vite", |
| | | "build": "node ./scripts/build.js", |
| | | "build:yw": "vite build --mode yw", |
| | | "lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/", |
| | | "fix-memory-limit": "cross-env LIMIT=8048 increase-memory-limit" |
| | | }, |
| | | "dependencies": { |
| | | "name": "phm", |
| | | "version": "2.4.31", |
| | | "description": "vue3 vite next admin template", |
| | | "author": "lyt_20201208", |
| | | "license": "MIT", |
| | | "scripts": { |
| | | "use": "node ./scripts/use.js", |
| | | "dev": "node ./scripts/dev.js", |
| | | "deploy": "node ./scripts/deploy.js", |
| | | "publish": "node ./scripts/publish.js", |
| | | "preview": "vite preview", |
| | | "dev:yw": "vite --mode yw", |
| | | "serve": "vite", |
| | | "build": "node ./scripts/build.js", |
| | | "build:yw": "vite build --mode yw", |
| | | "lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/", |
| | | "fix-memory-limit": "cross-env LIMIT=8048 increase-memory-limit" |
| | | }, |
| | | "dependencies": { |
| | | "@icon-park/vue-next": "^1.4.2", |
| | | "@amap/amap-jsapi-loader": "^1.0.1", |
| | | "@element-plus/icons-vue": "^2.1.0", |
| | | "@types/three": "^0.164.1", |
| | |
| | | "vue-grid-layout": "^3.0.0-beta1", |
| | | "vue-i18n": "^9.2.2", |
| | | "vue-router": "^4.1.6", |
| | | "xlsx": "^0.18.5" |
| | | "xlsx": "^0.18.5", |
| | | "@tailwindcss/typography": "^0.5.9", |
| | | "crypto-js": "^4.1.1", |
| | | "highlight.js": "^11.7.0", |
| | | "markdown-it": "^13.0.1" |
| | | }, |
| | | "devDependencies": { |
| | | "@types/node": "^18.15.0", |
| | |
| | | "vite-plugin-vue-setup-extend-plus": "^0.1.0", |
| | | "vue-eslint-parser": "^9.1.0" |
| | | }, |
| | | "browserslist": [ |
| | | "> 1%", |
| | | "last 2 versions", |
| | | "not dead" |
| | | ], |
| | | "bugs": { |
| | | "url": "https://gitee.com/lyt-top/vue-next-admin/issues" |
| | | }, |
| | | "engines": { |
| | | "node": ">=16.0.0", |
| | | "npm": ">= 7.0.0" |
| | | }, |
| | | "keywords": [ |
| | | "vue", |
| | | "vue3", |
| | | "vuejs/vue-next", |
| | | "element-ui", |
| | | "element-plus", |
| | | "vue-next-admin", |
| | | "next-admin" |
| | | ], |
| | | "repository": { |
| | | "type": "git", |
| | | "url": "https://gitee.com/lyt-top/vue-next-admin.git" |
| | | } |
| | | "browserslist": [ |
| | | "> 1%", |
| | | "last 2 versions", |
| | | "not dead" |
| | | ], |
| | | "bugs": { |
| | | "url": "https://gitee.com/lyt-top/vue-next-admin/issues" |
| | | }, |
| | | "engines": { |
| | | "node": ">=16.0.0", |
| | | "npm": ">= 7.0.0" |
| | | }, |
| | | "keywords": [ |
| | | "vue", |
| | | "vue3", |
| | | "vuejs/vue-next", |
| | | "element-ui", |
| | | "element-plus", |
| | | "vue-next-admin", |
| | | "next-admin" |
| | | ], |
| | | "repository": { |
| | | "type": "git", |
| | | "url": "https://gitee.com/lyt-top/vue-next-admin.git" |
| | | } |
| | | } |
¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="flex flex-col h-screen"> |
| | | <div class="flex flex-nowrap fixed w-full items-baseline top-0 px-6 py-4 bg-gray-100"> |
| | | <div class="text-2xl font-bold">ChatGPT</div> |
| | | <div class="ml-4 text-sm text-gray-500">åºäº OpenAI ç ChatGPT èªç¶è¯è¨æ¨¡å人工æºè½å¯¹è¯</div> |
| | | <div class="ml-auto px-3 py-2 text-sm cursor-pointer hover:bg-white rounded-md" @click="clickConfig()">设置</div> |
| | | </div> |
| | | |
| | | <div class="flex-1 mx-2 mt-20 mb-2" ref="chatListDom"> |
| | | <div |
| | | class="group flex flex-col px-4 py-3 hover:bg-slate-100 rounded-lg" |
| | | v-for="(item, index) of messageList.filter((v) => v.role !== 'system')" |
| | | :key="index" |
| | | > |
| | | <div class="flex justify-between items-center mb-2"> |
| | | <div class="font-bold">{{ roleAlias[item.role] }}ï¼</div> |
| | | <Copy class="invisible group-hover:visible" :content="item.content" /> |
| | | </div> |
| | | <div> |
| | | <div class="prose text-sm text-slate-600 leading-relaxed" v-if="item.content" v-html="md.render(item.content)"></div> |
| | | <Loding v-else /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="sticky bottom-0 w-full p-6 pb-8 bg-gray-100"> |
| | | <div class="-mt-2 mb-2 text-sm text-gray-500" v-if="isConfig">请è¾å
¥ API Keyï¼</div> |
| | | <div class="flex"> |
| | | <input |
| | | class="input" |
| | | :type="isConfig ? 'password' : 'text'" |
| | | :placeholder="isConfig ? 'sk-xxxxxxxxxx' : '请è¾å
¥'" |
| | | v-model="messageContent" |
| | | @keydown.enter="isTalking || sendOrSave()" |
| | | /> |
| | | <button class="btn" :disabled="isTalking" @click="sendOrSave()"> |
| | | {{ isConfig ? 'ä¿å' : 'åé' }} |
| | | </button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import type { ChatMessage } from './types'; |
| | | import { ref, watch, nextTick, onMounted } from 'vue'; |
| | | import cryptoJS from 'crypto-js'; |
| | | import Loding from './components/Loding.vue'; |
| | | import Copy from './components/Copy.vue'; |
| | | import { md } from './libs/markdown'; |
| | | let apiKey = ''; |
| | | let isConfig = ref(false); |
| | | let isTalking = ref(false); |
| | | let messageContent = ref(''); |
| | | const chatListDom = ref<HTMLDivElement>(); |
| | | const decoder = new TextDecoder('utf-8'); |
| | | const roleAlias = { user: 'ME', assistant: 'ChatGPT', system: 'System' }; |
| | | const messageList = ref<ChatMessage[]>([ |
| | | { |
| | | role: 'system', |
| | | content: 'ä½ æ¯ ChatGPTï¼OpenAI è®ç»ç大åè¯è¨æ¨¡åï¼å°½å¯è½ç®æ´å°åçã', |
| | | }, |
| | | { |
| | | role: 'assistant', |
| | | content: `ä½ å¥½ï¼ææ¯AIè¯è¨æ¨¡åï¼æå¯ä»¥æä¾ä¸äºå¸¸ç¨æå¡åä¿¡æ¯ï¼ä¾å¦ï¼ |
| | | |
| | | 1. ç¿»è¯ï¼æå¯ä»¥æä¸æç¿»è¯æè±æï¼è±æç¿»è¯æä¸æï¼è¿æå
¶ä»ä¸äºè¯è¨ç¿»è¯ï¼æ¯å¦æ³è¯ãæ¥è¯ã西ççè¯çã |
| | | |
| | | 2. å¨è¯¢æå¡ï¼å¦æä½ æä»»ä½é®é¢éè¦å¨è¯¢ï¼ä¾å¦å¥åº·ãæ³å¾ãæèµçæ¹é¢ï¼æå¯ä»¥å°½å¯è½ä¸ºä½ æä¾å¸®å©ã |
| | | |
| | | 3. é²èï¼å¦æä½ æå°å¯å¯ææ èï¼æä»¬å¯ä»¥èä¸äºæè¶£çè¯é¢ï¼ä»¥åè½»ä½ çååã |
| | | |
| | | 请åè¯æä½ éè¦åªæ¹é¢ç帮å©ï¼æä¼æ ¹æ®ä½ çéæ±ç»ä½ æä¾ç¸åºçä¿¡æ¯å建议ã`, |
| | | }, |
| | | ]); |
| | | |
| | | onMounted(() => { |
| | | if (getAPIKey()) { |
| | | switchConfigStatus(); |
| | | } |
| | | }); |
| | | |
| | | const sendChatMessage = async (content: string = messageContent.value) => { |
| | | try { |
| | | isTalking.value = true; |
| | | if (messageList.value.length === 2) { |
| | | messageList.value.pop(); |
| | | } |
| | | messageList.value.push({ role: 'user', content }); |
| | | clearMessageContent(); |
| | | messageList.value.push({ role: 'assistant', content: '' }); |
| | | |
| | | // const { body, status } = await chat(messageList.value, getAPIKey()); |
| | | // if (body) { |
| | | // const reader = body.getReader(); |
| | | // await readStream(reader, status); |
| | | // } |
| | | const a = new Promise<string>((resolve) => { |
| | | setTimeout(() => { |
| | | resolve('nihao '); |
| | | }, 500); |
| | | }); |
| | | |
| | | const msg = await a; |
| | | appendLastMessageContent(msg); |
| | | } catch (error: any) { |
| | | appendLastMessageContent(error); |
| | | } finally { |
| | | isTalking.value = false; |
| | | } |
| | | }; |
| | | |
| | | const readStream = async (reader: ReadableStreamDefaultReader<Uint8Array>, status: number) => { |
| | | let partialLine = ''; |
| | | |
| | | while (true) { |
| | | // eslint-disable-next-line no-await-in-loop |
| | | const { value, done } = await reader.read(); |
| | | if (done) break; |
| | | |
| | | const decodedText = decoder.decode(value, { stream: true }); |
| | | |
| | | if (status !== 200) { |
| | | const json = JSON.parse(decodedText); // start with "data: " |
| | | const content = json.error.message ?? decodedText; |
| | | appendLastMessageContent(content); |
| | | return; |
| | | } |
| | | |
| | | const chunk = partialLine + decodedText; |
| | | const newLines = chunk.split(/\r?\n/); |
| | | |
| | | partialLine = newLines.pop() ?? ''; |
| | | |
| | | for (const line of newLines) { |
| | | if (line.length === 0) continue; // ignore empty message |
| | | if (line.startsWith(':')) continue; // ignore sse comment message |
| | | if (line === 'data: [DONE]') return; // |
| | | |
| | | const json = JSON.parse(line.substring(6)); // start with "data: " |
| | | const content = status === 200 ? json.choices[0].delta.content ?? '' : json.error.message; |
| | | appendLastMessageContent(content); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | const appendLastMessageContent = (content: string) => (messageList.value[messageList.value.length - 1].content += content); |
| | | |
| | | const sendOrSave = () => { |
| | | if (!messageContent.value.length) return; |
| | | if (isConfig.value) { |
| | | if (saveAPIKey(messageContent.value.trim())) { |
| | | switchConfigStatus(); |
| | | } |
| | | clearMessageContent(); |
| | | } else { |
| | | sendChatMessage(); |
| | | } |
| | | }; |
| | | |
| | | const clickConfig = () => { |
| | | if (!isConfig.value) { |
| | | messageContent.value = getAPIKey(); |
| | | } else { |
| | | clearMessageContent(); |
| | | } |
| | | switchConfigStatus(); |
| | | }; |
| | | |
| | | const getSecretKey = () => 'lianginx'; |
| | | |
| | | const saveAPIKey = (apiKey: string) => { |
| | | if (apiKey.slice(0, 3) !== 'sk-' || apiKey.length !== 51) { |
| | | alert('API Key é误ï¼è¯·æ£æ¥åéæ°è¾å
¥ï¼'); |
| | | return false; |
| | | } |
| | | const aesAPIKey = cryptoJS.AES.encrypt(apiKey, getSecretKey()).toString(); |
| | | localStorage.setItem('apiKey', aesAPIKey); |
| | | return true; |
| | | }; |
| | | |
| | | const getAPIKey = () => { |
| | | if (apiKey) return apiKey; |
| | | const aesAPIKey = localStorage.getItem('apiKey') ?? ''; |
| | | apiKey = cryptoJS.AES.decrypt(aesAPIKey, getSecretKey()).toString(cryptoJS.enc.Utf8); |
| | | return apiKey; |
| | | }; |
| | | |
| | | const switchConfigStatus = () => (isConfig.value = !isConfig.value); |
| | | |
| | | const clearMessageContent = () => (messageContent.value = ''); |
| | | |
| | | const scrollToBottom = () => { |
| | | if (!chatListDom.value) return; |
| | | scrollTo(0, chatListDom.value.scrollHeight); |
| | | }; |
| | | |
| | | watch(messageList.value, () => nextTick(() => scrollToBottom())); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | pre { |
| | | font-family: -apple-system, 'Noto Sans', 'Helvetica Neue', Helvetica, 'Nimbus Sans L', Arial, 'Liberation Sans', 'PingFang SC', |
| | | 'Hiragino Sans GB', 'Noto Sans CJK SC', 'Source Han Sans SC', 'Source Han Sans CN', 'Microsoft YaHei', 'Wenquanyi Micro Hei', |
| | | 'WenQuanYi Zen Hei', 'ST Heiti', SimHei, 'WenQuanYi Zen Hei Sharp', sans-serif; |
| | | } |
| | | </style> |
¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import { Copy, Loading, CheckOne } from "@icon-park/vue-next"; |
| | | import type { Theme } from "@icon-park/vue-next/lib/runtime"; |
| | | import { ref } from "vue"; |
| | | |
| | | const porps = defineProps<{ content: string }>(); |
| | | const btnConfig: { |
| | | size: number; |
| | | fill: string; |
| | | theme: Theme; |
| | | } = { |
| | | size: 14, |
| | | fill: "#999", |
| | | theme: "outline", |
| | | }; |
| | | const btnTips = { |
| | | copy: "å¤å¶å
¨æ", |
| | | loading: "", |
| | | success: "å·²å¤å¶å°åªè´´æ¿ï¼", |
| | | error: "å¤å¶å¤±è´¥ï¼", |
| | | }; |
| | | const btnStatus = ref<"copy" | "loading" | "success" | "error">("copy"); |
| | | |
| | | const copyToClipboard = (content: string = porps.content) => { |
| | | btnStatus.value = "loading"; |
| | | navigator.clipboard |
| | | .writeText(content) |
| | | .then(() => setTimeout(() => (btnStatus.value = "success"), 150)) |
| | | .catch(() => (btnStatus.value = "error")) |
| | | .finally(() => setTimeout(() => (btnStatus.value = "copy"), 1500)); |
| | | }; |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="flex items-center cursor-pointer" @click="copyToClipboard()"> |
| | | <copy |
| | | v-show="btnStatus === 'copy'" |
| | | :theme="btnConfig.theme" |
| | | :size="btnConfig.size" |
| | | :fill="btnConfig.fill" |
| | | /> |
| | | <loading |
| | | class="rotate" |
| | | v-show="btnStatus === 'loading'" |
| | | :theme="btnConfig.theme" |
| | | :size="btnConfig.size" |
| | | :fill="btnConfig.fill" |
| | | /> |
| | | <check-one |
| | | v-show="btnStatus === 'success'" |
| | | :theme="btnConfig.theme" |
| | | :size="btnConfig.size" |
| | | :fill="btnConfig.fill" |
| | | /> |
| | | <close-one |
| | | v-show="btnStatus === 'error'" |
| | | :theme="btnConfig.theme" |
| | | :size="btnConfig.size" |
| | | :fill="btnConfig.fill" |
| | | /> |
| | | <span class="text-xs ml-0.5 text-gray-500 leading-none">{{ |
| | | btnTips[btnStatus] |
| | | }}</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | @keyframes spin { |
| | | 0% { |
| | | transform: rotate(0deg); |
| | | } |
| | | 100% { |
| | | transform: rotate(360deg); |
| | | } |
| | | } |
| | | |
| | | .rotate { |
| | | animation: spin 2s linear infinite; |
| | | } |
| | | </style> |
¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="com__box"> |
| | | <div class="loading"> |
| | | <div></div> |
| | | <div></div> |
| | | <div></div> |
| | | <div></div> |
| | | <div></div> |
| | | <div></div> |
| | | <div></div> |
| | | <div></div> |
| | | <div></div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .loading, |
| | | .loading > div { |
| | | position: relative; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .loading { |
| | | display: block; |
| | | font-size: 0; |
| | | color: #000; |
| | | } |
| | | |
| | | .loading.la-dark { |
| | | color: #333; |
| | | } |
| | | |
| | | .loading > div { |
| | | display: inline-block; |
| | | float: none; |
| | | background-color: currentColor; |
| | | border: 0 solid currentColor; |
| | | } |
| | | |
| | | .loading { |
| | | width: 17px; |
| | | height: 17px; |
| | | } |
| | | |
| | | .loading > div { |
| | | width: 3px; |
| | | height: 3px; |
| | | margin: 1px; |
| | | border-radius: 100%; |
| | | animation-name: ball-grid-beat; |
| | | animation-iteration-count: infinite; |
| | | } |
| | | |
| | | .loading > div:nth-child(1) { |
| | | animation-duration: 0.65s; |
| | | animation-delay: 0.03s; |
| | | } |
| | | |
| | | .loading > div:nth-child(2) { |
| | | animation-duration: 1.02s; |
| | | animation-delay: 0.09s; |
| | | } |
| | | |
| | | .loading > div:nth-child(3) { |
| | | animation-duration: 1.06s; |
| | | animation-delay: -0.69s; |
| | | } |
| | | |
| | | .loading > div:nth-child(4) { |
| | | animation-duration: 1.5s; |
| | | animation-delay: -0.41s; |
| | | } |
| | | |
| | | .loading > div:nth-child(5) { |
| | | animation-duration: 1.6s; |
| | | animation-delay: 0.04s; |
| | | } |
| | | |
| | | .loading > div:nth-child(6) { |
| | | animation-duration: 0.84s; |
| | | animation-delay: 0.07s; |
| | | } |
| | | |
| | | .loading > div:nth-child(7) { |
| | | animation-duration: 0.68s; |
| | | animation-delay: -0.66s; |
| | | } |
| | | |
| | | .loading > div:nth-child(8) { |
| | | animation-duration: 0.93s; |
| | | animation-delay: -0.76s; |
| | | } |
| | | |
| | | .loading > div:nth-child(9) { |
| | | animation-duration: 1.24s; |
| | | animation-delay: -0.76s; |
| | | } |
| | | |
| | | @keyframes ball-grid-beat { |
| | | 0% { |
| | | opacity: 1; |
| | | } |
| | | |
| | | 50% { |
| | | opacity: 0.35; |
| | | } |
| | | |
| | | 100% { |
| | | opacity: 1; |
| | | } |
| | | } |
| | | </style> |
¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { ChatMessage } from '../types'; |
| | | |
| | | export async function chat(messageList: ChatMessage[], apiKey: string) { |
| | | const result = await fetch('https://api.openai.com/v1/chat/completions', { |
| | | method: 'post', |
| | | // signal: AbortSignal.timeout(8000), |
| | | // å¼å¯åå°è¾¾è®¾å®æ¶é´ä¼ä¸ææµå¼è¾åº |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | Authorization: `Bearer ${apiKey}`, |
| | | }, |
| | | body: JSON.stringify({ |
| | | model: 'gpt-3.5-turbo', |
| | | stream: true, |
| | | messages: messageList, |
| | | }), |
| | | }).catch(error=>{ |
| | | throw error |
| | | }); |
| | | return result; |
| | | } |
¶Ô±ÈÐÂÎļþ |
| | |
| | | import highlight from 'highlight.js'; |
| | | import Markdown from 'markdown-it'; |
| | | |
| | | const mdOptions: Markdown.Options = { |
| | | linkify: true, |
| | | typographer: true, |
| | | breaks: true, |
| | | langPrefix: 'language-', |
| | | // 代ç é«äº® |
| | | highlight(str, lang) { |
| | | if (lang && highlight.getLanguage(lang)) { |
| | | try { |
| | | return '<pre class="hljs"><code>' + highlight.highlight(lang, str, true).value + '</code></pre>'; |
| | | } catch (__) { |
| | | } |
| | | } |
| | | return ''; |
| | | }, |
| | | }; |
| | | |
| | | export const md = new Markdown(mdOptions); |
¶Ô±ÈÐÂÎļþ |
| | |
| | | export interface ChatMessage { |
| | | role: "user" | "assistant" | "system"; |
| | | content: string; |
| | | } |
| | | |
| | |
| | | <div class="pc-chatRoom w100 h100"> |
| | | <div class="homeBox w100 h100"> |
| | | <div class="flex items-center flex-column mt-20"> |
| | | <waterTop /> |
| | | <waterCenter /> |
| | | <waterBottom /> |
| | | <div class="flex items-center flex-column" v-if="!isShowChat"> |
| | | <waterTop @sendClick="sendClick"/> |
| | | <waterCenter /> |
| | | <waterBottom /> |
| | | </div> |
| | | <div v-if="isShowChat"> |
| | | <Chat /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <p class="declare"> |
| | |
| | | </template> |
| | | |
| | | <script setup lang="ts"> |
| | | import { ref } from 'vue'; |
| | | import asideNew from './component/waterLeftAside/asideNew.vue'; |
| | | import asideTitle from './component/waterLeftAside/asideTitle.vue'; |
| | | import waterBottom from './component/waterRight/bottom.vue'; |
| | | import waterCenter from './component/waterRight/center.vue'; |
| | | import waterHeader from './component/waterRight/header.vue'; |
| | | import waterTop from './component/waterRight/top.vue'; |
| | | import Chat from '/@/components/chat/Chat.vue'; |
| | | |
| | | const isShowChat = ref(true); |
| | | |
| | | const sendClick = () => { |
| | | isShowChat.value =true; |
| | | }; |
| | | </script> |
| | | <style scoped lang="scss"> |
| | | .pc-chat_room { |
| | |
| | | <img src="/static/images/wave/HeadImg.png" class="set-img-icon box-border" /> |
| | | </el-button> |
| | | |
| | | <el-button title="AIçå¾" class="cursor-pointer" link> |
| | | <el-button title="åé" class="cursor-pointer" link @click="sendClick"> |
| | | <div class="send"> |
| | | <img src="/static/images/wave/QueryImg.png" /> |
| | | </div> |
| | |
| | | |
| | | <script setup lang="ts"> |
| | | import { reactive } from 'vue'; |
| | | const emits = defineEmits(['sendClick']); |
| | | |
| | | const sendClick = () => { |
| | | emits('sendClick'); |
| | | }; |
| | | let state = reactive({ |
| | | roleList: [ |
| | | { |
| | |
| | | preflight: false, |
| | | }, |
| | | theme: {}, |
| | | plugins: [], |
| | | plugins: [require("@tailwindcss/typography")], |
| | | }; |