|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 403 B |
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
@ -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
|
||||
// })
|
||||
|
||||
// }
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './chat'
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||