mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-10 03:16:51 +08:00
feat: chat in explore support agent (#647)
Co-authored-by: StyleZhang <jasonapring2015@outlook.com>
This commit is contained in:
@@ -14,13 +14,25 @@ import Confirm from '@/app/components/base/confirm'
|
||||
|
||||
const SelectedDiscoveryIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M13.4135 1.11725C13.5091 1.09983 13.6483 1.08355 13.8078 1.11745C14.0143 1.16136 14.2017 1.26953 14.343 1.42647C14.4521 1.54766 14.5076 1.67634 14.5403 1.76781C14.5685 1.84673 14.593 1.93833 14.6136 2.01504L15.5533 5.5222C15.5739 5.5989 15.5985 5.69049 15.6135 5.77296C15.6309 5.86852 15.6472 6.00771 15.6133 6.16722C15.5694 6.37378 15.4612 6.56114 15.3043 6.70245C15.1831 6.81157 15.0544 6.86706 14.9629 6.89975C14.884 6.92796 14.7924 6.95247 14.7157 6.97299L14.676 6.98364C14.3365 7.07461 14.0437 7.15309 13.7972 7.19802C13.537 7.24543 13.2715 7.26736 12.9946 7.20849C12.7513 7.15677 12.5213 7.06047 12.3156 6.92591L9.63273 7.64477C9.86399 7.97104 9.99992 8.36965 9.99992 8.80001C9.99992 9.2424 9.85628 9.65124 9.6131 9.98245L12.5508 14.291C12.7582 14.5952 12.6797 15.01 12.3755 15.2174C12.0713 15.4248 11.6566 15.3464 11.4492 15.0422L8.51171 10.7339C8.34835 10.777 8.17682 10.8 7.99992 10.8C7.82305 10.8 7.65155 10.777 7.48823 10.734L4.5508 15.0422C4.34338 15.3464 3.92863 15.4248 3.62442 15.2174C3.32021 15.01 3.24175 14.5952 3.44916 14.291L6.3868 9.98254C6.14358 9.65132 5.99992 9.24244 5.99992 8.80001C5.99992 8.73795 6.00274 8.67655 6.00827 8.61594L4.59643 8.99424C4.51973 9.01483 4.42813 9.03941 4.34567 9.05444C4.25011 9.07185 4.11092 9.08814 3.95141 9.05423C3.74485 9.01033 3.55748 8.90215 3.41618 8.74522C3.38535 8.71097 3.3588 8.67614 3.33583 8.64171L2.49206 8.8678C2.41536 8.88838 2.32376 8.91296 2.2413 8.92799C2.14574 8.94541 2.00655 8.96169 1.84704 8.92779C1.64048 8.88388 1.45311 8.77571 1.31181 8.61877C1.20269 8.49759 1.1472 8.3689 1.1145 8.27744C1.08629 8.1985 1.06177 8.10689 1.04125 8.03018L0.791701 7.09885C0.771119 7.02215 0.746538 6.93055 0.731508 6.84809C0.714092 6.75253 0.697808 6.61334 0.731712 6.45383C0.775619 6.24726 0.883793 6.0599 1.04073 5.9186C1.16191 5.80948 1.2906 5.75399 1.38206 5.72129C1.461 5.69307 1.55261 5.66856 1.62932 5.64804L2.47318 5.42193C2.47586 5.38071 2.48143 5.33735 2.49099 5.29237C2.5349 5.08581 2.64307 4.89844 2.80001 4.75714C2.92119 4.64802 3.04988 4.59253 3.14134 4.55983C3.22027 4.53162 3.31189 4.50711 3.3886 4.48658L11.1078 2.41824C11.2186 2.19888 11.3697 2.00049 11.5545 1.83406C11.7649 1.64462 12.0058 1.53085 12.2548 1.44183C12.4907 1.35749 12.7836 1.27904 13.123 1.18809L13.1628 1.17744C13.2395 1.15686 13.3311 1.13228 13.4135 1.11725ZM13.3642 2.5039C13.0648 2.58443 12.8606 2.64126 12.7036 2.69735C12.5325 2.75852 12.4742 2.80016 12.4467 2.82492C12.3421 2.91912 12.2699 3.04403 12.2407 3.18174C12.233 3.21793 12.2261 3.28928 12.2587 3.46805C12.2927 3.6545 12.3564 3.89436 12.4559 4.26563L12.5594 4.652C12.6589 5.02328 12.7236 5.26287 12.7874 5.44133C12.8486 5.61244 12.8902 5.67079 12.915 5.69829C13.0092 5.80291 13.1341 5.87503 13.2718 5.9043C13.308 5.91199 13.3793 5.91887 13.5581 5.88629C13.7221 5.85641 13.9273 5.80352 14.2269 5.72356L13.3642 2.5039Z" fill="#155EEF"/>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M13.4135 1.11725C13.5091 1.09983 13.6483 1.08355 13.8078 1.11745C14.0143 1.16136 14.2017 1.26953 14.343 1.42647C14.4521 1.54766 14.5076 1.67634 14.5403 1.76781C14.5685 1.84673 14.593 1.93833 14.6136 2.01504L15.5533 5.5222C15.5739 5.5989 15.5985 5.69049 15.6135 5.77296C15.6309 5.86852 15.6472 6.00771 15.6133 6.16722C15.5694 6.37378 15.4612 6.56114 15.3043 6.70245C15.1831 6.81157 15.0544 6.86706 14.9629 6.89975C14.884 6.92796 14.7924 6.95247 14.7157 6.97299L14.676 6.98364C14.3365 7.07461 14.0437 7.15309 13.7972 7.19802C13.537 7.24543 13.2715 7.26736 12.9946 7.20849C12.7513 7.15677 12.5213 7.06047 12.3156 6.92591L9.63273 7.64477C9.86399 7.97104 9.99992 8.36965 9.99992 8.80001C9.99992 9.2424 9.85628 9.65124 9.6131 9.98245L12.5508 14.291C12.7582 14.5952 12.6797 15.01 12.3755 15.2174C12.0713 15.4248 11.6566 15.3464 11.4492 15.0422L8.51171 10.7339C8.34835 10.777 8.17682 10.8 7.99992 10.8C7.82305 10.8 7.65155 10.777 7.48823 10.734L4.5508 15.0422C4.34338 15.3464 3.92863 15.4248 3.62442 15.2174C3.32021 15.01 3.24175 14.5952 3.44916 14.291L6.3868 9.98254C6.14358 9.65132 5.99992 9.24244 5.99992 8.80001C5.99992 8.73795 6.00274 8.67655 6.00827 8.61594L4.59643 8.99424C4.51973 9.01483 4.42813 9.03941 4.34567 9.05444C4.25011 9.07185 4.11092 9.08814 3.95141 9.05423C3.74485 9.01033 3.55748 8.90215 3.41618 8.74522C3.38535 8.71097 3.3588 8.67614 3.33583 8.64171L2.49206 8.8678C2.41536 8.88838 2.32376 8.91296 2.2413 8.92799C2.14574 8.94541 2.00655 8.96169 1.84704 8.92779C1.64048 8.88388 1.45311 8.77571 1.31181 8.61877C1.20269 8.49759 1.1472 8.3689 1.1145 8.27744C1.08629 8.1985 1.06177 8.10689 1.04125 8.03018L0.791701 7.09885C0.771119 7.02215 0.746538 6.93055 0.731508 6.84809C0.714092 6.75253 0.697808 6.61334 0.731712 6.45383C0.775619 6.24726 0.883793 6.0599 1.04073 5.9186C1.16191 5.80948 1.2906 5.75399 1.38206 5.72129C1.461 5.69307 1.55261 5.66856 1.62932 5.64804L2.47318 5.42193C2.47586 5.38071 2.48143 5.33735 2.49099 5.29237C2.5349 5.08581 2.64307 4.89844 2.80001 4.75714C2.92119 4.64802 3.04988 4.59253 3.14134 4.55983C3.22027 4.53162 3.31189 4.50711 3.3886 4.48658L11.1078 2.41824C11.2186 2.19888 11.3697 2.00049 11.5545 1.83406C11.7649 1.64462 12.0058 1.53085 12.2548 1.44183C12.4907 1.35749 12.7836 1.27904 13.123 1.18809L13.1628 1.17744C13.2395 1.15686 13.3311 1.13228 13.4135 1.11725ZM13.3642 2.5039C13.0648 2.58443 12.8606 2.64126 12.7036 2.69735C12.5325 2.75852 12.4742 2.80016 12.4467 2.82492C12.3421 2.91912 12.2699 3.04403 12.2407 3.18174C12.233 3.21793 12.2261 3.28928 12.2587 3.46805C12.2927 3.6545 12.3564 3.89436 12.4559 4.26563L12.5594 4.652C12.6589 5.02328 12.7236 5.26287 12.7874 5.44133C12.8486 5.61244 12.8902 5.67079 12.915 5.69829C13.0092 5.80291 13.1341 5.87503 13.2718 5.9043C13.308 5.91199 13.3793 5.91887 13.5581 5.88629C13.7221 5.85641 13.9273 5.80352 14.2269 5.72356L13.3642 2.5039Z" fill="#155EEF" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const DiscoveryIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.74786 9.89676L12.0003 14.6669M7.25269 9.89676L4.00027 14.6669M9.3336 8.80031C9.3336 9.53669 8.73665 10.1336 8.00027 10.1336C7.26389 10.1336 6.66694 9.53669 6.66694 8.80031C6.66694 8.06393 7.26389 7.46698 8.00027 7.46698C8.73665 7.46698 9.3336 8.06393 9.3336 8.80031ZM11.4326 3.02182L3.57641 5.12689C3.39609 5.1752 3.30593 5.19936 3.24646 5.25291C3.19415 5.30001 3.15809 5.36247 3.14345 5.43132C3.12681 5.5096 3.15097 5.59976 3.19929 5.78008L3.78595 7.96951C3.83426 8.14984 3.85842 8.24 3.91197 8.29947C3.95907 8.35178 4.02153 8.38784 4.09038 8.40248C4.16866 8.41911 4.25882 8.39496 4.43914 8.34664L12.2953 6.24158L11.4326 3.02182ZM14.5285 6.33338C13.8072 6.52665 13.4466 6.62328 13.1335 6.55673C12.8581 6.49819 12.6082 6.35396 12.4198 6.14471C12.2056 5.90682 12.109 5.54618 11.9157 4.82489L11.8122 4.43852C11.6189 3.71722 11.5223 3.35658 11.5889 3.04347C11.6474 2.76805 11.7916 2.51823 12.0009 2.32982C12.2388 2.11563 12.5994 2.019 13.3207 1.82573C13.501 1.77741 13.5912 1.75325 13.6695 1.76989C13.7383 1.78452 13.8008 1.82058 13.8479 1.87289C13.9014 1.93237 13.9256 2.02253 13.9739 2.20285L14.9057 5.68018C14.954 5.86051 14.9781 5.95067 14.9615 6.02894C14.9469 6.0978 14.9108 6.16025 14.8585 6.20736C14.799 6.2609 14.7088 6.28506 14.5285 6.33338ZM2.33475 8.22033L3.23628 7.97876C3.4166 7.93044 3.50676 7.90628 3.56623 7.85274C3.61854 7.80563 3.6546 7.74318 3.66924 7.67433C3.68588 7.59605 3.66172 7.50589 3.6134 7.32556L3.37184 6.42403C3.32352 6.24371 3.29936 6.15355 3.24581 6.09408C3.19871 6.04176 3.13626 6.00571 3.0674 5.99107C2.98912 5.97443 2.89896 5.99859 2.71864 6.04691L1.81711 6.28847C1.63678 6.33679 1.54662 6.36095 1.48715 6.4145C1.43484 6.4616 1.39878 6.52405 1.38415 6.59291C1.36751 6.67119 1.39167 6.76135 1.43998 6.94167L1.68155 7.8432C1.72987 8.02352 1.75402 8.11369 1.80757 8.17316C1.85467 8.22547 1.91713 8.26153 1.98598 8.27616C2.06426 8.2928 2.15442 8.26864 2.33475 8.22033Z" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M8.74786 9.89676L12.0003 14.6669M7.25269 9.89676L4.00027 14.6669M9.3336 8.80031C9.3336 9.53669 8.73665 10.1336 8.00027 10.1336C7.26389 10.1336 6.66694 9.53669 6.66694 8.80031C6.66694 8.06393 7.26389 7.46698 8.00027 7.46698C8.73665 7.46698 9.3336 8.06393 9.3336 8.80031ZM11.4326 3.02182L3.57641 5.12689C3.39609 5.1752 3.30593 5.19936 3.24646 5.25291C3.19415 5.30001 3.15809 5.36247 3.14345 5.43132C3.12681 5.5096 3.15097 5.59976 3.19929 5.78008L3.78595 7.96951C3.83426 8.14984 3.85842 8.24 3.91197 8.29947C3.95907 8.35178 4.02153 8.38784 4.09038 8.40248C4.16866 8.41911 4.25882 8.39496 4.43914 8.34664L12.2953 6.24158L11.4326 3.02182ZM14.5285 6.33338C13.8072 6.52665 13.4466 6.62328 13.1335 6.55673C12.8581 6.49819 12.6082 6.35396 12.4198 6.14471C12.2056 5.90682 12.109 5.54618 11.9157 4.82489L11.8122 4.43852C11.6189 3.71722 11.5223 3.35658 11.5889 3.04347C11.6474 2.76805 11.7916 2.51823 12.0009 2.32982C12.2388 2.11563 12.5994 2.019 13.3207 1.82573C13.501 1.77741 13.5912 1.75325 13.6695 1.76989C13.7383 1.78452 13.8008 1.82058 13.8479 1.87289C13.9014 1.93237 13.9256 2.02253 13.9739 2.20285L14.9057 5.68018C14.954 5.86051 14.9781 5.95067 14.9615 6.02894C14.9469 6.0978 14.9108 6.16025 14.8585 6.20736C14.799 6.2609 14.7088 6.28506 14.5285 6.33338ZM2.33475 8.22033L3.23628 7.97876C3.4166 7.93044 3.50676 7.90628 3.56623 7.85274C3.61854 7.80563 3.6546 7.74318 3.66924 7.67433C3.68588 7.59605 3.66172 7.50589 3.6134 7.32556L3.37184 6.42403C3.32352 6.24371 3.29936 6.15355 3.24581 6.09408C3.19871 6.04176 3.13626 6.00571 3.0674 5.99107C2.98912 5.97443 2.89896 5.99859 2.71864 6.04691L1.81711 6.28847C1.63678 6.33679 1.54662 6.36095 1.48715 6.4145C1.43484 6.4616 1.39878 6.52405 1.38415 6.59291C1.36751 6.67119 1.39167 6.76135 1.43998 6.94167L1.68155 7.8432C1.72987 8.02352 1.75402 8.11369 1.80757 8.17316C1.85467 8.22547 1.91713 8.26153 1.98598 8.27616C2.06426 8.2928 2.15442 8.26864 2.33475 8.22033Z" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const SelectedChatIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M8.00016 1.3335C4.31826 1.3335 1.3335 4.31826 1.3335 8.00016C1.3335 8.88571 1.50651 9.7325 1.8212 10.5074C1.84962 10.5773 1.86597 10.6178 1.87718 10.6476L1.88058 10.6568L1.88016 10.66C1.87683 10.6846 1.87131 10.7181 1.86064 10.7821L1.46212 13.1732C1.44424 13.2803 1.42423 13.4001 1.41638 13.5041C1.40782 13.6176 1.40484 13.7981 1.48665 13.9888C1.58779 14.2246 1.77569 14.4125 2.0115 14.5137C2.20224 14.5955 2.38274 14.5925 2.49619 14.5839C2.60025 14.5761 2.72006 14.5561 2.82715 14.5382L5.2182 14.1397C5.28222 14.129 5.31576 14.1235 5.34036 14.1202L5.34353 14.1197L5.35274 14.1231C5.38258 14.1344 5.42298 14.1507 5.49297 14.1791C6.26783 14.4938 7.11462 14.6668 8.00016 14.6668C11.6821 14.6668 14.6668 11.6821 14.6668 8.00016C14.6668 4.31826 11.6821 1.3335 8.00016 1.3335ZM4.00016 8.00016C4.00016 7.44788 4.44788 7.00016 5.00016 7.00016C5.55245 7.00016 6.00016 7.44788 6.00016 8.00016C6.00016 8.55245 5.55245 9.00016 5.00016 9.00016C4.44788 9.00016 4.00016 8.55245 4.00016 8.00016ZM7.00016 8.00016C7.00016 7.44788 7.44788 7.00016 8.00016 7.00016C8.55245 7.00016 9.00016 7.44788 9.00016 8.00016C9.00016 8.55245 8.55245 9.00016 8.00016 9.00016C7.44788 9.00016 7.00016 8.55245 7.00016 8.00016ZM11.0002 7.00016C10.4479 7.00016 10.0002 7.44788 10.0002 8.00016C10.0002 8.55245 10.4479 9.00016 11.0002 9.00016C11.5524 9.00016 12.0002 8.55245 12.0002 8.00016C12.0002 7.44788 11.5524 7.00016 11.0002 7.00016Z" fill="#155EEF"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const ChatIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 8H5.00667M8 8H8.00667M11 8H11.0067M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 8.7981 2.15582 9.5598 2.43871 10.2563C2.49285 10.3897 2.51992 10.4563 2.532 10.5102C2.54381 10.5629 2.54813 10.6019 2.54814 10.6559C2.54814 10.7111 2.53812 10.7713 2.51807 10.8916L2.12275 13.2635C2.08135 13.5119 2.06065 13.6361 2.09917 13.7259C2.13289 13.8045 2.19552 13.8671 2.27412 13.9008C2.36393 13.9393 2.48812 13.9186 2.73651 13.8772L5.10843 13.4819C5.22872 13.4619 5.28887 13.4519 5.34409 13.4519C5.3981 13.4519 5.43711 13.4562 5.48981 13.468C5.54369 13.4801 5.61035 13.5072 5.74366 13.5613C6.4402 13.8442 7.2019 14 8 14ZM5.33333 8C5.33333 8.1841 5.1841 8.33333 5 8.33333C4.81591 8.33333 4.66667 8.1841 4.66667 8C4.66667 7.81591 4.81591 7.66667 5 7.66667C5.1841 7.66667 5.33333 7.81591 5.33333 8ZM8.33333 8C8.33333 8.1841 8.1841 8.33333 8 8.33333C7.81591 8.33333 7.66667 8.1841 7.66667 8C7.66667 7.81591 7.81591 7.66667 8 7.66667C8.1841 7.66667 8.33333 7.81591 8.33333 8ZM11.3333 8C11.3333 8.1841 11.1841 8.33333 11 8.33333C10.8159 8.33333 10.6667 8.1841 10.6667 8C10.6667 7.81591 10.8159 7.66667 11 7.66667C11.1841 7.66667 11.3333 7.81591 11.3333 8Z" stroke="#344054" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
@@ -33,6 +45,7 @@ const SideBar: FC<{
|
||||
const segments = useSelectedLayoutSegments()
|
||||
const lastSegment = segments.slice(-1)[0]
|
||||
const isDiscoverySelected = lastSegment === 'apps'
|
||||
const isChatSelected = lastSegment === 'chat'
|
||||
const { installedApps, setInstalledApps } = useContext(ExploreContext)
|
||||
|
||||
const fetchInstalledAppList = async () => {
|
||||
@@ -81,6 +94,14 @@ const SideBar: FC<{
|
||||
{isDiscoverySelected ? <SelectedDiscoveryIcon /> : <DiscoveryIcon />}
|
||||
<div className='text-sm'>{t('explore.sidebar.discovery')}</div>
|
||||
</Link>
|
||||
<Link
|
||||
href='/explore/chat'
|
||||
className={cn(isChatSelected ? 'text-primary-600 bg-white font-semibold' : 'text-gray-700 font-medium', 'flex items-center h-9 pl-3 space-x-2 rounded-lg')}
|
||||
style={isChatSelected ? { boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)' } : {}}
|
||||
>
|
||||
{isChatSelected ? <SelectedChatIcon /> : <ChatIcon />}
|
||||
<div className='text-sm'>{t('explore.sidebar.chat')}</div>
|
||||
</Link>
|
||||
</div>
|
||||
{installedApps.length > 0 && (
|
||||
<div className='mt-10'>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import s from './style.module.css'
|
||||
import Config from '@/app/components/explore/universal-chat/config'
|
||||
|
||||
type Props = {
|
||||
modelId: string
|
||||
plugins: Record<string, boolean>
|
||||
dataSets: any[]
|
||||
}
|
||||
const ConfigViewPanel: FC<Props> = ({
|
||||
modelId,
|
||||
plugins,
|
||||
dataSets,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className={cn('absolute top-9 right-0 z-20 p-4 bg-white rounded-2xl shadow-md', s.panelBorder)}>
|
||||
<div className='w-[368px]'>
|
||||
<Config
|
||||
readonly
|
||||
modelId={modelId}
|
||||
plugins={plugins}
|
||||
dataSets={dataSets}
|
||||
/>
|
||||
<div className='mt-3 text-xs leading-[18px] text-500 font-normal'>{t('explore.universalChat.viewConfigDetailTip')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ConfigViewPanel)
|
||||
@@ -0,0 +1,9 @@
|
||||
.btn {
|
||||
background: url(~@/app/components/datasets/documents/assets/action.svg) center center no-repeat transparent;
|
||||
background-size: 16px 16px;
|
||||
/* mask-image: ; */
|
||||
}
|
||||
|
||||
.panelBorder {
|
||||
border: 0.5px solid rgba(0, 0, 0, .05);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useBoolean, useClickAway } from 'ahooks'
|
||||
import s from './style.module.css'
|
||||
import ModelIcon from '@/app/components/app/configuration/config-model/model-icon'
|
||||
import { Google, WebReader, Wikipedia } from '@/app/components/base/icons/src/public/plugins'
|
||||
import ConfigDetail from '@/app/components/explore/universal-chat/config-view/detail'
|
||||
|
||||
export type ISummaryProps = {
|
||||
modelId: string
|
||||
plugins: Record<string, boolean>
|
||||
dataSets: any[]
|
||||
}
|
||||
|
||||
const getColorInfo = (modelId: string) => {
|
||||
if (modelId === 'gpt-4')
|
||||
return s.gpt4
|
||||
|
||||
if (modelId === 'claude-2')
|
||||
return s.claude
|
||||
|
||||
return s.gpt3
|
||||
}
|
||||
|
||||
const getPlugIcon = (pluginId: string) => {
|
||||
const className = 'w-4 h-4'
|
||||
switch (pluginId) {
|
||||
case 'google_search':
|
||||
return <Google className={className} />
|
||||
case 'web_reader':
|
||||
return <WebReader className={className} />
|
||||
case 'wikipedia':
|
||||
return <Wikipedia className={className} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const Summary: FC<ISummaryProps> = ({
|
||||
modelId,
|
||||
plugins,
|
||||
dataSets,
|
||||
}) => {
|
||||
const pluginIds = Object.keys(plugins).filter(key => plugins[key])
|
||||
const [isShowConfig, { setFalse: hideConfig, toggle: toggleShowConfig }] = useBoolean(false)
|
||||
const configContentRef = React.useRef(null)
|
||||
|
||||
useClickAway(() => {
|
||||
hideConfig()
|
||||
}, configContentRef)
|
||||
return (
|
||||
<div ref={configContentRef} className='relative'>
|
||||
<div onClick={toggleShowConfig} className={cn(getColorInfo(modelId), 'flex items-center px-1 h-8 rounded-lg border cursor-pointer')}>
|
||||
<ModelIcon modelId={modelId} className='!w-6 !h-6' />
|
||||
<div className='ml-2 text-[13px] font-medium text-gray-900'>{modelId}</div>
|
||||
{
|
||||
pluginIds.length > 0 && (
|
||||
<div className='ml-1.5 flex items-center'>
|
||||
<div className='mr-1 h-3 w-[1px] bg-[#000] opacity-[0.05]'></div>
|
||||
<div className='flex space-x-1'>
|
||||
{pluginIds.map(pluginId => (
|
||||
<div
|
||||
key={pluginId}
|
||||
className={`flex items-center justify-center w-6 h-6 rounded-md ${s.border} bg-white`}
|
||||
>
|
||||
{getPlugIcon(pluginId)}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{isShowConfig && (
|
||||
<ConfigDetail
|
||||
modelId={modelId} plugins={plugins} dataSets={dataSets}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(Summary)
|
||||
@@ -0,0 +1,21 @@
|
||||
.border {
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.gpt3 {
|
||||
background: linear-gradient(0deg, #D3F8DF, #D3F8DF),
|
||||
linear-gradient(0deg, #EDFCF2, #EDFCF2);
|
||||
border: 1px solid rgba(211, 248, 223, 1)
|
||||
}
|
||||
|
||||
.gpt4 {
|
||||
background: linear-gradient(0deg, #EBE9FE, #EBE9FE),
|
||||
linear-gradient(0deg, #F4F3FF, #F4F3FF);
|
||||
border: 1px solid rgba(235, 233, 254, 1)
|
||||
}
|
||||
|
||||
.claude {
|
||||
background: linear-gradient(0deg, #F9EBDF, #F9EBDF),
|
||||
linear-gradient(0deg, #FCF3EB, #FCF3EB);
|
||||
border: 1px solid rgba(249, 235, 223, 1)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { isEqual } from 'lodash-es'
|
||||
import produce from 'immer'
|
||||
import FeaturePanel from '@/app/components/app/configuration/base/feature-panel'
|
||||
import OperationBtn from '@/app/components/app/configuration/base/operation-btn'
|
||||
import CardItem from '@/app/components/app/configuration/dataset-config/card-item'
|
||||
import SelectDataSet from '@/app/components/app/configuration/dataset-config/select-dataset'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
|
||||
type Props = {
|
||||
readonly?: boolean
|
||||
dataSets: DataSet[]
|
||||
onChange?: (data: DataSet[]) => void
|
||||
}
|
||||
|
||||
const DatasetConfig: FC<Props> = ({
|
||||
readonly,
|
||||
dataSets,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const selectedIds = dataSets.map(item => item.id)
|
||||
|
||||
const hasData = dataSets.length > 0
|
||||
const [isShowSelectDataSet, { setTrue: showSelectDataSet, setFalse: hideSelectDataSet }] = useBoolean(false)
|
||||
const handleSelect = (data: DataSet[]) => {
|
||||
if (isEqual(data.map(item => item.id), dataSets.map(item => item.id))) {
|
||||
hideSelectDataSet()
|
||||
return
|
||||
}
|
||||
|
||||
if (data.find(item => !item.name)) { // has not loaded selected dataset
|
||||
const newSelected = produce(data, (draft) => {
|
||||
data.forEach((item, index) => {
|
||||
if (!item.name) { // not fetched database
|
||||
const newItem = dataSets.find(i => i.id === item.id)
|
||||
if (newItem)
|
||||
draft[index] = newItem
|
||||
}
|
||||
})
|
||||
})
|
||||
onChange?.(newSelected)
|
||||
}
|
||||
else {
|
||||
onChange?.(data)
|
||||
}
|
||||
hideSelectDataSet()
|
||||
}
|
||||
const onRemove = (id: string) => {
|
||||
onChange?.(dataSets.filter(item => item.id !== id))
|
||||
}
|
||||
|
||||
return (
|
||||
<FeaturePanel
|
||||
className='mt-3'
|
||||
title={t('appDebug.feature.dataSet.title')}
|
||||
headerRight={!readonly && <OperationBtn type="add" onClick={showSelectDataSet} />}
|
||||
hasHeaderBottomBorder={!hasData}
|
||||
>
|
||||
{hasData
|
||||
? (
|
||||
<div className='max-h-[220px] overflow-y-auto'>
|
||||
{dataSets.map(item => (
|
||||
<CardItem
|
||||
className="mb-2 !w-full"
|
||||
key={item.id}
|
||||
config={item}
|
||||
onRemove={onRemove}
|
||||
readonly={readonly}
|
||||
// TODO: readonly remove btn
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className='pt-2 pb-1 text-xs text-gray-500'>{t('appDebug.feature.dataSet.noData')}</div>
|
||||
)}
|
||||
|
||||
{isShowSelectDataSet && (
|
||||
<SelectDataSet
|
||||
isShow={isShowSelectDataSet}
|
||||
onClose={hideSelectDataSet}
|
||||
selectedIds={selectedIds}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
)}
|
||||
</FeaturePanel>
|
||||
)
|
||||
}
|
||||
export default React.memo(DatasetConfig)
|
||||
51
web/app/components/explore/universal-chat/config/index.tsx
Normal file
51
web/app/components/explore/universal-chat/config/index.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import ModelConfig from './model-config'
|
||||
import DataConfig from './data-config'
|
||||
import PluginConfig from './plugins-config'
|
||||
|
||||
export type IConfigProps = {
|
||||
className?: string
|
||||
readonly?: boolean
|
||||
modelId: string
|
||||
onModelChange?: (modelId: string) => void
|
||||
plugins: Record<string, boolean>
|
||||
onPluginChange?: (key: string, value: boolean) => void
|
||||
dataSets: any[]
|
||||
onDataSetsChange?: (contexts: any[]) => void
|
||||
}
|
||||
|
||||
const Config: FC<IConfigProps> = ({
|
||||
className,
|
||||
readonly,
|
||||
modelId,
|
||||
onModelChange,
|
||||
plugins,
|
||||
onPluginChange,
|
||||
dataSets,
|
||||
onDataSetsChange,
|
||||
}) => {
|
||||
return (
|
||||
<div className={className}>
|
||||
<ModelConfig
|
||||
readonly={readonly}
|
||||
modelId={modelId}
|
||||
onChange={onModelChange}
|
||||
/>
|
||||
<PluginConfig
|
||||
readonly={readonly}
|
||||
config={plugins}
|
||||
onChange={onPluginChange}
|
||||
/>
|
||||
{(!readonly || (readonly && dataSets.length > 0)) && (
|
||||
<DataConfig
|
||||
readonly={readonly}
|
||||
dataSets={dataSets}
|
||||
onChange={onDataSetsChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Config)
|
||||
@@ -0,0 +1,61 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useBoolean, useClickAway } from 'ahooks'
|
||||
import { ChevronDownIcon } from '@heroicons/react/24/outline'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ModelIcon from '@/app/components/app/configuration/config-model/model-icon'
|
||||
import { UNIVERSAL_CHAT_MODEL_LIST as MODEL_LIST } from '@/config'
|
||||
import { Checked as CheckedIcon } from '@/app/components/base/icons/src/public/model'
|
||||
export type IModelConfigProps = {
|
||||
modelId: string
|
||||
onChange?: (model: string) => void
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
const ModelConfig: FC<IModelConfigProps> = ({
|
||||
modelId,
|
||||
onChange,
|
||||
readonly,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const currModel = MODEL_LIST.find(item => item.id === modelId)
|
||||
const [isShowOption, { setFalse: hideOption, toggle: toogleOption }] = useBoolean(false)
|
||||
const triggerRef = React.useRef(null)
|
||||
useClickAway(() => {
|
||||
hideOption()
|
||||
}, triggerRef)
|
||||
|
||||
return (
|
||||
<div className='flex items-center justify-between h-[52px] px-3 rounded-xl bg-gray-50'>
|
||||
<div className='text-sm font-semibold text-gray-800'>{t('explore.universalChat.model')}</div>
|
||||
<div className="relative z-10">
|
||||
<div
|
||||
ref={triggerRef}
|
||||
onClick={() => !readonly && toogleOption()}
|
||||
className={cn(
|
||||
readonly ? 'cursor-not-allowed' : 'cursor-pointer', 'flex items-center h-9 px-3 space-x-2 rounded-lg',
|
||||
isShowOption && 'bg-gray-100',
|
||||
)}>
|
||||
<ModelIcon modelId={currModel?.id as string} />
|
||||
<div className="text-sm gray-900">{currModel?.name}</div>
|
||||
{!readonly && <ChevronDownIcon className={cn(isShowOption && 'rotate-180', 'w-[14px] h-[14px] text-gray-500')} />}
|
||||
</div>
|
||||
{isShowOption && (
|
||||
<div className={cn('absolute top-10 right-0 bg-white rounded-lg shadow')}>
|
||||
{MODEL_LIST.map(item => (
|
||||
<div key={item.id} onClick={() => onChange?.(item.id)} className="w-[232px] flex items-center h-9 px-4 rounded-lg cursor-pointer hover:bg-gray-100">
|
||||
<ModelIcon className='shrink-0 mr-2' modelId={item?.id} />
|
||||
<div className="text-sm gray-900 whitespace-nowrap">{item.name}</div>
|
||||
{(item.id === currModel?.id) && <CheckedIcon className='absolute right-4' />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ModelConfig)
|
||||
@@ -0,0 +1,111 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Item from './item'
|
||||
import FeaturePanel from '@/app/components/app/configuration/base/feature-panel'
|
||||
import { Google, WebReader, Wikipedia } from '@/app/components/base/icons/src/public/plugins'
|
||||
import { getToolProviders } from '@/service/explore'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import AccountSetting from '@/app/components/header/account-setting'
|
||||
|
||||
export type IPluginsProps = {
|
||||
readonly?: boolean
|
||||
config: Record<string, boolean>
|
||||
onChange?: (key: string, value: boolean) => void
|
||||
}
|
||||
|
||||
const plugins = [
|
||||
{ key: 'google_search', icon: <Google /> },
|
||||
{ key: 'web_reader', icon: <WebReader /> },
|
||||
{ key: 'wikipedia', icon: <Wikipedia /> },
|
||||
]
|
||||
const Plugins: FC<IPluginsProps> = ({
|
||||
readonly,
|
||||
config,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isLoading, setIsLoading] = React.useState(!readonly)
|
||||
const [isSerpApiValid, setIsSerpApiValid] = React.useState(false)
|
||||
const checkSerpApiKey = async () => {
|
||||
if (readonly)
|
||||
return
|
||||
|
||||
const provides: any = await getToolProviders()
|
||||
const isSerpApiValid = !!provides.find((v: any) => v.tool_name === 'serpapi' && v.is_enabled)
|
||||
setIsSerpApiValid(isSerpApiValid)
|
||||
setIsLoading(false)
|
||||
}
|
||||
useEffect(() => {
|
||||
checkSerpApiKey()
|
||||
}, [])
|
||||
|
||||
const [showSetSerpAPIKeyModal, setShowSetAPIKeyModal] = React.useState(false)
|
||||
|
||||
const itemConfigs = plugins.map((plugin) => {
|
||||
const res: Record<string, any> = { ...plugin }
|
||||
const { key } = plugin
|
||||
res.name = t(`explore.universalChat.plugins.${key}.name`)
|
||||
if (key === 'web_reader')
|
||||
res.description = t(`explore.universalChat.plugins.${key}.description`)
|
||||
|
||||
if (key === 'google_search' && !isSerpApiValid && !readonly) {
|
||||
res.readonly = true
|
||||
res.more = (
|
||||
<div className='border-t border-[#FEF0C7] flex items-center h-[34px] pl-2 bg-[#FFFAEB] text-gray-700 text-xs '>
|
||||
<span className='whitespace-pre'>{t('explore.universalChat.plugins.google_search.more.left')}</span>
|
||||
<span className='cursor-pointer text-[#155EEF]' onClick={() => setShowSetAPIKeyModal(true)}>{t('explore.universalChat.plugins.google_search.more.link')}</span>
|
||||
<span className='whitespace-pre'>{t('explore.universalChat.plugins.google_search.more.right')}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return res
|
||||
})
|
||||
|
||||
const enabledPluginNum = Object.values(config).filter(v => v).length
|
||||
|
||||
return (
|
||||
<>
|
||||
<FeaturePanel
|
||||
className='mt-3'
|
||||
title={
|
||||
<div className='flex space-x-1'>
|
||||
<div>{t('explore.universalChat.plugins.name')}</div>
|
||||
<div className='text-[13px] font-normal text-gray-500'>({enabledPluginNum}/{plugins.length})</div>
|
||||
</div>}
|
||||
hasHeaderBottomBorder={false}
|
||||
>
|
||||
{isLoading
|
||||
? (
|
||||
<div className='flex items-center h-[166px]'>
|
||||
<Loading type='area' />
|
||||
</div>
|
||||
)
|
||||
: (<div className='space-y-2'>
|
||||
{itemConfigs.map(item => (
|
||||
<Item
|
||||
key={item.key}
|
||||
icon={item.icon}
|
||||
name={item.name}
|
||||
description={item.description}
|
||||
more={item.more}
|
||||
enabled={config[item.key]}
|
||||
onChange={enabled => onChange?.(item.key, enabled)}
|
||||
readonly={readonly || item.readonly}
|
||||
/>
|
||||
))}
|
||||
</div>)}
|
||||
</FeaturePanel>
|
||||
{
|
||||
showSetSerpAPIKeyModal && (
|
||||
<AccountSetting activeTab="plugin" onCancel={async () => {
|
||||
setShowSetAPIKeyModal(false)
|
||||
await checkSerpApiKey()
|
||||
}} />
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(Plugins)
|
||||
@@ -0,0 +1,3 @@
|
||||
.shadow {
|
||||
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import s from './item.module.css'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
|
||||
export type IItemProps = {
|
||||
icon: React.ReactNode
|
||||
name: string
|
||||
description?: string
|
||||
more?: React.ReactNode
|
||||
enabled: boolean
|
||||
onChange: (enabled: boolean) => void
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
const Item: FC<IItemProps> = ({
|
||||
icon,
|
||||
name,
|
||||
description,
|
||||
more,
|
||||
enabled,
|
||||
onChange,
|
||||
readonly,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn('bg-white rounded-xl border border-gray-200 overflow-hidden', s.shadow)}>
|
||||
<div className='flex justify-between items-center min-h-[48px] px-2'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
{icon}
|
||||
<div className='leading-[18px]'>
|
||||
<div className='text-[13px] font-medium text-gray-800'>{name}</div>
|
||||
{description && <div className='text-xs leading-[18px] text-gray-500'>{description}</div>}
|
||||
</div>
|
||||
</div>
|
||||
<Switch size='md' defaultValue={enabled} onChange={onChange} disabled={readonly} />
|
||||
</div>
|
||||
{more}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Item)
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useState } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useGetState } from 'ahooks'
|
||||
import type { ConversationItem } from '@/models/share'
|
||||
|
||||
const storageConversationIdKey = 'conversationIdInfo'
|
||||
|
||||
type ConversationInfoType = Omit<ConversationItem, 'inputs' | 'id'>
|
||||
function useConversation() {
|
||||
const [conversationList, setConversationList] = useState<ConversationItem[]>([])
|
||||
const [pinnedConversationList, setPinnedConversationList] = useState<ConversationItem[]>([])
|
||||
const [currConversationId, doSetCurrConversationId, getCurrConversationId] = useGetState<string>('-1')
|
||||
// when set conversation id, we do not have set appId
|
||||
const setCurrConversationId = (id: string, appId: string, isSetToLocalStroge = true, newConversationName = '') => {
|
||||
doSetCurrConversationId(id)
|
||||
if (isSetToLocalStroge && id !== '-1') {
|
||||
// conversationIdInfo: {[appId1]: conversationId1, [appId2]: conversationId2}
|
||||
const conversationIdInfo = globalThis.localStorage?.getItem(storageConversationIdKey) ? JSON.parse(globalThis.localStorage?.getItem(storageConversationIdKey) || '') : {}
|
||||
conversationIdInfo[appId] = id
|
||||
globalThis.localStorage?.setItem(storageConversationIdKey, JSON.stringify(conversationIdInfo))
|
||||
}
|
||||
}
|
||||
|
||||
const getConversationIdFromStorage = (appId: string) => {
|
||||
const conversationIdInfo = globalThis.localStorage?.getItem(storageConversationIdKey) ? JSON.parse(globalThis.localStorage?.getItem(storageConversationIdKey) || '') : {}
|
||||
const id = conversationIdInfo[appId]
|
||||
return id
|
||||
}
|
||||
|
||||
const isNewConversation = currConversationId === '-1'
|
||||
// input can be updated by user
|
||||
const [newConversationInputs, setNewConversationInputs] = useState<Record<string, any> | null>(null)
|
||||
const resetNewConversationInputs = () => {
|
||||
if (!newConversationInputs)
|
||||
return
|
||||
setNewConversationInputs(produce(newConversationInputs, (draft) => {
|
||||
Object.keys(draft).forEach((key) => {
|
||||
draft[key] = ''
|
||||
})
|
||||
}))
|
||||
}
|
||||
const [existConversationInputs, setExistConversationInputs] = useState<Record<string, any> | null>(null)
|
||||
const currInputs = isNewConversation ? newConversationInputs : existConversationInputs
|
||||
const setCurrInputs = isNewConversation ? setNewConversationInputs : setExistConversationInputs
|
||||
|
||||
// info is muted
|
||||
const [newConversationInfo, setNewConversationInfo] = useState<ConversationInfoType | null>(null)
|
||||
const [existConversationInfo, setExistConversationInfo] = useState<ConversationInfoType | null>(null)
|
||||
const currConversationInfo = isNewConversation ? newConversationInfo : existConversationInfo
|
||||
|
||||
return {
|
||||
conversationList,
|
||||
setConversationList,
|
||||
pinnedConversationList,
|
||||
setPinnedConversationList,
|
||||
currConversationId,
|
||||
getCurrConversationId,
|
||||
setCurrConversationId,
|
||||
getConversationIdFromStorage,
|
||||
isNewConversation,
|
||||
currInputs,
|
||||
newConversationInputs,
|
||||
existConversationInputs,
|
||||
resetNewConversationInputs,
|
||||
setCurrInputs,
|
||||
currConversationInfo,
|
||||
setNewConversationInfo,
|
||||
setExistConversationInfo,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConversation
|
||||
725
web/app/components/explore/universal-chat/index.tsx
Normal file
725
web/app/components/explore/universal-chat/index.tsx
Normal file
@@ -0,0 +1,725 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import produce from 'immer'
|
||||
import { useBoolean, useGetState } from 'ahooks'
|
||||
import AppUnavailable from '../../base/app-unavailable'
|
||||
import useConversation from './hooks/use-conversation'
|
||||
import s from './style.module.css'
|
||||
import Init from './init'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import Sidebar from '@/app/components/share/chat/sidebar'
|
||||
import {
|
||||
delConversation,
|
||||
fetchAppParams,
|
||||
fetchChatList,
|
||||
fetchConversations,
|
||||
fetchSuggestedQuestions,
|
||||
pinConversation,
|
||||
sendChatMessage,
|
||||
stopChatMessageResponding,
|
||||
unpinConversation,
|
||||
updateFeedback,
|
||||
} from '@/service/universal-chat'
|
||||
import type { ConversationItem, SiteInfo } from '@/models/share'
|
||||
import type { PromptConfig, SuggestedQuestionsAfterAnswerConfig } from '@/models/debug'
|
||||
import type { Feedbacktype, IChatItem } from '@/app/components/app/chat/type'
|
||||
import Chat from '@/app/components/app/chat'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
|
||||
import { userInputsFormToPromptVariables } from '@/utils/model-config'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import ConfigSummary from '@/app/components/explore/universal-chat/config-view/summary'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
import ItemOperation from '@/app/components/explore/item-operation'
|
||||
|
||||
const APP_ID = 'universal-chat'
|
||||
const DEFAULT_MODEL_ID = 'gpt-3.5-turbo' // gpt-4, claude-2
|
||||
const DEFAULT_PLUGIN = {
|
||||
google_search: false,
|
||||
web_reader: true,
|
||||
wikipedia: true,
|
||||
}
|
||||
export type IMainProps = {}
|
||||
|
||||
const Main: FC<IMainProps> = () => {
|
||||
const { t } = useTranslation()
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
|
||||
/*
|
||||
* app info
|
||||
*/
|
||||
const [appUnavailable, setAppUnavailable] = useState<boolean>(false)
|
||||
const [isUnknwonReason, setIsUnknwonReason] = useState<boolean>(false)
|
||||
const siteInfo: SiteInfo = (
|
||||
{
|
||||
title: 'universal Chatbot',
|
||||
icon: '',
|
||||
icon_background: '',
|
||||
description: '',
|
||||
default_language: 'en', // TODO
|
||||
prompt_public: true,
|
||||
}
|
||||
)
|
||||
const [promptConfig, setPromptConfig] = useState<PromptConfig | null>(null)
|
||||
const [inited, setInited] = useState<boolean>(false)
|
||||
// in mobile, show sidebar by click button
|
||||
const [isShowSidebar, { setTrue: showSidebar, setFalse: hideSidebar }] = useBoolean(false)
|
||||
/*
|
||||
* conversation info
|
||||
*/
|
||||
const [allConversationList, setAllConversationList] = useState<ConversationItem[]>([])
|
||||
const [isClearConversationList, { setTrue: clearConversationListTrue, setFalse: clearConversationListFalse }] = useBoolean(false)
|
||||
const [isClearPinnedConversationList, { setTrue: clearPinnedConversationListTrue, setFalse: clearPinnedConversationListFalse }] = useBoolean(false)
|
||||
const {
|
||||
conversationList,
|
||||
setConversationList,
|
||||
pinnedConversationList,
|
||||
setPinnedConversationList,
|
||||
currConversationId,
|
||||
getCurrConversationId,
|
||||
setCurrConversationId,
|
||||
getConversationIdFromStorage,
|
||||
isNewConversation,
|
||||
currConversationInfo,
|
||||
currInputs,
|
||||
newConversationInputs,
|
||||
// existConversationInputs,
|
||||
resetNewConversationInputs,
|
||||
setCurrInputs,
|
||||
setNewConversationInfo,
|
||||
setExistConversationInfo,
|
||||
} = useConversation()
|
||||
const [hasMore, setHasMore] = useState<boolean>(true)
|
||||
const [hasPinnedMore, setHasPinnedMore] = useState<boolean>(true)
|
||||
const onMoreLoaded = ({ data: conversations, has_more }: any) => {
|
||||
setHasMore(has_more)
|
||||
if (isClearConversationList) {
|
||||
setConversationList(conversations)
|
||||
clearConversationListFalse()
|
||||
}
|
||||
else {
|
||||
setConversationList([...conversationList, ...conversations])
|
||||
}
|
||||
}
|
||||
const onPinnedMoreLoaded = ({ data: conversations, has_more }: any) => {
|
||||
setHasPinnedMore(has_more)
|
||||
if (isClearPinnedConversationList) {
|
||||
setPinnedConversationList(conversations)
|
||||
clearPinnedConversationListFalse()
|
||||
}
|
||||
else {
|
||||
setPinnedConversationList([...pinnedConversationList, ...conversations])
|
||||
}
|
||||
}
|
||||
const [controlUpdateConversationList, setControlUpdateConversationList] = useState(0)
|
||||
const noticeUpdateList = () => {
|
||||
setHasMore(true)
|
||||
clearConversationListTrue()
|
||||
|
||||
setHasPinnedMore(true)
|
||||
clearPinnedConversationListTrue()
|
||||
|
||||
setControlUpdateConversationList(Date.now())
|
||||
}
|
||||
const handlePin = async (id: string) => {
|
||||
await pinConversation(id)
|
||||
setControlItemOpHide(Date.now())
|
||||
notify({ type: 'success', message: t('common.api.success') })
|
||||
noticeUpdateList()
|
||||
}
|
||||
|
||||
const handleUnpin = async (id: string) => {
|
||||
await unpinConversation(id)
|
||||
setControlItemOpHide(Date.now())
|
||||
notify({ type: 'success', message: t('common.api.success') })
|
||||
noticeUpdateList()
|
||||
}
|
||||
const [isShowConfirm, { setTrue: showConfirm, setFalse: hideConfirm }] = useBoolean(false)
|
||||
const [toDeleteConversationId, setToDeleteConversationId] = useState('')
|
||||
const handleDelete = (id: string) => {
|
||||
setToDeleteConversationId(id)
|
||||
hideSidebar() // mobile
|
||||
showConfirm()
|
||||
}
|
||||
|
||||
const didDelete = async () => {
|
||||
await delConversation(toDeleteConversationId)
|
||||
setControlItemOpHide(Date.now())
|
||||
notify({ type: 'success', message: t('common.api.success') })
|
||||
hideConfirm()
|
||||
if (currConversationId === toDeleteConversationId)
|
||||
handleConversationIdChange('-1')
|
||||
|
||||
noticeUpdateList()
|
||||
}
|
||||
|
||||
const [suggestedQuestionsAfterAnswerConfig, setSuggestedQuestionsAfterAnswerConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null)
|
||||
const [speechToTextConfig, setSpeechToTextConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null)
|
||||
|
||||
const [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = useGetState(false)
|
||||
|
||||
const conversationName = currConversationInfo?.name || t('share.chat.newChatDefaultName') as string
|
||||
const conversationIntroduction = currConversationInfo?.introduction || ''
|
||||
|
||||
const handleConversationSwitch = async () => {
|
||||
if (!inited)
|
||||
return
|
||||
|
||||
// update inputs of current conversation
|
||||
let notSyncToStateIntroduction = ''
|
||||
let notSyncToStateInputs: Record<string, any> | undefined | null = {}
|
||||
// debugger
|
||||
if (!isNewConversation) {
|
||||
const item = allConversationList.find(item => item.id === currConversationId) as any
|
||||
notSyncToStateInputs = item?.inputs || {}
|
||||
// setCurrInputs(notSyncToStateInputs)
|
||||
notSyncToStateIntroduction = item?.introduction || ''
|
||||
setExistConversationInfo({
|
||||
name: item?.name || '',
|
||||
introduction: notSyncToStateIntroduction,
|
||||
})
|
||||
const modelConfig = item?.model_config
|
||||
if (modelConfig) {
|
||||
setModeId(modelConfig.model_id)
|
||||
const pluginConfig: Record<string, boolean> = {}
|
||||
const datasetIds: string[] = []
|
||||
modelConfig.agent_mode.tools.forEach((item: any) => {
|
||||
const pluginName = Object.keys(item)[0]
|
||||
if (pluginName === 'dataset')
|
||||
datasetIds.push(item.dataset.id)
|
||||
else
|
||||
pluginConfig[pluginName] = item[pluginName].enabled
|
||||
})
|
||||
setPlugins(pluginConfig)
|
||||
if (datasetIds.length > 0) {
|
||||
const { data } = await fetchDatasets({ url: '/datasets', params: { page: 1, ids: datasetIds } })
|
||||
setDateSets(data)
|
||||
}
|
||||
else {
|
||||
setDateSets([])
|
||||
}
|
||||
}
|
||||
else {
|
||||
configSetDefaultValue()
|
||||
}
|
||||
}
|
||||
else {
|
||||
configSetDefaultValue()
|
||||
notSyncToStateInputs = newConversationInputs
|
||||
setCurrInputs(notSyncToStateInputs)
|
||||
}
|
||||
|
||||
// update chat list of current conversation
|
||||
if (!isNewConversation && !conversationIdChangeBecauseOfNew) {
|
||||
fetchChatList(currConversationId).then((res: any) => {
|
||||
const { data } = res
|
||||
const newChatList: IChatItem[] = generateNewChatListWithOpenstatement(notSyncToStateIntroduction, notSyncToStateInputs)
|
||||
|
||||
data.forEach((item: any) => {
|
||||
newChatList.push({
|
||||
id: `question-${item.id}`,
|
||||
content: item.query,
|
||||
isAnswer: false,
|
||||
})
|
||||
newChatList.push({
|
||||
...item,
|
||||
id: item.id,
|
||||
content: item.answer,
|
||||
feedback: item.feedback,
|
||||
isAnswer: true,
|
||||
})
|
||||
})
|
||||
setChatList(newChatList)
|
||||
setErrorHappened(false)
|
||||
})
|
||||
}
|
||||
|
||||
if (isNewConversation) {
|
||||
setChatList(generateNewChatListWithOpenstatement())
|
||||
setErrorHappened(false)
|
||||
}
|
||||
|
||||
setControlFocus(Date.now())
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
handleConversationSwitch()
|
||||
}, [currConversationId, inited])
|
||||
|
||||
const handleConversationIdChange = (id: string) => {
|
||||
if (id === '-1') {
|
||||
createNewChat()
|
||||
setConversationIdChangeBecauseOfNew(true)
|
||||
}
|
||||
else {
|
||||
setConversationIdChangeBecauseOfNew(false)
|
||||
}
|
||||
// trigger handleConversationSwitch
|
||||
setCurrConversationId(id, APP_ID)
|
||||
setIsShowSuggestion(false)
|
||||
hideSidebar()
|
||||
}
|
||||
|
||||
/*
|
||||
* chat info. chat is under conversation.
|
||||
*/
|
||||
const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([])
|
||||
const chatListDomRef = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
// scroll to bottom
|
||||
if (chatListDomRef.current)
|
||||
chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight
|
||||
}, [chatList, currConversationId])
|
||||
|
||||
// user can not edit inputs if user had send message
|
||||
const createNewChat = async () => {
|
||||
// if new chat is already exist, do not create new chat
|
||||
abortController?.abort()
|
||||
setResponsingFalse()
|
||||
if (conversationList.some(item => item.id === '-1'))
|
||||
return
|
||||
|
||||
setConversationList(produce(conversationList, (draft) => {
|
||||
draft.unshift({
|
||||
id: '-1',
|
||||
name: t('share.chat.newChatDefaultName'),
|
||||
inputs: newConversationInputs,
|
||||
introduction: conversationIntroduction,
|
||||
})
|
||||
}))
|
||||
configSetDefaultValue()
|
||||
}
|
||||
|
||||
// sometime introduction is not applied to state
|
||||
const generateNewChatListWithOpenstatement = (introduction?: string, inputs?: Record<string, any> | null) => {
|
||||
let caculatedIntroduction = introduction || conversationIntroduction || ''
|
||||
const caculatedPromptVariables = inputs || currInputs || null
|
||||
if (caculatedIntroduction && caculatedPromptVariables)
|
||||
caculatedIntroduction = replaceStringWithValues(caculatedIntroduction, promptConfig?.prompt_variables || [], caculatedPromptVariables)
|
||||
|
||||
const openstatement = {
|
||||
id: `${Date.now()}`,
|
||||
content: caculatedIntroduction,
|
||||
isAnswer: true,
|
||||
feedbackDisabled: true,
|
||||
isOpeningStatement: true,
|
||||
}
|
||||
if (caculatedIntroduction)
|
||||
return [openstatement]
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
const fetchAllConversations = () => {
|
||||
return fetchConversations(undefined, undefined, 100)
|
||||
}
|
||||
|
||||
const fetchInitData = async () => {
|
||||
return Promise.all([fetchAllConversations(), fetchAppParams()])
|
||||
}
|
||||
|
||||
// init
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const [conversationData, appParams]: any = await fetchInitData()
|
||||
const prompt_template = ''
|
||||
// handle current conversation id
|
||||
const { data: allConversations } = conversationData as { data: ConversationItem[]; has_more: boolean }
|
||||
const _conversationId = getConversationIdFromStorage(APP_ID)
|
||||
const isNotNewConversation = allConversations.some(item => item.id === _conversationId)
|
||||
setAllConversationList(allConversations)
|
||||
// fetch new conversation info
|
||||
const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text }: any = appParams
|
||||
const prompt_variables = userInputsFormToPromptVariables(user_input_form)
|
||||
|
||||
setNewConversationInfo({
|
||||
name: t('share.chat.newChatDefaultName'),
|
||||
introduction,
|
||||
})
|
||||
setPromptConfig({
|
||||
prompt_template,
|
||||
prompt_variables,
|
||||
} as PromptConfig)
|
||||
setSuggestedQuestionsAfterAnswerConfig(suggested_questions_after_answer)
|
||||
setSpeechToTextConfig(speech_to_text)
|
||||
|
||||
if (isNotNewConversation)
|
||||
setCurrConversationId(_conversationId, APP_ID, false)
|
||||
|
||||
setInited(true)
|
||||
}
|
||||
catch (e: any) {
|
||||
if (e.status === 404) {
|
||||
setAppUnavailable(true)
|
||||
}
|
||||
else {
|
||||
setIsUnknwonReason(true)
|
||||
setAppUnavailable(true)
|
||||
}
|
||||
}
|
||||
})()
|
||||
}, [])
|
||||
|
||||
const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false)
|
||||
const [abortController, setAbortController] = useState<AbortController | null>(null)
|
||||
const { notify } = useContext(ToastContext)
|
||||
const logError = (message: string) => {
|
||||
notify({ type: 'error', message })
|
||||
}
|
||||
|
||||
const checkCanSend = () => {
|
||||
if (currConversationId !== '-1')
|
||||
return true
|
||||
|
||||
const prompt_variables = promptConfig?.prompt_variables
|
||||
const inputs = currInputs
|
||||
if (!inputs || !prompt_variables || prompt_variables?.length === 0)
|
||||
return true
|
||||
|
||||
let hasEmptyInput = false
|
||||
const requiredVars = prompt_variables?.filter(({ key, name, required }) => {
|
||||
const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
|
||||
return res
|
||||
}) || [] // compatible with old version
|
||||
requiredVars.forEach(({ key }) => {
|
||||
if (hasEmptyInput)
|
||||
return
|
||||
|
||||
if (!inputs?.[key])
|
||||
hasEmptyInput = true
|
||||
})
|
||||
|
||||
if (hasEmptyInput) {
|
||||
logError(t('appDebug.errorMessage.valueOfVarRequired'))
|
||||
return false
|
||||
}
|
||||
return !hasEmptyInput
|
||||
}
|
||||
|
||||
const [controlFocus, setControlFocus] = useState(0)
|
||||
const [isShowSuggestion, setIsShowSuggestion] = useState(false)
|
||||
const doShowSuggestion = isShowSuggestion && !isResponsing
|
||||
const [suggestQuestions, setSuggestQuestions] = useState<string[]>([])
|
||||
const [messageTaskId, setMessageTaskId] = useState('')
|
||||
const [hasStopResponded, setHasStopResponded, getHasStopResponded] = useGetState(false)
|
||||
const [errorHappened, setErrorHappened] = useState(false)
|
||||
const [isResponsingConIsCurrCon, setIsResponsingConCurrCon, getIsResponsingConIsCurrCon] = useGetState(true)
|
||||
const handleSend = async (message: string) => {
|
||||
if (isResponsing) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
|
||||
return
|
||||
}
|
||||
const formattedPlugins = Object.keys(plugins).map(key => ({
|
||||
[key]: {
|
||||
enabled: plugins[key],
|
||||
},
|
||||
}))
|
||||
const formattedDataSets = dataSets.map(({ id }) => {
|
||||
return {
|
||||
dataset: {
|
||||
enabled: true,
|
||||
id,
|
||||
},
|
||||
}
|
||||
})
|
||||
const data = {
|
||||
query: message,
|
||||
conversation_id: isNewConversation ? null : currConversationId,
|
||||
model: modelId,
|
||||
tools: [...formattedPlugins, ...formattedDataSets],
|
||||
}
|
||||
|
||||
// qustion
|
||||
const questionId = `question-${Date.now()}`
|
||||
const questionItem = {
|
||||
id: questionId,
|
||||
content: message,
|
||||
agent_thoughts: [],
|
||||
isAnswer: false,
|
||||
}
|
||||
|
||||
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
|
||||
const placeholderAnswerItem = {
|
||||
id: placeholderAnswerId,
|
||||
content: '',
|
||||
isAnswer: true,
|
||||
}
|
||||
|
||||
const newList = [...getChatList(), questionItem, placeholderAnswerItem]
|
||||
setChatList(newList)
|
||||
|
||||
// answer
|
||||
const responseItem: IChatItem = {
|
||||
id: `${Date.now()}`,
|
||||
content: '',
|
||||
agent_thoughts: [],
|
||||
isAnswer: true,
|
||||
}
|
||||
|
||||
const prevTempNewConversationId = getCurrConversationId() || '-1'
|
||||
let tempNewConversationId = prevTempNewConversationId
|
||||
|
||||
setHasStopResponded(false)
|
||||
setResponsingTrue()
|
||||
setErrorHappened(false)
|
||||
setIsShowSuggestion(false)
|
||||
setIsResponsingConCurrCon(true)
|
||||
|
||||
sendChatMessage(data, {
|
||||
getAbortController: (abortController) => {
|
||||
setAbortController(abortController)
|
||||
},
|
||||
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => {
|
||||
responseItem.content = responseItem.content + message
|
||||
responseItem.id = messageId
|
||||
if (isFirstMessage && newConversationId)
|
||||
tempNewConversationId = newConversationId
|
||||
|
||||
setMessageTaskId(taskId)
|
||||
// has switched to other conversation
|
||||
if (prevTempNewConversationId !== getCurrConversationId()) {
|
||||
setIsResponsingConCurrCon(false)
|
||||
return
|
||||
}
|
||||
|
||||
// closesure new list is outdated.
|
||||
const newListWithAnswer = produce(
|
||||
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
|
||||
(draft) => {
|
||||
if (!draft.find(item => item.id === questionId))
|
||||
draft.push({ ...questionItem } as any)
|
||||
|
||||
draft.push({ ...responseItem })
|
||||
})
|
||||
|
||||
setChatList(newListWithAnswer)
|
||||
},
|
||||
async onCompleted(hasError?: boolean) {
|
||||
if (hasError) {
|
||||
setResponsingFalse()
|
||||
return
|
||||
}
|
||||
|
||||
if (getConversationIdChangeBecauseOfNew()) {
|
||||
const { data: allConversations }: any = await fetchAllConversations()
|
||||
setAllConversationList(allConversations)
|
||||
noticeUpdateList()
|
||||
}
|
||||
setConversationIdChangeBecauseOfNew(false)
|
||||
resetNewConversationInputs()
|
||||
setCurrConversationId(tempNewConversationId, APP_ID, true)
|
||||
if (getIsResponsingConIsCurrCon() && suggestedQuestionsAfterAnswerConfig?.enabled && !getHasStopResponded()) {
|
||||
const { data }: any = await fetchSuggestedQuestions(responseItem.id)
|
||||
setSuggestQuestions(data)
|
||||
setIsShowSuggestion(true)
|
||||
}
|
||||
setResponsingFalse()
|
||||
},
|
||||
onThought(thought) {
|
||||
// thought finished then start to return message. Warning: use push agent_thoughts.push would caused problem when the thought is more then 2
|
||||
responseItem.id = thought.message_id;
|
||||
(responseItem as any).agent_thoughts = [...(responseItem as any).agent_thoughts, thought] // .push(thought)
|
||||
// has switched to other conversation
|
||||
|
||||
if (prevTempNewConversationId !== getCurrConversationId()) {
|
||||
setIsResponsingConCurrCon(false)
|
||||
return
|
||||
}
|
||||
const newListWithAnswer = produce(
|
||||
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
|
||||
(draft) => {
|
||||
if (!draft.find(item => item.id === questionId))
|
||||
draft.push({ ...questionItem })
|
||||
draft.push({ ...responseItem })
|
||||
})
|
||||
setChatList(newListWithAnswer)
|
||||
},
|
||||
onError() {
|
||||
setErrorHappened(true)
|
||||
// role back placeholder answer
|
||||
setChatList(produce(getChatList(), (draft) => {
|
||||
draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1)
|
||||
}))
|
||||
setResponsingFalse()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleFeedback = async (messageId: string, feedback: Feedbacktype) => {
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } })
|
||||
const newChatList = chatList.map((item) => {
|
||||
if (item.id === messageId) {
|
||||
return {
|
||||
...item,
|
||||
feedback,
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
setChatList(newChatList)
|
||||
notify({ type: 'success', message: t('common.api.success') })
|
||||
}
|
||||
|
||||
const renderSidebar = () => {
|
||||
if (!APP_ID || !promptConfig)
|
||||
return null
|
||||
return (
|
||||
<Sidebar
|
||||
list={conversationList}
|
||||
isClearConversationList={isClearConversationList}
|
||||
pinnedList={pinnedConversationList}
|
||||
isClearPinnedConversationList={isClearPinnedConversationList}
|
||||
onMoreLoaded={onMoreLoaded}
|
||||
onPinnedMoreLoaded={onPinnedMoreLoaded}
|
||||
isNoMore={!hasMore}
|
||||
isPinnedNoMore={!hasPinnedMore}
|
||||
onCurrentIdChange={handleConversationIdChange}
|
||||
currentId={currConversationId}
|
||||
copyRight={''}
|
||||
isInstalledApp={false}
|
||||
isUniversalChat
|
||||
installedAppId={''}
|
||||
siteInfo={siteInfo}
|
||||
onPin={handlePin}
|
||||
onUnpin={handleUnpin}
|
||||
controlUpdateList={controlUpdateConversationList}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const [modelId, setModeId] = useState(DEFAULT_MODEL_ID)
|
||||
// const currModel = MODEL_LIST.find(item => item.id === modelId)
|
||||
|
||||
const [plugins, setPlugins] = useState<Record<string, boolean>>(DEFAULT_PLUGIN)
|
||||
const handlePluginsChange = (key: string, value: boolean) => {
|
||||
setPlugins({
|
||||
...plugins,
|
||||
[key]: value,
|
||||
})
|
||||
}
|
||||
const [dataSets, setDateSets] = useState<DataSet[]>([])
|
||||
const configSetDefaultValue = () => {
|
||||
setModeId(DEFAULT_MODEL_ID)
|
||||
setPlugins(DEFAULT_PLUGIN)
|
||||
setDateSets([])
|
||||
}
|
||||
const isCurrConversationPinned = !!pinnedConversationList.find(item => item.id === currConversationId)
|
||||
const [controlItemOpHide, setControlItemOpHide] = useState(0)
|
||||
if (appUnavailable)
|
||||
return <AppUnavailable isUnknwonReason={isUnknwonReason} />
|
||||
|
||||
if (!promptConfig)
|
||||
return <Loading type='app' />
|
||||
|
||||
return (
|
||||
<div className='bg-gray-100'>
|
||||
<div
|
||||
className={cn(
|
||||
'flex rounded-t-2xl bg-white overflow-hidden rounded-b-2xl',
|
||||
)}
|
||||
style={{
|
||||
boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)',
|
||||
}}
|
||||
>
|
||||
{/* sidebar */}
|
||||
{!isMobile && renderSidebar()}
|
||||
{isMobile && isShowSidebar && (
|
||||
<div className='fixed inset-0 z-50'
|
||||
style={{ backgroundColor: 'rgba(35, 56, 118, 0.2)' }}
|
||||
onClick={hideSidebar}
|
||||
>
|
||||
<div className='inline-block' onClick={e => e.stopPropagation()}>
|
||||
{renderSidebar()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* main */}
|
||||
<div className={cn(
|
||||
s.installedApp,
|
||||
'flex-grow flex flex-col overflow-y-auto',
|
||||
)
|
||||
}>
|
||||
{(!isNewConversation || isResponsing || errorHappened) && (
|
||||
<div className='mb-5 antialiased font-sans shrink-0 relative mobile:min-h-[48px] tablet:min-h-[64px]'>
|
||||
<div className='absolute z-10 top-0 left-0 right-0 flex items-center justify-between border-b border-gray-100 mobile:h-12 tablet:h-16 px-8 bg-white'>
|
||||
<div className='text-gray-900'>{conversationName}</div>
|
||||
<div className='flex items-center shrink-0 ml-2 space-x-2'>
|
||||
<ConfigSummary
|
||||
modelId={modelId}
|
||||
plugins={plugins}
|
||||
dataSets={dataSets}
|
||||
/>
|
||||
<div className={cn('flex w-8 h-8 justify-center items-center shrink-0 rounded-lg border border-gray-200')} onClick={e => e.stopPropagation()}>
|
||||
<ItemOperation
|
||||
key={controlItemOpHide}
|
||||
className='!w-8 !h-8'
|
||||
isPinned={isCurrConversationPinned}
|
||||
togglePin={() => isCurrConversationPinned ? handleUnpin(currConversationId) : handlePin(currConversationId)}
|
||||
isShowDelete
|
||||
onDelete={() => handleDelete(currConversationId)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={cn(doShowSuggestion ? 'pb-[140px]' : (isResponsing ? 'pb-[113px]' : 'pb-[76px]'), 'relative grow h-[200px] pc:w-[794px] max-w-full mobile:w-full mx-auto mb-3.5 overflow-hidden')}>
|
||||
<div className={cn('pc:w-[794px] max-w-full mobile:w-full mx-auto h-full overflow-y-auto')} ref={chatListDomRef}>
|
||||
<Chat
|
||||
isShowConfigElem={isNewConversation && chatList.length === 0}
|
||||
configElem={<Init
|
||||
modelId={modelId}
|
||||
onModelChange={setModeId}
|
||||
plugins={plugins}
|
||||
onPluginChange={handlePluginsChange}
|
||||
dataSets={dataSets}
|
||||
onDataSetsChange={setDateSets}
|
||||
/>}
|
||||
chatList={chatList}
|
||||
onSend={handleSend}
|
||||
isHideFeedbackEdit
|
||||
onFeedback={handleFeedback}
|
||||
isResponsing={isResponsing}
|
||||
canStopResponsing={!!messageTaskId && isResponsingConIsCurrCon}
|
||||
abortResponsing={async () => {
|
||||
await stopChatMessageResponding(messageTaskId)
|
||||
setHasStopResponded(true)
|
||||
setResponsingFalse()
|
||||
}}
|
||||
checkCanSend={checkCanSend}
|
||||
controlFocus={controlFocus}
|
||||
isShowSuggestion={doShowSuggestion}
|
||||
suggestionList={suggestQuestions}
|
||||
isShowSpeechToText={speechToTextConfig?.enabled}
|
||||
dataSets={dataSets}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isShowConfirm && (
|
||||
<Confirm
|
||||
title={t('share.chat.deleteConversation.title')}
|
||||
content={t('share.chat.deleteConversation.content')}
|
||||
isShow={isShowConfirm}
|
||||
onClose={hideConfirm}
|
||||
onConfirm={didDelete}
|
||||
onCancel={hideConfirm}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Main)
|
||||
43
web/app/components/explore/universal-chat/init/index.tsx
Normal file
43
web/app/components/explore/universal-chat/init/index.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import type { IConfigProps } from '../config'
|
||||
import Config from '../config'
|
||||
import s from './style.module.css'
|
||||
|
||||
const Line = (
|
||||
<svg width="720" height="1" viewBox="0 0 720 1" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line y1="0.5" x2="720" y2="0.5" stroke="url(#paint0_linear_6845_53470)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_6845_53470" x1="0" y1="1" x2="720" y2="1" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#F2F4F7" stopOpacity="0"/>
|
||||
<stop offset="0.491667" stopColor="#F2F4F7"/>
|
||||
<stop offset="1" stopColor="#F2F4F7" stopOpacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const Init: FC<IConfigProps> = ({
|
||||
...configProps
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='h-full flex items-center'>
|
||||
<div>
|
||||
<div className='w-[480px] mx-auto text-center'>
|
||||
<div className={cn(s.textGradient, 'mb-2 leading-[32px] font-semibold text-[24px]')}>{t('explore.universalChat.welcome')}</div>
|
||||
<div className='mb-2 font-normal text-sm text-gray-500'>{t('explore.universalChat.welcomeDescribe')}</div>
|
||||
</div>
|
||||
<div className='flex mb-2 mx-auto h-8 items-center'>
|
||||
{Line}
|
||||
</div>
|
||||
<Config className='w-[480px] mx-auto' {...configProps} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Init)
|
||||
@@ -0,0 +1,9 @@
|
||||
.textGradient {
|
||||
background: linear-gradient(to right, rgba(16, 74, 225, 1) 0, rgba(0, 152, 238, 1) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-fill-color: transparent;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.installedApp {
|
||||
height: calc(100vh - 74px);
|
||||
}
|
||||
Reference in New Issue
Block a user