master
ihzero 2023-08-17 03:10:38 +08:00
parent fab450f6ec
commit 4a22a05e40
40 changed files with 4708 additions and 2608 deletions

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="zh-cn">
<html lang="zh-cn" data-theme="dark" class="dark">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">

View File

@ -19,10 +19,12 @@
"markdown-it": "^13.0.1",
"markdown-it-link-attributes": "^4.0.1",
"md5": "^2.3.0",
"mitt": "^3.0.1",
"pinia": "^2.1.3",
"qrcode.vue": "^3.4.1",
"resize-observer-polyfill": "^1.5.1",
"swiper": "^10.1.0",
"uuid": "^9.0.0",
"vant": "^4.6.4",
"vue": "^3.3.4",
"vue-router": "^4.2.2"

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

View File

@ -67,7 +67,26 @@ export default defineComponent({
})
}
function scrollToBottomIfAtBottom(){
const scrollbar = unref(scrollbarRef)
if (!scrollbar) {
return
}
nextTick(() => {
const wrap = unref(scrollbar.wrap)
if (!wrap) {
return
}
const threshold = 100
const distanceToBottom = wrap.scrollHeight - wrap.scrollTop - wrap.clientHeight
if (distanceToBottom <= threshold){
scrollBottom()
}
})
}
return {
scrollToBottomIfAtBottom,
scrollbarRef,
scrollTo,
scrollBottom,

View File

@ -36,7 +36,7 @@ export default defineComponent({
.svg-icon {
width: 1em;
height: 1em;
display: inline-block;
// display: inline-block;
overflow: hidden;
fill: currentcolor;
vertical-align: -0.15em;

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1692201855353" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2233" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M643.789 573.133V236.237c0-33.895-27.546-61.44-61.44-61.44H180.53c-33.894 0-61.44 27.545-61.44 61.44v336.998c0 33.895 27.546 61.44 61.44 61.44H230.4v54.887c0 11.57 6.451 22.118 16.794 27.34 4.403 2.253 9.216 3.38 13.926 3.38 6.349 0 12.698-1.946 18.022-5.837l109.978-79.872h193.229c33.894 0 61.44-27.546 61.44-61.44z" p-id="2234"></path><path d="M843.469 308.94H678.912v313.754c0 22.63-18.33 40.96-40.96 40.96H461.619v40.653c0 33.895 27.546 61.44 61.44 61.44h92.058l109.568 77.824c5.325 3.789 11.571 5.735 17.817 5.735 5.325 0 10.65-1.332 15.463-4.199 9.625-5.53 15.257-16.077 15.257-27.136v-47.82h70.247c33.894 0 61.44-27.546 61.44-61.44V370.38c0-33.895-27.546-61.44-61.44-61.44z" p-id="2235"></path></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1692200947773" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1961" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M626.899888 1023.999H397.100112c-16.299984 0-29.499971-13.099987-29.599971-29.399971v-72.999929h-24.699976c-16.299984 0-29.499971-13.099987-29.599971-29.399971V789.799229c0.1-16.299984 13.299987-29.499971 29.599971-29.399972h4.699996v-33.199967c-62.599939-29.498971-116.099887-75.398926-154.699849-132.89887-102.5989-151.199852-81.49892-354.298654 49.899951-481.19753 69.899932-70.599931 164.499839-111.099892 263.799742-113.09989h5.499995c100.899901-0.4 197.799807 39.199962 269.699737 109.999893 128.598874 125.999877 151.498852 324.798683 54.898946 476.697534-38.999962 60.99994-94.399908 109.799893-159.999844 140.499863v33.199967h4.699996c16.299984 0 29.499971 13.099987 29.599971 29.399972v102.3999c-0.1 16.299984-13.299987 29.499971-29.599971 29.399971h-24.799976v72.999929c-0.1 16.298984-13.299987 29.398971-29.599971 29.398971zM426.600083 921.5991v43.599957h170.699834v-43.599957H426.600083z m-54.299947-102.3999v43.599957h279.399728v-43.599957H372.300136zM512.1 58.803943h-4.899995c-170.499833 2.399998-317.09969 147.699856-320.098688 317.29869-2.799997 133.49987 77.299925 254.799751 201.099804 304.798702 11.099989 4.499996 18.299982 15.299985 18.299982 27.199973v52.399949h75.799926V602.199412l-99.799903-137.499866c-9.499991-13.099987-6.599994-31.399969 6.499994-40.89996 0.1 0 0.1-0.1 0.2-0.1 13.199987-9.499991 31.699969-6.499994 41.29996 6.599994L512 542.50147l81.399921-112.19989c9.599991-13.199987 27.999973-16.099984 41.299959-6.599994 13.199987 9.399991 16.199984 27.699973 6.799994 40.89996 0 0.1-0.1 0.1-0.1 0.2l-99.799903 137.499866v158.199845h75.899926v-52.399949c0-11.899988 7.199993-22.699978 18.299982-27.199973 165.099839-67.699934 244.098762-256.39975 176.399828-421.498588-49.999951-121.699881-168.599835-200.999804-300.099707-200.599804z m157.199846 346.598661c-1.899998 0-3.899996-0.2-5.799994-0.599999-1.899998-0.4-3.699996-0.899999-5.499995-1.599999-1.799998-0.699999-3.499997-1.599998-5.099995-2.699997-1.599998-1.099999-3.099997-2.299998-4.499995-3.699997-11.499989-11.399989-11.599989-29.899971-0.2-41.399959l0.2-0.2c1.399999-1.399999 2.899997-2.599997 4.499995-3.599996 1.599998-1.099999 3.299997-1.999998 5.099995-2.699998 1.799998-0.699999 3.599996-1.299999 5.499995-1.699998 1.899998-0.4 3.799996-0.599999 5.699994-0.599999 7.899992 0 15.399985 3.099997 20.99998 8.599991 1.399999 1.399999 2.599997 2.899997 3.699996 4.499996 3.199997 4.799995 4.999995 10.49999 4.999995 16.299984 0 7.799992-3.099997 15.299985-8.699991 20.799979-1.399999 1.399999-2.899997 2.599997-4.499996 3.699997-3.199997 2.099998-6.799993 3.599996-10.599989 4.399996-1.999998 0.3-3.899996 0.5-5.799995 0.499999z m-314.599692 0c-1.899998 0-3.899996-0.2-5.799995-0.599999-1.899998-0.4-3.699996-0.899999-5.499994-1.599999-10.999989-4.499996-18.199982-15.199985-18.299982-27.199973 0-1.899998 0.2-3.799996 0.599999-5.699995 0.4-1.899998 0.899999-3.699996 1.699998-5.499994 0.699999-1.799998 1.599998-3.499997 2.699998-5.099995 2.099998-3.199997 4.899995-5.999994 8.199992-8.099992 1.599998-1.099999 3.299997-1.999998 5.099995-2.699998 1.799998-0.699999 3.599996-1.299999 5.499994-1.699998 3.799996-0.799999 7.699992-0.799999 11.599989 0 1.899998 0.4 3.699996 0.899999 5.499995 1.699998 1.799998 0.699999 3.499997 1.599998 5.099995 2.699998 1.599998 1.099999 3.099997 2.299998 4.499995 3.599996 4.099996 4.099996 6.899993 9.299991 8.099992 14.999985 0.4 1.899998 0.599999 3.799996 0.6 5.699995 0 11.899988-7.199993 22.699978-18.199983 27.199973-1.799998 0.699999-3.599996 1.299999-5.499994 1.599999-1.999998 0.5-3.899996 0.699999-5.899994 0.699999z" p-id="1962"></path></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1692202471749" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2370" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M842.947 778.117l-266.1-266.104 266.1-266.13c8.675-8.674 13.447-20.208 13.439-32.478-0.009-12.232-4.773-23.715-13.415-32.332-8.655-8.678-20.15-13.45-32.385-13.457-12.286 0-23.808 4.771-32.474 13.434L512.019 447.144 245.882 181.05c-8.663-8.663-20.175-13.434-32.416-13.434-12.24 0-23.752 4.77-32.414 13.432-8.66 8.637-13.43 20.125-13.438 32.357-0.008 12.27 4.764 23.804 13.438 32.477l266.135 266.13L181.05 778.118c-8.664 8.663-13.436 20.173-13.436 32.415 0 12.24 4.773 23.753 13.438 32.418 8.662 8.663 20.173 13.432 32.413 13.432 12.24 0 23.754-4.77 32.416-13.432L512.015 576.85l266.102 266.1c8.663 8.664 20.186 13.433 32.443 13.433 12.265-0.008 23.749-4.771 32.369-13.412 17.887-17.89 17.893-46.98 0.018-64.854z" p-id="2371"></path></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1692200956313" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2097" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M848.38 225.06c-21.61-77.78-92.89-135.1-177.48-135.1H210.33C108.69 89.96 26 172.65 26 274.29v337.28c0 94.1 70.88 171.8 162.02 182.87 21.77 78.35 93.12 134.22 177.39 134.22h448.07C915.22 928.66 998 846 998 744.38V405.92c0-88.98-64.85-164.47-149.62-180.86zM99.42 611.57V274.29c0-61.16 49.76-110.91 110.91-110.91H670.9c61.18 0 110.94 49.76 110.94 110.91v337.28c0 61.18-49.76 110.94-110.94 110.94H210.33c-61.16-0.01-110.91-49.76-110.91-110.94z m825.16 132.8c0 61.13-49.85 110.87-111.1 110.87H365.41c-42.65 0-79.76-23.77-98.28-59.32H670.9c101.66 0 184.35-82.69 184.35-184.35V303.44c40.44 16.61 69.33 56.45 69.33 102.48v338.45z" p-id="2098"></path></svg>

After

Width:  |  Height:  |  Size: 981 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1692202869664" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3477" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 268.190476c134.656 0 243.809524 109.153524 243.809524 243.809524h-73.142857a170.666667 170.666667 0 1 0-176.518096 170.569143L512 682.666667c154.624 0 285.257143 102.814476 327.192381 243.809523h-77.287619c-39.009524-99.888762-136.192-170.666667-249.904762-170.666666s-210.895238 70.777905-249.904762 170.666666H184.807619a342.25981 342.25981 0 0 1 188.074667-214.259809A243.370667 243.370667 0 0 1 268.190476 512c0-134.656 109.153524-243.809524 243.809524-243.809524z m0-170.666666c180.077714 0 327.582476 139.459048 340.431238 316.245333A121.904762 121.904762 0 0 1 780.190476 633.904762l-158.47619 0.024381a60.952381 60.952381 0 1 1 0-73.142857l158.47619-0.024381a48.761905 48.761905 0 1 0 0-97.52381v-24.380952c0-148.114286-120.07619-268.190476-268.190476-268.190476s-268.190476 120.07619-268.190476 268.190476v24.380952a48.761905 48.761905 0 1 0 0 97.52381v73.142857a121.904762 121.904762 0 0 1-72.240762-220.111238C184.417524 236.982857 331.922286 97.52381 512 97.52381z" p-id="3478"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1692209388226" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6273" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M469.333333 469.333333V170.666667h85.333334v298.666666h298.666666v85.333334h-298.666666v298.666666h-85.333334v-298.666666H170.666667v-85.333334h298.666666z" fill="#444444" p-id="6274"></path></svg>

After

Width:  |  Height:  |  Size: 530 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1691809983739" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4003" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M892.064 261.888a31.936 31.936 0 0 0-45.216 1.472L421.664 717.248l-220.448-185.216a32 32 0 1 0-41.152 48.992l243.648 204.704a31.872 31.872 0 0 0 20.576 7.488 31.808 31.808 0 0 0 23.36-10.112L893.536 307.136a32 32 0 0 0-1.472-45.248z" p-id="4004"></path></svg>

After

Width:  |  Height:  |  Size: 592 B

View File

@ -0,0 +1,9 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg t="1692202905353" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="5313" xmlns:xlink="http://www.w3.org/1999/xlink"
width="200" height="200">
<path
d="M522.44898 104.489796c230.838857 0 417.959184 187.120327 417.959183 417.959184s-187.120327 417.959184-417.959183 417.959183S104.489796 753.287837 104.489796 522.44898 291.610122 104.489796 522.44898 104.489796z m0 83.591837C337.773714 188.081633 188.081633 337.773714 188.081633 522.44898s149.692082 334.367347 334.367347 334.367347 334.367347-149.692082 334.367347-334.367347S707.124245 188.081633 522.44898 188.081633z m-10.574368 445.314612c10.574367 0 19.079837 0.585143 25.537306 1.755428 6.478367 1.170286 11.452082 3.531755 14.983837 7.042613 3.510857 3.531755 5.955918 8.526367 7.314286 14.983836 1.379265 6.457469 2.068898 14.774857 2.068898 24.931266 0 10.574367-0.689633 19.079837-2.048 25.537306-1.379265 6.478367-3.824327 11.452082-7.335184 14.983837-3.531755 3.510857-8.526367 5.851429-14.983837 7.042612a148.396408 148.396408 0 0 1-25.537306 1.755428c-10.553469 0-19.079837-0.585143-25.537306-1.755428-6.457469-1.170286-11.535673-3.531755-15.25551-7.042612-3.719837-3.531755-6.164898-8.526367-7.335184-14.983837a148.396408 148.396408 0 0 1-1.755428-25.537306c0-10.156408 0.585143-18.473796 1.755428-24.931266 1.170286-6.478367 3.615347-11.452082 7.335184-14.983836 3.719837-3.510857 8.798041-5.851429 15.25551-7.042613a148.396408 148.396408 0 0 1 25.537306-1.755428zM506.608327 313.469388c23.865469 0 44.408163 1.567347 61.628081 4.702041 17.219918 3.134694 31.409633 8.609959 42.569143 16.425795 11.138612 7.836735 19.267918 18.494694 24.346122 31.994776 5.099102 13.500082 7.627755 30.427429 7.627756 50.782041 0 13.688163-0.877714 25.432816-2.633143 35.213061a95.252898 95.252898 0 0 1-8.798041 26.707592c-4.116898 8.024816-9.613061 15.46449-16.446694 22.31902-6.854531 6.854531-15.15102 14.001633-24.952163 21.420408-8.986122 6.645551-16.237714 12.538776-21.71298 17.61698a113.078857 113.078857 0 0 0-12.914939 13.792653c-3.134694 4.116898-5.287184 8.212898-6.457469 12.329796a50.176 50.176 0 0 0-1.755429 13.792653v13.500082h-71.617306v-26.415021l0.125388-6.03951c0.250776-5.914122 0.898612-11.431184 1.922612-16.572082 1.379265-6.833633 3.824327-13.374694 7.335184-19.644081 3.531755-6.269388 8.233796-12.538776 14.085224-18.808163 5.872327-6.269388 13.123918-13.10302 21.733878-20.542694 6.269388-5.078204 11.431184-9.780245 15.548082-14.085225a66.037551 66.037551 0 0 0 9.696653-12.622367 39.497143 39.497143 0 0 0 4.681143-13.20751 101.146122 101.146122 0 0 0 1.170285-16.425796c0-8.986122-0.961306-16.237714-2.925714-21.733878a24.868571 24.868571 0 0 0-10.281796-12.914939c-4.890122-3.134694-11.347592-5.266286-19.35151-6.436571a209.606531 209.606531 0 0 0-29.654204-1.776327 271.673469 271.673469 0 0 0-37.281959 2.654041c-12.705959 1.755429-24.158041 4.20049-34.335347 7.314286v-72.766694l5.642449-1.525551a291.317551 291.317551 0 0 1 33.68751-6.123102c15.25551-1.94351 31.702204-2.925714 49.319184-2.925714z"
fill="currentColor" p-id="5314"></path>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1692211019161" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1961" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M917 467c-24.9 0-45 20.1-45 45v300c0 33.1-26.9 60-60 60H212c-33.1 0-60-26.9-60-60V212c0-33.1 26.9-60 60-60h300c24.9 0 45-20.1 45-45s-20.1-45-45-45H182c-66.3 0-120 53.7-120 120v660c0 66.3 53.7 120 120 120h660c66.3 0 120-53.7 120-120V512c0-24.9-20.1-45-45-45z" p-id="1962"></path><path d="M356.2 682.8c17.5 17.5 46.1 17.5 63.6 0l487.9-487.9c17.5-17.5 17.5-46.1 0-63.6s-46.1-17.5-63.6 0L356.2 619.1c-17.5 17.5-17.5 46.2 0 63.7z" p-id="1963"></path></svg>

After

Width:  |  Height:  |  Size: 784 B

View File

@ -3,7 +3,7 @@
<Header />
<div class="layoutContent">
<Content />
<Footer />
<Footer v-if="isFooter" />
</div>
</div>
</template>
@ -12,6 +12,16 @@
import Header from './Header.vue';
import Content from './Content.vue';
import Footer from './Footer.vue';
import { useRouter } from 'vue-router';
import { watch,ref } from 'vue'
const router = useRouter();
const isFooter = ref(true);
watch(router.currentRoute, (to) => {
isFooter.value = to.meta?.footer ?? true;
},{
immediate: true
})
</script>

View File

@ -3,7 +3,7 @@ import 'virtual:svg-icons-register';
import 'virtual:windi.css'
import "@/assets/css/iconfont.css";
import mitt from "mitt";
import { createApp } from "vue";
import { createPinia } from "pinia";
@ -12,7 +12,7 @@ import router from "./router";
import { registerSvgIcon } from './icons/index.js'
import httpConfig from "./io/httpConfig";
import 'vant/es/toast/style';
import 'vant/es/dialog/style';
import 'amfe-flexible'
function platCheck() {
@ -25,7 +25,7 @@ function platCheck() {
}
platCheck();
const mitter = mitt();
const app = createApp(App);
registerSvgIcon(app);
@ -35,3 +35,4 @@ app.use(router);
httpConfig();
app.mount("#app");
app.config.globalProperties.$mitt = mitter;

View File

@ -1,5 +1,6 @@
import { createRouter, createWebHistory } from "vue-router";
import Layout from '@/layouts/index.vue';
import ChatLayout from '@/views/chat/layout.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -18,7 +19,26 @@ const router = createRouter({
},
component: () => import("@/views/home/index.vue"),
},
{
{
path: "chat-layout",
name: "ChatLayout",
meta: {
title: "AI助理布局",
},
redirect :'/chat',
component:ChatLayout,
children: [{
path: '/chat/:uuid?',
name: 'Chat',
component: () => import("@/views/chat/index.vue"),
meta: {
title: 'AI助理',
footer: false,
}
}]
},
{
path: "insights",
name: "Insights",
meta: {

View File

@ -0,0 +1,39 @@
import { defineStore } from 'pinia'
export const useAiChat = defineStore('aichat-store', {
state: () => ({
history: [],
}),
getters: {
getHistoryByUuid: (state) => (uuid) => {
return state.history.find((item) => item.uuid === uuid)?.data ?? []
}
},
actions: {
addHistory(uuid, chat) {
const index = this.history.findIndex((item) => item.uuid === uuid)
if (index >= 0) {
this.history[index].data.push(chat)
} else {
this.history.push({
uuid,
data: [chat],
})
}
},
updateChatByUuid(uuid, index, chat) {
const historyIndex = this.history.findIndex((item) => item.uuid === uuid)
if (historyIndex >= 0) {
this.history[historyIndex].data[index] = chat
}
},
updateChatSomeByUuid(uuid, index, chat) {
const historyIndex = this.history.findIndex((item) => item.uuid === uuid)
if (historyIndex >= 0) {
this.history[historyIndex].data[index] = {
...this.history[historyIndex].data[index], ...chat
}
}
}
},
})

153
src/stores/chat.js 100644
View File

@ -0,0 +1,153 @@
import { defineStore } from "pinia";
import router from '@/router'
import http from '@/io/request'
import { v4 as uuidv4 } from 'uuid'
export const useChat = defineStore("chat-store", {
state: () => ({
active: null,
history: [],
chat: [],
templates:[]
}),
getters: {
getCurrentChat() {
return this.chat.find(item => item.id === this.active)?.messages ?? []
},
},
actions: {
deleteConversation(uuid) {
this.history = this.history.filter(item => item.id !== uuid)
if (uuid === this.active) {
this.setActive(null)
}
},
setChat(uuid, data) {
const index = this.chat.findIndex(item => item.id === uuid)
if (index > -1) {
this.chat[index] = data
} else {
this.chat.push(data)
}
},
setHistory(history) {
this.history = history;
},
addHistory(history) {
this.history.push(history);
},
updateChatSome(index, chat) {
const chatIndex = this.chat.findIndex(item => item.id === this.active)
if (index !== -1) {
const lastItem = this.chat[chatIndex].messages[index]
let key = Object.keys(lastItem)[0];
const tem = lastItem[key]
this.chat[chatIndex].messages[index][key] = { ...tem, ...chat }
}
},
addChatByUuid(uuid, chat) {
if (!uuid) {
const uuid = uuidv4()
this.history.unshift({ id: uuid, title: '新的对话' })
this.chat.push({ id: uuid, messages: [{ [chat.id]: chat }] })
this.active = uuid
}
const index = this.chat.findIndex(item => item.id === uuid)
if (index !== -1) {
const lastItem = this.chat[index].messages[this.chat[index].messages.length - 1]
let key = Object.keys(lastItem)[0];
// this.chat[index].messages.push({ [chat.id]: { ...chat, parent_id: lastItem[key].id } })
this.chat[index].messages.push({ [chat.id]: { ...chat } })
}
},
updateChatByUuid(uuid, index, chat) {
if (uuid !== this.active) {
const active = this.active
this.chat[this.chat.findIndex(e => e.id == active)].id = uuid
this.history[this.history.findIndex(e => e.id == active)].id = uuid
this.active = uuid
}
const chatIndex = this.chat.findIndex(item => item.id === uuid)
if (chatIndex !== -1) {
this.chat[chatIndex].messages[index] = chat
}
},
async setActive(uuid) {
this.active = uuid
return await this.reloadRoute(uuid)
},
getChat(uuid) {
if (!uuid) return;
return new Promise((resolve, reject) => {
http.get(`/api/v1/conversations/${uuid}`, {
requestBaseUrl: 'chat',
}).then((res) => {
const { messages } = res;
const temp = messages.map((item) => {
var key = Object.keys(item)[0];
const { role } = item[key]
const obj = Object.assign({}, { ...item[key] }, { loading: false, inversion: role == 'user' })
return {
[key]: obj
};
});
this.setChat(uuid, { ...res, messages: temp })
resolve(res)
}).catch((err) => {
if (err.errcode == 10020) {
this.setActive(null)
}
reject(err)
})
})
},
getHistory(page = 1, limit = 200) {
http.get('/api/v1/conversations', {
params: {
page: page,
limit: limit,
},
requestBaseUrl: 'chat',
})
.then((res) => {
this.setHistory(res.conversations)
})
},
async reloadRoute(uuid) {
await router.push({ name: 'Chat', params: { uuid } })
},
// getTemplates(page=1 , limit=200,) {
// http.get('/api/prompt-templates', {
// params: {
// recommendable:1,
// page: page,
// limit: limit,
// },
// })
// .then((res) => {
// this.templates = res.templates
// })
// }
}
});

View File

@ -0,0 +1 @@
export * from './chat'

View File

@ -1,5 +1,5 @@
<template>
<Layout>
<Layout path="/chat">
<div class="h-full flex flex-col justify-center text-white">
<TitleComp title="AI助理" :src="TitleSrc" />
<div class="mt-36px text-27px font-bold">

View File

@ -11,7 +11,7 @@
<li :class="{ active: !cid }" @click="cid = 0" >全部</li>
<li :class="{ active: cid == item.id }" @click="changeCategory(item.id)" v-for="item in categories">{{ item.name }}</li>
</ul>
<CardList :key="cid" :cid="cid" type="policy"></CardList>
<CardList :key="cid" :cid="cid" :type="type"></CardList>
</div>
</div>
</template>
@ -23,17 +23,23 @@ import { showToast } from 'vant';
import { useRouter, useRoute } from 'vue-router';
import CardList from '../components/CategoryCardList.vue';
const router = useRouter()
const route = useRoute()
const cid = ref(0);
const banner = ref();
const categories = ref([]);
const { key, id } = route.query
const type = ref(key ?? 'policy')
onBeforeMount(() => {
getBanner();
getCategories();
});
const getBanner = () => {
let params = { key: 'pc_policy' };
let params = { key: `pc_${type.value}` }
http('/api/banner', params, 'get').then(res => {
banner.value = Array.isArray(res.data) ? res.data[0] : {};
}).catch(err => {
@ -42,9 +48,12 @@ const getBanner = () => {
};
const getCategories = () => {
let params = {
type_key: 'policy',
};
let params = {}
if (id) {
params.parent_id = id
} else {
params.type_key = 'policy'
}
http('/api/keywords', params, 'get').then(res => {
categories.value = res.data || [];
}).catch(err => {

View File

@ -0,0 +1,388 @@
<template>
<div class="fixed w-86 h-full z-999" :style="styleObj">
<!-- <vue-draggable-resizable
:x="0"
:y="0"
:z="999"
:resizable="true"
w="auto"
h="auto"
> -->
<div class="w-86 bg-[#161718] p-3" ref="floatWindow">
<div class="flex text-white items-center justify-between">
<div class="flex items-center">
<Avatar />
<span class="font-bold text-lg ml-4">海兔AI智慧助理</span>
</div>
<SvgIcon
class="text-white text-xl cursor-pointer"
name="close"
></SvgIcon>
</div>
<div class="border-2px border-[#A6A8AF] p-4 mt-4">
<div class="grid grid-cols-3 gap-x-2.5 options">
<div
class="border-2px border-[#414548] h-6.5 text-center text-sm leading-6.5 text-white cursor-pointer"
:class="{ active: optionIndex === index }"
@click="changeOption(index)"
v-for="(item, index) in options"
:key="item.name"
>
{{ item.name }}
</div>
</div>
<div class="w-full h-2px bg-[#3662FE] my-2.5"></div>
<div class="flex justify-end">
<SvgIcon
v-if="loading"
@click="handleStop"
class="text-white text-xl ml-2 cursor-pointer"
name="pause"
></SvgIcon>
<SvgIcon class="text-white text-xl ml-2" name="right-arrow"></SvgIcon>
</div>
<div class="mt-5 h-97 -mx-4">
<ScrollContainer ref="scrollRefAi">
<div class="px-4">
<div class="text-base text-white opacity-40 leading-6">
<div class="text-center" v-if="contentLoading">
<a-spin size="small" />
</div>
<div v-else>
{{ contenText }}
</div>
<!-- 大自然是人类赖以生存发展的基本条件尊重自然顺应自然保护自然是全面建设社会主义现代化国家的内在要求大自然是人类赖以生存发展的基本条件尊重自然顺应自然保护自然是全面建设社会主义现代化国家是全面建设社会主义现代化国家的内在要求 -->
</div>
<Message
class="my-4"
v-for="(item, index) in dataSources"
:key="index"
:text="item.text"
:loading="item.loading"
:inversion="item.inversion"
></Message>
</div>
</ScrollContainer>
</div>
<div class="flex">
<a-input
@pressEnter="sendMessage"
v-model:value="prompt"
size="small"
class="flex-1 text-sm rounded-r-none rounded-4px bg-[#414548] bg-opacity-40 text-white placeholder-[#FFFFFF40] border-[#414548]"
placeholder="向我提问有关文本的任何问题"
></a-input>
<a-button
@click="sendMessage"
type="primary"
class="rounded-r-4px rounded-l-none px-6 h-full !w-19"
>
<template #icon>
<SvgIcon class="text-white text-xl" name="send"></SvgIcon>
</template>
</a-button>
</div>
<div class="opacity-40 text-sm text-center mt-4">
文案仅供参考使用前请核实风险自负
</div>
</div>
</div>
<!-- </vue-draggable-resizable> -->
</div>
</template>
<script setup>
import Message from './message.vue'
import Avatar from './avatar.vue'
import ScrollContainer from '@/components/ScrollContainer/index.vue'
import { ref, computed, nextTick } from 'vue'
import http from '@/io/request'
import { v4 as uuidv4 } from 'uuid'
import { useAiChat } from '@/stores/aichat'
import VueDraggableResizable from 'vue-draggable-resizable/src/components/vue-draggable-resizable.vue'
import 'vue-draggable-resizable/dist/VueDraggableResizable.css'
const props = defineProps({
top: {
type: String,
default: '140px',
},
right: {
type: String,
default: '0px',
},
content: {
type: String,
default: '',
},
})
const styleObj = computed(() => {
return {
top: props.top,
right: props.right,
}
})
const contentLoading = ref(false)
const contenText = ref('')
let controller = new AbortController()
const scrollRefAi = ref(null)
const aiChatStore = useAiChat()
const loading = ref(false)
const uuid = uuidv4()
const options = [
{
name: 'AI总结',
value: `Instructions: Summarize the highlights of the content and output a useful summary in a few sentences,usage and download address not included.
You must write the summary in Chinese (China) language.
"""
{0}
"""`,
},
{
name: 'Q&A',
value: `
Instructions: List the highlights of the content in the form of Q&As, no less than 3 Q&As. Here is an example of the template output you should use:
##### Who are you?
I am AI.
Be sure not to write out the template examples.
Please answer using Chinese (China) language.
"""
{0}
"""
`,
},
{
name: '重点内容',
value: `
Instructions: Summarize this content into a bulleted list of the most important information.
Please answer using Chinese (China) language.
"""
{0}
"""
`,
},
]
const optionIndex = ref(null)
const changeOption = (index) => {
optionIndex.value = index
contenText.value = ''
autoMessage()
}
const currentOption = computed(() => {
if(optionIndex.value === null) return null
return options[optionIndex.value] ?? null
})
const dataSources = computed(() => {
return aiChatStore.getHistoryByUuid(uuid)
})
const prompt = ref('')
const autoMessage = async ()=>{
const message = replacePlaceholder(currentOption.value.value, truncateRichText(props.content, 4000))
if(loading.value) return
contentLoading.value = true
loading.value = true
try {
const fetchChatAPIOnce = async () => {
await http.post(
'/api/v1/answer',
{
prompt: message,
},
{
signal: controller.signal,
requestBaseUrl: 'chat',
onDownloadProgress: async ({ event }) => {
contentLoading.value = false
const xhr = event.target
const { responseText } = xhr
if (xhr.status == 200) {
const arr = parseEventMessages(responseText)
const msg = arr.reduce((acc, item) => {
return acc + item.text
}, '')
contenText.value = msg
scrollToBottomIfAtBottom()
}
},
}
)
}
await fetchChatAPIOnce()
} catch (error) {
if (error.message === 'canceled') {
return
}
const errorMessage = error?.errmsg ?? '好像出错了,请稍后再试。'
contenText.value = errorMessage
}finally{
contentLoading.value = false
loading.value = false
}
}
function truncateRichText(richText, maxLength) {
//
const plainText = richText.replace(/<[^>]+>/g, '');
// maxLength
const truncatedText = plainText.substring(0, maxLength);
return truncatedText;
}
function replacePlaceholder(originalText, replacement) {
return originalText.replace('{0}', replacement);
}
const sendMessage = async () => {
const message = prompt.value
if (loading.value) return
if (!message || message.trim() === '') {
return
}
aiChatStore.addHistory(uuid, {
text: message,
inversion: true,
loading: false,
})
scrollToBottom()
aiChatStore.addHistory(uuid, {
text: '⋯',
inversion: false,
loading: true,
})
prompt.value = ''
loading.value = true
scrollToBottom()
try {
const fetchChatAPIOnce = async () => {
await http.post(
'/api/v1/answer',
{
prompt: message,
},
{
signal: controller.signal,
requestBaseUrl: 'chat',
onDownloadProgress: async ({ event }) => {
const xhr = event.target
const { responseText } = xhr
if (xhr.status == 200) {
const arr = parseEventMessages(responseText)
const { conversation_id, message_id } = arr[0]
const msg = arr.reduce((acc, item) => {
return acc + item.text
}, '')
aiChatStore.updateChatByUuid(uuid, dataSources.value.length - 1, {
text: msg,
inversion: false,
loading: true,
})
scrollToBottomIfAtBottom()
}
},
}
)
aiChatStore.updateChatSomeByUuid(uuid, dataSources.value.length - 1, {
loading: false,
})
}
await fetchChatAPIOnce()
} catch (error) {
if (error.message === 'canceled') {
aiChatStore.updateChatSomeByUuid(uuid, dataSources.value.length - 1, {
loading: false,
})
return
}
const errorMessage = error?.errmsg ?? '好像出错了,请稍后再试。'
aiChatStore.updateChatSomeByUuid(uuid, dataSources.value.length - 1, {
loading: false,
text: errorMessage,
})
} finally {
loading.value = false
}
}
function handleStop() {
if (loading.value) {
controller.abort()
loading.value = false
}
}
function parseEventMessages(dataString) {
const lines = dataString.trim().split('\n')
const eventMessages = []
let currentEvent = {}
for (const line of lines) {
if (line.startsWith('event:message')) {
if (Object.keys(currentEvent).length > 0) {
eventMessages.push(currentEvent)
currentEvent = {}
}
} else if (line.startsWith('data:')) {
const jsonData = line.substring('data:'.length)
currentEvent = JSON.parse(jsonData)
}
}
if (Object.keys(currentEvent).length > 0) {
eventMessages.push(currentEvent)
}
return eventMessages
}
function scrollToBottom() {
nextTick()
scrollRefAi.value.scrollBottom()
}
function scrollToBottomIfAtBottom() {
nextTick()
scrollRefAi.value.scrollToBottomIfAtBottom()
}
</script>
<style lang="scss" scoped>
:deep(.ant-input-group) {
.ant-input-group-addon {
height: 100%;
// display: inline-block;
&:last-child {
padding: 0;
}
}
}
.options {
.active {
@apply bg-[#3662FE] text-white border-[#3662FE];
}
}
</style>

View File

@ -0,0 +1,16 @@
<template>
<div class="text-50px">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32"
aria-hidden="true"
width="1em"
height="1em"
>
<path
d="M29.71,13.09A8.09,8.09,0,0,0,20.34,2.68a8.08,8.08,0,0,0-13.7,2.9A8.08,8.08,0,0,0,2.3,18.9,8,8,0,0,0,3,25.45a8.08,8.08,0,0,0,8.69,3.87,8,8,0,0,0,6,2.68,8.09,8.09,0,0,0,7.7-5.61,8,8,0,0,0,5.33-3.86A8.09,8.09,0,0,0,29.71,13.09Zm-12,16.82a6,6,0,0,1-3.84-1.39l.19-.11,6.37-3.68a1,1,0,0,0,.53-.91v-9l2.69,1.56a.08.08,0,0,1,.05.07v7.44A6,6,0,0,1,17.68,29.91ZM4.8,24.41a6,6,0,0,1-.71-4l.19.11,6.37,3.68a1,1,0,0,0,1,0l7.79-4.49V22.8a.09.09,0,0,1,0,.08L13,26.6A6,6,0,0,1,4.8,24.41ZM3.12,10.53A6,6,0,0,1,6.28,7.9v7.57a1,1,0,0,0,.51.9l7.75,4.47L11.85,22.4a.14.14,0,0,1-.09,0L5.32,18.68a6,6,0,0,1-2.2-8.18Zm22.13,5.14-7.78-4.52L20.16,9.6a.08.08,0,0,1,.09,0l6.44,3.72a6,6,0,0,1-.9,10.81V16.56A1.06,1.06,0,0,0,25.25,15.67Zm2.68-4-.19-.12-6.36-3.7a1,1,0,0,0-1.05,0l-7.78,4.49V9.2a.09.09,0,0,1,0-.09L19,5.4a6,6,0,0,1,8.91,6.21ZM11.08,17.15,8.38,15.6a.14.14,0,0,1-.05-.08V8.1a6,6,0,0,1,9.84-4.61L18,3.6,11.61,7.28a1,1,0,0,0-.53.91ZM12.54,14,16,12l3.47,2v4L16,20l-3.47-2Z"
fill="currentColor"
/>
</svg>
</div>
</template>

View File

@ -0,0 +1,344 @@
<template>
<LayoutContent>
<div class="flex flex-col w-full h-full">
<ScrollContainer id="scrollRef" ref="scrollRef">
<div class="p-4">
<MessageGroup
v-for="(item, i) in dataSources"
:key="i"
:arr="item"
></MessageGroup>
</div>
</ScrollContainer>
<div class="p-2">
<div class="border border-[#414548]">
<div class="border-b border-[#414548] h-9 flex items-center px-5">
<SvgIcon
@click="handleRefresh"
name="refresh"
class="text-white text-base cursor-pointer mr-6"
></SvgIcon>
<SvgIcon
@click="handleStop"
name="stop"
class="text-white text-base cursor-pointer mr-6"
></SvgIcon>
</div>
<a-textarea
ref="inputRef"
class="bg-transparent border-none rounded-none text-white minp focus:shadow-none placeholder-[#FFFFFF40]"
placeholder="请输入内容"
:rows="4"
v-model:value="inputValue"
@pressEnter="handleEnter"
/>
<div class="flex pb-5 px-5 pt-4 justify-between">
<div class="flex items-end">
<a-button
@click="handleClear"
type="primary"
class="rounded-4px h-9.5 px-6 !bg-[#414548] mr-2.5 border-[#414548] text-white"
>
<template #icon>
<SvgIcon name="delete" class="text-lg mr-2" />
</template>
清除</a-button
>
<a-button type="primary" class="rounded-4px h-9.5 px-6">
<template #icon>
<SvgIcon name="cloud-upload" class="text-lg mr-2" />
</template>
上传</a-button
>
<div class="ml-2">
<span class="text-[#EC4B4B] text-base">*</span>
<span class="opacity-40 text-xs">
可支持的格式.doc.docx.pdf小于20M
</span>
</div>
</div>
<div>
<a-button
@click="handleSubmit"
type="primary"
class="rounded-4px h-9.5 px-6"
>
<template #icon>
<SvgIcon name="generated" class="text-sm mr-2" />
</template>
生成</a-button
>
</div>
</div>
</div>
</div>
</div>
</LayoutContent>
</template>
<script setup>
import { Layout } from 'ant-design-vue'
import MessageGroup from './message-group.vue'
import { ref, computed, onMounted, nextTick, onBeforeMount,getCurrentInstance } from 'vue'
import ScrollContainer from '@/components/ScrollContainer/index.vue'
import http from '@/io/request'
import { v4 as uuidv4 } from 'uuid'
import { useRoute } from 'vue-router'
import { useChat } from '@/stores'
const { proxy } = getCurrentInstance();
let controller = new AbortController()
const inputRef = ref(null)
const LayoutContent = Layout.Content
const route = useRoute()
const { uuid } = route.params
const chatStore = useChat()
const scrollRef = ref(null)
const loading = ref(false)
const inputValue = ref('')
const currentChart = computed(() => chatStore.getCurrentChat)
const conversationList = computed(() => {
return chatStore.getCurrentChat.filter((item) => {
const key = Object.keys(item)[0]
const value = item[key]
return !value.inversion
})
})
const conversationUserList = computed(() => {
return chatStore.getCurrentChat.filter((item) => {
const key = Object.keys(item)[0]
const value = item[key]
return value.inversion
})
})
const dataSources = computed(() => {
const arr = chatStore.getCurrentChat
return groupDataByParentId(arr)
})
function groupDataByParentId(data) {
return data.reduce((groupedData, item) => {
const itemId = Object.keys(item)[0]
const itemData = item[itemId]
if (itemData.parent_id === '') {
groupedData[itemId] = []
} else {
const parentItemId = itemData.parent_id
groupedData[parentItemId] = groupedData[parentItemId] || []
groupedData[parentItemId].push(itemData)
}
return groupedData
}, {})
}
function handleRefresh() {
if (currentChart.value.length && !loading.value) onConversation('variant')
}
function handleStop() {
if (loading.value) {
controller.abort()
loading.value = false
}
}
function handleClear() {
inputValue.value = ''
}
function handleSubmit() {
onConversation()
}
async function onConversation(action = 'next') {
let message = inputValue.value
if (loading.value) return
if ((!message || message.trim() === '') && action == 'next') return
const params = {
action: action,
conversation_id: chatStore.active,
message: {
text: message,
id: uuidv4(),
},
}
if (action == 'next') {
const lastContext =
conversationList.value[conversationList.value.length - 1]
params.parent_message_id = uuidv4()
if (lastContext) {
params.parent_message_id = lastContext[Object.keys(lastContext)[0]]?.id
}
chatStore.addChatByUuid(chatStore.active, {
text: message,
inversion: true,
parent_id: params.parent_message_id || '',
id: params.message.id,
})
scrollToBottom()
chatStore.addChatByUuid(chatStore.active, {
text: '⋯',
id: uuidv4(),
parent_id: params.message.id,
inversion: false,
})
}
if (action == 'variant') {
const lastUserContext =
conversationUserList.value[conversationUserList.value.length - 1]
if (lastUserContext) {
const obj = lastUserContext[Object.keys(lastUserContext)[0]]
params.message.text = obj.text
params.message.id = obj.id
params.parent_message_id = obj.id
}
chatStore.addChatByUuid(chatStore.active, {
text: '⋯',
id: uuidv4(),
parent_id: params.parent_message_id,
inversion: false,
})
}
loading.value = true
inputValue.value = ''
scrollToBottom()
let tempMessage_id = null
try {
const fetchChatAPIOnce = async () => {
await http.post('/api/v1/conversation', params, {
signal: controller.signal,
requestBaseUrl: 'chat',
onDownloadProgress: async ({ event }) => {
const xhr = event.target
const { responseText } = xhr
if (xhr.status == 200) {
const arr = parseEventMessages(responseText)
const { conversation_id, message_id } = arr[0]
tempMessage_id = message_id
const msg = arr.reduce((acc, item) => {
return acc + item.text
}, '')
chatStore.updateChatByUuid(
conversation_id,
chatStore.getCurrentChat.length - 1,
{
[message_id]: {
conversation_id: conversation_id,
id: message_id,
text: msg,
inversion: false,
loading: true,
parent_id: params.message.id || '',
},
}
)
scrollToBottomIfAtBottom()
}
},
})
chatStore.updateChatSome(chatStore.getCurrentChat.length - 1, {
loading: false,
})
await chatStore.getChat(chatStore.active)
}
await fetchChatAPIOnce()
} catch (error) {
if (error.message === 'canceled') {
chatStore.updateChatSome(chatStore.getCurrentChat.length - 1, {
loading: false,
})
return
}
const errorMessage = error?.errmsg ?? '好像出错了,请稍后再试。'
chatStore.updateChatSome(chatStore.getCurrentChat.length - 1, {
loading: false,
text: errorMessage,
})
} finally {
loading.value = false
}
}
function parseEventMessages(dataString) {
const lines = dataString.trim().split('\n')
const eventMessages = []
let currentEvent = {}
for (const line of lines) {
if (line.startsWith('event:message')) {
if (Object.keys(currentEvent).length > 0) {
eventMessages.push(currentEvent)
currentEvent = {}
}
} else if (line.startsWith('data:')) {
const jsonData = line.substring('data:'.length)
currentEvent = JSON.parse(jsonData)
}
}
if (Object.keys(currentEvent).length > 0) {
eventMessages.push(currentEvent)
}
return eventMessages
}
function handleEnter(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
handleSubmit()
}
}
function handleDataChange(data) {
inputValue.value = data
}
onMounted(async () => {
proxy.$mitt.on('temp-copy', handleDataChange)
if (inputRef.value) {
inputRef.value.focus()
}
await chatStore.getChat(uuid)
scrollToBottom()
})
function scrollToBottom() {
nextTick()
scrollRef.value.scrollBottom()
}
function scrollToBottomIfAtBottom() {
nextTick()
scrollRef.value.scrollToBottomIfAtBottom()
}
</script>
<style lang="scss" scoped>
.scroll-smooth {
scroll-behavior: smooth;
}
</style>

View File

@ -0,0 +1,151 @@
<template>
<div class="h-46px flex items-center justify-between px-20px">
<div class="flex items-center flex-1 h-full">
<SvgIcon class="text-white text-40px mr-13px" name="dh"></SvgIcon>
<div
class="flex items-center flex-1 h-full border border-[#414548] rounded-2px bg-[#414548] bg-opacity-40 px-17px"
>
<div
@click="handleSelect"
class="cursor-pointer text-white opacity-40 line-clamp-1 flex-1 text-22px"
v-if="!isEdit"
>
{{ data.title }}
</div>
<div class="flex-1" v-else @click.prevent.stop="() => {}">
<van-field
v-model="nameValue"
@blur="checkAddress($event)"
ref="nameRef"
class="!bg-transparent border-none text-white !h-full cu-field-edit"
></van-field>
</div>
</div>
</div>
<div class="flex items-center ml-10px">
<SvgIcon
v-if="!isEdit"
@click.stop="handleEdit"
class="text-28px mx-8px text-white"
name="编辑"
>
</SvgIcon>
<SvgIcon
v-else
class="text-28px mx-8px text-white"
name="对勾"
@click.stop="handleChangeName"
></SvgIcon>
<SvgIcon
@click.prevent.stop="handleDelete"
@click.stop="handleEdit"
class="text-28px ml-8px text-white"
name="qx"
>
</SvgIcon>
</div>
</div>
</template>
<script setup>
import { toRaw, ref, createVNode } from 'vue'
import { useRouter } from 'vue-router'
import { useChat } from '@/stores'
import http from '@/io/request'
import { showToast, showConfirmDialog } from 'vant'
const chatStore = useChat()
const props = defineProps({
data: {
type: Object,
default: () => ({}),
},
})
const nameRef = ref(null)
const isEdit = ref(false)
const nameValue = ref(props.data.title)
const emit = defineEmits(['onEdit', 'onDelete'])
const checkAddress = (e) => {
setTimeout(() => {
isEdit.value = false
}, 200)
}
const handleEdit = () => {
isEdit.value = true
nameValue.value = props.data.title
setTimeout(() => {
nameRef.value.focus()
}, 200)
}
const handleChangeName = () => {
if (!nameValue.value) {
showToast('请输入名称')
return
}
http
.put(
`/api/v1/conversations/${props.data.id}`,
{
title: nameValue.value,
},
{
requestBaseUrl: 'chat',
}
)
.then((res) => {
props.data.title = nameValue.value
isEdit.value = false
})
.catch((err) => {})
}
const handleDelete = () => {
showConfirmDialog({
title: '删除对话?',
message: `这将删除${props.data.title}`,
confirmButtonText: '删除',
cancelButtonText: '取消',
})
.then(() => {
http
.delete(`/api/v1/conversations/${props.data.id}`, {
requestBaseUrl: 'chat',
})
.then((res) => {
chatStore.deleteConversation(props.data.id)
})
})
.catch(() => {
// on cancel
})
}
const handleSelect = async () => {
const uuid = props.data.id
if (isActive(uuid)) return
await chatStore.setActive(uuid)
}
function isActive(uuid) {
return chatStore.active === uuid
}
</script>
<style lang="scss">
.cu-field-edit {
&.van-field {
padding: 0 !important;
}
.van-field__control {
color: white;
font-size: 22px;
}
}
</style>

View File

@ -0,0 +1,58 @@
<template>
<div class="mb-37px">
<Message
:text="currentMsg.text"
:loading="currentMsg.loading"
:inversion="currentMsg.inversion"
></Message>
<div
class="flex items-center text-22px mt-20px ml-70px"
v-if="messageLength > 1"
>
<SvgIcon
@click="handlePrev"
name="arrow-right"
class="text-white cursor-pointer text-22px transform rotate-180"
></SvgIcon>
<div class="px-20px">{{ currentIndex + 1 }} / {{ messageLength }}</div>
<SvgIcon
@click="handleNext"
name="arrow-right"
class="text-white text-22px cursor-pointer"
></SvgIcon>
</div>
</div>
</template>
<script setup>
import Message from './message.vue'
import { ref, computed ,watch} from 'vue'
const props = defineProps({
arr: {
type: Array,
default: () => [],
},
})
const currentMsg = computed(() => props.arr[currentIndex.value])
const currentIndex = ref(props.arr.length-1)
const messageLength = computed(() => props.arr.length)
const handleNext = () => {
if (currentIndex.value < messageLength.value - 1) {
currentIndex.value++
}
}
const handlePrev = () => {
if (currentIndex.value > 0) {
currentIndex.value--
}
}
watch(
() => props.arr.length,
() => {
currentIndex.value = props.arr.length - 1
}
)
</script>

View File

@ -0,0 +1,52 @@
<template>
<div
ref="messageRef"
class="flex w-full overflow-hidden"
:class="[{ 'flex-row-reverse': inversion }]"
>
<div
class="flex items-center justify-center flex-shrink-0 h-50px overflow-hidden rounded-full basis-50px mt-4px"
:class="[inversion ? 'ml-19px' : 'mr-19px']"
>
<AvatarComponent />
</div>
<div
class="overflow-hidden text-22px"
:class="[inversion ? 'items-end' : 'items-start']"
>
<div
class="flex items-end gap-1"
:class="[inversion ? 'flex-row-reverse' : 'flex-row']"
>
<TextComponent
ref="textRef"
:inversion="inversion"
:text="text"
:loading="loading"
:as-raw-text="asRawText"
/>
</div>
</div>
</div>
</template>
<script setup>
import TextComponent from './text.vue'
import AvatarComponent from './avatar.vue'
import { ref } from 'vue'
const props = defineProps({
text: {
type: String,
default: '',
},
inversion: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
})
const asRawText = ref(props.inversion)
</script>

View File

@ -0,0 +1,15 @@
<template>
<div class="cursor-pointer w-full">
<div class="h-80px overflow-hidden flex items-center">
<div class="w-60px flex items-center h-full justify-center">
<slot name="icon"></slot>
</div>
<div class="flex-1 text-27px font-normal text-white flex items-center justify-between pr-20px">
<div class="flex-1">
<slot></slot>
</div>
<slot name="right"></slot>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,168 @@
<template>
<div
class="w-74px border-t-7px border-[#3662FE] bg-gradient-to-b to-[#101011] from-[#414548] h-full flex items-center pt-26px flex-col"
>
<div class="h-full flex items-center pt-26px flex-col" @click.prevent.stop="show = true">
<SvgIcon class="text-white text-35px mb-46px" name="dp"></SvgIcon>
<SvgIcon class="text-white text-35px mb-46px" name="xx"></SvgIcon>
<SvgIcon
class="text-white text-35px mb-46px transform rotate-45"
name="qx"
></SvgIcon>
<SvgIcon class="text-white text-40px mb-46px" name="dh"></SvgIcon>
<div class="flex-1"></div>
<SvgIcon class="text-white text-40px mb-46px" name="人工客服"></SvgIcon>
<SvgIcon class="text-white text-40px mb-46px" name="疑问"></SvgIcon>
</div>
<van-popup v-model:show="show" position="left" class="slider-class">
<div class="w-479px h-full flex flex-col">
<div class="h-100px"></div>
<div
class="bg-gradient-to-b to-[#101011] from-[#414548] flex-1 border-t-7px border-[#3662FE] flex flex-col items-center"
>
<!-- @click="openTemplateModal" -->
<SilderBtn>
<template #icon>
<SvgIcon class="text-white text-35px" name="dp"></SvgIcon>
</template>
<div>推荐模板</div>
</SilderBtn>
<van-divider
class="text-[#414548] w-full !my-0 opacity-25"
></van-divider>
<SilderBtn>
<template #icon>
<SvgIcon class="text-white text-35px" name="xx"></SvgIcon>
</template>
<template #right>
<SvgIcon
@click="templatesRefresh"
class="text-31px text-white cursor-pointer"
name="refresh"
></SvgIcon>
</template>
<div>常用模板</div>
</SilderBtn>
<van-divider
class="text-[#414548] !my-0 opacity-25 w-full"
></van-divider>
<div class="w-full px-8px py-18px">
<div class="grid grid-cols-2 gap-y-10px gap-x-14px">
<TemplateItem
v-for="(item, i) in templates"
:key="i"
:data="item"
/>
</div>
</div>
<SilderBtn>
<template #icon>
<SvgIcon
class="text-white text-35px transform rotate-45"
name="qx"
></SvgIcon>
</template>
<div>新建对话</div>
</SilderBtn>
<van-divider
class="text-[#414548] w-full !my-0 opacity-25"
></van-divider>
<div class="flex-1 w-full">
<GroupItem
class="my-20px"
v-for="(item, i) in dataSources"
:key="i"
:data="item"
/>
</div>
<SilderBtn>
<template #icon>
<SvgIcon
class="text-white text-35px transform rotate-45"
name="人工客服"
></SvgIcon>
</template>
<div>人工编辑</div>
</SilderBtn>
<SilderBtn>
<template #icon>
<SvgIcon
class="text-white text-35px transform rotate-45"
name="疑问"
></SvgIcon>
</template>
<div>帮助</div>
</SilderBtn>
</div>
</div>
</van-popup>
</div>
</template>
<script setup>
import SilderBtn from './sider-btn.vue'
import TemplateItem from './template-item.vue'
import GroupItem from './group-item.vue'
import ScrollContainer from '@/components/ScrollContainer/index.vue'
// import TemplateModal from './template-modal.vue'
import { ref, computed, onMounted } from 'vue'
import { useChat } from '@/stores'
import http from '@/io/request'
const show = ref(false)
const chatStore = useChat()
let totalNumber = 1
const dataSources = computed(() => chatStore.history)
const pageNum = ref(1)
const open = ref(false)
const templates = ref([])
function openTemplateModal() {
open.value = true
}
function handleNewChat() {
chatStore.setActive(null)
}
function templatesRefresh() {
pageNum.value++
getTemplates()
}
function getTemplates() {
http
.get('/api/prompt-templates', {
params: {
recommendable: 1,
per_page: 8,
page: (pageNum.value % totalNumber) + 1,
},
})
.then(({ data, last_page }) => {
totalNumber = last_page
templates.value = data
})
}
onMounted(() => {
getTemplates()
})
</script>
<style lang="scss">
.slider-class {
// top: 136px !important;
height: 100% !important;
// background: linear-gradient(0deg, #101011 0%, #414548 100%);
background: transparent;
}
</style>

View File

@ -0,0 +1,84 @@
.markdown-body {
background-color: transparent;
font-size: 14px;
p {
white-space: pre-wrap;
}
ol {
list-style-type: decimal;
}
ul {
list-style-type: disc;
}
pre code,
pre tt {
line-height: 1.65;
}
.highlight pre,
pre {
background-color: #fff;
}
code.hljs {
padding: 0;
}
.code-block {
&-wrapper {
position: relative;
padding-top: 24px;
}
&-header {
position: absolute;
top: 5px;
right: 0;
width: 100%;
padding: 0 1rem;
display: flex;
justify-content: flex-end;
align-items: center;
color: #b3b3b3;
&__copy {
cursor: pointer;
margin-left: 0.5rem;
user-select: none;
&:hover {
color: #65a665;
}
}
}
}
}
html.dark {
.message-reply {
.whitespace-pre-wrap {
white-space: pre-wrap;
color: var(--n-text-color);
}
}
.highlight pre,
pre {
background-color: #282c34;
}
}
@media screen and (max-width: 533px) {
.markdown-body .code-block-wrapper {
padding: unset;
code {
padding: 24px 16px 16px 16px;
}
}
}

View File

@ -0,0 +1,43 @@
<template>
<div class="py-3 px-4 bg-[#414548] w-79 h-122 rounded-4px">
<div class="flex items-center">
<div class="flex-1 text-xl">{{ data.title }}</div>
<div
class="bg-[#3662FE] w-7 h-7 rounded-4px flex items-center justify-center"
>
<SvgIcon
@click="onCopy"
class="text-white text-lg cursor-pointer"
name="ic_copy"
></SvgIcon>
</div>
</div>
<div class="mt-8 text-base leading-24px">
{{ data.content }}
</div>
</div>
</template>
<script setup>
import { copyToClip } from '@/utils/copy'
import { message } from 'ant-design-vue'
import { getCurrentInstance } from 'vue'
const props = defineProps({
data: {
type: Object,
default: () => {},
},
})
const { proxy } = getCurrentInstance();
const onCopy = () => {
copyToClip(props.data.content)
.then((res) => {
proxy.$mitt.emit('temp-copy', props.data.content);
message.success('复制成功~')
})
.catch(() => {
message.error('复制失败~')
})
}
</script>

View File

@ -0,0 +1,39 @@
<template>
<div
class="bg-[#414548] rounded-2px h-60px flex items-center justify-between px-18px w-full"
>
<div class="line-clamp-1 text-22px">
{{ data.title }}
</div>
<img
@click="handleCopy"
class="w-26px cursor-pointer flex-none"
src="@/assets/images/copy-icon.png"
/>
</div>
</template>
<script setup>
import { getCurrentInstance } from 'vue'
import { copyToClip } from '@/utils/copy'
import { showToast } from 'vant';
const props = defineProps({
data: {
type: Object,
default: () => {},
},
})
const { proxy } = getCurrentInstance();
const handleCopy = () => {
copyToClip(props.data.content)
.then(() => {
showToast('复制成功~')
proxy.$mitt.emit('temp-copy', props.data.content);
})
.catch(() => {
showToast('复制失败~')
})
}
</script>

View File

@ -0,0 +1,222 @@
<template>
<a-modal
v-bind="getBindValue"
centered
:title="null"
:footer="null"
width="85.81rem"
wrapClassName="template-modal"
>
<div class="text-white">
<div class="flex items-center justify-center">
<img
class="w-10.5"
src="@/assets/images/tip-icon-blue.png"
alt=""
srcset=""
/>
<div class="text-2xl">海兔AIGC提示模板大全</div>
</div>
<div class="mt-2.25 opacity-40 text-center">
Haitu AIGC Writing Inspiration Complete Works
</div>
<div class="my-6 flex items-center justify-center">
<a-cascader
:allowClear="false"
class="cu-cascader w-100"
:fieldNames="{ label: 'title', value: 'id' }"
v-model:value="value"
:options="options"
placeholder="请选择"
@change="changeCategories"
popupClassName="cu-cascader-popup"
/>
</div>
<div
ref="scrollbarRef"
@scroll="handleScroll"
class="max-h-36.5rem overflow-hidden overflow-y-auto"
>
<div
class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4"
>
<TemplateCard
class="mx-auto"
v-for="(item, i) in templates"
:key="i"
:data="item"
></TemplateCard>
</div>
</div>
</div>
</a-modal>
</template>
<script>
import { ref, computed, unref, defineComponent, onMounted, nextTick } from 'vue'
import ScrollContainer from '@/components/ScrollContainer/index.vue'
import TemplateCard from './template-card.vue'
import http from '@/io/request'
export default defineComponent({
components: {
ScrollContainer,
TemplateCard,
},
setup(props, { attrs }) {
const pageNum = ref(1)
const scrollbarRef = ref(null)
const templates = ref([])
const options = ref([])
const value = ref([])
const isLoading = ref(false)
const modelOpen = ref(false)
const getBindValue = computed(() => {
const attr = {
...attrs,
...unref(props),
open: unref(modelOpen),
maskClosable: false,
dialogClass: 'ttata',
}
return attr
})
const getCategories = () => {
http.get('/api/prompt-categories').then((res) => {
options.value = res
})
}
const getTemplates = () => {
http
.get('/api/prompt-templates', {
params: {
per_page: 20,
page: pageNum.value,
category_id: value.value[value.value.length - 1] ?? null,
},
})
.then((res) => {
if (pageNum.value == 1) templates.value = []
templates.value = templates.value.concat(res.data)
pageNum.value++
isLoading.value = false
if (templates.value.length >= res.total) {
isLoading.value = true
}
})
.catch((e) => {
isLoading.value = true
})
}
const changeCategories = async (e) => {
pageNum.value = 1
isLoading.value = false
getTemplates()
await nextTick()
scrollbarRef.value.scrollTop = 0
}
const handleScroll = (e) => {
if (isLoading.value) return
const container = scrollbarRef.value
const scrollHeight = container.scrollHeight
const scrollTop = container.scrollTop
const clientHeight = container.clientHeight
const extra = 20
if (scrollTop < 100) return
if (scrollHeight - scrollTop - extra <= clientHeight) {
isLoading.value = true
// loadMore()
}
}
const loadMore = () => {
getTemplates()
}
onMounted(() => {
getCategories()
getTemplates()
})
return {
value,
options,
getBindValue,
templates,
scrollbarRef,
handleScroll,
changeCategories,
}
},
})
</script>
<style lang="scss">
.cu-cascader {
.ant-select-selector {
background-color: #414548 !important;
color: white !important;
}
.ant-select-selection-placeholder {
color: rgb(139, 138, 138) !important;
opacity: 20 !important;
}
.ant-select-clear {
background: transparent;
}
.ant-select-arrow {
color: white;
}
.ant-select-selector {
border: none !important;
}
}
.template-modal {
.ant-modal {
&-content {
background: #161718;
}
&-close {
color: #fff;
&:hover {
color: #afadad;
}
}
}
}
</style>
<style lang="scss">
.cu-cascader-popup {
background-color: #27292b;
.ant-cascader-dropdown
.ant-cascader-menu-item-active:not(.ant-cascader-menu-item-disabled),
.ant-cascader-dropdown
.ant-cascader-menu-item-active:not(.ant-cascader-menu-item-disabled):hover {
background-color: #242627;
}
.ant-cascader-menu-item,
.ant-cascader-menu-item-expand-icon {
color: white !important;
}
.ant-cascader-menu-item:hover {
background-color: #414548 !important;
}
.ant-cascader-menu-item-active {
background-color: #414548 !important;
}
.ant-select-dropdown {
color: white;
}
}
</style>

View File

@ -0,0 +1,141 @@
<template>
<div class="text-white" :class="wrapClass">
<div ref="textRef" class="leading-relaxed break-words">
<div v-if="!inversion">
<div
class="markdown-body"
:class="{ 'markdown-body-generate': loading }"
v-if="!asRawText"
v-html="text"
/>
<div v-else class="whitespace-pre-wrap" v-text="text" />
</div>
<div v-else class="whitespace-pre-wrap" v-text="text" />
<!-- <template v-if="loading">
<span class="dark:text-white w-[4px] h-[20px] block animate-blink" />
</template> -->
</div>
</div>
</template>
<script setup>
import MarkdownIt from 'markdown-it'
import mdKatex from '@traptitech/markdown-it-katex'
import mila from 'markdown-it-link-attributes'
import hljs from 'highlight.js'
import { computed, onMounted, onUnmounted, onUpdated, ref } from 'vue'
import { copyToClip } from '@/utils/copy'
const props = defineProps({
text: {
type: String,
default: '',
},
inversion: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
asRawText: {
type: Boolean,
default: false,
},
})
const textRef = ref()
const mdi = new MarkdownIt({
html: false,
linkify: true,
highlight(code, language) {
const validLang = !!(language && hljs.getLanguage(language))
if (validLang) {
const lang = language ?? ''
return highlightBlock(
hljs.highlight(code, { language: lang }).value,
lang
)
}
return highlightBlock(hljs.highlightAuto(code).value, '')
},
})
mdi.use(mila, { attrs: { target: '_blank', rel: 'noopener' } })
mdi.use(mdKatex, {
blockClass: 'katexmath-block rounded-6px p-[10px]',
errorColor: ' #cc0000',
})
const wrapClass = computed(() => {
return [
'text-wrap',
'min-w-[20px]',
'rounded-6px',
'px-12px py-12px',
props.inversion ? 'bg-[#d2f9d1]' : 'bg-[#f4f6f8]',
props.inversion ? 'dark:bg-[#414548]' : 'dark:bg-[#414548]',
props.inversion ? 'message-request' : 'message-reply',
]
})
const text = computed(() => {
const value = props.text ?? ''
if (!props.asRawText) return mdi.render(value)
return value
})
function highlightBlock(str, lang) {
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">复制</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`
}
function addCopyEvents() {
if (textRef.value) {
const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy')
copyBtn.forEach((btn) => {
btn.addEventListener('click', () => {
const code = btn.parentElement?.nextElementSibling?.textContent
if (code) {
copyToClip(code).then(() => {
btn.textContent = '复制成功'
setTimeout(() => {
btn.textContent = '复制代码'
}, 1000)
})
}
})
})
}
}
function removeCopyEvents() {
if (textRef.value) {
const copyBtn = textRef.value.querySelectorAll('.code-block-header__copy')
copyBtn.forEach((btn) => {
btn.removeEventListener('click', () => {})
})
}
}
onMounted(() => {
addCopyEvents()
})
onUpdated(() => {
addCopyEvents()
})
onUnmounted(() => {
removeCopyEvents()
})
</script>
<style lang="scss">
@import url(./style.scss);
.last-child {
div + div {
margin-left: 17px;
}
}
</style>

View File

@ -0,0 +1,363 @@
<template>
<div class="flex flex-col w-full h-full">
<ScrollContainer id="scrollRef" ref="scrollRef">
<div class="p-26px">
<MessageGroup
v-for="(item, i) in dataSources"
:key="i"
:arr="item"
></MessageGroup>
</div>
</ScrollContainer>
<div class="p-18px">
<div class="border border-[#414548]">
<div class="border-b border-[#414548] h-74px flex items-center px-25px">
<SvgIcon
@click="handleRefresh"
name="refresh"
class="text-white text-26px cursor-pointer mr-28px"
></SvgIcon>
<SvgIcon
@click="handleStop"
name="stop"
class="text-white text-26px cursor-pointer mr-28px"
></SvgIcon>
</div>
<van-field
ref="inputRef"
class="cu-field-msg !text-white"
type="textarea"
placeholder="请输入内容"
:rows="3"
v-model="inputValue"
:border="false"
@pressEnter="handleEnter"
/>
<div class="border-t border-[#414548] pb-10px px-14px">
<div class="flex items-end my-14px">
<div class="flex">
<div class="text-[#EC4B4B] text-18px">*</div>
<div class="opacity-40 text-21px">
可支持的格式.doc.docx.pdf小于20M
</div>
</div>
</div>
<div class="flex items-center">
<van-button
@click="handleClear"
type="primary"
class="!rounded-2px !h-53px !bg-[#414548] !mr-14px border-[#414548] text-white !border-none text-22px"
>
<template #icon>
<SvgIcon name="delete" class="text-25px mr-13px" />
</template>
清除</van-button
>
<van-button
type="primary"
class="!rounded-2px !h-53px !mr-14px border-[#414548] text-white !border-none text-22px"
>
<template #icon>
<SvgIcon name="cloud-upload" class="text-25px mr-13px" />
</template>
上传</van-button
>
<div class="flex-1"></div>
<van-button
@click="handleSubmit"
type="primary"
class="!rounded-2px !h-53px border-[#414548] text-white !border-none text-22px"
>
<template #icon>
<SvgIcon name="generated" class="text-25px mr-13px" />
</template>
生成</van-button
>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import MessageGroup from './components/message-group.vue'
import {
ref,
computed,
onMounted,
nextTick,
onBeforeMount,
getCurrentInstance,
} from 'vue'
import ScrollContainer from '@/components/ScrollContainer/index.vue'
import http from '@/io/request'
import { v4 as uuidv4 } from 'uuid'
import { useRoute } from 'vue-router'
import { useChat } from '@/stores'
const { proxy } = getCurrentInstance()
let controller = new AbortController()
const inputRef = ref(null)
const route = useRoute()
const { uuid } = route.params
const chatStore = useChat()
const scrollRef = ref(null)
const loading = ref(false)
const inputValue = ref('')
const currentChart = computed(() => chatStore.getCurrentChat)
const conversationList = computed(() => {
return chatStore.getCurrentChat.filter((item) => {
const key = Object.keys(item)[0]
const value = item[key]
return !value.inversion
})
})
const conversationUserList = computed(() => {
return chatStore.getCurrentChat.filter((item) => {
const key = Object.keys(item)[0]
const value = item[key]
return value.inversion
})
})
const dataSources = computed(() => {
const arr = chatStore.getCurrentChat
return groupDataByParentId(arr)
})
function groupDataByParentId(data) {
return data.reduce((groupedData, item) => {
const itemId = Object.keys(item)[0]
const itemData = item[itemId]
if (itemData.parent_id === '') {
groupedData[itemId] = []
} else {
const parentItemId = itemData.parent_id
groupedData[parentItemId] = groupedData[parentItemId] || []
groupedData[parentItemId].push(itemData)
}
return groupedData
}, {})
}
function handleRefresh() {
if (currentChart.value.length && !loading.value) onConversation('variant')
}
function handleStop() {
if (loading.value) {
controller.abort()
loading.value = false
}
}
function handleClear() {
inputValue.value = ''
}
function handleSubmit() {
onConversation()
}
async function onConversation(action = 'next') {
let message = inputValue.value
if (loading.value) return
if ((!message || message.trim() === '') && action == 'next') return
const params = {
action: action,
conversation_id: chatStore.active,
message: {
text: message,
id: uuidv4(),
},
}
if (action == 'next') {
const lastContext =
conversationList.value[conversationList.value.length - 1]
params.parent_message_id = uuidv4()
if (lastContext) {
params.parent_message_id = lastContext[Object.keys(lastContext)[0]]?.id
}
chatStore.addChatByUuid(chatStore.active, {
text: message,
inversion: true,
parent_id: params.parent_message_id || '',
id: params.message.id,
})
scrollToBottom()
chatStore.addChatByUuid(chatStore.active, {
text: '⋯',
id: uuidv4(),
parent_id: params.message.id,
inversion: false,
})
}
if (action == 'variant') {
const lastUserContext =
conversationUserList.value[conversationUserList.value.length - 1]
if (lastUserContext) {
const obj = lastUserContext[Object.keys(lastUserContext)[0]]
params.message.text = obj.text
params.message.id = obj.id
params.parent_message_id = obj.id
}
chatStore.addChatByUuid(chatStore.active, {
text: '⋯',
id: uuidv4(),
parent_id: params.parent_message_id,
inversion: false,
})
}
loading.value = true
inputValue.value = ''
scrollToBottom()
let tempMessage_id = null
try {
const fetchChatAPIOnce = async () => {
await http.post('/api/v1/conversation', params, {
signal: controller.signal,
requestBaseUrl: 'chat',
onDownloadProgress: async ({ event }) => {
const xhr = event.target
const { responseText } = xhr
if (xhr.status == 200) {
const arr = parseEventMessages(responseText)
const { conversation_id, message_id } = arr[0]
tempMessage_id = message_id
const msg = arr.reduce((acc, item) => {
return acc + item.text
}, '')
chatStore.updateChatByUuid(
conversation_id,
chatStore.getCurrentChat.length - 1,
{
[message_id]: {
conversation_id: conversation_id,
id: message_id,
text: msg,
inversion: false,
loading: true,
parent_id: params.message.id || '',
},
}
)
scrollToBottomIfAtBottom()
}
},
})
chatStore.updateChatSome(chatStore.getCurrentChat.length - 1, {
loading: false,
})
await chatStore.getChat(chatStore.active)
}
await fetchChatAPIOnce()
} catch (error) {
if (error.message === 'canceled') {
chatStore.updateChatSome(chatStore.getCurrentChat.length - 1, {
loading: false,
})
return
}
const errorMessage = error?.errmsg ?? '好像出错了,请稍后再试。'
chatStore.updateChatSome(chatStore.getCurrentChat.length - 1, {
loading: false,
text: errorMessage,
})
} finally {
loading.value = false
}
}
function parseEventMessages(dataString) {
const lines = dataString.trim().split('\n')
const eventMessages = []
let currentEvent = {}
for (const line of lines) {
if (line.startsWith('event:message')) {
if (Object.keys(currentEvent).length > 0) {
eventMessages.push(currentEvent)
currentEvent = {}
}
} else if (line.startsWith('data:')) {
const jsonData = line.substring('data:'.length)
currentEvent = JSON.parse(jsonData)
}
}
if (Object.keys(currentEvent).length > 0) {
eventMessages.push(currentEvent)
}
return eventMessages
}
function handleEnter(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
handleSubmit()
}
}
function handleDataChange(data) {
inputValue.value = data
}
onMounted(async () => {
proxy.$mitt.on('temp-copy', handleDataChange)
if (inputRef.value) {
inputRef.value.focus()
}
await chatStore.getChat(uuid)
scrollToBottom()
})
function scrollToBottom() {
nextTick()
scrollRef.value.scrollBottom()
}
function scrollToBottomIfAtBottom() {
nextTick()
scrollRef.value.scrollToBottomIfAtBottom()
}
</script>
<style lang="scss" scoped>
.scroll-smooth {
scroll-behavior: smooth;
}
</style>
<style lang="scss">
.cu-field-msg {
&.van-cell {
background-color: transparent;
padding: 14px 18px;
font-size: 22px;
placeholder-color: #999999;
}
.van-field__control {
color: white;
}
}
</style>

View File

@ -0,0 +1,21 @@
<template>
<div class="flex h-full pt-11px relative">
<Slider />
<div class="flex-1">
<RouterView />
</div>
</div>
</template>
<script setup>
import Slider from './components/slider.vue'
import { RouterView } from 'vue-router'
import { useChat } from '@/stores'
import { useRoute } from 'vue-router'
const route = useRoute()
const chatStore = useChat()
const { uuid } = route.params
chatStore.setActive(uuid)
chatStore.getHistory()
</script>

View File

@ -46,14 +46,14 @@ const list = [
icon: '行业竞争力',
title: '行业洞察',
des: '“宏观经济政策”macroeconomic policy是指国家或政府有意识有计划地运用一定的政策工具调节控制宏观经济的运行以达到一定的政',
path: '/insights/category',
path: '/business/insight',
bg: bg02,
},
{
icon: '政治',
title: '法律法规',
des: '“宏观经济政策”macroeconomic policy是指国家或政府有意识有计划地运用一定的政策工具',
path: '/insights/legal/index',
path: '/business/legal/policy',
bg: bg03,
},
{

View File

@ -4,7 +4,7 @@
class="h-181px w-full relative bg-gray-500 bg-opacity-10 rounded-2px bg-img"
@click="
$router.push({
path: '/insights/legal/policy',
path: '/business/legal/policy',
query: {
key: data.type_key,
id: data.id,

4908
yarn.lock

File diff suppressed because it is too large Load Diff