Initial commit
65
web/app/components/app-sidebar/basic.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
InformationCircleIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
import Tooltip from '../base/tooltip'
|
||||
import AppIcon from '../base/app-icon'
|
||||
|
||||
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'
|
||||
|
||||
export function randomString(length: number) {
|
||||
let result = ''
|
||||
for (let i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)]
|
||||
return result
|
||||
}
|
||||
|
||||
export type IAppBasicProps = {
|
||||
iconType?: 'app' | 'api' | 'dataset'
|
||||
iconUrl?: string
|
||||
name: string
|
||||
type: string | React.ReactNode
|
||||
hoverTip?: string
|
||||
textStyle?: { main?: string; extra?: string }
|
||||
}
|
||||
|
||||
const AlgorithmSvg = <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.5 3.5C8.5 4.60457 9.39543 5.5 10.5 5.5C11.6046 5.5 12.5 4.60457 12.5 3.5C12.5 2.39543 11.6046 1.5 10.5 1.5C9.39543 1.5 8.5 2.39543 8.5 3.5Z" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M12.5 9C12.5 10.1046 13.3954 11 14.5 11C15.6046 11 16.5 10.1046 16.5 9C16.5 7.89543 15.6046 7 14.5 7C13.3954 7 12.5 7.89543 12.5 9Z" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M8.5 3.5H5.5L3.5 6.5" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M8.5 14.5C8.5 15.6046 9.39543 16.5 10.5 16.5C11.6046 16.5 12.5 15.6046 12.5 14.5C12.5 13.3954 11.6046 12.5 10.5 12.5C9.39543 12.5 8.5 13.3954 8.5 14.5Z" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M8.5 14.5H5.5L3.5 11.5" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M12.5 9H1.5" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
||||
const DatasetSvg = <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.833497 5.13481C0.833483 4.69553 0.83347 4.31654 0.858973 4.0044C0.88589 3.67495 0.94532 3.34727 1.10598 3.03195C1.34567 2.56155 1.72812 2.17909 2.19852 1.93941C2.51384 1.77875 2.84152 1.71932 3.17097 1.6924C3.48312 1.6669 3.86209 1.66691 4.30137 1.66693L7.62238 1.66684C8.11701 1.66618 8.55199 1.66561 8.95195 1.80356C9.30227 1.92439 9.62134 2.12159 9.88607 2.38088C10.1883 2.67692 10.3823 3.06624 10.603 3.50894L11.3484 5.00008H14.3679C15.0387 5.00007 15.5924 5.00006 16.0434 5.03691C16.5118 5.07518 16.9424 5.15732 17.3468 5.36339C17.974 5.68297 18.4839 6.19291 18.8035 6.82011C19.0096 7.22456 19.0917 7.65515 19.13 8.12356C19.1668 8.57455 19.1668 9.12818 19.1668 9.79898V13.5345C19.1668 14.2053 19.1668 14.7589 19.13 15.2099C19.0917 15.6784 19.0096 16.1089 18.8035 16.5134C18.4839 17.1406 17.974 17.6505 17.3468 17.9701C16.9424 18.1762 16.5118 18.2583 16.0434 18.2966C15.5924 18.3334 15.0387 18.3334 14.3679 18.3334H5.63243C4.96163 18.3334 4.40797 18.3334 3.95698 18.2966C3.48856 18.2583 3.05798 18.1762 2.65353 17.9701C2.02632 17.6505 1.51639 17.1406 1.19681 16.5134C0.990734 16.1089 0.908597 15.6784 0.870326 15.2099C0.833478 14.7589 0.833487 14.2053 0.833497 13.5345V5.13481ZM7.51874 3.33359C8.17742 3.33359 8.30798 3.34447 8.4085 3.37914C8.52527 3.41942 8.63163 3.48515 8.71987 3.57158C8.79584 3.64598 8.86396 3.7579 9.15852 4.34704L9.48505 5.00008L2.50023 5.00008C2.50059 4.61259 2.50314 4.34771 2.5201 4.14012C2.5386 3.91374 2.57 3.82981 2.59099 3.7886C2.67089 3.6318 2.79837 3.50432 2.95517 3.42442C2.99638 3.40343 3.08031 3.37203 3.30669 3.35353C3.54281 3.33424 3.85304 3.33359 4.3335 3.33359H7.51874Z" fill="#444CE7" />
|
||||
</svg>
|
||||
|
||||
const ICON_MAP = {
|
||||
'app': <AppIcon className='border !border-[rgba(0,0,0,0.05)]' />,
|
||||
'api': <AppIcon innerIcon={AlgorithmSvg} className='border !bg-purple-50 !border-purple-200' />,
|
||||
'dataset': <AppIcon innerIcon={DatasetSvg} className='!border-[0.5px] !border-indigo-100 !bg-indigo-25' />
|
||||
}
|
||||
|
||||
export default function AppBasic({ iconUrl, name, type, hoverTip, textStyle, iconType = 'app' }: IAppBasicProps) {
|
||||
return (
|
||||
<div className="flex items-start">
|
||||
{iconUrl && (
|
||||
<div className='flex-shrink-0 mr-3'>
|
||||
{/* <img className="inline-block rounded-lg h-9 w-9" src={iconUrl} alt={name} /> */}
|
||||
{ICON_MAP[iconType]}
|
||||
</div>
|
||||
)}
|
||||
<div className="group">
|
||||
<div className={`flex flex-row items-center text-sm font-semibold text-gray-700 group-hover:text-gray-900 ${textStyle?.main}`}>
|
||||
{name}
|
||||
{hoverTip
|
||||
&& <Tooltip content={hoverTip} selector={`a${randomString(16)}`}>
|
||||
<InformationCircleIcon className='w-4 h-4 ml-1 text-gray-400' />
|
||||
</Tooltip>}
|
||||
</div>
|
||||
<div className={`text-xs font-normal text-gray-500 group-hover:text-gray-700 ${textStyle?.extra}`}>{type}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
39
web/app/components/app-sidebar/index.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react'
|
||||
import type { FC } from 'react'
|
||||
import NavLink from './navLink'
|
||||
import AppBasic from './basic'
|
||||
|
||||
export type IAppDetailNavProps = {
|
||||
iconType?: 'app' | 'dataset'
|
||||
title: string
|
||||
desc: string
|
||||
navigation: Array<{
|
||||
name: string
|
||||
href: string
|
||||
icon: any
|
||||
selectedIcon: any
|
||||
}>
|
||||
extraInfo?: React.ReactNode
|
||||
}
|
||||
|
||||
const sampleAppIconUrl = 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80'
|
||||
|
||||
const AppDetailNav: FC<IAppDetailNavProps> = ({ title, desc, navigation, extraInfo, iconType = 'app' }) => {
|
||||
return (
|
||||
<div className="flex flex-col w-56 overflow-y-auto bg-white border-r border-gray-200 shrink-0">
|
||||
<div className="flex flex-shrink-0 p-4">
|
||||
<AppBasic iconType={iconType} iconUrl={sampleAppIconUrl} name={title} type={desc} />
|
||||
</div>
|
||||
<nav className="flex-1 p-4 space-y-1 bg-white">
|
||||
{navigation.map((item, index) => {
|
||||
return (
|
||||
<NavLink key={index} iconMap={{ selected: item.selectedIcon, normal: item.icon }} name={item.name} href={item.href} />
|
||||
)
|
||||
})}
|
||||
{extraInfo ?? null}
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(AppDetailNav)
|
||||
39
web/app/components/app-sidebar/navLink.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
'use client'
|
||||
import { useSelectedLayoutSegment } from 'next/navigation'
|
||||
import classNames from 'classnames'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function NavLink({
|
||||
name,
|
||||
href,
|
||||
iconMap,
|
||||
}: {
|
||||
name: string
|
||||
href: string
|
||||
iconMap: { selected: any; normal: any }
|
||||
}) {
|
||||
const segment = useSelectedLayoutSegment()
|
||||
const isActive = href.toLowerCase().split('/')?.pop() === segment?.toLowerCase()
|
||||
const NavIcon = isActive ? iconMap.selected : iconMap.normal
|
||||
|
||||
return (
|
||||
<Link
|
||||
prefetch
|
||||
key={name}
|
||||
href={href}
|
||||
className={classNames(
|
||||
isActive ? 'bg-primary-50 text-primary-600 font-semibold' : 'text-gray-700 hover:bg-gray-100 hover:text-gray-700',
|
||||
'group flex items-center rounded-md px-2 py-2 text-sm font-normal',
|
||||
)}
|
||||
>
|
||||
<NavIcon
|
||||
className={classNames(
|
||||
'mr-2 h-4 w-4 flex-shrink-0',
|
||||
isActive ? 'text-primary-600' : 'text-gray-700',
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{name}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
3
web/app/components/app-sidebar/style.module.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.sidebar {
|
||||
border-right: 1px solid #F3F4F6;
|
||||
}
|
||||
3
web/app/components/app/chat/icons/answer.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.03647 1.5547C0.59343 0.890144 1.06982 0 1.86852 0H8V12L1.03647 1.5547Z" fill="#F3F4F6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 202 B |
BIN
web/app/components/app/chat/icons/default-avatar.jpg
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
3
web/app/components/app/chat/icons/edit.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 11.9998L13.3332 12.7292C12.9796 13.1159 12.5001 13.3332 12.0001 13.3332C11.5001 13.3332 11.0205 13.1159 10.6669 12.7292C10.3128 12.3432 9.83332 12.1265 9.33345 12.1265C8.83359 12.1265 8.35409 12.3432 7.99998 12.7292M2 13.3332H3.11636C3.44248 13.3332 3.60554 13.3332 3.75899 13.2963C3.89504 13.2637 4.0251 13.2098 4.1444 13.1367C4.27895 13.0542 4.39425 12.9389 4.62486 12.7083L13 4.33316C13.5523 3.78087 13.5523 2.88544 13 2.33316C12.4477 1.78087 11.5523 1.78087 11 2.33316L2.62484 10.7083C2.39424 10.9389 2.27894 11.0542 2.19648 11.1888C2.12338 11.3081 2.0695 11.4381 2.03684 11.5742C2 11.7276 2 11.8907 2 12.2168V13.3332Z" stroke="#6B7280" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 804 B |
3
web/app/components/app/chat/icons/question.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.96353 1.5547C7.40657 0.890144 6.93018 0 6.13148 0H0V12L6.96353 1.5547Z" fill="#E1EFFE"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 202 B |
10
web/app/components/app/chat/icons/robot.svg
Normal file
|
After Width: | Height: | Size: 36 KiB |
3
web/app/components/app/chat/icons/send-active.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.447 16.8939C23.6128 16.8108 23.7523 16.6832 23.8498 16.5253C23.9473 16.3674 23.9989 16.1855 23.9989 15.9999C23.9989 15.8144 23.9473 15.6325 23.8498 15.4746C23.7523 15.3167 23.6128 15.1891 23.447 15.1059L9.44697 8.10595C9.27338 8.01909 9.07827 7.98463 8.88543 8.00677C8.69259 8.02891 8.51036 8.1067 8.36098 8.23064C8.2116 8.35458 8.10151 8.51931 8.04415 8.70475C7.9868 8.89019 7.98465 9.08831 8.03797 9.27495L9.46697 14.2749C9.52674 14.4839 9.65297 14.6677 9.82655 14.7985C10.0001 14.9294 10.2116 15.0001 10.429 14.9999H15C15.2652 14.9999 15.5195 15.1053 15.7071 15.2928C15.8946 15.4804 16 15.7347 16 15.9999C16 16.2652 15.8946 16.5195 15.7071 16.7071C15.5195 16.8946 15.2652 16.9999 15 16.9999H10.429C10.2116 16.9998 10.0001 17.0705 9.82655 17.2013C9.65297 17.3322 9.52674 17.516 9.46697 17.7249L8.03897 22.7249C7.98554 22.9115 7.98756 23.1096 8.04478 23.2951C8.10201 23.4805 8.21195 23.6453 8.36122 23.7693C8.51049 23.8934 8.69263 23.9713 8.88542 23.9936C9.07821 24.0159 9.27332 23.9816 9.44697 23.8949L23.447 16.8949V16.8939Z" fill="#1C64F2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
3
web/app/components/app/chat/icons/send.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.447 16.8939C23.6128 16.8108 23.7523 16.6832 23.8498 16.5253C23.9473 16.3674 23.9989 16.1855 23.9989 15.9999C23.9989 15.8144 23.9473 15.6325 23.8498 15.4746C23.7523 15.3167 23.6128 15.1891 23.447 15.1059L9.44697 8.10595C9.27338 8.01909 9.07827 7.98463 8.88543 8.00677C8.69259 8.02891 8.51036 8.1067 8.36098 8.23064C8.2116 8.35458 8.10151 8.51931 8.04415 8.70475C7.9868 8.89019 7.98465 9.08831 8.03797 9.27495L9.46697 14.2749C9.52674 14.4839 9.65297 14.6677 9.82655 14.7985C10.0001 14.9294 10.2116 15.0001 10.429 14.9999H15C15.2652 14.9999 15.5195 15.1053 15.7071 15.2928C15.8946 15.4804 16 15.7347 16 15.9999C16 16.2652 15.8946 16.5195 15.7071 16.7071C15.5195 16.8946 15.2652 16.9999 15 16.9999H10.429C10.2116 16.9998 10.0001 17.0705 9.82655 17.2013C9.65297 17.3322 9.52674 17.516 9.46697 17.7249L8.03897 22.7249C7.98554 22.9115 7.98756 23.1096 8.04478 23.2951C8.10201 23.4805 8.21195 23.6453 8.36122 23.7693C8.51049 23.8934 8.69263 23.9713 8.88542 23.9936C9.07821 24.0159 9.27332 23.9816 9.44697 23.8949L23.447 16.8949V16.8939Z" fill="#D1D5DB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
19
web/app/components/app/chat/icons/typing.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_2358_1380)">
|
||||
<rect x="2" y="1" width="16" height="16" rx="8" fill="white"/>
|
||||
<path opacity="0.7" d="M13.5 9H13.505M14 9C14 9.13261 13.9473 9.25979 13.8536 9.35355C13.7598 9.44732 13.6326 9.5 13.5 9.5C13.3674 9.5 13.2402 9.44732 13.1464 9.35355C13.0527 9.25979 13 9.13261 13 9C13 8.86739 13.0527 8.74021 13.1464 8.64645C13.2402 8.55268 13.3674 8.5 13.5 8.5C13.6326 8.5 13.7598 8.55268 13.8536 8.64645C13.9473 8.74021 14 8.86739 14 9Z" stroke="#155EEF" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path opacity="0.6" d="M10 9H10.005M10.5 9C10.5 9.13261 10.4473 9.25979 10.3536 9.35355C10.2598 9.44732 10.1326 9.5 10 9.5C9.86739 9.5 9.74021 9.44732 9.64645 9.35355C9.55268 9.25979 9.5 9.13261 9.5 9C9.5 8.86739 9.55268 8.74021 9.64645 8.64645C9.74021 8.55268 9.86739 8.5 10 8.5C10.1326 8.5 10.2598 8.55268 10.3536 8.64645C10.4473 8.74021 10.5 8.86739 10.5 9Z" stroke="#155EEF" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path opacity="0.3" d="M6.5 9H6.505M7 9C7 9.13261 6.94732 9.25979 6.85355 9.35355C6.75979 9.44732 6.63261 9.5 6.5 9.5C6.36739 9.5 6.24021 9.44732 6.14645 9.35355C6.05268 9.25979 6 9.13261 6 9C6 8.86739 6.05268 8.74021 6.14645 8.64645C6.24021 8.55268 6.36739 8.5 6.5 8.5C6.63261 8.5 6.75979 8.55268 6.85355 8.64645C6.94732 8.74021 7 8.86739 7 9Z" stroke="#155EEF" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_2358_1380" x="0" y="0" width="20" height="20" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2358_1380"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2358_1380" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
10
web/app/components/app/chat/icons/user.svg
Normal file
|
After Width: | Height: | Size: 74 KiB |
559
web/app/components/app/chat/index.tsx
Normal file
@@ -0,0 +1,559 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import cn from 'classnames'
|
||||
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
|
||||
import { UserCircleIcon } from '@heroicons/react/24/solid'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { randomString } from '../../app-sidebar/basic'
|
||||
import s from './style.module.css'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import AutoHeightTextarea from '@/app/components/base/auto-height-textarea'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { Annotation, MessageRating } from '@/models/log'
|
||||
import AppContext from '@/context/app-context'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import LoadingAnim from './loading-anim'
|
||||
|
||||
const stopIcon = (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.00004 0.583313C3.45621 0.583313 0.583374 3.45615 0.583374 6.99998C0.583374 10.5438 3.45621 13.4166 7.00004 13.4166C10.5439 13.4166 13.4167 10.5438 13.4167 6.99998C13.4167 3.45615 10.5439 0.583313 7.00004 0.583313ZM4.73029 4.98515C4.66671 5.10993 4.66671 5.27328 4.66671 5.59998V8.39998C4.66671 8.72668 4.66671 8.89003 4.73029 9.01481C4.78621 9.12457 4.87545 9.21381 4.98521 9.26973C5.10999 9.33331 5.27334 9.33331 5.60004 9.33331H8.40004C8.72674 9.33331 8.89009 9.33331 9.01487 9.26973C9.12463 9.21381 9.21387 9.12457 9.2698 9.01481C9.33337 8.89003 9.33337 8.72668 9.33337 8.39998V5.59998C9.33337 5.27328 9.33337 5.10993 9.2698 4.98515C9.21387 4.87539 9.12463 4.78615 9.01487 4.73023C8.89009 4.66665 8.72674 4.66665 8.40004 4.66665H5.60004C5.27334 4.66665 5.10999 4.66665 4.98521 4.73023C4.87545 4.78615 4.78621 4.87539 4.73029 4.98515Z" fill="#667085" />
|
||||
</svg>
|
||||
)
|
||||
export type Feedbacktype = {
|
||||
rating: MessageRating
|
||||
content?: string | null
|
||||
}
|
||||
|
||||
export type FeedbackFunc = (messageId: string, feedback: Feedbacktype) => Promise<any>
|
||||
export type SubmitAnnotationFunc = (messageId: string, content: string) => Promise<any>
|
||||
|
||||
export type DisplayScene = 'web' | 'console'
|
||||
|
||||
export type IChatProps = {
|
||||
chatList: IChatItem[]
|
||||
/**
|
||||
* Whether to display the editing area and rating status
|
||||
*/
|
||||
feedbackDisabled?: boolean
|
||||
/**
|
||||
* Whether to display the input area
|
||||
*/
|
||||
isHideFeedbackEdit?: boolean
|
||||
isHideSendInput?: boolean
|
||||
onFeedback?: FeedbackFunc
|
||||
onSubmitAnnotation?: SubmitAnnotationFunc
|
||||
checkCanSend?: () => boolean
|
||||
onSend?: (message: string) => void
|
||||
displayScene?: DisplayScene
|
||||
useCurrentUserAvatar?: boolean
|
||||
isResponsing?: boolean
|
||||
abortResponsing?: () => void
|
||||
controlClearQuery?: number
|
||||
controlFocus?: number
|
||||
isShowSuggestion?: boolean
|
||||
suggestionList?: string[]
|
||||
}
|
||||
|
||||
export type MessageMore = {
|
||||
time: string
|
||||
tokens: number
|
||||
latency: number | string
|
||||
}
|
||||
|
||||
export type IChatItem = {
|
||||
id: string
|
||||
content: string
|
||||
/**
|
||||
* Specific message type
|
||||
*/
|
||||
isAnswer: boolean
|
||||
/**
|
||||
* The user feedback result of this message
|
||||
*/
|
||||
feedback?: Feedbacktype
|
||||
/**
|
||||
* The admin feedback result of this message
|
||||
*/
|
||||
adminFeedback?: Feedbacktype
|
||||
/**
|
||||
* Whether to hide the feedback area
|
||||
*/
|
||||
feedbackDisabled?: boolean
|
||||
/**
|
||||
* More information about this message
|
||||
*/
|
||||
more?: MessageMore
|
||||
annotation?: Annotation
|
||||
useCurrentUserAvatar?: boolean
|
||||
isOpeningStatement?: boolean
|
||||
}
|
||||
|
||||
const OperationBtn = ({ innerContent, onClick, className }: { innerContent: React.ReactNode; onClick?: () => void; className?: string }) => (
|
||||
<div
|
||||
className={`relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-gray-500 hover:text-gray-800 ${className ?? ''}`}
|
||||
style={{ boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.05)' }}
|
||||
onClick={onClick && onClick}
|
||||
>
|
||||
{innerContent}
|
||||
</div>
|
||||
)
|
||||
|
||||
const MoreInfo: FC<{ more: MessageMore; isQuestion: boolean }> = ({ more, isQuestion }) => {
|
||||
const { t } = useTranslation()
|
||||
return (<div className={`mt-1 space-x-2 text-xs text-gray-400 ${isQuestion ? 'mr-2 text-right ' : 'ml-2 text-left float-right'}`}>
|
||||
<span>{`${t('appLog.detail.timeConsuming')} ${more.latency}${t('appLog.detail.second')}`}</span>
|
||||
<span>{`${t('appLog.detail.tokenCost')} ${more.tokens}`}</span>
|
||||
<span>· </span>
|
||||
<span>{more.time} </span>
|
||||
</div>)
|
||||
}
|
||||
|
||||
const OpeningStatementIcon: FC<{ className?: string }> = ({ className }) => (
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M6.25002 1C3.62667 1 1.50002 3.12665 1.50002 5.75C1.50002 6.28 1.58702 6.79071 1.7479 7.26801C1.7762 7.35196 1.79285 7.40164 1.80368 7.43828L1.80722 7.45061L1.80535 7.45452C1.79249 7.48102 1.77339 7.51661 1.73766 7.58274L0.911727 9.11152C0.860537 9.20622 0.807123 9.30503 0.770392 9.39095C0.733879 9.47635 0.674738 9.63304 0.703838 9.81878C0.737949 10.0365 0.866092 10.2282 1.05423 10.343C1.21474 10.4409 1.38213 10.4461 1.475 10.4451C1.56844 10.444 1.68015 10.4324 1.78723 10.4213L4.36472 10.1549C4.406 10.1506 4.42758 10.1484 4.44339 10.1472L4.44542 10.147L4.45161 10.1492C4.47103 10.1562 4.49738 10.1663 4.54285 10.1838C5.07332 10.3882 5.64921 10.5 6.25002 10.5C8.87338 10.5 11 8.37335 11 5.75C11 3.12665 8.87338 1 6.25002 1ZM4.48481 4.29111C5.04844 3.81548 5.7986 3.9552 6.24846 4.47463C6.69831 3.9552 7.43879 3.82048 8.01211 4.29111C8.58544 4.76175 8.6551 5.562 8.21247 6.12453C7.93825 6.47305 7.24997 7.10957 6.76594 7.54348C6.58814 7.70286 6.49924 7.78255 6.39255 7.81466C6.30103 7.84221 6.19589 7.84221 6.10436 7.81466C5.99767 7.78255 5.90878 7.70286 5.73098 7.54348C5.24694 7.10957 4.55867 6.47305 4.28444 6.12453C3.84182 5.562 3.92117 4.76675 4.48481 4.29111Z" fill="#667085" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const RatingIcon: FC<{ isLike: boolean }> = ({ isLike }) => {
|
||||
return isLike ? <HandThumbUpIcon className='w-4 h-4' /> : <HandThumbDownIcon className='w-4 h-4' />
|
||||
}
|
||||
|
||||
const EditIcon: FC<{ className?: string }> = ({ className }) => {
|
||||
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<path d="M14 11.9998L13.3332 12.7292C12.9796 13.1159 12.5001 13.3332 12.0001 13.3332C11.5001 13.3332 11.0205 13.1159 10.6669 12.7292C10.3128 12.3432 9.83332 12.1265 9.33345 12.1265C8.83359 12.1265 8.35409 12.3432 7.99998 12.7292M2 13.3332H3.11636C3.44248 13.3332 3.60554 13.3332 3.75899 13.2963C3.89504 13.2637 4.0251 13.2098 4.1444 13.1367C4.27895 13.0542 4.39425 12.9389 4.62486 12.7083L13 4.33316C13.5523 3.78087 13.5523 2.88544 13 2.33316C12.4477 1.78087 11.5523 1.78087 11 2.33316L2.62484 10.7083C2.39424 10.9389 2.27894 11.0542 2.19648 11.1888C2.12338 11.3081 2.0695 11.4381 2.03684 11.5742C2 11.7276 2 11.8907 2 12.2168V13.3332Z" stroke="#6B7280" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const EditIconSolid: FC<{ className?: string }> = ({ className }) => {
|
||||
return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<path fill-rule="evenodd" clipRule="evenodd" d="M10.8374 8.63108C11.0412 8.81739 11.0554 9.13366 10.8691 9.33747L10.369 9.88449C10.0142 10.2725 9.52293 10.5001 9.00011 10.5001C8.47746 10.5001 7.98634 10.2727 7.63157 9.8849C7.45561 9.69325 7.22747 9.59515 7.00014 9.59515C6.77271 9.59515 6.54446 9.69335 6.36846 9.88517C6.18177 10.0886 5.86548 10.1023 5.66201 9.91556C5.45853 9.72888 5.44493 9.41259 5.63161 9.20911C5.98678 8.82201 6.47777 8.59515 7.00014 8.59515C7.52251 8.59515 8.0135 8.82201 8.36867 9.20911L8.36924 9.20974C8.54486 9.4018 8.77291 9.50012 9.00011 9.50012C9.2273 9.50012 9.45533 9.40182 9.63095 9.20979L10.131 8.66276C10.3173 8.45895 10.6336 8.44476 10.8374 8.63108Z" fill="#6B7280" />
|
||||
<path fill-rule="evenodd" clipRule="evenodd" d="M7.89651 1.39656C8.50599 0.787085 9.49414 0.787084 10.1036 1.39656C10.7131 2.00604 10.7131 2.99419 10.1036 3.60367L3.82225 9.88504C3.81235 9.89494 3.80254 9.90476 3.79281 9.91451C3.64909 10.0585 3.52237 10.1855 3.3696 10.2791C3.23539 10.3613 3.08907 10.4219 2.93602 10.4587C2.7618 10.5005 2.58242 10.5003 2.37897 10.5001C2.3652 10.5001 2.35132 10.5001 2.33732 10.5001H1.50005C1.22391 10.5001 1.00005 10.2763 1.00005 10.0001V9.16286C1.00005 9.14886 1.00004 9.13497 1.00003 9.1212C0.999836 8.91776 0.999669 8.73838 1.0415 8.56416C1.07824 8.4111 1.13885 8.26479 1.22109 8.13058C1.31471 7.97781 1.44166 7.85109 1.58566 7.70736C1.5954 7.69764 1.60523 7.68783 1.61513 7.67793L7.89651 1.39656Z" fill="#6B7280" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
const TryToAskIcon = (
|
||||
<svg width="11" height="10" viewBox="0 0 11 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.88889 0.683718C5.827 0.522805 5.67241 0.416626 5.5 0.416626C5.3276 0.416626 5.173 0.522805 5.11111 0.683718L4.27279 2.86334C4.14762 3.18877 4.10829 3.28255 4.05449 3.35821C4.00051 3.43413 3.93418 3.50047 3.85826 3.55445C3.78259 3.60825 3.68881 3.64758 3.36338 3.77275L1.18376 4.61106C1.02285 4.67295 0.916668 4.82755 0.916668 4.99996C0.916668 5.17236 1.02285 5.32696 1.18376 5.38885L3.36338 6.22717C3.68881 6.35234 3.78259 6.39167 3.85826 6.44547C3.93418 6.49945 4.00051 6.56578 4.05449 6.6417C4.10829 6.71737 4.14762 6.81115 4.27279 7.13658L5.11111 9.3162C5.173 9.47711 5.3276 9.58329 5.5 9.58329C5.67241 9.58329 5.82701 9.47711 5.8889 9.3162L6.72721 7.13658C6.85238 6.81115 6.89171 6.71737 6.94551 6.6417C6.99949 6.56578 7.06583 6.49945 7.14175 6.44547C7.21741 6.39167 7.31119 6.35234 7.63662 6.22717L9.81624 5.38885C9.97715 5.32696 10.0833 5.17236 10.0833 4.99996C10.0833 4.82755 9.97715 4.67295 9.81624 4.61106L7.63662 3.77275C7.31119 3.64758 7.21741 3.60825 7.14175 3.55445C7.06583 3.50047 6.99949 3.43413 6.94551 3.35821C6.89171 3.28255 6.85238 3.18877 6.72721 2.86334L5.88889 0.683718Z" fill="#667085" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const Divider: FC<{ name: string }> = ({ name }) => {
|
||||
const { t } = useTranslation()
|
||||
return <div className='flex items-center my-2'>
|
||||
<span className='text-xs text-gray-500 inline-flex items-center mr-2'>
|
||||
<EditIconSolid className='mr-1' />{t('appLog.detail.annotationTip', { user: name })}
|
||||
</span>
|
||||
<div className='h-[1px] bg-gray-200 flex-1'></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const IconWrapper: FC<{ children: React.ReactNode | string }> = ({ children }) => {
|
||||
return <div className={'rounded-lg h-6 w-6 flex items-center justify-center hover:bg-gray-100'}>
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
|
||||
type IAnswerProps = {
|
||||
item: IChatItem
|
||||
feedbackDisabled: boolean
|
||||
isHideFeedbackEdit: boolean
|
||||
onFeedback?: FeedbackFunc
|
||||
onSubmitAnnotation?: SubmitAnnotationFunc
|
||||
displayScene: DisplayScene
|
||||
isResponsing?: boolean
|
||||
}
|
||||
|
||||
// The component needs to maintain its own state to control whether to display input component
|
||||
const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, isHideFeedbackEdit = false, onFeedback, onSubmitAnnotation, displayScene = 'web', isResponsing }) => {
|
||||
const { id, content, more, feedback, adminFeedback, annotation: initAnnotation } = item
|
||||
const [showEdit, setShowEdit] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [annotation, setAnnotation] = useState<Annotation | undefined | null>(initAnnotation)
|
||||
const [inputValue, setInputValue] = useState<string>(initAnnotation?.content ?? '')
|
||||
const [localAdminFeedback, setLocalAdminFeedback] = useState<Feedbacktype | undefined | null>(adminFeedback)
|
||||
const { userProfile } = useContext(AppContext)
|
||||
const { t } = useTranslation()
|
||||
|
||||
/**
|
||||
* Render feedback results (distinguish between users and administrators)
|
||||
* User reviews cannot be cancelled in Console
|
||||
* @param rating feedback result
|
||||
* @param isUserFeedback Whether it is user's feedback
|
||||
* @param isWebScene Whether it is web scene
|
||||
* @returns comp
|
||||
*/
|
||||
const renderFeedbackRating = (rating: MessageRating | undefined, isUserFeedback = true, isWebScene = true) => {
|
||||
if (!rating)
|
||||
return null
|
||||
|
||||
const isLike = rating === 'like'
|
||||
const ratingIconClassname = isLike ? 'text-primary-600 bg-primary-100 hover:bg-primary-200' : 'text-red-600 bg-red-100 hover:bg-red-200'
|
||||
const UserSymbol = <UserCircleIcon className='absolute top-[-2px] left-[18px] w-3 h-3 rounded-lg text-gray-400 bg-white' />
|
||||
// The tooltip is always displayed, but the content is different for different scenarios.
|
||||
return (
|
||||
<Tooltip
|
||||
selector={`user-feedback-${randomString(16)}`}
|
||||
content={(isWebScene || (!isUserFeedback && !isWebScene)) ? isLike ? '取消赞同' : '取消反对' : (!isWebScene && isUserFeedback) ? `用户表示${isLike ? '赞同' : '反对'}` : ''}
|
||||
>
|
||||
<div
|
||||
className={`relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-gray-500 hover:text-gray-800 ${(!isWebScene && isUserFeedback) ? '!cursor-default' : ''}`}
|
||||
style={{ boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.05)' }}
|
||||
{...((isWebScene || (!isUserFeedback && !isWebScene))
|
||||
? {
|
||||
onClick: async () => {
|
||||
const res = await onFeedback?.(id, { rating: null })
|
||||
if (res && !isWebScene)
|
||||
setLocalAdminFeedback({ rating: null })
|
||||
},
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
<div className={`${ratingIconClassname} rounded-lg h-6 w-6 flex items-center justify-center`}>
|
||||
<RatingIcon isLike={isLike} />
|
||||
</div>
|
||||
{!isWebScene && isUserFeedback && UserSymbol}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Different scenarios have different operation items.
|
||||
* @param isWebScene Whether it is web scene
|
||||
* @returns comp
|
||||
*/
|
||||
const renderItemOperation = (isWebScene = true) => {
|
||||
const userOperation = () => {
|
||||
return feedback?.rating
|
||||
? null
|
||||
: <div className='flex gap-1'>
|
||||
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('appLog.detail.operation.like') as string}>
|
||||
{OperationBtn({ innerContent: <IconWrapper><RatingIcon isLike={true} /></IconWrapper>, onClick: () => onFeedback?.(id, { rating: 'like' }) })}
|
||||
</Tooltip>
|
||||
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('appLog.detail.operation.dislike') as string}>
|
||||
{OperationBtn({ innerContent: <IconWrapper><RatingIcon isLike={false} /></IconWrapper>, onClick: () => onFeedback?.(id, { rating: 'dislike' }) })}
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
|
||||
const adminOperation = () => {
|
||||
return <div className='flex gap-1'>
|
||||
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('appLog.detail.operation.addAnnotation') as string}>
|
||||
{OperationBtn({
|
||||
innerContent: <IconWrapper><EditIcon className='hover:text-gray-800' /></IconWrapper>,
|
||||
onClick: () => setShowEdit(true),
|
||||
})}
|
||||
</Tooltip>
|
||||
{!localAdminFeedback?.rating && <>
|
||||
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('appLog.detail.operation.like') as string}>
|
||||
{OperationBtn({
|
||||
innerContent: <IconWrapper><RatingIcon isLike={true} /></IconWrapper>,
|
||||
onClick: async () => {
|
||||
const res = await onFeedback?.(id, { rating: 'like' })
|
||||
if (res)
|
||||
setLocalAdminFeedback({ rating: 'like' })
|
||||
},
|
||||
})}
|
||||
</Tooltip>
|
||||
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('appLog.detail.operation.dislike') as string}>
|
||||
{OperationBtn({
|
||||
innerContent: <IconWrapper><RatingIcon isLike={false} /></IconWrapper>,
|
||||
onClick: async () => {
|
||||
const res = await onFeedback?.(id, { rating: 'dislike' })
|
||||
if (res)
|
||||
setLocalAdminFeedback({ rating: 'dislike' })
|
||||
},
|
||||
})}
|
||||
</Tooltip>
|
||||
</>}
|
||||
</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${s.itemOperation} flex gap-2`}>
|
||||
{isWebScene ? userOperation() : adminOperation()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={id}>
|
||||
<div className='flex items-start'>
|
||||
<div className={`${s.answerIcon} w-10 h-10 shrink-0`}>
|
||||
{isResponsing &&
|
||||
<div className={s.typeingIcon}>
|
||||
<LoadingAnim type='avatar' />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className={`${s.answerWrap} ${showEdit ? 'w-full' : ''}`}>
|
||||
<div className={`${s.answer} relative text-sm text-gray-900`}>
|
||||
<div className={'ml-2 py-3 px-4 bg-gray-100 rounded-tr-2xl rounded-b-2xl'}>
|
||||
{item.isOpeningStatement && (
|
||||
<div className='flex items-center mb-1 gap-1'>
|
||||
<OpeningStatementIcon />
|
||||
<div className='text-xs text-gray-500'>{t('appDebug.openingStatement.title')}</div>
|
||||
</div>
|
||||
)}
|
||||
{(isResponsing && !content) ? (
|
||||
<div className='flex items-center justify-center w-6 h-5'>
|
||||
<LoadingAnim type='text' />
|
||||
</div>
|
||||
) : (
|
||||
<Markdown content={content} />
|
||||
)}
|
||||
{!showEdit
|
||||
? (annotation?.content
|
||||
&& <>
|
||||
<Divider name={annotation?.account?.name || userProfile?.name} />
|
||||
{annotation.content}
|
||||
</>)
|
||||
: <>
|
||||
<Divider name={annotation?.account?.name || userProfile?.name} />
|
||||
<AutoHeightTextarea
|
||||
placeholder={t('appLog.detail.operation.annotationPlaceholder') as string}
|
||||
value={inputValue}
|
||||
onChange={e => setInputValue(e.target.value)}
|
||||
minHeight={58}
|
||||
className={`${cn(s.textArea)} !py-2 resize-none block w-full !px-3 bg-gray-50 border border-gray-200 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm text-gray-700 tracking-[0.2px]`}
|
||||
/>
|
||||
<div className="mt-2 flex flex-row">
|
||||
<Button
|
||||
type='primary'
|
||||
className='mr-2'
|
||||
loading={loading}
|
||||
onClick={async () => {
|
||||
if (!inputValue)
|
||||
return
|
||||
setLoading(true)
|
||||
const res = await onSubmitAnnotation?.(id, inputValue)
|
||||
if (res)
|
||||
setAnnotation({ ...annotation, content: inputValue } as any)
|
||||
setLoading(false)
|
||||
setShowEdit(false)
|
||||
}}>{t('common.operation.confirm')}</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setInputValue(annotation?.content ?? '')
|
||||
setShowEdit(false)
|
||||
}}>{t('common.operation.cancel')}</Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
<div className='absolute top-[-14px] right-[-14px] flex flex-row justify-end gap-1'>
|
||||
{!feedbackDisabled && !item.feedbackDisabled && renderItemOperation(displayScene !== 'console')}
|
||||
{/* Admin feedback is displayed only in the background. */}
|
||||
{!feedbackDisabled && renderFeedbackRating(localAdminFeedback?.rating, false, false)}
|
||||
{/* User feedback must be displayed */}
|
||||
{!feedbackDisabled && renderFeedbackRating(feedback?.rating, !isHideFeedbackEdit, displayScene !== 'console')}
|
||||
</div>
|
||||
</div>
|
||||
{more && <MoreInfo more={more} isQuestion={false} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type IQuestionProps = Pick<IChatItem, 'id' | 'content' | 'more' | 'useCurrentUserAvatar'>
|
||||
|
||||
const Question: FC<IQuestionProps> = ({ id, content, more, useCurrentUserAvatar }) => {
|
||||
const { userProfile } = useContext(AppContext)
|
||||
const userName = userProfile?.name
|
||||
return (
|
||||
<div className='flex items-start justify-end' key={id}>
|
||||
<div>
|
||||
<div className={`${s.question} relative text-sm text-gray-900`}>
|
||||
<div
|
||||
className={'mr-2 py-3 px-4 bg-blue-500 rounded-tl-2xl rounded-b-2xl'}
|
||||
>
|
||||
<Markdown content={content} />
|
||||
</div>
|
||||
</div>
|
||||
{more && <MoreInfo more={more} isQuestion={true} />}
|
||||
</div>
|
||||
{useCurrentUserAvatar ? (
|
||||
<div className='w-10 h-10 shrink-0 leading-10 text-center mr-2 rounded-full bg-primary-600 text-white'>
|
||||
{userName?.[0].toLocaleUpperCase()}
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${s.questionIcon} w-10 h-10 shrink-0 `}></div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Chat: FC<IChatProps> = ({
|
||||
chatList,
|
||||
feedbackDisabled = false,
|
||||
isHideFeedbackEdit = false,
|
||||
isHideSendInput = false,
|
||||
onFeedback,
|
||||
onSubmitAnnotation,
|
||||
checkCanSend,
|
||||
onSend = () => { },
|
||||
displayScene,
|
||||
useCurrentUserAvatar,
|
||||
isResponsing,
|
||||
abortResponsing,
|
||||
controlClearQuery,
|
||||
controlFocus,
|
||||
isShowSuggestion,
|
||||
suggestionList
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const isUseInputMethod = useRef(false)
|
||||
|
||||
const [query, setQuery] = React.useState('')
|
||||
const handleContentChange = (e: any) => {
|
||||
const value = e.target.value
|
||||
setQuery(value)
|
||||
}
|
||||
|
||||
const logError = (message: string) => {
|
||||
notify({ type: 'error', message, duration: 3000 })
|
||||
}
|
||||
|
||||
const valid = () => {
|
||||
if (!query || query.trim() === '') {
|
||||
logError('Message cannot be empty')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (controlClearQuery) {
|
||||
setQuery('')
|
||||
}
|
||||
}, [controlClearQuery])
|
||||
|
||||
const handleSend = () => {
|
||||
if (!valid() || (checkCanSend && !checkCanSend()))
|
||||
return
|
||||
onSend(query)
|
||||
if (!isResponsing) {
|
||||
setQuery('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyUp = (e: any) => {
|
||||
if (e.code === 'Enter') {
|
||||
e.preventDefault()
|
||||
// prevent send message when using input method enter
|
||||
if (!e.shiftKey && !isUseInputMethod.current) {
|
||||
handleSend()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const haneleKeyDown = (e: any) => {
|
||||
isUseInputMethod.current = e.nativeEvent.isComposing
|
||||
if (e.code === 'Enter' && !e.shiftKey) {
|
||||
setQuery(query.replace(/\n$/, ''))
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn(!feedbackDisabled && 'px-3.5', 'h-full')}>
|
||||
{/* Chat List */}
|
||||
<div className="h-full space-y-[30px]">
|
||||
{chatList.map((item) => {
|
||||
if (item.isAnswer) {
|
||||
const isLast = item.id === chatList[chatList.length - 1].id
|
||||
return <Answer
|
||||
key={item.id}
|
||||
item={item}
|
||||
feedbackDisabled={feedbackDisabled}
|
||||
isHideFeedbackEdit={isHideFeedbackEdit}
|
||||
onFeedback={onFeedback}
|
||||
onSubmitAnnotation={onSubmitAnnotation}
|
||||
displayScene={displayScene ?? 'web'}
|
||||
isResponsing={isResponsing && isLast}
|
||||
/>
|
||||
}
|
||||
return <Question key={item.id} id={item.id} content={item.content} more={item.more} useCurrentUserAvatar={useCurrentUserAvatar} />
|
||||
})}
|
||||
</div>
|
||||
{
|
||||
!isHideSendInput && (
|
||||
<div className={cn(!feedbackDisabled && '!left-3.5 !right-3.5', 'absolute z-10 bottom-0 left-0 right-0')}>
|
||||
{isResponsing && (
|
||||
<div className='flex justify-center mb-4'>
|
||||
<Button className='flex items-center space-x-1 bg-white' onClick={() => abortResponsing?.()}>
|
||||
{stopIcon}
|
||||
<span className='text-xs text-gray-500 font-normal'>{t('appDebug.operation.stopResponding')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
isShowSuggestion && (
|
||||
<div className='pt-2 mb-2 '>
|
||||
<div className='flex items-center justify-center mb-2.5'>
|
||||
<div className='grow h-[1px]'
|
||||
style={{
|
||||
background: 'linear-gradient(270deg, #F3F4F6 0%, rgba(243, 244, 246, 0) 100%)'
|
||||
}}></div>
|
||||
<div className='shrink-0 flex items-center px-3 space-x-1'>
|
||||
{TryToAskIcon}
|
||||
<span className='text-xs text-gray-500 font-medium'>{t('appDebug.feature.suggestedQuestionsAfterAnswer.tryToAsk')}</span>
|
||||
</div>
|
||||
<div className='grow h-[1px]'
|
||||
style={{
|
||||
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)'
|
||||
}}></div>
|
||||
</div>
|
||||
<div className='flex justify-center overflow-x-scroll pb-2'>
|
||||
{suggestionList?.map((item, index) => (
|
||||
<div className='shrink-0 flex justify-center mr-2'>
|
||||
<Button
|
||||
key={index}
|
||||
onClick={() => setQuery(item)}
|
||||
>
|
||||
<span className='text-primary-600 text-xs font-medium'>{item}</span>
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
<div className="relative">
|
||||
<AutoHeightTextarea
|
||||
value={query}
|
||||
onChange={handleContentChange}
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={haneleKeyDown}
|
||||
minHeight={48}
|
||||
autoFocus
|
||||
controlFocus={controlFocus}
|
||||
className={`${cn(s.textArea)} resize-none block w-full pl-3 bg-gray-50 border border-gray-200 rounded-md focus:outline-none sm:text-sm text-gray-700`}
|
||||
/>
|
||||
<div className="absolute top-0 right-2 flex items-center h-[48px]">
|
||||
<div className={`${s.count} mr-4 h-5 leading-5 text-sm bg-gray-50 text-gray-500`}>{query.trim().length}</div>
|
||||
<Tooltip
|
||||
selector='send-tip'
|
||||
htmlContent={
|
||||
<div>
|
||||
<div>{t('common.operation.send')} Enter</div>
|
||||
<div>{t('common.operation.lineBreak')} Shift Enter</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={`${s.sendBtn} w-8 h-8 cursor-pointer rounded-md`} onClick={handleSend}></div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Chat)
|
||||
16
web/app/components/app/chat/loading-anim/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import s from './style.module.css'
|
||||
|
||||
export interface ILoaidingAnimProps {
|
||||
type: 'text' | 'avatar'
|
||||
}
|
||||
|
||||
const LoaidingAnim: FC<ILoaidingAnimProps> = ({
|
||||
type
|
||||
}) => {
|
||||
return (
|
||||
<div className={`${s['dot-flashing']} ${s[type]}`}></div>
|
||||
)
|
||||
}
|
||||
export default React.memo(LoaidingAnim)
|
||||
82
web/app/components/app/chat/loading-anim/style.module.css
Normal file
@@ -0,0 +1,82 @@
|
||||
.dot-flashing {
|
||||
position: relative;
|
||||
animation: 1s infinite linear alternate;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
.dot-flashing::before,
|
||||
.dot-flashing::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
animation: 1s infinite linear alternate;
|
||||
}
|
||||
|
||||
.dot-flashing::before {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.dot-flashing::after {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes dot-flashing {
|
||||
0% {
|
||||
background-color: #667085;
|
||||
}
|
||||
|
||||
50%,
|
||||
100% {
|
||||
background-color: rgba(102, 112, 133, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dot-flashing-avatar {
|
||||
0% {
|
||||
background-color: #155EEF;
|
||||
}
|
||||
|
||||
50%,
|
||||
100% {
|
||||
background-color: rgba(21, 94, 239, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.text,
|
||||
.text::before,
|
||||
.text::after {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background-color: #667085;
|
||||
color: #667085;
|
||||
animation-name: dot-flashing;
|
||||
}
|
||||
|
||||
.text::before {
|
||||
left: -7px;
|
||||
}
|
||||
|
||||
.text::after {
|
||||
left: 7px;
|
||||
}
|
||||
|
||||
.avatar,
|
||||
.avatar::before,
|
||||
.avatar::after {
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
border-radius: 50%;
|
||||
background-color: #155EEF;
|
||||
color: #155EEF;
|
||||
animation-name: dot-flashing-avatar;
|
||||
}
|
||||
|
||||
.avatar::before {
|
||||
left: -5px;
|
||||
}
|
||||
|
||||
.avatar::after {
|
||||
left: 5px;
|
||||
}
|
||||
91
web/app/components/app/chat/style.module.css
Normal file
@@ -0,0 +1,91 @@
|
||||
.answerIcon {
|
||||
position: relative;
|
||||
background: url(./icons/robot.svg);
|
||||
}
|
||||
|
||||
.typeingIcon {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
|
||||
.questionIcon {
|
||||
background: url(./icons/default-avatar.jpg);
|
||||
background-size: contain;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.answer::before,
|
||||
.question::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 8px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.answer::before {
|
||||
left: 0;
|
||||
background: url(./icons/answer.svg) no-repeat;
|
||||
}
|
||||
|
||||
.answerWrap .itemOperation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.answerWrap:hover .itemOperation {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.question::before {
|
||||
right: 0;
|
||||
background: url(./icons/question.svg) no-repeat;
|
||||
}
|
||||
|
||||
.textArea {
|
||||
padding-top: 13px;
|
||||
padding-bottom: 13px;
|
||||
padding-right: 90px;
|
||||
border-radius: 12px;
|
||||
line-height: 20px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.textArea:hover {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* .textArea:focus {
|
||||
box-shadow: 0px 3px 15px -3px rgba(0, 0, 0, 0.1), 0px 4px 6px rgba(0, 0, 0, 0.05);
|
||||
} */
|
||||
|
||||
.count {
|
||||
/* display: none; */
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.sendBtn {
|
||||
background: url(./icons/send.svg) center center no-repeat;
|
||||
}
|
||||
|
||||
.sendBtn:hover {
|
||||
background-image: url(./icons/send-active.svg);
|
||||
background-color: #EBF5FF;
|
||||
}
|
||||
|
||||
.textArea:focus+div .count {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.textArea:focus+div .sendBtn {
|
||||
background-image: url(./icons/send-active.svg);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
'use client'
|
||||
import React, { FC, ReactNode } from 'react'
|
||||
import cn from 'classnames'
|
||||
|
||||
export interface IFeaturePanelProps {
|
||||
className?: string
|
||||
headerIcon: ReactNode
|
||||
title: ReactNode
|
||||
headerRight: ReactNode
|
||||
hasHeaderBottomBorder?: boolean
|
||||
isFocus?: boolean
|
||||
noBodySpacing?: boolean
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
const FeaturePanel: FC<IFeaturePanelProps> = ({
|
||||
className,
|
||||
headerIcon,
|
||||
title,
|
||||
headerRight,
|
||||
hasHeaderBottomBorder,
|
||||
isFocus,
|
||||
noBodySpacing,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(className, isFocus && 'border border-[#2D0DEE]', 'rounded-xl bg-gray-50 pt-2 pb-3', noBodySpacing && '!pb-0')}
|
||||
style={isFocus ? {
|
||||
boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
|
||||
} : {}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className={cn('pb-2 px-3', hasHeaderBottomBorder && 'border-b border-gray-100')}>
|
||||
<div className='flex justify-between items-center h-8'>
|
||||
<div className='flex items-center space-x-1 shrink-0'>
|
||||
<div className='flex items-center justify-center w-4 h-4'>{headerIcon}</div>
|
||||
<div className='text-sm font-semibold text-gray-800'>{title}</div>
|
||||
</div>
|
||||
<div>
|
||||
{headerRight}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Body */}
|
||||
{children && (
|
||||
<div className={cn(!noBodySpacing && 'mt-1 px-3')}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(FeaturePanel)
|
||||
@@ -0,0 +1,23 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
|
||||
export interface IGroupNameProps {
|
||||
name: string
|
||||
}
|
||||
|
||||
const GroupName: FC<IGroupNameProps> = ({
|
||||
name
|
||||
}) => {
|
||||
return (
|
||||
<div className='flex items-center mb-1'>
|
||||
<div className='mr-3 leading-[18px] text-xs font-semibold text-gray-500 uppercase'>{name}</div>
|
||||
<div className='grow h-[1px]'
|
||||
style={{
|
||||
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)',
|
||||
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(GroupName)
|
||||
@@ -0,0 +1,13 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
|
||||
const MoreLikeThisIcon: FC = ({ }) => {
|
||||
return (
|
||||
<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="M5.83914 0.666748H10.1609C10.6975 0.666741 11.1404 0.666734 11.5012 0.696212C11.8759 0.726829 12.2204 0.792538 12.544 0.957399C13.0457 1.21306 13.4537 1.62101 13.7093 2.12277C13.8742 2.44633 13.9399 2.7908 13.9705 3.16553C14 3.52633 14 3.96923 14 4.50587V7.41171C14 7.62908 14 7.73776 13.9652 7.80784C13.9303 7.87806 13.8939 7.91566 13.8249 7.95288C13.756 7.99003 13.6262 7.99438 13.3665 8.00307C12.8879 8.01909 12.4204 8.14633 11.997 8.36429C10.9478 7.82388 9.62021 7.82912 8.53296 8.73228C7.15064 9.88056 6.92784 11.8645 8.0466 13.2641C8.36602 13.6637 8.91519 14.1949 9.40533 14.6492C9.49781 14.7349 9.54405 14.7777 9.5632 14.8041C9.70784 15.003 9.5994 15.2795 9.35808 15.3271C9.32614 15.3334 9.26453 15.3334 9.14129 15.3334H5.83912C5.30248 15.3334 4.85958 15.3334 4.49878 15.304C4.12405 15.2733 3.77958 15.2076 3.45603 15.0428C2.95426 14.7871 2.54631 14.3792 2.29065 13.8774C2.12579 13.5538 2.06008 13.2094 2.02946 12.8346C1.99999 12.4738 1.99999 12.0309 2 11.4943V4.50587C1.99999 3.96924 1.99999 3.52632 2.02946 3.16553C2.06008 2.7908 2.12579 2.44633 2.29065 2.12277C2.54631 1.62101 2.95426 1.21306 3.45603 0.957399C3.77958 0.792538 4.12405 0.726829 4.49878 0.696212C4.85957 0.666734 5.3025 0.666741 5.83914 0.666748ZM4.66667 5.33342C4.29848 5.33342 4 5.63189 4 6.00008C4 6.36827 4.29848 6.66675 4.66667 6.66675H8.66667C9.03486 6.66675 9.33333 6.36827 9.33333 6.00008C9.33333 5.63189 9.03486 5.33342 8.66667 5.33342H4.66667ZM4 8.66675C4 8.29856 4.29848 8.00008 4.66667 8.00008H6C6.36819 8.00008 6.66667 8.29856 6.66667 8.66675C6.66667 9.03494 6.36819 9.33342 6 9.33342H4.66667C4.29848 9.33342 4 9.03494 4 8.66675ZM4.66667 2.66675C4.29848 2.66675 4 2.96523 4 3.33342C4 3.7016 4.29848 4.00008 4.66667 4.00008H10.6667C11.0349 4.00008 11.3333 3.7016 11.3333 3.33342C11.3333 2.96523 11.0349 2.66675 10.6667 2.66675H4.66667Z" fill="#DD2590" />
|
||||
<path d="M11.9977 10.0256C11.3313 9.26808 10.2199 9.06432 9.3849 9.75796C8.54988 10.4516 8.43232 11.6113 9.08807 12.4317C9.58479 13.0531 10.9986 14.3025 11.655 14.8719C11.7744 14.9754 11.8341 15.0272 11.9037 15.0477C11.9642 15.0654 12.0312 15.0654 12.0917 15.0477C12.1613 15.0272 12.221 14.9754 12.3404 14.8719C12.9968 14.3025 14.4106 13.0531 14.9074 12.4317C15.5631 11.6113 15.4599 10.4443 14.6105 9.75796C13.7612 9.07161 12.6642 9.26808 11.9977 10.0256Z" fill="#DD2590" />
|
||||
</svg>
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(MoreLikeThisIcon)
|
||||
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
import React, { FC, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
|
||||
export interface IRemoveIconProps {
|
||||
className?: string
|
||||
isHoverStatus?: boolean
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const RemoveIcon: FC<IRemoveIconProps> = ({
|
||||
className,
|
||||
isHoverStatus,
|
||||
onClick
|
||||
}) => {
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const computedIsHovered = isHoverStatus || isHovered
|
||||
return (
|
||||
<div
|
||||
className={cn(className, computedIsHovered && 'bg-[#FEE4E2]', 'flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-[#FEE4E2]')}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 6H14M6 8H18M16.6667 8L16.1991 15.0129C16.129 16.065 16.0939 16.5911 15.8667 16.99C15.6666 17.3412 15.3648 17.6235 15.0011 17.7998C14.588 18 14.0607 18 13.0062 18H10.9938C9.93927 18 9.41202 18 8.99889 17.7998C8.63517 17.6235 8.33339 17.3412 8.13332 16.99C7.90607 16.5911 7.871 16.065 7.80086 15.0129L7.33333 8M10.6667 11V14.3333M13.3333 11V14.3333" stroke={computedIsHovered ? "#D92D20" : "#667085"} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(RemoveIcon)
|
||||
@@ -0,0 +1,11 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
|
||||
const SuggestedQuestionsAfterAnswerIcon: FC = () => {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.8275 1.33325H5.17245C4.63581 1.33324 4.19289 1.33324 3.8321 1.36272C3.45737 1.39333 3.1129 1.45904 2.78934 1.6239C2.28758 1.87956 1.87963 2.28751 1.62397 2.78928C1.45911 3.11284 1.3934 3.4573 1.36278 3.83204C1.3333 4.19283 1.33331 4.63574 1.33332 5.17239L1.33328 9.42497C1.333 9.95523 1.33278 10.349 1.42418 10.6901C1.67076 11.6103 2.38955 12.3291 3.3098 12.5757C3.51478 12.6306 3.73878 12.6525 3.99998 12.6611L3.99998 13.5806C3.99995 13.7374 3.99992 13.8973 4.01182 14.0283C4.0232 14.1536 4.05333 14.3901 4.21844 14.5969C4.40843 14.8349 4.69652 14.9734 5.00106 14.973C5.26572 14.9728 5.46921 14.8486 5.57416 14.7792C5.6839 14.7066 5.80872 14.6067 5.93117 14.5087L7.53992 13.2217C7.88564 12.9451 7.98829 12.8671 8.09494 12.8126C8.20192 12.7579 8.3158 12.718 8.43349 12.6938C8.55081 12.6697 8.67974 12.6666 9.12248 12.6666H10.8275C11.3642 12.6666 11.8071 12.6666 12.1679 12.6371C12.5426 12.6065 12.8871 12.5408 13.2106 12.3759C13.7124 12.1203 14.1203 11.7123 14.376 11.2106C14.5409 10.887 14.6066 10.5425 14.6372 10.1678C14.6667 9.80701 14.6667 9.36411 14.6667 8.82747V5.17237C14.6667 4.63573 14.6667 4.19283 14.6372 3.83204C14.6066 3.4573 14.5409 3.11284 14.376 2.78928C14.1203 2.28751 13.7124 1.87956 13.2106 1.6239C12.8871 1.45904 12.5426 1.39333 12.1679 1.36272C11.8071 1.33324 11.3642 1.33324 10.8275 1.33325ZM8.99504 4.99992C8.99504 4.44763 9.44275 3.99992 9.99504 3.99992C10.5473 3.99992 10.995 4.44763 10.995 4.99992C10.995 5.5522 10.5473 5.99992 9.99504 5.99992C9.44275 5.99992 8.99504 5.5522 8.99504 4.99992ZM4.92837 7.79996C5.222 7.57974 5.63816 7.63837 5.85961 7.93051C5.90071 7.98295 5.94593 8.03229 5.99199 8.08035C6.09019 8.18282 6.23775 8.32184 6.42882 8.4608C6.81353 8.74059 7.3454 8.99996 7.99504 8.99996C8.64469 8.99996 9.17655 8.74059 9.56126 8.4608C9.75233 8.32184 9.89989 8.18282 9.99809 8.08035C10.0441 8.0323 10.0894 7.98294 10.1305 7.93051C10.3519 7.63837 10.7681 7.57974 11.0617 7.79996C11.3563 8.02087 11.416 8.43874 11.195 8.73329C11.1967 8.73112 11.1928 8.7361 11.186 8.74466C11.1697 8.7651 11.1372 8.80597 11.1261 8.81916C11.087 8.86575 11.0317 8.92884 10.9607 9.00289C10.8194 9.15043 10.6128 9.34474 10.3455 9.53912C9.81353 9.92599 9.01206 10.3333 7.99504 10.3333C6.97802 10.3333 6.17655 9.92599 5.64459 9.53912C5.37733 9.34474 5.17072 9.15043 5.02934 9.00289C4.95837 8.92884 4.90305 8.86575 4.86395 8.81916C4.84438 8.79585 4.82881 8.77659 4.81731 8.76207C4.58702 8.46455 4.61798 8.03275 4.92837 7.79996ZM5.99504 3.99992C5.44275 3.99992 4.99504 4.44763 4.99504 4.99992C4.99504 5.5522 5.44275 5.99992 5.99504 5.99992C6.54732 5.99992 6.99504 5.5522 6.99504 4.99992C6.99504 4.44763 6.54732 3.99992 5.99504 3.99992Z" fill="#06AED4" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
export default React.memo(SuggestedQuestionsAfterAnswerIcon)
|
||||
11
web/app/components/app/configuration/base/icons/var-icon.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
|
||||
const VarIcon: FC = () => {
|
||||
return (
|
||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M3.71922 2.96714C3.91321 3.05239 4.06546 3.21112 4.14255 3.4085C4.21964 3.60588 4.21528 3.82578 4.13042 4.01994C3.51419 5.43299 3.1974 6.95838 3.20002 8.49994C3.20002 10.0943 3.53282 11.6087 4.13122 12.9799C4.20853 13.173 4.20766 13.3885 4.1288 13.5809C4.04993 13.7732 3.89927 13.9274 3.70873 14.0106C3.51819 14.0938 3.30274 14.0995 3.10802 14.0266C2.91331 13.9537 2.75464 13.8078 2.66562 13.6199C1.96073 12.0052 1.59791 10.2619 1.60002 8.49994C1.60002 6.68074 1.98002 4.94794 2.66562 3.37994C2.70767 3.28364 2.76829 3.19658 2.84401 3.12371C2.91973 3.05085 3.00906 2.99361 3.1069 2.95528C3.20474 2.91694 3.30917 2.89826 3.41424 2.9003C3.5193 2.90233 3.62293 2.92505 3.71922 2.96714ZM10.368 6.09994C10.0082 6.10002 9.65293 6.18102 9.32862 6.33695C9.0043 6.49288 8.7192 6.71974 8.49441 7.00074L8.23201 7.32874L8.14321 7.10554C8.02447 6.80879 7.81958 6.55441 7.55494 6.37518C7.2903 6.19594 6.97804 6.10008 6.65841 6.09994H6.40001C6.18784 6.09994 5.98436 6.18423 5.83433 6.33426C5.6843 6.48429 5.60002 6.68777 5.60002 6.89994C5.60002 7.11212 5.6843 7.3156 5.83433 7.46563C5.98436 7.61566 6.18784 7.69994 6.40001 7.69994H6.65841L7.08401 8.76394L6.25602 9.79994C6.18103 9.8936 6.08594 9.96919 5.97779 10.0211C5.86964 10.073 5.75119 10.1 5.63122 10.0999H5.60002C5.38784 10.0999 5.18436 10.1842 5.03433 10.3343C4.8843 10.4843 4.80001 10.6878 4.80001 10.8999C4.80001 11.1121 4.8843 11.3156 5.03433 11.4656C5.18436 11.6157 5.38784 11.6999 5.60002 11.6999H5.63122C5.99107 11.6999 6.34629 11.6189 6.67061 11.4629C6.99493 11.307 7.28003 11.0801 7.50481 10.7991L7.76721 10.4711L7.85601 10.6943C7.97481 10.9912 8.17982 11.2457 8.44462 11.4249C8.70942 11.6042 9.02185 11.7 9.34161 11.6999H9.60001C9.81219 11.6999 10.0157 11.6157 10.1657 11.4656C10.3157 11.3156 10.4 11.1121 10.4 10.8999C10.4 10.6878 10.3157 10.4843 10.1657 10.3343C10.0157 10.1842 9.81219 10.0999 9.60001 10.0999H9.34161L8.91602 9.03594L9.74401 7.99994C9.819 7.90629 9.91409 7.8307 10.0222 7.77877C10.1304 7.72684 10.2488 7.6999 10.3688 7.69994H10.4C10.6122 7.69994 10.8157 7.61566 10.9657 7.46563C11.1157 7.3156 11.2 7.11212 11.2 6.89994C11.2 6.68777 11.1157 6.48429 10.9657 6.33426C10.8157 6.18423 10.6122 6.09994 10.4 6.09994H10.3688H10.368ZM11.8672 4.01994C11.7837 3.82571 11.7804 3.6063 11.8581 3.40966C11.9359 3.21303 12.0883 3.05517 12.2821 2.97059C12.4759 2.88602 12.6953 2.8816 12.8923 2.9583C13.0894 3.03501 13.248 3.1866 13.3336 3.37994C14.0388 4.99468 14.4019 6.73795 14.4 8.49994C14.4 10.3191 14.02 12.0519 13.3344 13.6199C13.2946 13.7194 13.2352 13.8098 13.1597 13.8859C13.0843 13.9619 12.9943 14.0221 12.8952 14.0627C12.7961 14.1033 12.6898 14.1236 12.5827 14.1224C12.4756 14.1212 12.3698 14.0985 12.2716 14.0556C12.1735 14.0127 12.0849 13.9506 12.0112 13.8728C11.9375 13.7951 11.8802 13.7033 11.8426 13.603C11.805 13.5027 11.788 13.3959 11.7925 13.2888C11.797 13.1818 11.8229 13.0768 11.8688 12.9799C12.4853 11.567 12.8024 10.0416 12.8 8.49994C12.8 6.90554 12.4672 5.39114 11.868 4.01994H11.8672Z" fill="#FF8A4C" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
export default React.memo(VarIcon)
|
||||
@@ -0,0 +1,39 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PlusIcon } from '@heroicons/react/20/solid'
|
||||
|
||||
export interface IOperationBtnProps {
|
||||
type: 'add' | 'edit'
|
||||
actionName?: string
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const iconMap = {
|
||||
add: <PlusIcon className='w-3.5 h-3.5' />,
|
||||
edit: (<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.99998 11.6666H12.25M1.75 11.6666H2.72682C3.01217 11.6666 3.15485 11.6666 3.28912 11.6344C3.40816 11.6058 3.52196 11.5587 3.62635 11.4947C3.74408 11.4226 3.84497 11.3217 4.04675 11.1199L11.375 3.79164C11.8583 3.30839 11.8583 2.52488 11.375 2.04164C10.8918 1.55839 10.1083 1.55839 9.62501 2.04164L2.29674 9.3699C2.09496 9.57168 1.99407 9.67257 1.92192 9.7903C1.85795 9.89469 1.81081 10.0085 1.78224 10.1275C1.75 10.2618 1.75 10.4045 1.75 10.6898V11.6666Z" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const OperationBtn: FC<IOperationBtnProps> = ({
|
||||
type,
|
||||
actionName,
|
||||
onClick
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div
|
||||
className='flex items-center rounded-md h-7 px-3 space-x-1 text-gray-700 cursor-pointer hover:bg-gray-200'
|
||||
onClick={onClick}>
|
||||
<div>
|
||||
{iconMap[type]}
|
||||
</div>
|
||||
<div className='text-xs font-medium'>
|
||||
{actionName || t(`common.operation.${type}`)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(OperationBtn)
|
||||
@@ -0,0 +1,36 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
|
||||
import s from './style.module.css'
|
||||
|
||||
export interface IVarHighlightProps {
|
||||
name: string
|
||||
}
|
||||
|
||||
const VarHighlight: FC<IVarHighlightProps> = ({
|
||||
name,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
className={`${s.item} flex mb-2 items-center justify-center rounded-md px-1 h-5 text-xs font-medium text-primary-600`}
|
||||
>
|
||||
<span className='opacity-60'>{'{{'}</span>
|
||||
<span>{name}</span>
|
||||
<span className='opacity-60'>{'}}'}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const varHighlightHTML = ({ name }: IVarHighlightProps) => {
|
||||
const html = `<div class="${s.item} inline-flex mb-2 items-center justify-center px-1 rounded-md h-5 text-xs font-medium text-primary-600">
|
||||
<span class='opacity-60'>{{</span>
|
||||
<span>${name}</span>
|
||||
<span class='opacity-60'>}}</span>
|
||||
</div>`
|
||||
return html
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default React.memo(VarHighlight)
|
||||
@@ -0,0 +1,3 @@
|
||||
.item {
|
||||
background-color: rgba(21, 94, 239, 0.05);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import WarningMask from '.'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
export interface IFormattingChangedProps {
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
const icon = (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.33337 6.66667C1.33337 6.66667 2.67003 4.84548 3.75593 3.75883C4.84183 2.67218 6.34244 2 8.00004 2C11.3137 2 14 4.68629 14 8C14 11.3137 11.3137 14 8.00004 14C5.26465 14 2.95678 12.1695 2.23455 9.66667M1.33337 6.66667V2.66667M1.33337 6.66667H5.33337" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const FormattingChanged: FC<IFormattingChangedProps> = ({
|
||||
onConfirm,
|
||||
onCancel
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<WarningMask
|
||||
title={t('appDebug.formattingChangedTitle')}
|
||||
description={t('appDebug.formattingChangedText')}
|
||||
footer={
|
||||
<div className='flex space-x-2'>
|
||||
<Button type='primary' className='flex items-center space-x-2' onClick={onConfirm}>
|
||||
{icon}
|
||||
<span>{t('common.operation.refresh')}</span>
|
||||
</Button>
|
||||
<Button onClick={onCancel}>{t('common.operation.cancel') as string}</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(FormattingChanged)
|
||||
@@ -0,0 +1,37 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import WarningMask from '.'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
export interface IHasNotSetAPIProps {
|
||||
isTrailFinished: boolean
|
||||
onSetting: () => void
|
||||
}
|
||||
|
||||
const icon = (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 6.00001L14 2.00001M14 2.00001H9.99999M14 2.00001L8 8M6.66667 2H5.2C4.0799 2 3.51984 2 3.09202 2.21799C2.71569 2.40973 2.40973 2.71569 2.21799 3.09202C2 3.51984 2 4.07989 2 5.2V10.8C2 11.9201 2 12.4802 2.21799 12.908C2.40973 13.2843 2.71569 13.5903 3.09202 13.782C3.51984 14 4.07989 14 5.2 14H10.8C11.9201 14 12.4802 14 12.908 13.782C13.2843 13.5903 13.5903 13.2843 13.782 12.908C14 12.4802 14 11.9201 14 10.8V9.33333" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
|
||||
)
|
||||
|
||||
const HasNotSetAPI: FC<IHasNotSetAPIProps> = ({
|
||||
isTrailFinished,
|
||||
onSetting
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<WarningMask
|
||||
title={isTrailFinished ? t('appDebug.notSetAPIKey.trailFinished') : t('appDebug.notSetAPIKey.title')}
|
||||
description={t('appDebug.notSetAPIKey.description')}
|
||||
footer={
|
||||
<Button type='primary' className='flex items-center space-x-2' onClick={onSetting}>
|
||||
<span>{t('appDebug.notSetAPIKey.settingBtn')}</span>
|
||||
{icon}
|
||||
</Button>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(HasNotSetAPI)
|
||||
@@ -0,0 +1,42 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
|
||||
import s from './style.module.css'
|
||||
|
||||
export interface IWarningMaskProps {
|
||||
title: string
|
||||
description: string
|
||||
footer: React.ReactNode
|
||||
}
|
||||
|
||||
const warningIcon = (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.99996 13.3334V10.0001M9.99996 6.66675H10.0083M18.3333 10.0001C18.3333 14.6025 14.6023 18.3334 9.99996 18.3334C5.39759 18.3334 1.66663 14.6025 1.66663 10.0001C1.66663 5.39771 5.39759 1.66675 9.99996 1.66675C14.6023 1.66675 18.3333 5.39771 18.3333 10.0001Z" stroke="#F79009" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const WarningMask: FC<IWarningMaskProps> = ({
|
||||
title,
|
||||
description,
|
||||
footer,
|
||||
}) => {
|
||||
return (
|
||||
<div className={`${s.mask} absolute z-10 inset-0 pt-16`}
|
||||
>
|
||||
<div className='mx-auto w-[535px]'>
|
||||
<div className={`${s.icon} flex items-center justify-center w-11 h-11 rounded-xl bg-white`}>{warningIcon}</div>
|
||||
<div className='mt-4 text-[24px] leading-normal font-semibold text-gray-800'>
|
||||
{title}
|
||||
</div>
|
||||
<div className='mt-3 text-base text-gray-500'>
|
||||
{description}
|
||||
</div>
|
||||
<div className='mt-6'>
|
||||
{footer}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(WarningMask)
|
||||
@@ -0,0 +1,8 @@
|
||||
.mask {
|
||||
background-color: rgba(239, 244, 255, 0.9);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.icon {
|
||||
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
|
||||
}
|
||||
243
web/app/components/app/configuration/config-model/index.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Slider from '@/app/components/base/slider'
|
||||
|
||||
export type IParamIteProps = {
|
||||
id: number
|
||||
name: string
|
||||
tip: string
|
||||
value: number
|
||||
step?: number
|
||||
min?: number
|
||||
max: number
|
||||
onChange: (id: number, value: number) => void
|
||||
}
|
||||
|
||||
const ParamIte: FC<IParamIteProps> = ({ id, name, tip, step = 0.1, min = 0, max, value, onChange }) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-[6px]">{name}</span>
|
||||
{/* Give tooltip different tip to avoiding hide bug */}
|
||||
<Tooltip htmlContent={<div className="w-[200px]">{tip}</div>} position='top' selector={`param-name-tooltip-${id}`}>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.66667 10.6667H8V8H7.33333M8 5.33333H8.00667M14 8C14 8.78793 13.8448 9.56815 13.5433 10.2961C13.2417 11.0241 12.7998 11.6855 12.2426 12.2426C11.6855 12.7998 11.0241 13.2417 10.2961 13.5433C9.56815 13.8448 8.78793 14 8 14C7.21207 14 6.43185 13.8448 5.7039 13.5433C4.97595 13.2417 4.31451 12.7998 3.75736 12.2426C3.20021 11.6855 2.75825 11.0241 2.45672 10.2961C2.15519 9.56815 2 8.78793 2 8C2 6.4087 2.63214 4.88258 3.75736 3.75736C4.88258 2.63214 6.4087 2 8 2C9.5913 2 11.1174 2.63214 12.2426 3.75736C13.3679 4.88258 14 6.4087 14 8Z" stroke="#9CA3AF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-4 w-[120px]">
|
||||
<Slider value={max < 5 ? value * 10 : value} min={min < 0 ? min * 10 : min} max={max < 5 ? max * 10 : max} onChange={value => onChange(id, value / (max < 5 ? 10 : 1))} />
|
||||
</div>
|
||||
<input type="number" min={min} max={max} step={step} className="block w-[64px] h-9 leading-9 rounded-lg border-0 pl-1 pl py-1.5 bg-gray-50 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-primary-600" value={value} onChange={(e) => {
|
||||
const value = parseFloat(e.target.value)
|
||||
if (value < 0 || value > max)
|
||||
return
|
||||
|
||||
onChange(id, value)
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ParamIte)
|
||||
@@ -0,0 +1,72 @@
|
||||
'use client'
|
||||
import React, { FC, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useClickAway } from 'ahooks'
|
||||
import VarHighlight from '../../base/var-highlight'
|
||||
|
||||
export interface IConfirmAddVarProps {
|
||||
varNameArr: string[]
|
||||
onConfrim: () => void
|
||||
onCancel: () => void
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
const VarIcon = (
|
||||
<svg width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.8683 0.704745C13.7051 0.374685 13.3053 0.239393 12.9752 0.402563C12.6452 0.565732 12.5099 0.965573 12.673 1.29563C13.5221 3.01316 13.9999 4.94957 13.9999 7.00019C13.9999 9.05081 13.5221 10.9872 12.673 12.7047C12.5099 13.0348 12.6452 13.4346 12.9752 13.5978C13.3053 13.761 13.7051 13.6257 13.8683 13.2956C14.8063 11.3983 15.3333 9.26009 15.3333 7.00019C15.3333 4.74029 14.8063 2.60209 13.8683 0.704745Z" fill="#FD853A" />
|
||||
<path d="M3.32687 1.29563C3.49004 0.965573 3.35475 0.565732 3.02469 0.402563C2.69463 0.239393 2.29479 0.374685 2.13162 0.704745C1.19364 2.60209 0.666626 4.74029 0.666626 7.00019C0.666626 9.26009 1.19364 11.3983 2.13162 13.2956C2.29479 13.6257 2.69463 13.761 3.02469 13.5978C3.35475 13.4346 3.49004 13.0348 3.32687 12.7047C2.47779 10.9872 1.99996 9.05081 1.99996 7.00019C1.99996 4.94957 2.47779 3.01316 3.32687 1.29563Z" fill="#FD853A" />
|
||||
<path d="M9.33238 4.8413C9.74208 4.36081 10.3411 4.08337 10.9726 4.08337H11.0324C11.4006 4.08337 11.6991 4.38185 11.6991 4.75004C11.6991 5.11823 11.4006 5.41671 11.0324 5.41671H10.9726C10.7329 5.41671 10.5042 5.52196 10.347 5.7064L8.78693 7.536L9.28085 9.27382C9.29145 9.31112 9.32388 9.33337 9.35696 9.33337H10.2864C10.6545 9.33337 10.953 9.63185 10.953 10C10.953 10.3682 10.6545 10.6667 10.2864 10.6667H9.35696C8.72382 10.6667 8.17074 10.245 7.99832 9.63834L7.74732 8.75524L6.76373 9.90878C6.35403 10.3893 5.75501 10.6667 5.1235 10.6667H5.06372C4.69553 10.6667 4.39705 10.3682 4.39705 10C4.39705 9.63185 4.69553 9.33337 5.06372 9.33337H5.1235C5.3632 9.33337 5.59189 9.22812 5.74915 9.04368L7.30926 7.21399L6.81536 5.47626C6.80476 5.43897 6.77233 5.41671 6.73925 5.41671H5.80986C5.44167 5.41671 5.14319 5.11823 5.14319 4.75004C5.14319 4.38185 5.44167 4.08337 5.80986 4.08337H6.73925C7.37239 4.08337 7.92547 4.50508 8.0979 5.11174L8.34887 5.99475L9.33238 4.8413Z" fill="#FD853A" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const ConfirmAddVar: FC<IConfirmAddVarProps> = ({
|
||||
varNameArr,
|
||||
onConfrim,
|
||||
onCancel,
|
||||
onHide,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const mainContentRef = useRef<HTMLDivElement>(null)
|
||||
useClickAway(() => {
|
||||
onHide()
|
||||
}, mainContentRef)
|
||||
return (
|
||||
<div className='absolute inset-0 flex items-center justify-center rounded-xl'
|
||||
style={{
|
||||
backgroundColor: 'rgba(35, 56, 118, 0.2)'
|
||||
}}>
|
||||
<div
|
||||
ref={mainContentRef}
|
||||
className='w-[420px] rounded-xl bg-gray-50 p-6'
|
||||
style={{
|
||||
boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)'
|
||||
}}
|
||||
>
|
||||
<div className='flex items-start space-x-3'>
|
||||
<div
|
||||
className='shrink-0 flex items-center justify-center h-10 w-10 rounded-xl border border-gray-100'
|
||||
style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)'
|
||||
}}
|
||||
>{VarIcon}</div>
|
||||
<div className='grow-1'>
|
||||
<div className='text-sm font-medium text-gray-900'>{t('appDebug.autoAddVar')}</div>
|
||||
<div className='flex flex-wrap mt-[15px] max-h-[66px] overflow-y-auto px-1 space-x-1'>
|
||||
{varNameArr.map((name) => (
|
||||
<VarHighlight key={name} name={name} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-7 flex justify-end space-x-2'>
|
||||
<Button className='w-20 h-8 !text-[13px]' onClick={onCancel}>{t('common.operation.cancel')}</Button>
|
||||
<Button className='w-20 h-8 !text-[13px]' type='primary' onClick={onConfrim}>{t('common.operation.add')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ConfirmAddVar)
|
||||
95
web/app/components/app/configuration/config-prompt/index.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import BlockInput from '@/app/components/base/block-input'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { AppType } from '@/types/app'
|
||||
import { getNewVar } from '@/utils/var'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import ConfirmAddVar from './confirm-add-var'
|
||||
|
||||
export type IPromptProps = {
|
||||
mode: AppType
|
||||
promptTemplate: string
|
||||
promptVariables: PromptVariable[]
|
||||
onChange: (promp: string, promptVariables: PromptVariable[]) => void
|
||||
}
|
||||
|
||||
const Prompt: FC<IPromptProps> = ({
|
||||
mode,
|
||||
promptTemplate,
|
||||
promptVariables,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const promptVariablesObj = (() => {
|
||||
const obj: Record<string, boolean> = {}
|
||||
promptVariables.forEach((item) => {
|
||||
obj[item.key] = true
|
||||
})
|
||||
return obj
|
||||
})()
|
||||
|
||||
const [newPromptVariables, setNewPromptVariables] = React.useState<PromptVariable[]>(promptVariables)
|
||||
const [newTemplates, setNewTemplates] = React.useState('')
|
||||
const [isShowConfirmAddVar, { setTrue: showConfirmAddVar, setFalse: hideConfirmAddVar }] = useBoolean(false)
|
||||
|
||||
const handleChange = (newTemplates: string, keys: string[]) => {
|
||||
// const hasRemovedKeysInput = promptVariables.filter(input => keys.includes(input.key))
|
||||
const newPromptVariables = keys.filter(key => !(key in promptVariablesObj)).map(key => getNewVar(key))
|
||||
if (newPromptVariables.length > 0) {
|
||||
setNewPromptVariables(newPromptVariables)
|
||||
setNewTemplates(newTemplates)
|
||||
showConfirmAddVar()
|
||||
return
|
||||
}
|
||||
onChange(newTemplates, [])
|
||||
}
|
||||
|
||||
const handleAutoAdd = (isAdd: boolean) => {
|
||||
return () => {
|
||||
onChange(newTemplates, isAdd ? newPromptVariables : [])
|
||||
hideConfirmAddVar()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='relative rounded-xl border border-[#2D0DEE] bg-gray-25'>
|
||||
<div className="flex items-center h-11 pl-3 gap-1">
|
||||
<svg width="14" height="13" viewBox="0 0 14 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M3.00001 0.100098C3.21218 0.100098 3.41566 0.184383 3.56569 0.334412C3.71572 0.484441 3.80001 0.687924 3.80001 0.900098V1.7001H4.60001C4.81218 1.7001 5.01566 1.78438 5.16569 1.93441C5.31572 2.08444 5.40001 2.28792 5.40001 2.5001C5.40001 2.71227 5.31572 2.91575 5.16569 3.06578C5.01566 3.21581 4.81218 3.3001 4.60001 3.3001H3.80001V4.1001C3.80001 4.31227 3.71572 4.51575 3.56569 4.66578C3.41566 4.81581 3.21218 4.9001 3.00001 4.9001C2.78783 4.9001 2.58435 4.81581 2.43432 4.66578C2.28429 4.51575 2.20001 4.31227 2.20001 4.1001V3.3001H1.40001C1.18783 3.3001 0.98435 3.21581 0.834321 3.06578C0.684292 2.91575 0.600006 2.71227 0.600006 2.5001C0.600006 2.28792 0.684292 2.08444 0.834321 1.93441C0.98435 1.78438 1.18783 1.7001 1.40001 1.7001H2.20001V0.900098C2.20001 0.687924 2.28429 0.484441 2.43432 0.334412C2.58435 0.184383 2.78783 0.100098 3.00001 0.100098ZM3.00001 8.1001C3.21218 8.1001 3.41566 8.18438 3.56569 8.33441C3.71572 8.48444 3.80001 8.68792 3.80001 8.9001V9.7001H4.60001C4.81218 9.7001 5.01566 9.78438 5.16569 9.93441C5.31572 10.0844 5.40001 10.2879 5.40001 10.5001C5.40001 10.7123 5.31572 10.9158 5.16569 11.0658C5.01566 11.2158 4.81218 11.3001 4.60001 11.3001H3.80001V12.1001C3.80001 12.3123 3.71572 12.5158 3.56569 12.6658C3.41566 12.8158 3.21218 12.9001 3.00001 12.9001C2.78783 12.9001 2.58435 12.8158 2.43432 12.6658C2.28429 12.5158 2.20001 12.3123 2.20001 12.1001V11.3001H1.40001C1.18783 11.3001 0.98435 11.2158 0.834321 11.0658C0.684292 10.9158 0.600006 10.7123 0.600006 10.5001C0.600006 10.2879 0.684292 10.0844 0.834321 9.93441C0.98435 9.78438 1.18783 9.7001 1.40001 9.7001H2.20001V8.9001C2.20001 8.68792 2.28429 8.48444 2.43432 8.33441C2.58435 8.18438 2.78783 8.1001 3.00001 8.1001ZM8.60001 0.100098C8.77656 0.100041 8.94817 0.158388 9.0881 0.266047C9.22802 0.373706 9.32841 0.52463 9.37361 0.695298L10.3168 4.2601L13 5.8073C13.1216 5.87751 13.2226 5.9785 13.2928 6.10011C13.363 6.22173 13.4 6.35967 13.4 6.5001C13.4 6.64052 13.363 6.77847 13.2928 6.90008C13.2226 7.02169 13.1216 7.12268 13 7.1929L10.3168 8.7409L9.37281 12.3049C9.32753 12.4754 9.22716 12.6262 9.08732 12.7337C8.94748 12.8413 8.77602 12.8996 8.59961 12.8996C8.42319 12.8996 8.25173 12.8413 8.11189 12.7337C7.97205 12.6262 7.87169 12.4754 7.82641 12.3049L6.88321 8.7401L4.20001 7.1929C4.0784 7.12268 3.97742 7.02169 3.90721 6.90008C3.837 6.77847 3.80004 6.64052 3.80004 6.5001C3.80004 6.35967 3.837 6.22173 3.90721 6.10011C3.97742 5.9785 4.0784 5.87751 4.20001 5.8073L6.88321 4.2593L7.82721 0.695298C7.87237 0.524762 7.97263 0.373937 8.1124 0.266291C8.25216 0.158646 8.42359 0.100217 8.60001 0.100098Z" fill="#5850EC" />
|
||||
</svg>
|
||||
<div className="h2">{mode === AppType.chat ? t('appDebug.chatSubTitle') : t('appDebug.completionSubTitle')}</div>
|
||||
<Tooltip
|
||||
htmlContent={<div className='w-[180px]'>
|
||||
{t('appDebug.promptTip')}
|
||||
</div>}
|
||||
selector='config-prompt-tooltip'>
|
||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.66667 11.1667H8V8.5H7.33333M8 5.83333H8.00667M14 8.5C14 9.28793 13.8448 10.0681 13.5433 10.7961C13.2417 11.5241 12.7998 12.1855 12.2426 12.7426C11.6855 13.2998 11.0241 13.7417 10.2961 14.0433C9.56815 14.3448 8.78793 14.5 8 14.5C7.21207 14.5 6.43185 14.3448 5.7039 14.0433C4.97595 13.7417 4.31451 13.2998 3.75736 12.7426C3.20021 12.1855 2.75825 11.5241 2.45672 10.7961C2.15519 10.0681 2 9.28793 2 8.5C2 6.9087 2.63214 5.38258 3.75736 4.25736C4.88258 3.13214 6.4087 2.5 8 2.5C9.5913 2.5 11.1174 3.13214 12.2426 4.25736C13.3679 5.38258 14 6.9087 14 8.5Z" stroke="#9CA3AF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<BlockInput
|
||||
value={promptTemplate}
|
||||
onConfirm={(value: string, vars: string[]) => {
|
||||
handleChange(value, vars)
|
||||
}}
|
||||
/>
|
||||
|
||||
{isShowConfirmAddVar && (
|
||||
<ConfirmAddVar
|
||||
varNameArr={newPromptVariables.map((v) => v.name)}
|
||||
onConfrim={handleAutoAdd(true)}
|
||||
onCancel={handleAutoAdd(false)}
|
||||
onHide={hideConfirmAddVar}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Prompt)
|
||||
@@ -0,0 +1,115 @@
|
||||
'use client'
|
||||
import React, { FC, useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import ModalFoot from '../modal-foot'
|
||||
import ConfigSelect, { Options } from '../config-select'
|
||||
import ConfigString from '../config-string'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
import SelectTypeItem from '../select-type-item'
|
||||
import { getNewVar } from '@/utils/var'
|
||||
|
||||
import s from './style.module.css'
|
||||
|
||||
export interface IConfigModalProps {
|
||||
payload: PromptVariable
|
||||
type?: string
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
onConfirm: (newValue: { type: string, value: any }) => void
|
||||
}
|
||||
|
||||
const ConfigModal: FC<IConfigModalProps> = ({
|
||||
payload,
|
||||
isShow,
|
||||
onClose,
|
||||
onConfirm
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { type, name, key, options, max_length } = payload || getNewVar('')
|
||||
|
||||
const [tempType, setTempType] = useState(type)
|
||||
useEffect(() => {
|
||||
setTempType(type)
|
||||
}, [type])
|
||||
const handleTypeChange = (type: string) => {
|
||||
return () => {
|
||||
setTempType(type)
|
||||
}
|
||||
}
|
||||
|
||||
const isStringInput = tempType === 'string'
|
||||
const title = isStringInput ? t('appDebug.variableConig.maxLength') : t('appDebug.variableConig.options')
|
||||
|
||||
// string type
|
||||
const [tempMaxLength, setTempMaxValue] = useState(max_length)
|
||||
useEffect(() => {
|
||||
setTempMaxValue(max_length)
|
||||
}, [max_length])
|
||||
|
||||
// select type
|
||||
const [tempOptions, setTempOptions] = useState<Options>(options || [])
|
||||
useEffect(() => {
|
||||
setTempOptions(options || [])
|
||||
}, [options])
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (isStringInput) {
|
||||
onConfirm({ type: tempType, value: tempMaxLength })
|
||||
} else {
|
||||
if (tempOptions.length === 0) {
|
||||
Toast.notify({ type: 'error', message: 'At least one option requied' })
|
||||
return
|
||||
}
|
||||
const obj: Record<string, boolean> = {}
|
||||
let hasRepeatedItem = false
|
||||
tempOptions.forEach(o => {
|
||||
if (obj[o]) {
|
||||
hasRepeatedItem = true
|
||||
return
|
||||
}
|
||||
obj[o] = true
|
||||
})
|
||||
if (hasRepeatedItem) {
|
||||
Toast.notify({ type: 'error', message: 'Has repeat items' })
|
||||
return
|
||||
}
|
||||
onConfirm({ type: tempType, value: tempOptions })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('appDebug.variableConig.modalTitle')}
|
||||
isShow={isShow}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className='mb-8'>
|
||||
<div className='mt-2 mb-8 text-sm text-gray-500'>{t('appDebug.variableConig.description', { varName: `{{${name || key}}}` })}</div>
|
||||
<div className='mb-2'>
|
||||
<div className={s.title}>{t('appDebug.variableConig.fieldType')}</div>
|
||||
<div className='flex space-x-2'>
|
||||
<SelectTypeItem type='string' selected={isStringInput} onClick={handleTypeChange('string')} />
|
||||
<SelectTypeItem type='select' selected={!isStringInput} onClick={handleTypeChange('select')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<div className={s.title}>{title}</div>
|
||||
{isStringInput ? (
|
||||
<ConfigString value={tempMaxLength} onChange={setTempMaxValue} />
|
||||
) : (
|
||||
<ConfigSelect options={tempOptions} onChange={setTempOptions} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<ModalFoot
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={onClose}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
export default React.memo(ConfigModal)
|
||||
@@ -0,0 +1,8 @@
|
||||
.title {
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
font-weight: 500;
|
||||
color: #101828;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
'use client'
|
||||
import React, { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PlusIcon } from '@heroicons/react/24/outline'
|
||||
import RemoveIcon from '../../base/icons/remove-icon'
|
||||
|
||||
import s from './style.module.css'
|
||||
|
||||
export type Options = string[]
|
||||
export interface IConfigSelectProps {
|
||||
options: Options
|
||||
onChange: (options: Options) => void
|
||||
}
|
||||
|
||||
|
||||
const ConfigSelect: FC<IConfigSelectProps> = ({
|
||||
options,
|
||||
onChange
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div>
|
||||
{options.length > 0 && (
|
||||
<div className='mb-1 space-y-1 '>
|
||||
{options.map((o, index) => (
|
||||
<div className={`${s.inputWrap} relative`}>
|
||||
<input
|
||||
key={index}
|
||||
type="input"
|
||||
value={o || ''}
|
||||
onChange={e => {
|
||||
let value = e.target.value
|
||||
onChange(options.map((item, i) => {
|
||||
if (index === i) {
|
||||
return value
|
||||
}
|
||||
return item
|
||||
}))
|
||||
}}
|
||||
className={`${s.input} w-full px-3 text-sm leading-9 text-gray-900 border-0 grow h-9 bg-transparent focus:outline-none cursor-pointer`}
|
||||
/>
|
||||
<RemoveIcon
|
||||
className={`${s.deleteBtn} absolute top-1/2 translate-y-[-50%] right-1.5 items-center justify-center w-6 h-6 rounded-md cursor-pointer hover:bg-[#FEE4E2]`}
|
||||
onClick={() => {
|
||||
onChange(options.filter((_, i) => index !== i))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
onClick={() => { onChange([...options, '']) }}
|
||||
className='flex items-center h-9 px-3 gap-2 rounded-lg cursor-pointer text-gray-400 bg-gray-100'>
|
||||
<PlusIcon width={16} height={16}></PlusIcon>
|
||||
<div className='text-gray-500 text-[13px]'>{t('appDebug.variableConig.addOption')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ConfigSelect)
|
||||
@@ -0,0 +1,20 @@
|
||||
.inputWrap {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #EAECF0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.deleteBtn {
|
||||
display: none;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.inputWrap:hover {
|
||||
border-color: #FEE4E2;
|
||||
background-color: #FFFBFA;
|
||||
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
|
||||
}
|
||||
|
||||
.inputWrap:hover .deleteBtn {
|
||||
display: flex;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
'use client'
|
||||
import React, { FC, } from 'react'
|
||||
|
||||
export interface IConfigStringProps {
|
||||
value: number | undefined
|
||||
onChange: (value: number | undefined) => void
|
||||
}
|
||||
|
||||
const MAX_LENGTH = 64
|
||||
|
||||
const ConfigString: FC<IConfigStringProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
type="number"
|
||||
max={MAX_LENGTH}
|
||||
min={1}
|
||||
value={value || ''}
|
||||
onChange={e => {
|
||||
let value = parseInt(e.target.value, 10)
|
||||
if (value > MAX_LENGTH) {
|
||||
value = MAX_LENGTH
|
||||
} else if (value < 1) {
|
||||
value = 1
|
||||
}
|
||||
onChange(value)
|
||||
}}
|
||||
className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ConfigString)
|
||||
228
web/app/components/app/configuration/config-var/index.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Panel from '../base/feature-panel'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
import { Cog8ToothIcon, TrashIcon } from '@heroicons/react/24/outline'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import EditModel from './config-model'
|
||||
import { DEFAULT_VALUE_MAX_LEN, getMaxVarNameLength } from '@/config'
|
||||
import { getNewVar } from '@/utils/var'
|
||||
import OperationBtn from '../base/operation-btn'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import IconTypeIcon from './input-type-icon'
|
||||
import { checkKeys } from '@/utils/var'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
import s from './style.module.css'
|
||||
import VarIcon from '../base/icons/var-icon'
|
||||
|
||||
export type IConfigVarProps = {
|
||||
promptVariables: PromptVariable[]
|
||||
onPromptVariablesChange: (promptVariables: PromptVariable[]) => void
|
||||
}
|
||||
|
||||
const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, onPromptVariablesChange }) => {
|
||||
const { t } = useTranslation()
|
||||
const hasVar = promptVariables.length > 0
|
||||
const promptVariableObj = (() => {
|
||||
const obj: Record<string, boolean> = {}
|
||||
promptVariables.forEach((item) => {
|
||||
obj[item.key] = true
|
||||
})
|
||||
return obj
|
||||
})()
|
||||
|
||||
const updatePromptVariable = (key: string, updateKey: string, newValue: any) => {
|
||||
if (!(key in promptVariableObj))
|
||||
return
|
||||
const newPromptVariables = promptVariables.map((item) => {
|
||||
if (item.key === key)
|
||||
return {
|
||||
...item,
|
||||
[updateKey]: newValue
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
onPromptVariablesChange(newPromptVariables)
|
||||
}
|
||||
|
||||
const batchUpdatePromptVariable = (key: string, updateKeys: string[], newValues: any[]) => {
|
||||
if (!(key in promptVariableObj))
|
||||
return
|
||||
const newPromptVariables = promptVariables.map((item) => {
|
||||
if (item.key === key) {
|
||||
const newItem: any = { ...item }
|
||||
updateKeys.forEach((updateKey, i) => {
|
||||
newItem[updateKey] = newValues[i]
|
||||
})
|
||||
return newItem
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
onPromptVariablesChange(newPromptVariables)
|
||||
}
|
||||
|
||||
|
||||
const updatePromptKey = (index: number, newKey: string) => {
|
||||
const { isValid, errorKey, errorMessageKey } = checkKeys([newKey], true)
|
||||
if (!isValid) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey })
|
||||
})
|
||||
return
|
||||
}
|
||||
const newPromptVariables = promptVariables.map((item, i) => {
|
||||
if (i === index)
|
||||
return {
|
||||
...item,
|
||||
key: newKey,
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
|
||||
onPromptVariablesChange(newPromptVariables)
|
||||
}
|
||||
|
||||
const updatePromptNameIfNameEmpty = (index: number, newKey: string) => {
|
||||
if (!newKey) return
|
||||
const newPromptVariables = promptVariables.map((item, i) => {
|
||||
if (i === index && !item.name)
|
||||
return {
|
||||
...item,
|
||||
name: newKey,
|
||||
}
|
||||
return item
|
||||
})
|
||||
|
||||
onPromptVariablesChange(newPromptVariables)
|
||||
}
|
||||
|
||||
const handleAddVar = () => {
|
||||
const newVar = getNewVar('')
|
||||
onPromptVariablesChange([...promptVariables, newVar])
|
||||
}
|
||||
|
||||
const handleRemoveVar = (index: number) => {
|
||||
onPromptVariablesChange(promptVariables.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
const [currKey, setCurrKey] = useState<string | null>(null)
|
||||
const currItem = currKey ? promptVariables.find(item => item.key === currKey) : null
|
||||
const [isShowEditModal, { setTrue: showEditModal, setFalse: hideEditModal }] = useBoolean(false)
|
||||
const handleConfig = (key: string) => {
|
||||
setCurrKey(key)
|
||||
showEditModal()
|
||||
}
|
||||
|
||||
return (
|
||||
<Panel
|
||||
className="mt-4"
|
||||
headerIcon={
|
||||
<VarIcon />
|
||||
}
|
||||
title={
|
||||
<div className='flex items-center gap-2'>
|
||||
<div>{t('appDebug.variableTitle')}</div>
|
||||
<Tooltip htmlContent={<div className='w-[180px]'>
|
||||
{t('appDebug.variableTip')}
|
||||
</div>} selector='config-var-tooltip'>
|
||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.66667 11.1667H8V8.5H7.33333M8 5.83333H8.00667M14 8.5C14 9.28793 13.8448 10.0681 13.5433 10.7961C13.2417 11.5241 12.7998 12.1855 12.2426 12.7426C11.6855 13.2998 11.0241 13.7417 10.2961 14.0433C9.56815 14.3448 8.78793 14.5 8 14.5C7.21207 14.5 6.43185 14.3448 5.7039 14.0433C4.97595 13.7417 4.31451 13.2998 3.75736 12.7426C3.20021 12.1855 2.75825 11.5241 2.45672 10.7961C2.15519 10.0681 2 9.28793 2 8.5C2 6.9087 2.63214 5.38258 3.75736 4.25736C4.88258 3.13214 6.4087 2.5 8 2.5C9.5913 2.5 11.1174 3.13214 12.2426 4.25736C13.3679 5.38258 14 6.9087 14 8.5Z" stroke="#9CA3AF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
headerRight={<OperationBtn type="add" onClick={handleAddVar} />}
|
||||
>
|
||||
{!hasVar && (
|
||||
<div className='pt-2 pb-1 text-xs text-gray-500'>{t('appDebug.notSetVar')}</div>
|
||||
)}
|
||||
{hasVar && (
|
||||
<div className='rounded-lg border border-gray-200 bg-white'>
|
||||
<table className={`${s.table} w-full border-collapse border-0 rounded-lg text-sm`}>
|
||||
<thead className="border-b border-gray-200 text-gray-500 text-xs font-medium">
|
||||
<tr className='uppercase'>
|
||||
<td>{t('appDebug.variableTable.key')}</td>
|
||||
<td>{t('appDebug.variableTable.name')}</td>
|
||||
<td>{t('appDebug.variableTable.optional')}</td>
|
||||
<td>{t('appDebug.variableTable.action')}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-gray-700">
|
||||
{promptVariables.map(({ key, name, type, required }, index) => (
|
||||
<tr key={index} className="h-9 leading-9">
|
||||
<td className="w-[160px] border-b border-gray-100 pl-3">
|
||||
<div className='flex items-center space-x-1'>
|
||||
<IconTypeIcon type={type} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="key"
|
||||
value={key}
|
||||
onChange={e => updatePromptKey(index, e.target.value)}
|
||||
onBlur={e => updatePromptNameIfNameEmpty(index, e.target.value)}
|
||||
maxLength={getMaxVarNameLength(name)}
|
||||
className="h-6 leading-6 block w-full rounded-md border-0 py-1.5 text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-1 border-b border-gray-100">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={key}
|
||||
value={name}
|
||||
onChange={e => updatePromptVariable(key, 'name', e.target.value)}
|
||||
maxLength={getMaxVarNameLength(name)}
|
||||
className="h-6 leading-6 block w-full rounded-md border-0 py-1.5 text-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
|
||||
/>
|
||||
</td>
|
||||
<td className='w-[84px] border-b border-gray-100'>
|
||||
<div className='flex items-center h-full'>
|
||||
<Switch defaultValue={!required} size='md' onChange={(value) => updatePromptVariable(key, 'required', !value)} />
|
||||
</div>
|
||||
</td>
|
||||
<td className='w-20 border-b border-gray-100'>
|
||||
<div className='flex h-full items-center space-x-1'>
|
||||
<div className='flex items-center justify-items-center w-6 h-6 text-gray-500 cursor-pointer' onClick={() => handleConfig(key)}>
|
||||
<Cog8ToothIcon width={16} height={16} />
|
||||
</div>
|
||||
<div className='flex items-center justify-items-center w-6 h-6 text-gray-500 cursor-pointer' onClick={() => handleRemoveVar(index)} >
|
||||
<TrashIcon width={16} height={16} />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isShowEditModal && (
|
||||
<EditModel
|
||||
payload={currItem as PromptVariable}
|
||||
isShow={isShowEditModal}
|
||||
onClose={hideEditModal}
|
||||
onConfirm={({ type, value }) => {
|
||||
if (type === 'string') {
|
||||
batchUpdatePromptVariable(currKey as string, ['type', 'max_length'], [type, value || DEFAULT_VALUE_MAX_LEN])
|
||||
} else {
|
||||
batchUpdatePromptVariable(currKey as string, ['type', 'options'], [type, value || []])
|
||||
}
|
||||
hideEditModal()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
export default React.memo(ConfigVar)
|
||||
@@ -0,0 +1,34 @@
|
||||
'use client'
|
||||
import React, { FC, ReactNode } from 'react'
|
||||
import { ReactElement } from 'react-markdown/lib/react-markdown'
|
||||
|
||||
export interface IInputTypeIconProps {
|
||||
type: string
|
||||
}
|
||||
|
||||
const IconMap = (type: string) => {
|
||||
const icons: Record<string, ReactNode> = {
|
||||
'string': (
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M3.52593 0.166672H8.47411C8.94367 0.166665 9.33123 0.166659 9.64692 0.192452C9.97481 0.219242 10.2762 0.276738 10.5593 0.420991C10.9984 0.644695 11.3553 1.00165 11.579 1.44069C11.7233 1.72381 11.7808 2.02522 11.8076 2.35311C11.8334 2.6688 11.8334 3.05634 11.8334 3.5259V8.47411C11.8334 8.94367 11.8334 9.33121 11.8076 9.6469C11.7808 9.97479 11.7233 10.2762 11.579 10.5593C11.3553 10.9984 10.9984 11.3553 10.5593 11.579C10.2762 11.7233 9.97481 11.7808 9.64692 11.8076C9.33123 11.8334 8.94369 11.8333 8.47413 11.8333H3.52592C3.05636 11.8333 2.66882 11.8334 2.35312 11.8076C2.02523 11.7808 1.72382 11.7233 1.44071 11.579C1.00167 11.3553 0.644711 10.9984 0.421006 10.5593C0.276753 10.2762 0.219257 9.97479 0.192468 9.6469C0.166674 9.33121 0.16668 8.94366 0.166687 8.4741V3.52591C0.16668 3.05635 0.166674 2.6688 0.192468 2.35311C0.219257 2.02522 0.276753 1.72381 0.421006 1.44069C0.644711 1.00165 1.00167 0.644695 1.44071 0.420991C1.72382 0.276738 2.02523 0.219242 2.35312 0.192452C2.66882 0.166659 3.05637 0.166665 3.52593 0.166672ZM3.08335 3.08334C3.08335 2.76117 3.34452 2.50001 3.66669 2.50001H8.33335C8.65552 2.50001 8.91669 2.76117 8.91669 3.08334C8.91669 3.4055 8.65552 3.66667 8.33335 3.66667H6.58335V8.91667C6.58335 9.23884 6.32219 9.5 6.00002 9.5C5.67785 9.5 5.41669 9.23884 5.41669 8.91667V3.66667H3.66669C3.34452 3.66667 3.08335 3.4055 3.08335 3.08334Z" fill="#98A2B3" />
|
||||
</svg>
|
||||
),
|
||||
'select': (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M7.48913 4.08334H3.01083C2.70334 4.08333 2.43804 4.08333 2.21955 4.10118C1.98893 4.12002 1.75955 4.16162 1.53883 4.27408C1.20955 4.44186 0.941831 4.70958 0.774053 5.03886C0.66159 5.25958 0.619989 5.48896 0.601147 5.71958C0.583295 5.93807 0.583304 6.20334 0.583313 6.51084V10.9892C0.583304 11.2967 0.583295 11.5619 0.601147 11.7804C0.619989 12.0111 0.66159 12.2404 0.774053 12.4612C0.941831 12.7904 1.20955 13.0582 1.53883 13.2259C1.75955 13.3384 1.98893 13.38 2.21955 13.3988C2.43803 13.4167 2.70329 13.4167 3.01077 13.4167H7.48912C7.7966 13.4167 8.06193 13.4167 8.28041 13.3988C8.51103 13.38 8.74041 13.3384 8.96113 13.2259C9.29041 13.0582 9.55813 12.7904 9.72591 12.4612C9.83837 12.2404 9.87997 12.0111 9.89882 11.7804C9.91667 11.5619 9.91666 11.2967 9.91665 10.9892V6.51087C9.91666 6.20336 9.91667 5.93808 9.89882 5.71958C9.87997 5.48896 9.83837 5.25958 9.72591 5.03886C9.55813 4.70958 9.29041 4.44186 8.96113 4.27408C8.74041 4.16162 8.51103 4.12002 8.28041 4.10118C8.06192 4.08333 7.79663 4.08333 7.48913 4.08334ZM7.70413 7.70416C7.93193 7.47635 7.93193 7.107 7.70413 6.8792C7.47632 6.65139 7.10697 6.65139 6.87917 6.8792L4.66665 9.09172L3.91246 8.33753C3.68465 8.10973 3.31531 8.10973 3.0875 8.33753C2.8597 8.56534 2.8597 8.93468 3.0875 9.16249L4.25417 10.3292C4.48197 10.557 4.85132 10.557 5.07913 10.3292L7.70413 7.70416Z" fill="#98A2B3" />
|
||||
<path d="M10.9891 0.583344H6.51083C6.20334 0.583334 5.93804 0.583326 5.71955 0.601177C5.48893 0.620019 5.25955 0.66162 5.03883 0.774084C4.70955 0.941862 4.44183 1.20958 4.27405 1.53886C4.16159 1.75958 4.11999 1.98896 4.10115 2.21958C4.08514 2.41545 4.08349 2.64892 4.08333 2.91669L7.51382 2.91668C7.79886 2.91662 8.10791 2.91654 8.37541 2.9384C8.67818 2.96314 9.07818 3.02436 9.49078 3.23459C10.0396 3.51422 10.4858 3.96041 10.7654 4.50922C10.9756 4.92182 11.0369 5.32182 11.0616 5.62459C11.0835 5.8921 11.0834 6.20115 11.0833 6.48619L11.0833 9.91666C11.3511 9.9165 11.5845 9.91485 11.7804 9.89885C12.011 9.88 12.2404 9.8384 12.4611 9.72594C12.7904 9.55816 13.0581 9.29045 13.2259 8.96116C13.3384 8.74044 13.38 8.51106 13.3988 8.28044C13.4167 8.06196 13.4167 7.7967 13.4166 7.48922V3.01087C13.4167 2.70339 13.4167 2.43807 13.3988 2.21958C13.38 1.98896 13.3384 1.75958 13.2259 1.53886C13.0581 1.20958 12.7904 0.941862 12.4611 0.774084C12.2404 0.66162 12.011 0.620019 11.7804 0.601177C11.5619 0.583326 11.2966 0.583334 10.9891 0.583344Z" fill="#98A2B3" />
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
return icons[type] as any
|
||||
}
|
||||
|
||||
const InputTypeIcon: FC<IInputTypeIconProps> = ({
|
||||
type
|
||||
}) => {
|
||||
const Icon = IconMap(type)
|
||||
return Icon
|
||||
}
|
||||
|
||||
export default React.memo(InputTypeIcon)
|
||||
@@ -0,0 +1,23 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
export interface IModalFootProps {
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
const ModalFoot: FC<IModalFootProps> = ({
|
||||
onConfirm,
|
||||
onCancel
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='flex justify-end gap-2'>
|
||||
<Button onClick={onCancel}>{t('common.operation.cancel')}</Button>
|
||||
<Button type='primary' onClick={onConfirm}>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ModalFoot)
|
||||
@@ -0,0 +1,61 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
|
||||
import s from './style.module.css'
|
||||
|
||||
export interface ISelectTypeItemProps {
|
||||
type: string
|
||||
selected: boolean
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const Icon = ({ type, selected }: Partial<ISelectTypeItemProps>) => {
|
||||
switch (type) {
|
||||
case 'select':
|
||||
return selected ? (
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M8.89233 4.66669H3.77428C3.42285 4.66668 3.11966 4.66667 2.86995 4.68707C2.60639 4.7086 2.34424 4.75615 2.09199 4.88468C1.71567 5.07642 1.4097 5.38238 1.21796 5.75871C1.08943 6.01096 1.04188 6.27311 1.02035 6.53667C0.999949 6.78638 0.999959 7.08954 0.99997 7.44096V12.5591C0.999959 12.9105 0.999949 13.2137 1.02035 13.4634C1.04188 13.7269 1.08943 13.9891 1.21796 14.2413C1.4097 14.6177 1.71567 14.9236 2.09199 15.1154C2.34424 15.2439 2.60639 15.2914 2.86995 15.313C3.11965 15.3334 3.4228 15.3334 3.77421 15.3334H8.89232C9.24372 15.3334 9.54696 15.3334 9.79666 15.313C10.0602 15.2914 10.3224 15.2439 10.5746 15.1154C10.9509 14.9236 11.2569 14.6177 11.4487 14.2413C11.5772 13.9891 11.6247 13.7269 11.6463 13.4634C11.6667 13.2137 11.6667 12.9105 11.6666 12.559V7.44101C11.6667 7.08957 11.6667 6.78639 11.6463 6.53667C11.6247 6.27311 11.5772 6.01096 11.4487 5.75871C11.2569 5.38238 10.9509 5.07642 10.5746 4.88468C10.3224 4.75615 10.0602 4.7086 9.79666 4.68707C9.54695 4.66667 9.24376 4.66668 8.89233 4.66669ZM9.13804 8.80476C9.39839 8.54441 9.39839 8.1223 9.13804 7.86195C8.87769 7.6016 8.45558 7.6016 8.19523 7.86195L5.66664 10.3905L4.80471 9.52862C4.54436 9.26827 4.12225 9.26827 3.8619 9.52862C3.60155 9.78897 3.60155 10.2111 3.8619 10.4714L5.19523 11.8048C5.45558 12.0651 5.87769 12.0651 6.13804 11.8048L9.13804 8.80476Z" fill="#155EEF" />
|
||||
<path d="M12.8923 0.666688H7.77427C7.42285 0.666676 7.11966 0.666666 6.86995 0.687068C6.60639 0.708602 6.34424 0.756146 6.09199 0.884676C5.71567 1.07642 5.40971 1.38238 5.21796 1.75871C5.08943 2.01096 5.04188 2.27311 5.02035 2.53667C5.00206 2.76053 5.00018 3.02734 4.99999 3.33337L8.92055 3.33336C9.2463 3.33329 9.59951 3.3332 9.90523 3.35818C10.2512 3.38645 10.7084 3.45642 11.1799 3.69668C11.8071 4.01626 12.3171 4.5262 12.6367 5.1534C12.8769 5.62495 12.9469 6.08209 12.9752 6.42811C13.0001 6.73384 13.0001 7.08704 13 7.4128L13 11.3333C13.306 11.3332 13.5728 11.3313 13.7967 11.313C14.0602 11.2914 14.3224 11.2439 14.5746 11.1154C14.9509 10.9236 15.2569 10.6177 15.4487 10.2413C15.5772 9.98908 15.6247 9.72694 15.6463 9.46338C15.6667 9.21368 15.6666 8.91052 15.6666 8.55912V3.44101C15.6666 3.0896 15.6667 2.78637 15.6463 2.53667C15.6247 2.27311 15.5772 2.01096 15.4487 1.75871C15.2569 1.38238 14.9509 1.07642 14.5746 0.884676C14.3224 0.756146 14.0602 0.708602 13.7967 0.687068C13.5469 0.666666 13.2438 0.666676 12.8923 0.666688Z" fill="#155EEF" />
|
||||
</svg>
|
||||
|
||||
) : (
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M8.89233 4.66667H3.77428C3.42285 4.66666 3.11966 4.66665 2.86995 4.68705C2.60639 4.70859 2.34424 4.75613 2.09199 4.88466C1.71567 5.07641 1.4097 5.38237 1.21796 5.75869C1.08943 6.01095 1.04188 6.27309 1.02035 6.53665C0.999949 6.78636 0.999959 7.08953 0.99997 7.44095V12.559C0.999959 12.9105 0.999949 13.2137 1.02035 13.4634C1.04188 13.7269 1.08943 13.9891 1.21796 14.2413C1.4097 14.6176 1.71567 14.9236 2.09199 15.1154C2.34424 15.2439 2.60639 15.2914 2.86995 15.313C3.11965 15.3334 3.4228 15.3334 3.77421 15.3333H8.89232C9.24372 15.3334 9.54696 15.3334 9.79666 15.313C10.0602 15.2914 10.3224 15.2439 10.5746 15.1154C10.9509 14.9236 11.2569 14.6176 11.4487 14.2413C11.5772 13.9891 11.6247 13.7269 11.6463 13.4634C11.6667 13.2136 11.6667 12.9105 11.6666 12.559V7.44099C11.6667 7.08955 11.6667 6.78637 11.6463 6.53665C11.6247 6.27309 11.5772 6.01095 11.4487 5.75869C11.2569 5.38237 10.9509 5.07641 10.5746 4.88466C10.3224 4.75613 10.0602 4.70859 9.79666 4.68705C9.54695 4.66665 9.24376 4.66666 8.89233 4.66667ZM9.13804 8.80474C9.39839 8.54439 9.39839 8.12228 9.13804 7.86193C8.87769 7.60159 8.45558 7.60159 8.19523 7.86193L5.66664 10.3905L4.80471 9.5286C4.54436 9.26825 4.12225 9.26825 3.8619 9.5286C3.60155 9.78895 3.60155 10.2111 3.8619 10.4714L5.19523 11.8047C5.45558 12.0651 5.87769 12.0651 6.13804 11.8047L9.13804 8.80474Z" fill="#667085" />
|
||||
<path d="M12.8923 0.666672H7.77427C7.42285 0.666661 7.11966 0.666651 6.86995 0.687053C6.60639 0.708587 6.34424 0.756131 6.09199 0.884661C5.71567 1.07641 5.40971 1.38237 5.21796 1.75869C5.08943 2.01095 5.04188 2.27309 5.02035 2.53665C5.00206 2.76051 5.00018 3.02733 4.99999 3.33336L8.92055 3.33335C9.2463 3.33327 9.59951 3.33319 9.90523 3.35816C10.2512 3.38644 10.7084 3.4564 11.1799 3.69667C11.8071 4.01625 12.3171 4.52618 12.6367 5.15339C12.8769 5.62493 12.9469 6.08208 12.9752 6.42809C13.0001 6.73382 13.0001 7.08702 13 7.41279L13 11.3333C13.306 11.3331 13.5728 11.3313 13.7967 11.313C14.0602 11.2914 14.3224 11.2439 14.5746 11.1154C14.9509 10.9236 15.2569 10.6176 15.4487 10.2413C15.5772 9.98907 15.6247 9.72692 15.6463 9.46336C15.6667 9.21366 15.6666 8.91051 15.6666 8.5591V3.44099C15.6666 3.08959 15.6667 2.78635 15.6463 2.53665C15.6247 2.27309 15.5772 2.01095 15.4487 1.75869C15.2569 1.38237 14.9509 1.07641 14.5746 0.884661C14.3224 0.756131 14.0602 0.708587 13.7967 0.687053C13.5469 0.666651 13.2438 0.666661 12.8923 0.666672Z" fill="#667085" />
|
||||
</svg>
|
||||
|
||||
)
|
||||
case 'string':
|
||||
default:
|
||||
return selected ? (
|
||||
<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="M5.17246 1.33333H10.8275C11.3642 1.33332 11.8071 1.33331 12.1679 1.36279C12.5426 1.39341 12.8871 1.45912 13.2106 1.62398C13.7124 1.87964 14.1203 2.28759 14.376 2.78935C14.5409 3.11291 14.6066 3.45738 14.6372 3.83211C14.6667 4.19291 14.6667 4.63581 14.6667 5.17245V10.8275C14.6667 11.3642 14.6667 11.8071 14.6372 12.1679C14.6066 12.5426 14.5409 12.8871 14.376 13.2106C14.1203 13.7124 13.7124 14.1203 13.2106 14.376C12.8871 14.5409 12.5426 14.6066 12.1679 14.6372C11.8071 14.6667 11.3642 14.6667 10.8275 14.6667H5.17245C4.63581 14.6667 4.1929 14.6667 3.83211 14.6372C3.45738 14.6066 3.11291 14.5409 2.78935 14.376C2.28759 14.1203 1.87964 13.7124 1.62398 13.2106C1.45912 12.8871 1.39341 12.5426 1.36279 12.1679C1.33331 11.8071 1.33332 11.3642 1.33333 10.8275V5.17245C1.33332 4.63581 1.33331 4.1929 1.36279 3.83211C1.39341 3.45738 1.45912 3.11291 1.62398 2.78935C1.87964 2.28759 2.28759 1.87964 2.78935 1.62398C3.11291 1.45912 3.45738 1.39341 3.83211 1.36279C4.1929 1.33331 4.63583 1.33332 5.17246 1.33333ZM4.66666 4.66666C4.66666 4.29847 4.96514 3.99999 5.33333 3.99999H10.6667C11.0349 3.99999 11.3333 4.29847 11.3333 4.66666C11.3333 5.03485 11.0349 5.33333 10.6667 5.33333H8.66666V11.3333C8.66666 11.7015 8.36818 12 7.99999 12C7.6318 12 7.33333 11.7015 7.33333 11.3333V5.33333H5.33333C4.96514 5.33333 4.66666 5.03485 4.66666 4.66666Z" fill="#155EEF" />
|
||||
</svg>
|
||||
|
||||
) : (<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="M5.17246 1.33331H10.8275C11.3642 1.33331 11.8071 1.3333 12.1679 1.36278C12.5426 1.39339 12.8871 1.4591 13.2106 1.62396C13.7124 1.87963 14.1203 2.28757 14.376 2.78934C14.5409 3.1129 14.6066 3.45737 14.6372 3.8321C14.6667 4.19289 14.6667 4.63579 14.6667 5.17243V10.8275C14.6667 11.3642 14.6667 11.8071 14.6372 12.1679C14.6066 12.5426 14.5409 12.8871 14.376 13.2106C14.1203 13.7124 13.7124 14.1203 13.2106 14.376C12.8871 14.5409 12.5426 14.6066 12.1679 14.6372C11.8071 14.6667 11.3642 14.6667 10.8275 14.6666H5.17245C4.63581 14.6667 4.1929 14.6667 3.83211 14.6372C3.45738 14.6066 3.11291 14.5409 2.78935 14.376C2.28759 14.1203 1.87964 13.7124 1.62398 13.2106C1.45912 12.8871 1.39341 12.5426 1.36279 12.1679C1.33331 11.8071 1.33332 11.3642 1.33333 10.8275V5.17244C1.33332 4.6358 1.33331 4.19289 1.36279 3.8321C1.39341 3.45737 1.45912 3.1129 1.62398 2.78934C1.87964 2.28757 2.28759 1.87963 2.78935 1.62396C3.11291 1.4591 3.45738 1.39339 3.83211 1.36278C4.1929 1.3333 4.63583 1.33331 5.17246 1.33331ZM4.66666 4.66665C4.66666 4.29846 4.96514 3.99998 5.33333 3.99998H10.6667C11.0349 3.99998 11.3333 4.29846 11.3333 4.66665C11.3333 5.03484 11.0349 5.33331 10.6667 5.33331H8.66666V11.3333C8.66666 11.7015 8.36818 12 7.99999 12C7.6318 12 7.33333 11.7015 7.33333 11.3333V5.33331H5.33333C4.96514 5.33331 4.66666 5.03484 4.66666 4.66665Z" fill="#667085" />
|
||||
</svg>)
|
||||
}
|
||||
}
|
||||
|
||||
const SelectTypeItem: FC<ISelectTypeItemProps> = ({
|
||||
type,
|
||||
selected,
|
||||
onClick
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const typeName = t(`appDebug.variableConig.${type}`)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(s.item, selected && s.selected, 'space-x-2')}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon type={type} selected={selected} />
|
||||
<span className={cn(s.text)}>{typeName}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(SelectTypeItem)
|
||||
@@ -0,0 +1,38 @@
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
width: 133px;
|
||||
padding-left: 12px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #EAECF0;
|
||||
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
|
||||
background-color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item:not(.selected):hover {
|
||||
border-color: #B2CCFF;
|
||||
background-color: #F5F8FF;
|
||||
box-shadow: 0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06);
|
||||
}
|
||||
|
||||
.item.selected {
|
||||
border-color: #528BFF;
|
||||
background-color: #F5F8FF;
|
||||
box-shadow: 0px 1px 3px rgba(16, 24, 40, 0.1), 0px 1px 2px rgba(16, 24, 40, 0.06);
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 13px;
|
||||
color: #667085;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.item.selected.text {
|
||||
color: #155EEF;
|
||||
}
|
||||
|
||||
.item:not(.selected):hover {
|
||||
color: #344054;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
.table td {
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.table thead td {
|
||||
height: 33px;
|
||||
line-height: 33px;
|
||||
}
|
||||
|
||||
.table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PlusIcon } from '@heroicons/react/24/solid'
|
||||
|
||||
export interface IAddFeatureBtnProps {
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const AddFeatureBtn: FC<IAddFeatureBtnProps> = ({
|
||||
onClick
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div
|
||||
className='
|
||||
flex items-center h-8 space-x-2 px-3
|
||||
border border-primary-100 rounded-lg bg-primary-25 hover:bg-primary-50 cursor-pointer
|
||||
text-xs font-semibold text-primary-600 uppercase
|
||||
'
|
||||
style={{
|
||||
boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
<PlusIcon className='w-4 h-4 font-semibold' />
|
||||
<div>{t('appDebug.operation.addFeature')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(AddFeatureBtn)
|
||||
@@ -0,0 +1,42 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
|
||||
export interface IFeatureItemProps {
|
||||
icon: React.ReactNode
|
||||
title: string
|
||||
description: string
|
||||
value: boolean
|
||||
onChange: (value: boolean) => void
|
||||
}
|
||||
|
||||
const FeatureItem: FC<IFeatureItemProps> = ({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
value,
|
||||
onChange
|
||||
}) => {
|
||||
return (
|
||||
<div className='flex justify-between p-3 rounded-xl border border-transparent bg-gray-50 hover:border-gray-200 cursor-pointer'>
|
||||
<div className='flex space-x-3 mr-2'>
|
||||
{/* icon */}
|
||||
<div
|
||||
className='shrink-0 flex items-center justify-center w-8 h-8 rounded-lg border border-gray-200 bg-white'
|
||||
style={{
|
||||
boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)',
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
<div>
|
||||
<div className='text-sm font-semibold text-gray-800'>{title}</div>
|
||||
<div className='text-xs font-normal text-gray-500'>{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Switch onChange={onChange} defaultValue={value} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(FeatureItem)
|
||||
@@ -0,0 +1,92 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import FeatureItem from './feature-item'
|
||||
import FeatureGroup from '../feature-group'
|
||||
import MoreLikeThisIcon from '../../../base/icons/more-like-this-icon'
|
||||
import SuggestedQuestionsAfterAnswerIcon from '@/app/components/app/configuration/base/icons/suggested-questions-after-answer-icon'
|
||||
|
||||
interface IConfig {
|
||||
openingStatement: boolean
|
||||
moreLikeThis: boolean
|
||||
suggestedQuestionsAfterAnswer: boolean
|
||||
}
|
||||
|
||||
export interface IChooseFeatureProps {
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
config: IConfig
|
||||
isChatApp: boolean
|
||||
onChange: (key: string, value: boolean) => void
|
||||
}
|
||||
|
||||
const OpeningStatementIcon = (
|
||||
<svg width="15" height="13" viewBox="0 0 15 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M8.33328 0.333252C4.83548 0.333252 1.99995 3.16878 1.99995 6.66659C1.99995 7.37325 2.11594 8.05419 2.33045 8.6906C2.36818 8.80254 2.39039 8.86877 2.40482 8.91762L2.40955 8.93407L2.40705 8.93928C2.38991 8.97462 2.36444 9.02207 2.31681 9.11025L1.21555 11.1486C1.1473 11.2749 1.07608 11.4066 1.02711 11.5212C0.978424 11.6351 0.899569 11.844 0.938369 12.0916C0.98385 12.3819 1.15471 12.6375 1.40556 12.7905C1.61957 12.9211 1.84276 12.9281 1.96659 12.9267C2.09117 12.9252 2.24012 12.9098 2.3829 12.895L5.81954 12.5397C5.87458 12.534 5.90335 12.5311 5.92443 12.5295L5.92715 12.5293L5.93539 12.5322C5.96129 12.5415 5.99642 12.555 6.05705 12.5784C6.76435 12.8509 7.53219 12.9999 8.33328 12.9999C11.8311 12.9999 14.6666 10.1644 14.6666 6.66659C14.6666 3.16878 11.8311 0.333252 8.33328 0.333252ZM5.97966 4.7214C6.73118 4.08722 7.73139 4.27352 8.3312 4.96609C8.931 4.27352 9.9183 4.09389 10.6827 4.7214C11.4472 5.34892 11.5401 6.41591 10.9499 7.16596C10.5843 7.63065 9.66655 8.47935 9.02117 9.05789C8.78411 9.2704 8.66558 9.37666 8.52332 9.41947C8.40129 9.4562 8.2611 9.4562 8.13907 9.41947C7.99682 9.37666 7.87829 9.2704 7.64122 9.05789C6.99584 8.47935 6.07814 7.63065 5.71251 7.16596C5.12234 6.41591 5.22815 5.35559 5.97966 4.7214Z" fill="#DD2590" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const ChooseFeature: FC<IChooseFeatureProps> = ({
|
||||
isShow,
|
||||
onClose,
|
||||
isChatApp,
|
||||
config,
|
||||
onChange
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
onClose={onClose}
|
||||
className='w-[400px]'
|
||||
title={t('appDebug.operation.addFeature')}
|
||||
closable
|
||||
>
|
||||
<div className='pt-5 pb-10'>
|
||||
{/* Chat Feature */}
|
||||
{isChatApp && (
|
||||
<FeatureGroup
|
||||
title={t('appDebug.feature.groupChat.title')}
|
||||
description={t('appDebug.feature.groupChat.description') as string}
|
||||
>
|
||||
<>
|
||||
<FeatureItem
|
||||
icon={OpeningStatementIcon}
|
||||
title={t('appDebug.feature.conversationOpener.title')}
|
||||
description={t('appDebug.feature.conversationOpener.description')}
|
||||
value={config.openingStatement}
|
||||
onChange={(value) => onChange('openingStatement', value)}
|
||||
/>
|
||||
<FeatureItem
|
||||
icon={<SuggestedQuestionsAfterAnswerIcon />}
|
||||
title={t('appDebug.feature.suggestedQuestionsAfterAnswer.title')}
|
||||
description={t('appDebug.feature.suggestedQuestionsAfterAnswer.description')}
|
||||
value={config.suggestedQuestionsAfterAnswer}
|
||||
onChange={(value) => onChange('suggestedQuestionsAfterAnswer', value)}
|
||||
/>
|
||||
</>
|
||||
</FeatureGroup>
|
||||
)}
|
||||
|
||||
{/* Text Generation Feature */}
|
||||
{!isChatApp && (
|
||||
<FeatureGroup title={t('appDebug.feature.groupExperience.title')}>
|
||||
<>
|
||||
<FeatureItem
|
||||
icon={<MoreLikeThisIcon />}
|
||||
title={t('appDebug.feature.moreLikeThis.title')}
|
||||
description={t('appDebug.feature.moreLikeThis.description')}
|
||||
value={config.moreLikeThis}
|
||||
onChange={(value) => onChange('moreLikeThis', value)}
|
||||
/>
|
||||
</>
|
||||
</FeatureGroup>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
export default React.memo(ChooseFeature)
|
||||
@@ -0,0 +1,30 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import GroupName from '@/app/components/app/configuration/base/group-name'
|
||||
|
||||
export interface IFeatureGroupProps {
|
||||
title: string
|
||||
description?: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const FeatureGroup: FC<IFeatureGroupProps> = ({
|
||||
title,
|
||||
description,
|
||||
children
|
||||
}) => {
|
||||
return (
|
||||
<div className='mb-6'>
|
||||
<div className='mb-2'>
|
||||
<GroupName name={title} />
|
||||
{description && (
|
||||
<div className='text-xs font-normal text-gray-500'>{description}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(FeatureGroup)
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
function useFeature({
|
||||
introduction,
|
||||
setIntroduction,
|
||||
moreLikeThis,
|
||||
setMoreLikeThis,
|
||||
suggestedQuestionsAfterAnswer,
|
||||
setSuggestedQuestionsAfterAnswer,
|
||||
}: {
|
||||
introduction: string
|
||||
setIntroduction: (introduction: string) => void
|
||||
moreLikeThis: boolean
|
||||
setMoreLikeThis: (moreLikeThis: boolean) => void
|
||||
suggestedQuestionsAfterAnswer: boolean
|
||||
setSuggestedQuestionsAfterAnswer: (suggestedQuestionsAfterAnswer: boolean) => void
|
||||
}) {
|
||||
const [tempshowOpeningStatement, setTempShowOpeningStatement] = React.useState(!!introduction)
|
||||
useEffect(() => {
|
||||
// wait to api data back
|
||||
if (!!introduction) {
|
||||
setTempShowOpeningStatement(true)
|
||||
}
|
||||
}, [introduction])
|
||||
|
||||
// const [tempMoreLikeThis, setTempMoreLikeThis] = React.useState(moreLikeThis)
|
||||
// useEffect(() => {
|
||||
// setTempMoreLikeThis(moreLikeThis)
|
||||
// }, [moreLikeThis])
|
||||
|
||||
const featureConfig = {
|
||||
openingStatement: tempshowOpeningStatement,
|
||||
moreLikeThis: moreLikeThis,
|
||||
suggestedQuestionsAfterAnswer: suggestedQuestionsAfterAnswer
|
||||
}
|
||||
const handleFeatureChange = (key: string, value: boolean) => {
|
||||
switch (key) {
|
||||
case 'openingStatement':
|
||||
if (!value) {
|
||||
setIntroduction('')
|
||||
}
|
||||
setTempShowOpeningStatement(value)
|
||||
break
|
||||
case 'moreLikeThis':
|
||||
setMoreLikeThis(value)
|
||||
break
|
||||
case 'suggestedQuestionsAfterAnswer':
|
||||
setSuggestedQuestionsAfterAnswer(value)
|
||||
break
|
||||
}
|
||||
}
|
||||
return {
|
||||
featureConfig,
|
||||
handleFeatureChange
|
||||
}
|
||||
}
|
||||
|
||||
export default useFeature
|
||||
153
web/app/components/app/configuration/config/index.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import produce from 'immer'
|
||||
import AddFeatureBtn from './feature/add-feature-btn'
|
||||
import ChooseFeature from './feature/choose-feature'
|
||||
import useFeature from './feature/use-feature'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import DatasetConfig from '../dataset-config'
|
||||
import ChatGroup from '../features/chat-group'
|
||||
import ExperienceEnchanceGroup from '../features/experience-enchance-group'
|
||||
import Toolbox from '../toolbox'
|
||||
import ConfigPrompt from '@/app/components/app/configuration/config-prompt'
|
||||
import ConfigVar from '@/app/components/app/configuration/config-var'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
import { AppType } from '@/types/app'
|
||||
import { useBoolean } from 'ahooks'
|
||||
|
||||
const Config: FC = () => {
|
||||
const {
|
||||
mode,
|
||||
introduction,
|
||||
setIntroduction,
|
||||
modelConfig,
|
||||
setModelConfig,
|
||||
setPrevPromptConfig,
|
||||
setFormattingChanged,
|
||||
moreLikeThisConifg,
|
||||
setMoreLikeThisConifg,
|
||||
suggestedQuestionsAfterAnswerConfig,
|
||||
setSuggestedQuestionsAfterAnswerConfig
|
||||
} = useContext(ConfigContext)
|
||||
const isChatApp = mode === AppType.chat
|
||||
|
||||
const promptTemplate = modelConfig.configs.prompt_template
|
||||
const promptVariables = modelConfig.configs.prompt_variables
|
||||
const handlePromptChange = (newTemplate: string, newVariables: PromptVariable[]) => {
|
||||
const newModelConfig = produce(modelConfig, (draft) => {
|
||||
draft.configs.prompt_template = newTemplate
|
||||
draft.configs.prompt_variables = [...draft.configs.prompt_variables, ...newVariables]
|
||||
})
|
||||
|
||||
if (modelConfig.configs.prompt_template !== newTemplate) {
|
||||
setFormattingChanged(true)
|
||||
}
|
||||
|
||||
setPrevPromptConfig(modelConfig.configs)
|
||||
setModelConfig(newModelConfig)
|
||||
}
|
||||
|
||||
const handlePromptVariablesNameChange = (newVariables: PromptVariable[]) => {
|
||||
setPrevPromptConfig(modelConfig.configs)
|
||||
const newModelConfig = produce(modelConfig, (draft) => {
|
||||
draft.configs.prompt_variables = newVariables
|
||||
})
|
||||
setModelConfig(newModelConfig)
|
||||
}
|
||||
|
||||
const [showChooseFeature, {
|
||||
setTrue: showChooseFeatureTrue,
|
||||
setFalse: showChooseFeatureFalse
|
||||
}] = useBoolean(false)
|
||||
const { featureConfig, handleFeatureChange } = useFeature({
|
||||
introduction,
|
||||
setIntroduction,
|
||||
moreLikeThis: moreLikeThisConifg.enabled,
|
||||
setMoreLikeThis: (value) => {
|
||||
setMoreLikeThisConifg(produce(moreLikeThisConifg, (draft) => {
|
||||
draft.enabled = value
|
||||
}))
|
||||
},
|
||||
suggestedQuestionsAfterAnswer: suggestedQuestionsAfterAnswerConfig.enabled,
|
||||
setSuggestedQuestionsAfterAnswer: (value) => {
|
||||
setSuggestedQuestionsAfterAnswerConfig(produce(suggestedQuestionsAfterAnswerConfig, (draft) => {
|
||||
draft.enabled = value
|
||||
}))
|
||||
},
|
||||
})
|
||||
|
||||
const hasChatConfig = isChatApp && (featureConfig.openingStatement || featureConfig.suggestedQuestionsAfterAnswer)
|
||||
const hasToolbox = false
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="pb-[20px]">
|
||||
<div className='flex justify-between items-center mb-4'>
|
||||
<AddFeatureBtn onClick={showChooseFeatureTrue} />
|
||||
<div>
|
||||
{/* AutoMatic */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showChooseFeature && (
|
||||
<ChooseFeature
|
||||
isShow={showChooseFeature}
|
||||
onClose={showChooseFeatureFalse}
|
||||
isChatApp={isChatApp}
|
||||
config={featureConfig}
|
||||
onChange={handleFeatureChange}
|
||||
/>
|
||||
)}
|
||||
{/* Template */}
|
||||
<ConfigPrompt
|
||||
mode={mode as AppType}
|
||||
promptTemplate={promptTemplate}
|
||||
promptVariables={promptVariables}
|
||||
onChange={handlePromptChange}
|
||||
/>
|
||||
|
||||
{/* Variables */}
|
||||
<ConfigVar
|
||||
promptVariables={promptVariables}
|
||||
onPromptVariablesChange={handlePromptVariablesNameChange}
|
||||
/>
|
||||
|
||||
{/* Dataset */}
|
||||
<DatasetConfig />
|
||||
|
||||
{/* ChatConifig */}
|
||||
{
|
||||
hasChatConfig && (
|
||||
<ChatGroup
|
||||
isShowOpeningStatement={featureConfig.openingStatement}
|
||||
openingStatementConfig={
|
||||
{
|
||||
promptTemplate,
|
||||
value: introduction,
|
||||
onChange: setIntroduction
|
||||
}
|
||||
}
|
||||
isShowSuggestedQuestionsAfterAnswer={featureConfig.suggestedQuestionsAfterAnswer}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{/* TextnGeneration config */}
|
||||
{moreLikeThisConifg.enabled && (
|
||||
<ExperienceEnchanceGroup />
|
||||
)}
|
||||
|
||||
|
||||
{/* Toolbox */}
|
||||
{
|
||||
hasToolbox && (
|
||||
<Toolbox searchToolConfig={false} sensitiveWordAvoidanceConifg={false} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(Config)
|
||||
@@ -0,0 +1,24 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import s from './style.module.css'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
export type IContrlBtnGroupProps = {
|
||||
onSave: () => void
|
||||
onReset: () => void
|
||||
}
|
||||
|
||||
const ContrlBtnGroup: FC<IContrlBtnGroupProps> = ({ onSave, onReset }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="fixed left-[224px] bottom-0 w-[519px] h-[64px]">
|
||||
<div className={`${s.ctrlBtn} flex items-center h-full pl-4 gap-2 bg-white`}>
|
||||
<Button type='primary' onClick={onSave}>{t('appDebug.operation.applyConfig')}</Button>
|
||||
<Button type='default' onClick={onReset}>{t('appDebug.operation.resetConfig')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ContrlBtnGroup)
|
||||
@@ -0,0 +1,6 @@
|
||||
.ctrlBtn {
|
||||
left: -16px;
|
||||
right: -16px;
|
||||
bottom: -16px;
|
||||
border-top: 1px solid #F3F4F6;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import cn from 'classnames'
|
||||
import TypeIcon from '../type-icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import RemoveIcon from '../../base/icons/remove-icon'
|
||||
import s from './style.module.css'
|
||||
|
||||
export interface ICardItemProps {
|
||||
className?: string
|
||||
config: any
|
||||
onRemove: (id: string) => void
|
||||
}
|
||||
|
||||
|
||||
|
||||
// const RemoveIcon = ({ className, onClick }: { className: string, onClick: () => void }) => (
|
||||
// <svg className={className} onClick={onClick} width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
// <path d="M10 6H14M6 8H18M16.6667 8L16.1991 15.0129C16.129 16.065 16.0939 16.5911 15.8667 16.99C15.6666 17.3412 15.3648 17.6235 15.0011 17.7998C14.588 18 14.0607 18 13.0062 18H10.9938C9.93927 18 9.41202 18 8.99889 17.7998C8.63517 17.6235 8.33339 17.3412 8.13332 16.99C7.90607 16.5911 7.871 16.065 7.80086 15.0129L7.33333 8M10.6667 11V14.3333M13.3333 11V14.3333" stroke="#667085" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
// </svg>
|
||||
// )
|
||||
|
||||
const CardItem: FC<ICardItemProps> = ({
|
||||
className,
|
||||
config,
|
||||
onRemove
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
cn(className, s.card,
|
||||
'flex items-center justify-between rounded-xl px-3 py-2.5 bg-white border border-gray-200 cursor-pointer')
|
||||
}>
|
||||
<div className='shrink-0 flex items-center space-x-2'>
|
||||
<TypeIcon type="upload_file" />
|
||||
<div>
|
||||
<div className='w-[160px] text-[13px] leading-[18px] font-medium text-gray-800 overflow-hidden text-ellipsis whitespace-nowrap'>{config.name}</div>
|
||||
<div className='flex text-xs text-gray-500'>
|
||||
{formatNumber(config.word_count)} {t('appDebug.feature.dataSet.words')} · {formatNumber(config.document_count)} {t('appDebug.feature.dataSet.textBlocks')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RemoveIcon className={`${s.deleteBtn} shrink-0`} onClick={() => onRemove(config.id)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(CardItem)
|
||||
@@ -0,0 +1,16 @@
|
||||
.card {
|
||||
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
|
||||
width: calc(50% - 4px);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06);
|
||||
}
|
||||
|
||||
.deleteBtn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.card:hover .deleteBtn {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import FeaturePanel from '../base/feature-panel'
|
||||
import OperationBtn from '../base/operation-btn'
|
||||
import CardItem from './card-item'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import SelectDataSet from './select-dataset'
|
||||
import { DataSet } from '@/models/datasets'
|
||||
import { isEqual } from 'lodash-es'
|
||||
|
||||
const Icon = (
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M12.6667 5.34368C12.6667 5.32614 12.6667 5.31738 12.6659 5.30147C12.6502 4.97229 12.3607 4.68295 12.0315 4.66737C12.0156 4.66662 12.0104 4.66662 12 4.66663H9.8391C9.30248 4.66662 8.85957 4.66661 8.49878 4.69609C8.12405 4.72671 7.77958 4.79242 7.45603 4.95728C6.95426 5.21294 6.54631 5.62089 6.29065 6.12265C6.12579 6.44621 6.06008 6.79068 6.02946 7.16541C5.99999 7.5262 5.99999 7.96911 6 8.50574V15.4942C5.99999 16.0308 5.99999 16.4737 6.02946 16.8345C6.06008 17.2092 6.12579 17.5537 6.29065 17.8773C6.54631 18.379 6.95426 18.787 7.45603 19.0426C7.77958 19.2075 8.12405 19.2732 8.49878 19.3038C8.85958 19.3333 9.30248 19.3333 9.83912 19.3333H14.1609C14.6975 19.3333 15.1404 19.3333 15.5012 19.3038C15.8759 19.2732 16.2204 19.2075 16.544 19.0426C17.0457 18.787 17.4537 18.379 17.7093 17.8773C17.8742 17.5537 17.9399 17.2092 17.9705 16.8345C18 16.4737 18 16.0308 18 15.4942V10.6666C18 10.6562 18 10.6511 17.9993 10.6352C17.9837 10.306 17.6943 10.0164 17.3651 10.0007C17.3492 9.99997 17.3405 9.99997 17.323 9.99997L14.3787 9.99997C14.2105 9.99999 14.0466 10 13.9078 9.98867C13.7555 9.97622 13.5756 9.94684 13.3947 9.85464C13.1438 9.72681 12.9398 9.52284 12.812 9.27195C12.7198 9.09101 12.6904 8.91118 12.678 8.75879C12.6666 8.62001 12.6666 8.45615 12.6667 8.2879L12.6667 5.34368ZM9.33333 12.6666C8.96514 12.6666 8.66667 12.9651 8.66667 13.3333C8.66667 13.7015 8.96514 14 9.33333 14H14.6667C15.0349 14 15.3333 13.7015 15.3333 13.3333C15.3333 12.9651 15.0349 12.6666 14.6667 12.6666H9.33333ZM9.33333 15.3333C8.96514 15.3333 8.66667 15.6318 8.66667 16C8.66667 16.3681 8.96514 16.6666 9.33333 16.6666H13.3333C13.7015 16.6666 14 16.3681 14 16C14 15.6318 13.7015 15.3333 13.3333 15.3333H9.33333Z" fill="#6938EF" />
|
||||
<path d="M16.6053 8.66662C16.8011 8.66662 16.8989 8.66663 16.9791 8.61747C17.0923 8.54806 17.16 8.38452 17.129 8.25538C17.107 8.16394 17.0432 8.10018 16.9155 7.97265L14.694 5.75111C14.5664 5.62345 14.5027 5.55962 14.4112 5.53764C14.2821 5.5066 14.1186 5.57429 14.0492 5.68752C14 5.7677 14 5.86557 14 6.06132L14 8.13327C14 8.31994 14 8.41328 14.0363 8.48459C14.0683 8.54731 14.1193 8.5983 14.182 8.63026C14.2533 8.66659 14.3466 8.66659 14.5333 8.66659L16.6053 8.66662Z" fill="#6938EF" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const DatasetConfig: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
dataSets: dataSet,
|
||||
setDataSets: setDataSet,
|
||||
setFormattingChanged
|
||||
} = useContext(ConfigContext)
|
||||
const selectedIds = dataSet.map((item) => item.id)
|
||||
|
||||
const hasData = dataSet.length > 0
|
||||
const [isShowSelectDataSet, { setTrue: showSelectDataSet, setFalse: hideSelectDataSet }] = useBoolean(false)
|
||||
const handleSelect = (data: DataSet[]) => {
|
||||
if (isEqual(data, dataSet)) {
|
||||
hideSelectDataSet()
|
||||
}
|
||||
setFormattingChanged(true)
|
||||
setDataSet(data)
|
||||
hideSelectDataSet()
|
||||
}
|
||||
const onRemove = (id: string) => {
|
||||
setDataSet(dataSet.filter((item) => item.id !== id))
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<FeaturePanel
|
||||
className='mt-3'
|
||||
headerIcon={Icon}
|
||||
title={t('appDebug.feature.dataSet.title')}
|
||||
headerRight={<OperationBtn type="add" onClick={showSelectDataSet} />}
|
||||
hasHeaderBottomBorder={!hasData}
|
||||
>
|
||||
{hasData ? (
|
||||
<div className='flex flex-wrap justify-between'>
|
||||
{dataSet.map((item) => (
|
||||
<CardItem
|
||||
className="mb-2"
|
||||
key={item.id}
|
||||
config={item}
|
||||
onRemove={onRemove}
|
||||
/>
|
||||
))}
|
||||
</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)
|
||||
@@ -0,0 +1,130 @@
|
||||
'use client'
|
||||
import React, { FC, useEffect } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { DataSet } from '@/models/datasets'
|
||||
import TypeIcon from '../type-icon'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import Link from 'next/link'
|
||||
|
||||
import s from './style.module.css'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
export interface ISelectDataSetProps {
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
selectedIds: string[]
|
||||
onSelect: (dataSet: DataSet[]) => void
|
||||
}
|
||||
|
||||
const SelectDataSet: FC<ISelectDataSetProps> = ({
|
||||
isShow,
|
||||
onClose,
|
||||
selectedIds,
|
||||
onSelect,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [selected, setSelected] = React.useState<DataSet[]>([])
|
||||
const [loaded, setLoaded] = React.useState(false)
|
||||
const [datasets, setDataSets] = React.useState<DataSet[] | null>(null)
|
||||
const hasNoData = !datasets || datasets?.length === 0
|
||||
// Only one dataset can be selected. Historical data retains data and supports multiple selections, but when saving, only one can be selected. This is based on considerations of performance and accuracy.
|
||||
const canSelectMulti = selectedIds.length > 1
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { data } = await fetchDatasets({ url: '/datasets', params: { page: 1 } })
|
||||
setDataSets(data)
|
||||
setLoaded(true)
|
||||
setSelected(data.filter((item) => selectedIds.includes(item.id)))
|
||||
})()
|
||||
}, [])
|
||||
const toggleSelect = (dataSet: DataSet) => {
|
||||
const isSelected = selected.some((item) => item.id === dataSet.id)
|
||||
if (isSelected) {
|
||||
setSelected(selected.filter((item) => item.id !== dataSet.id))
|
||||
}
|
||||
else {
|
||||
if (canSelectMulti) {
|
||||
setSelected([...selected, dataSet])
|
||||
} else {
|
||||
setSelected([dataSet])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelect = () => {
|
||||
if (selected.length > 1) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('appDebug.feature.dataSet.notSupportSelectMulti')
|
||||
})
|
||||
return
|
||||
}
|
||||
onSelect(selected)
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
onClose={onClose}
|
||||
className='w-[400px]'
|
||||
title={t('appDebug.feature.dataSet.selectTitle')}
|
||||
>
|
||||
{!loaded && (
|
||||
<div className='flex h-[200px]'>
|
||||
<Loading type='area' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(loaded && hasNoData) && (
|
||||
<div className='flex items-center justify-center mt-6 rounded-lg space-x-1 h-[128px] text-[13px] border'
|
||||
style={{
|
||||
background: 'rgba(0, 0, 0, 0.02)',
|
||||
borderColor: 'rgba(0, 0, 0, 0.02'
|
||||
}}
|
||||
>
|
||||
<span className='text-gray-500'>{t('appDebug.feature.dataSet.noDataSet')}</span>
|
||||
<Link href="/datasets/create" className='font-normal text-[#155EEF]'>{t('appDebug.feature.dataSet.toCreate')}</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{datasets && datasets?.length > 0 && (
|
||||
<>
|
||||
<div className='mt-7 space-y-1 max-h-[286px] overflow-y-auto'>
|
||||
{datasets.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={cn(s.item, selected.some(i => i.id === item.id) && s.selected, 'flex justify-between items-center h-10 px-2 rounded-lg bg-white border border-gray-200 cursor-pointer')}
|
||||
onClick={() => toggleSelect(item)}
|
||||
>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<TypeIcon type="upload_file" size='md' />
|
||||
<div className='max-w-[200px] text-[13px] font-medium text-gray-800 overflow-hidden text-ellipsis whitespace-nowrap'>{item.name}</div>
|
||||
</div>
|
||||
|
||||
<div className='max-w-[140px] flex text-xs text-gray-500 overflow-hidden text-ellipsis whitespace-nowrap'>
|
||||
{formatNumber(item.word_count)} {t('appDebug.feature.dataSet.words')} · {formatNumber(item.document_count)} {t('appDebug.feature.dataSet.textBlocks')}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{loaded && (
|
||||
<div className='flex justify-between items-center mt-8'>
|
||||
<div className='text-sm font-medium text-gray-700'>
|
||||
{selected.length > 0 && `${selected.length} ${t('appDebug.feature.dataSet.selected')}`}
|
||||
</div>
|
||||
<div className='flex space-x-2'>
|
||||
<Button className='!w-24 !h-9' onClick={onClose}>{t('common.operation.cancel')}</Button>
|
||||
<Button className='!w-24 !h-9' type='primary' onClick={handleSelect} disabled={hasNoData}>{t('common.operation.add')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
export default React.memo(SelectDataSet)
|
||||
@@ -0,0 +1,9 @@
|
||||
.item {
|
||||
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
|
||||
}
|
||||
|
||||
.item:hover,
|
||||
.item.selected {
|
||||
background: #F5F8FF;
|
||||
border-color: #528BFF;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
|
||||
export interface ITypeIconProps {
|
||||
type: 'upload_file'
|
||||
size?: 'md' | 'lg'
|
||||
}
|
||||
|
||||
// data_source_type: current only support upload_file
|
||||
const Icon = ({ type, size = "lg" }: ITypeIconProps) => {
|
||||
const len = size === "lg" ? 32 : 24
|
||||
const iconMap = {
|
||||
upload_file: (
|
||||
<svg width={len} height={len} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.25" y="0.25" width="31.5" height="31.5" rx="7.75" fill="#F5F8FF" />
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M8.66669 12.1078C8.66668 11.7564 8.66667 11.4532 8.68707 11.2035C8.7086 10.9399 8.75615 10.6778 8.88468 10.4255C9.07642 10.0492 9.38238 9.74322 9.75871 9.55147C10.011 9.42294 10.2731 9.3754 10.5367 9.35387C10.7864 9.33346 11.0896 9.33347 11.441 9.33349L14.0978 9.33341C14.4935 9.33289 14.8415 9.33243 15.1615 9.4428C15.4417 9.53946 15.697 9.69722 15.9087 9.90465C16.1506 10.1415 16.3058 10.4529 16.4823 10.8071L17.0786 12H19.4942C20.0309 12 20.4738 12 20.8346 12.0295C21.2093 12.0601 21.5538 12.1258 21.8773 12.2907C22.3791 12.5463 22.787 12.9543 23.0427 13.456C23.2076 13.7796 23.2733 14.1241 23.3039 14.4988C23.3334 14.8596 23.3334 15.3025 23.3334 15.8391V18.8276C23.3334 19.3642 23.3334 19.8071 23.3039 20.1679C23.2733 20.5426 23.2076 20.8871 23.0427 21.2107C22.787 21.7124 22.3791 22.1204 21.8773 22.376C21.5538 22.5409 21.2093 22.6066 20.8346 22.6372C20.4738 22.6667 20.0309 22.6667 19.4942 22.6667H12.5058C11.9692 22.6667 11.5263 22.6667 11.1655 22.6372C10.7907 22.6066 10.4463 22.5409 10.1227 22.376C9.62095 22.1204 9.213 21.7124 8.95734 21.2107C8.79248 20.8871 8.72677 20.5426 8.69615 20.1679C8.66667 19.8071 8.66668 19.3642 8.66669 18.8276V12.1078ZM14.0149 10.6668C14.5418 10.6668 14.6463 10.6755 14.7267 10.7033C14.8201 10.7355 14.9052 10.7881 14.9758 10.8572C15.0366 10.9167 15.0911 11.0063 15.3267 11.4776L15.5879 12L10.0001 12C10.0004 11.69 10.0024 11.4781 10.016 11.312C10.0308 11.1309 10.0559 11.0638 10.0727 11.0308C10.1366 10.9054 10.2386 10.8034 10.364 10.7395C10.397 10.7227 10.4641 10.6976 10.6452 10.6828C10.8341 10.6673 11.0823 10.6668 11.4667 10.6668H14.0149Z" fill="#444CE7" />
|
||||
<rect x="0.25" y="0.25" width="31.5" height="31.5" rx="7.75" stroke="#E0EAFF" strokeWidth="0.5" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
return iconMap[type]
|
||||
|
||||
}
|
||||
|
||||
const TypeIcon: FC<ITypeIconProps> = ({
|
||||
type,
|
||||
size = 'lg',
|
||||
}) => {
|
||||
return (
|
||||
<Icon type={type} size={size} ></Icon>
|
||||
)
|
||||
}
|
||||
export default React.memo(TypeIcon)
|
||||
417
web/app/components/app/configuration/debug/index.tsx
Normal file
@@ -0,0 +1,417 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import React, { useEffect, useState, useRef } from 'react'
|
||||
import cn from 'classnames'
|
||||
import produce from 'immer'
|
||||
import { useBoolean, useGetState } from 'ahooks'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { AppType } from '@/types/app'
|
||||
import PromptValuePanel, { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
|
||||
import type { IChatItem } from '@/app/components/app/chat'
|
||||
import Chat from '@/app/components/app/chat'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { sendChatMessage, sendCompletionMessage, fetchSuggestedQuestions, fetchConvesationMessages } from '@/service/debug'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { ModelConfig as BackendModelConfig } from '@/types/app'
|
||||
import { promptVariablesToUserInputsForm } from '@/utils/model-config'
|
||||
import HasNotSetAPIKEY from '../base/warning-mask/has-not-set-api'
|
||||
import FormattingChanged from '../base/warning-mask/formatting-changed'
|
||||
import TextGeneration from '@/app/components/app/text-generate/item'
|
||||
import GroupName from '../base/group-name'
|
||||
import dayjs from 'dayjs'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
|
||||
interface IDebug {
|
||||
hasSetAPIKEY: boolean
|
||||
onSetting: () => void
|
||||
}
|
||||
|
||||
const Debug: FC<IDebug> = ({
|
||||
hasSetAPIKEY = true,
|
||||
onSetting
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
appId,
|
||||
mode,
|
||||
introduction,
|
||||
suggestedQuestionsAfterAnswerConfig,
|
||||
moreLikeThisConifg,
|
||||
inputs,
|
||||
// setInputs,
|
||||
formattingChanged,
|
||||
setFormattingChanged,
|
||||
conversationId,
|
||||
setConversationId,
|
||||
controlClearChatMessage,
|
||||
dataSets,
|
||||
modelConfig,
|
||||
completionParams,
|
||||
} = useContext(ConfigContext)
|
||||
|
||||
|
||||
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])
|
||||
|
||||
const getIntroduction = () => replaceStringWithValues(introduction, modelConfig.configs.prompt_variables, inputs)
|
||||
useEffect(() => {
|
||||
if (introduction && !chatList.some(item => !item.isAnswer)) {
|
||||
setChatList([{
|
||||
id: `${Date.now()}`,
|
||||
content: getIntroduction(),
|
||||
isAnswer: true,
|
||||
isOpeningStatement: true
|
||||
}])
|
||||
}
|
||||
}, [introduction, modelConfig.configs.prompt_variables, inputs])
|
||||
|
||||
const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false)
|
||||
const [abortController, setAbortController] = useState<AbortController | null>(null)
|
||||
const [isShowFormattingChangeConfirm, setIsShowFormattingChangeConfirm] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (formattingChanged && chatList.some(item => !item.isAnswer)) {
|
||||
setIsShowFormattingChangeConfirm(true)
|
||||
}
|
||||
setFormattingChanged(false)
|
||||
}, [formattingChanged])
|
||||
|
||||
const clearConversation = () => {
|
||||
setConversationId(null)
|
||||
abortController?.abort()
|
||||
setResponsingFalse()
|
||||
setChatList(introduction ? [{
|
||||
id: `${Date.now()}`,
|
||||
content: getIntroduction(),
|
||||
isAnswer: true,
|
||||
isOpeningStatement: true
|
||||
}] : [])
|
||||
setIsShowSuggestion(false)
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
clearConversation()
|
||||
setIsShowFormattingChangeConfirm(false)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsShowFormattingChangeConfirm(false)
|
||||
}
|
||||
|
||||
const { notify } = useContext(ToastContext)
|
||||
const logError = (message: string) => {
|
||||
notify({ type: 'error', message })
|
||||
}
|
||||
|
||||
const checkCanSend = () => {
|
||||
let hasEmptyInput = false
|
||||
const requiredVars = modelConfig.configs.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
|
||||
// debugger
|
||||
requiredVars.forEach(({ key }) => {
|
||||
if (hasEmptyInput) {
|
||||
return
|
||||
}
|
||||
if (!inputs[key]) {
|
||||
hasEmptyInput = true
|
||||
}
|
||||
})
|
||||
|
||||
if (hasEmptyInput) {
|
||||
logError(t('appDebug.errorMessage.valueOfVarRequired'))
|
||||
return false
|
||||
}
|
||||
return !hasEmptyInput
|
||||
}
|
||||
|
||||
const [isShowSuggestion, setIsShowSuggestion] = useState(false)
|
||||
const doShowSuggestion = isShowSuggestion && !isResponsing
|
||||
const [suggestQuestions, setSuggestQuestions] = useState<string[]>([])
|
||||
const onSend = async (message: string) => {
|
||||
if (isResponsing) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
|
||||
return false
|
||||
}
|
||||
|
||||
const postDatasets = dataSets.map(({ id }) => ({
|
||||
dataset: {
|
||||
enabled: true,
|
||||
id,
|
||||
}
|
||||
}))
|
||||
|
||||
const postModelConfig: BackendModelConfig = {
|
||||
pre_prompt: modelConfig.configs.prompt_template,
|
||||
user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
|
||||
opening_statement: introduction,
|
||||
more_like_this: {
|
||||
enabled: false
|
||||
},
|
||||
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
|
||||
agent_mode: {
|
||||
enabled: true,
|
||||
tools: [...postDatasets]
|
||||
},
|
||||
model: {
|
||||
provider: modelConfig.provider,
|
||||
name: modelConfig.model_id,
|
||||
completion_params: completionParams as any
|
||||
},
|
||||
}
|
||||
|
||||
const data = {
|
||||
conversation_id: conversationId,
|
||||
inputs,
|
||||
query: message,
|
||||
model_config: postModelConfig,
|
||||
}
|
||||
|
||||
// qustion
|
||||
const questionId = `question-${Date.now()}`
|
||||
const questionItem = {
|
||||
id: questionId,
|
||||
content: message,
|
||||
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 = {
|
||||
id: `${Date.now()}`,
|
||||
content: '',
|
||||
isAnswer: true,
|
||||
}
|
||||
|
||||
let _newConversationId: null | string = null
|
||||
|
||||
setResponsingTrue()
|
||||
setIsShowSuggestion(false)
|
||||
sendChatMessage(appId, data, {
|
||||
getAbortController: (abortController) => {
|
||||
setAbortController(abortController)
|
||||
},
|
||||
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId }: any) => {
|
||||
responseItem.content = responseItem.content + message
|
||||
if (isFirstMessage && newConversationId) {
|
||||
setConversationId(newConversationId)
|
||||
_newConversationId = newConversationId
|
||||
}
|
||||
if (messageId) {
|
||||
responseItem.id = messageId
|
||||
}
|
||||
// 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 })
|
||||
}
|
||||
draft.push({ ...responseItem })
|
||||
})
|
||||
setChatList(newListWithAnswer)
|
||||
},
|
||||
async onCompleted(hasError?: boolean) {
|
||||
setResponsingFalse()
|
||||
if (hasError) {
|
||||
return
|
||||
}
|
||||
if (_newConversationId) {
|
||||
const { data }: any = await fetchConvesationMessages(appId, _newConversationId as string)
|
||||
const newResponseItem = data.find((item: any) => item.id === responseItem.id)
|
||||
if (!newResponseItem) {
|
||||
return
|
||||
}
|
||||
setChatList(produce(getChatList(), draft => {
|
||||
const index = draft.findIndex(item => item.id === responseItem.id)
|
||||
if (index !== -1) {
|
||||
draft[index] = {
|
||||
...draft[index],
|
||||
more: {
|
||||
time: dayjs.unix(newResponseItem.created_at).format('hh:mm A'),
|
||||
tokens: newResponseItem.answer_tokens,
|
||||
latency: (newResponseItem.provider_response_latency / 1000).toFixed(2),
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
if (suggestedQuestionsAfterAnswerConfig.enabled) {
|
||||
const { data }: any = await fetchSuggestedQuestions(appId, responseItem.id)
|
||||
setSuggestQuestions(data)
|
||||
setIsShowSuggestion(true)
|
||||
}
|
||||
},
|
||||
onError() {
|
||||
setResponsingFalse()
|
||||
// role back placeholder answer
|
||||
setChatList(produce(getChatList(), draft => {
|
||||
draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1)
|
||||
}))
|
||||
}
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (controlClearChatMessage)
|
||||
setChatList([])
|
||||
}, [controlClearChatMessage])
|
||||
|
||||
const [completionQuery, setCompletionQuery] = useState('')
|
||||
const [completionRes, setCompletionRes] = useState(``)
|
||||
|
||||
const sendTextCompletion = async () => {
|
||||
if (isResponsing) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
|
||||
return false
|
||||
}
|
||||
|
||||
if (!checkCanSend())
|
||||
return
|
||||
|
||||
if (!completionQuery) {
|
||||
logError(t('appDebug.errorMessage.queryRequired'))
|
||||
return false
|
||||
}
|
||||
|
||||
const postDatasets = dataSets.map(({ id }) => ({
|
||||
dataset: {
|
||||
enabled: true,
|
||||
id,
|
||||
}
|
||||
}))
|
||||
|
||||
const postModelConfig: BackendModelConfig = {
|
||||
pre_prompt: modelConfig.configs.prompt_template,
|
||||
user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
|
||||
opening_statement: introduction,
|
||||
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
|
||||
more_like_this: moreLikeThisConifg,
|
||||
agent_mode: {
|
||||
enabled: true,
|
||||
tools: [...postDatasets]
|
||||
},
|
||||
model: {
|
||||
provider: modelConfig.provider,
|
||||
name: modelConfig.model_id,
|
||||
completion_params: completionParams as any
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
const data = {
|
||||
inputs,
|
||||
query: completionQuery,
|
||||
model_config: postModelConfig,
|
||||
}
|
||||
|
||||
setCompletionRes('')
|
||||
const res: string[] = []
|
||||
|
||||
setResponsingTrue()
|
||||
sendCompletionMessage(appId, data, {
|
||||
onData: (data: string) => {
|
||||
res.push(data)
|
||||
setCompletionRes(res.join(''))
|
||||
},
|
||||
onCompleted() {
|
||||
setResponsingFalse()
|
||||
},
|
||||
onError() {
|
||||
setResponsingFalse()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="shrink-0">
|
||||
<div className='flex items-center justify-between mb-2'>
|
||||
<div className='h2 '>{t('appDebug.inputs.title')}</div>
|
||||
{mode === 'chat' && (
|
||||
<Button className='flex items-center gap-1 !h-8 !bg-white' onClick={clearConversation}>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.66663 2.66629V5.99963H3.05463M3.05463 5.99963C3.49719 4.90505 4.29041 3.98823 5.30998 3.39287C6.32954 2.7975 7.51783 2.55724 8.68861 2.70972C9.85938 2.8622 10.9465 3.39882 11.7795 4.23548C12.6126 5.07213 13.1445 6.16154 13.292 7.33296M3.05463 5.99963H5.99996M13.3333 13.333V9.99963H12.946M12.946 9.99963C12.5028 11.0936 11.7093 12.0097 10.6898 12.6045C9.67038 13.1993 8.48245 13.4393 7.31203 13.2869C6.1416 13.1344 5.05476 12.5982 4.22165 11.7621C3.38854 10.926 2.8562 9.83726 2.70796 8.66629M12.946 9.99963H9.99996" stroke="#1C64F2" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
<span className='text-primary-600 text-[13px] font-semibold'>{t('common.operation.refresh')}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<PromptValuePanel
|
||||
appType={mode as AppType}
|
||||
value={completionQuery}
|
||||
onChange={setCompletionQuery}
|
||||
onSend={sendTextCompletion}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col grow">
|
||||
{/* Chat */}
|
||||
{mode === AppType.chat && (
|
||||
<div className="mt-[34px] h-full flex flex-col">
|
||||
<div className={cn(doShowSuggestion ? 'pb-[140px]' : 'pb-[66px]', "relative mt-1.5 grow h-[200px] overflow-hidden")}>
|
||||
<div className="h-full overflow-y-auto" ref={chatListDomRef}>
|
||||
{/* {JSON.stringify(chatList)} */}
|
||||
<Chat
|
||||
chatList={chatList}
|
||||
onSend={onSend}
|
||||
checkCanSend={checkCanSend}
|
||||
feedbackDisabled
|
||||
useCurrentUserAvatar
|
||||
isResponsing={isResponsing}
|
||||
abortResponsing={() => {
|
||||
abortController?.abort()
|
||||
setResponsingFalse()
|
||||
}}
|
||||
isShowSuggestion={doShowSuggestion}
|
||||
suggestionList={suggestQuestions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Text Generation */}
|
||||
{mode === AppType.completion && (
|
||||
<div className="mt-6">
|
||||
<GroupName name={t('appDebug.result')} />
|
||||
{(completionRes || isResponsing) && (
|
||||
<TextGeneration
|
||||
className="mt-2"
|
||||
content={completionRes}
|
||||
isLoading={!completionRes && isResponsing}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isShowFormattingChangeConfirm && (
|
||||
<FormattingChanged
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!hasSetAPIKEY && (<HasNotSetAPIKEY isTrailFinished={!IS_CE_EDITION} onSetting={onSetting} />)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(Debug)
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import GroupName from '../../base/group-name'
|
||||
import OpeningStatement, { IOpeningStatementProps } from './opening-statement'
|
||||
import SuggestedQuestionsAfterAnswer from './suggested-questions-after-answer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
/*
|
||||
* Include
|
||||
* 1. Conversation Opener
|
||||
* 2. Opening Suggestion
|
||||
* 3. Next question suggestion
|
||||
*/
|
||||
interface ChatGroupProps {
|
||||
isShowOpeningStatement: boolean
|
||||
openingStatementConfig: IOpeningStatementProps
|
||||
isShowSuggestedQuestionsAfterAnswer: boolean
|
||||
}
|
||||
const ChatGroup: FC<ChatGroupProps> = ({
|
||||
isShowOpeningStatement,
|
||||
openingStatementConfig,
|
||||
isShowSuggestedQuestionsAfterAnswer
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='mt-7'>
|
||||
<GroupName name={t('appDebug.feature.groupChat.title')} />
|
||||
<div className='space-y-3'>
|
||||
{isShowOpeningStatement && (
|
||||
<OpeningStatement {...openingStatementConfig} />
|
||||
)}
|
||||
{isShowSuggestedQuestionsAfterAnswer && (
|
||||
<SuggestedQuestionsAfterAnswer />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ChatGroup)
|
||||
@@ -0,0 +1,177 @@
|
||||
'use client'
|
||||
import React, { FC, useEffect, useRef, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import produce from 'immer'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import Panel from '@/app/components/app/configuration/base/feature-panel'
|
||||
import Button from '@/app/components/base/button'
|
||||
import OperationBtn from '@/app/components/app/configuration/base/operation-btn'
|
||||
import { getInputKeys } from '@/app/components/base/block-input'
|
||||
import ConfirmAddVar from '@/app/components/app/configuration/config-prompt/confirm-add-var'
|
||||
import { getNewVar } from '@/utils/var'
|
||||
import { varHighlightHTML } from '@/app/components/app/configuration/base/var-highlight'
|
||||
|
||||
export interface IOpeningStatementProps {
|
||||
promptTemplate: string
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
// regex to match the {{}} and replace it with a span
|
||||
const regex = /\{\{([^}]+)\}\}/g
|
||||
|
||||
const OpeningStatement: FC<IOpeningStatementProps> = ({
|
||||
value = '',
|
||||
onChange
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
modelConfig,
|
||||
setModelConfig,
|
||||
} = useContext(ConfigContext)
|
||||
const promptVariables = modelConfig.configs.prompt_variables
|
||||
const [notIncludeKeys, setNotIncludeKeys] = useState<string[]>([])
|
||||
|
||||
const hasValue = !!(value || '').trim()
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const [isFocus, { setTrue: didSetFocus, setFalse: setBlur }] = useBoolean(false)
|
||||
const setFocus = () => {
|
||||
didSetFocus()
|
||||
setTimeout(() => {
|
||||
const input = inputRef.current
|
||||
if (input) {
|
||||
input.focus()
|
||||
input.setSelectionRange(input.value.length, input.value.length)
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
const [tempValue, setTempValue] = useState(value)
|
||||
useEffect(() => {
|
||||
setTempValue(value || '')
|
||||
}, [value])
|
||||
|
||||
const coloredContent = (tempValue || '')
|
||||
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
|
||||
.replace(/\n/g, '<br />')
|
||||
|
||||
|
||||
const handleEdit = () => {
|
||||
setFocus()
|
||||
}
|
||||
|
||||
const [isShowConfirmAddVar, { setTrue: showConfirmAddVar, setFalse: hideConfirmAddVar }] = useBoolean(false)
|
||||
|
||||
const handleCancel = () => {
|
||||
setBlur()
|
||||
setTempValue(value)
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
const keys = getInputKeys(tempValue)
|
||||
const promptKeys = promptVariables.map((item) => item.key)
|
||||
let notIncludeKeys: string[] = []
|
||||
|
||||
if (promptKeys.length === 0) {
|
||||
if (keys.length > 0) {
|
||||
notIncludeKeys = keys
|
||||
}
|
||||
} else {
|
||||
notIncludeKeys = keys.filter((key) => !promptKeys.includes(key))
|
||||
}
|
||||
|
||||
if (notIncludeKeys.length > 0) {
|
||||
setNotIncludeKeys(notIncludeKeys)
|
||||
showConfirmAddVar()
|
||||
return
|
||||
}
|
||||
setBlur()
|
||||
onChange(tempValue)
|
||||
}
|
||||
|
||||
const cancelAutoAddVar = () => {
|
||||
onChange(tempValue)
|
||||
hideConfirmAddVar()
|
||||
setBlur()
|
||||
}
|
||||
|
||||
const autoAddVar = () => {
|
||||
const newModelConfig = produce(modelConfig, (draft) => {
|
||||
draft.configs.prompt_variables = [...draft.configs.prompt_variables, ...notIncludeKeys.map((key) => getNewVar(key))]
|
||||
})
|
||||
onChange(tempValue)
|
||||
setModelConfig(newModelConfig)
|
||||
hideConfirmAddVar()
|
||||
setBlur()
|
||||
}
|
||||
|
||||
const headerRight = (
|
||||
<OperationBtn type='edit' actionName={hasValue ? '' : t('appDebug.openingStatement.writeOpner') as string} onClick={handleEdit} />
|
||||
)
|
||||
|
||||
return (
|
||||
<Panel
|
||||
className={cn(isShowConfirmAddVar && 'h-[220px]', 'relative mt-4')}
|
||||
title={t('appDebug.openingStatement.title')}
|
||||
headerIcon={
|
||||
<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.33353 1.33301C4.83572 1.33301 2.00019 4.16854 2.00019 7.66634C2.00019 8.37301 2.11619 9.05395 2.3307 9.69036C2.36843 9.80229 2.39063 9.86853 2.40507 9.91738L2.40979 9.93383L2.40729 9.93903C2.39015 9.97437 2.36469 10.0218 2.31705 10.11L1.2158 12.1484C1.14755 12.2746 1.07633 12.4064 1.02735 12.5209C0.978668 12.6348 0.899813 12.8437 0.938613 13.0914C0.984094 13.3817 1.15495 13.6373 1.40581 13.7903C1.61981 13.9208 1.843 13.9279 1.96683 13.9264C2.09141 13.925 2.24036 13.9095 2.38314 13.8947L5.81978 13.5395C5.87482 13.5338 5.9036 13.5309 5.92468 13.5292L5.92739 13.529L5.93564 13.532C5.96154 13.5413 5.99666 13.5548 6.0573 13.5781C6.76459 13.8506 7.53244 13.9997 8.33353 13.9997C11.8313 13.9997 14.6669 11.1641 14.6669 7.66634C14.6669 4.16854 11.8313 1.33301 8.33353 1.33301ZM5.9799 5.72116C6.73142 5.08698 7.73164 5.27327 8.33144 5.96584C8.93125 5.27327 9.91854 5.09365 10.683 5.72116C11.4474 6.34867 11.5403 7.41567 10.9501 8.16572C10.5845 8.6304 9.6668 9.47911 9.02142 10.0576C8.78435 10.2702 8.66582 10.3764 8.52357 10.4192C8.40154 10.456 8.26134 10.456 8.13931 10.4192C7.99706 10.3764 7.87853 10.2702 7.64147 10.0576C6.99609 9.47911 6.07839 8.6304 5.71276 8.16572C5.12259 7.41567 5.22839 6.35534 5.9799 5.72116Z" fill="#E74694" />
|
||||
</svg>
|
||||
}
|
||||
headerRight={headerRight}
|
||||
hasHeaderBottomBorder={!hasValue}
|
||||
isFocus={isFocus}
|
||||
>
|
||||
<div className='text-gray-700 text-sm'>
|
||||
{(hasValue || (!hasValue && isFocus)) ? (
|
||||
<>
|
||||
{isFocus ? (
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
value={tempValue}
|
||||
rows={3}
|
||||
onChange={e => setTempValue(e.target.value)}
|
||||
className="w-full px-0 text-sm border-0 bg-transparent focus:outline-none "
|
||||
placeholder={t('appDebug.openingStatement.placeholder') as string}
|
||||
>
|
||||
</textarea>
|
||||
) : (
|
||||
<div dangerouslySetInnerHTML={{
|
||||
__html: coloredContent
|
||||
}}></div>
|
||||
)}
|
||||
|
||||
{/* Operation Bar */}
|
||||
{isFocus && (
|
||||
<div className='mt-2 flex items-center justify-between'>
|
||||
<div className='text-xs text-gray-500'>{t('appDebug.openingStatement.varTip')}</div>
|
||||
|
||||
<div className='flex gap-2'>
|
||||
<Button className='!h-8 text-sm' onClick={handleCancel}>{t('common.operation.cancel')}</Button>
|
||||
<Button className='!h-8 text-sm' onClick={handleConfirm} type="primary">{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</>) : (
|
||||
<div className='pt-2 pb-1 text-xs text-gray-500'>{t('appDebug.openingStatement.noDataPlaceHolder')}</div>
|
||||
)}
|
||||
|
||||
{isShowConfirmAddVar && (
|
||||
<ConfirmAddVar
|
||||
varNameArr={notIncludeKeys}
|
||||
onConfrim={autoAddVar}
|
||||
onCancel={cancelAutoAddVar}
|
||||
onHide={hideConfirmAddVar}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
export default React.memo(OpeningStatement)
|
||||
@@ -0,0 +1,33 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Panel from '@/app/components/app/configuration/base/feature-panel'
|
||||
import SuggestedQuestionsAfterAnswerIcon from '@/app/components/app/configuration/base/icons/suggested-questions-after-answer-icon'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
const SuggestedQuestionsAfterAnswer: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Panel
|
||||
title={
|
||||
<div className='flex items-center gap-2'>
|
||||
<div>{t('appDebug.feature.suggestedQuestionsAfterAnswer.title')}</div>
|
||||
<Tooltip htmlContent={<div className='w-[180px]'>
|
||||
{t('appDebug.feature.suggestedQuestionsAfterAnswer.description')}
|
||||
</div>} selector='suggestion-question-tooltip'>
|
||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.66667 11.1667H8V8.5H7.33333M8 5.83333H8.00667M14 8.5C14 9.28793 13.8448 10.0681 13.5433 10.7961C13.2417 11.5241 12.7998 12.1855 12.2426 12.7426C11.6855 13.2998 11.0241 13.7417 10.2961 14.0433C9.56815 14.3448 8.78793 14.5 8 14.5C7.21207 14.5 6.43185 14.3448 5.7039 14.0433C4.97595 13.7417 4.31451 13.2998 3.75736 12.7426C3.20021 12.1855 2.75825 11.5241 2.45672 10.7961C2.15519 10.0681 2 9.28793 2 8.5C2 6.9087 2.63214 5.38258 3.75736 4.25736C4.88258 3.13214 6.4087 2.5 8 2.5C9.5913 2.5 11.1174 3.13214 12.2426 4.25736C13.3679 5.38258 14 6.9087 14 8.5Z" stroke="#9CA3AF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
headerIcon={<SuggestedQuestionsAfterAnswerIcon />}
|
||||
headerRight={
|
||||
<div className='text-xs text-gray-500'>{t('appDebug.feature.suggestedQuestionsAfterAnswer.resDes')}</div>
|
||||
}
|
||||
noBodySpacing
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(SuggestedQuestionsAfterAnswer)
|
||||
@@ -0,0 +1,21 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import GroupName from '../../base/group-name'
|
||||
import MoreLikeThis from './more-like-this'
|
||||
|
||||
/*
|
||||
* Include
|
||||
* 1. More like this
|
||||
*/
|
||||
const ExperienceEnchanceGroup: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='mt-7'>
|
||||
<GroupName name={t('appDebug.feature.groupExperience.title')} />
|
||||
<MoreLikeThis />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ExperienceEnchanceGroup)
|
||||
@@ -0,0 +1,50 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import Panel from '@/app/components/app/configuration/base/feature-panel'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MoreLikeThisIcon from '../../../base/icons/more-like-this-icon'
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline'
|
||||
import { useLocalStorageState } from 'ahooks'
|
||||
|
||||
const GENERATE_NUM = 1
|
||||
|
||||
const warningIcon = (
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M6.40616 0.834307C6.14751 0.719294 5.85222 0.719294 5.59356 0.834307C5.3938 0.923133 5.26403 1.07959 5.17373 1.20708C5.08495 1.33242 4.9899 1.49664 4.88536 1.67723L0.751783 8.81705C0.646828 8.9983 0.551451 9.16302 0.486781 9.3028C0.421056 9.44487 0.349754 9.63584 0.372478 9.85381C0.401884 10.1359 0.549654 10.3922 0.779012 10.5589C0.956259 10.6878 1.15726 10.7218 1.31314 10.7361C1.46651 10.7501 1.65684 10.7501 1.86628 10.7501H10.1334C10.3429 10.7501 10.5332 10.7501 10.6866 10.7361C10.8425 10.7218 11.0435 10.6878 11.2207 10.5589C11.4501 10.3922 11.5978 10.1359 11.6272 9.85381C11.65 9.63584 11.5787 9.44487 11.5129 9.3028C11.4483 9.16303 11.3529 8.99833 11.248 8.81709L7.11436 1.67722C7.00983 1.49663 6.91477 1.33242 6.82599 1.20708C6.73569 1.07959 6.60593 0.923133 6.40616 0.834307ZM6.49988 4.50012C6.49988 4.22398 6.27602 4.00012 5.99988 4.00012C5.72374 4.00012 5.49988 4.22398 5.49988 4.50012V6.50012C5.49988 6.77626 5.72374 7.00012 5.99988 7.00012C6.27602 7.00012 6.49988 6.77626 6.49988 6.50012V4.50012ZM5.99988 8.00012C5.72374 8.00012 5.49988 8.22398 5.49988 8.50012C5.49988 8.77626 5.72374 9.00012 5.99988 9.00012H6.00488C6.28102 9.00012 6.50488 8.77626 6.50488 8.50012C6.50488 8.22398 6.28102 8.00012 6.00488 8.00012H5.99988Z" fill="#F79009" />
|
||||
</svg>
|
||||
|
||||
)
|
||||
const MoreLikeThis: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [isHideTip, setIsHideTip] = useLocalStorageState('isHideMoreLikeThisTip', {
|
||||
defaultValue: false,
|
||||
})
|
||||
|
||||
const headerRight = (
|
||||
<div className='text-xs text-gray-500'>{t('appDebug.feature.moreLikeThis.generateNumTip')} {GENERATE_NUM}</div>
|
||||
)
|
||||
return (
|
||||
<Panel
|
||||
className='mt-4'
|
||||
title={t('appDebug.feature.moreLikeThis.title')}
|
||||
headerIcon={<MoreLikeThisIcon />}
|
||||
headerRight={headerRight}
|
||||
noBodySpacing
|
||||
>
|
||||
{!isHideTip && (
|
||||
<div className='flex justify-between items-center h-9 px-3 rounded-b-xl bg-[#FFFAEB] text-xs text-gray-700'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<div>{warningIcon}</div>
|
||||
<div>{t('appDebug.feature.moreLikeThis.tip')}</div>
|
||||
</div>
|
||||
<div className='flex items-center justify-center w-4 h-4 cursor-pointer' onClick={() => setIsHideTip(true)}>
|
||||
<XMarkIcon className="w-3 h-3" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
export default React.memo(MoreLikeThis)
|
||||
317
web/app/components/app/configuration/index.tsx
Normal file
@@ -0,0 +1,317 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import produce from 'immer'
|
||||
import type { CompletionParams, Inputs, ModelConfig, PromptConfig, PromptVariable, MoreLikeThisConfig } from '@/models/debug'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import type { ModelConfig as BackendModelConfig } from '@/types/app'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import ConfigModel from '@/app/components/app/configuration/config-model'
|
||||
import Config from '@/app/components/app/configuration/config'
|
||||
import Debug from '@/app/components/app/configuration/debug'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import type { AppDetailResponse } from '@/models/app'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { fetchTenantInfo } from '@/service/common'
|
||||
import { fetchAppDetail, updateAppModelConfig } from '@/service/apps'
|
||||
import { userInputsFormToPromptVariables, promptVariablesToUserInputsForm } from '@/utils/model-config'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
import AccountSetting from '@/app/components/header/account-setting'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import Button from '../../base/button'
|
||||
import Loading from '../../base/loading'
|
||||
|
||||
const Configuration: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
|
||||
const [hasFetchedDetail, setHasFetchedDetail] = useState(false)
|
||||
const [hasFetchedKey, setHasFetchedKey] = useState(false)
|
||||
const isLoading = !hasFetchedDetail || !hasFetchedKey
|
||||
const pathname = usePathname()
|
||||
const matched = pathname.match(/\/app\/([^/]+)/)
|
||||
const appId = (matched?.length && matched[1]) ? matched[1] : ''
|
||||
const [mode, setMode] = useState('')
|
||||
const [pusblisedConfig, setPusblisedConfig] = useState<{
|
||||
modelConfig: ModelConfig,
|
||||
completionParams: CompletionParams
|
||||
} | null>(null)
|
||||
|
||||
const [conversationId, setConversationId] = useState<string | null>('')
|
||||
const [introduction, setIntroduction] = useState<string>('')
|
||||
const [controlClearChatMessage, setControlClearChatMessage] = useState(0)
|
||||
const [prevPromptConfig, setPrevPromptConfig] = useState<PromptConfig>({
|
||||
prompt_template: '',
|
||||
prompt_variables: [],
|
||||
})
|
||||
const [moreLikeThisConifg, setMoreLikeThisConifg] = useState<MoreLikeThisConfig>({
|
||||
enabled: false,
|
||||
})
|
||||
const [suggestedQuestionsAfterAnswerConfig, setSuggestedQuestionsAfterAnswerConfig] = useState<MoreLikeThisConfig>({
|
||||
enabled: false,
|
||||
})
|
||||
const [formattingChanged, setFormattingChanged] = useState(false)
|
||||
const [inputs, setInputs] = useState<Inputs>({})
|
||||
const [query, setQuery] = useState('')
|
||||
const [completionParams, setCompletionParams] = useState<CompletionParams>({
|
||||
max_tokens: 16,
|
||||
temperature: 1, // 0-2
|
||||
top_p: 1,
|
||||
presence_penalty: 1, // -2-2
|
||||
frequency_penalty: 1, // -2-2
|
||||
})
|
||||
const [modelConfig, doSetModelConfig] = useState<ModelConfig>({
|
||||
provider: 'openai',
|
||||
model_id: 'gpt-3.5-turbo',
|
||||
configs: {
|
||||
prompt_template: '',
|
||||
prompt_variables: [] as PromptVariable[],
|
||||
},
|
||||
})
|
||||
|
||||
const setModelConfig = (newModelConfig: ModelConfig) => {
|
||||
doSetModelConfig(newModelConfig)
|
||||
}
|
||||
|
||||
const setModelId = (modelId: string) => {
|
||||
const newModelConfig = produce(modelConfig, (draft) => {
|
||||
draft.model_id = modelId
|
||||
})
|
||||
setModelConfig(newModelConfig)
|
||||
}
|
||||
|
||||
const syncToPublishedConfig = (_pusblisedConfig: any) => {
|
||||
setModelConfig(_pusblisedConfig.modelConfig)
|
||||
setCompletionParams(_pusblisedConfig.completionParams)
|
||||
}
|
||||
|
||||
const [dataSets, setDataSets] = useState<DataSet[]>([])
|
||||
|
||||
const [hasSetCustomAPIKEY, setHasSetCustomerAPIKEY] = useState(true)
|
||||
const [isTrailFinished, setIsTrailFinished] = useState(false)
|
||||
const hasSetAPIKEY = hasSetCustomAPIKEY || !isTrailFinished
|
||||
|
||||
const [isShowSetAPIKey, { setTrue: showSetAPIKey, setFalse: hideSetAPIkey }] = useBoolean()
|
||||
|
||||
const checkAPIKey = async () => {
|
||||
const { in_trail, trial_end_reason } = await fetchTenantInfo({ url: '/info' })
|
||||
const isTrailFinished = in_trail && trial_end_reason === 'trial_exceeded'
|
||||
const hasSetCustomAPIKEY = trial_end_reason === 'using_custom'
|
||||
setHasSetCustomerAPIKEY(hasSetCustomAPIKEY)
|
||||
setIsTrailFinished(isTrailFinished)
|
||||
setHasFetchedKey(true)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
checkAPIKey()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
(fetchAppDetail({ url: '/apps', id: appId }) as any).then(async (res: AppDetailResponse) => {
|
||||
setMode(res.mode)
|
||||
const modelConfig = res.model_config
|
||||
const model = res.model_config.model
|
||||
|
||||
let datasets: any = null
|
||||
if (modelConfig.agent_mode?.enabled) {
|
||||
datasets = modelConfig.agent_mode?.tools.filter(({ dataset }: any) => dataset?.enabled)
|
||||
}
|
||||
|
||||
if (dataSets && datasets?.length && datasets?.length > 0) {
|
||||
const { data: dataSetsWithDetail } = await fetchDatasets({ url: '/datasets', params: { page: 1, ids: datasets.map(({ dataset }: any) => dataset.id) } })
|
||||
datasets = dataSetsWithDetail
|
||||
setDataSets(datasets)
|
||||
}
|
||||
const config = {
|
||||
modelConfig: {
|
||||
provider: model.provider,
|
||||
model_id: model.name,
|
||||
configs: {
|
||||
prompt_template: modelConfig.pre_prompt,
|
||||
prompt_variables: userInputsFormToPromptVariables(modelConfig.user_input_form)
|
||||
},
|
||||
},
|
||||
completionParams: model.completion_params,
|
||||
}
|
||||
syncToPublishedConfig(config)
|
||||
setPusblisedConfig(config)
|
||||
setIntroduction(modelConfig.opening_statement)
|
||||
if (modelConfig.more_like_this) {
|
||||
setMoreLikeThisConifg(modelConfig.more_like_this)
|
||||
}
|
||||
if (modelConfig.suggested_questions_after_answer) {
|
||||
setSuggestedQuestionsAfterAnswerConfig(modelConfig.suggested_questions_after_answer)
|
||||
}
|
||||
setHasFetchedDetail(true)
|
||||
})
|
||||
}, [appId])
|
||||
|
||||
const saveAppConfig = async () => {
|
||||
const modelId = modelConfig.model_id
|
||||
const promptTemplate = modelConfig.configs.prompt_template
|
||||
const promptVariables = modelConfig.configs.prompt_variables
|
||||
|
||||
// not save empty key adn name
|
||||
// const missingNameItem = promptVariables.find(item => item.name.trim() === '')
|
||||
// if (missingNameItem) {
|
||||
// notify({ type: 'error', message: t('appDebug.errorMessage.nameOfKeyRequired', { key: missingNameItem.key }) })
|
||||
// return
|
||||
// }
|
||||
|
||||
const postDatasets = dataSets.map(({ id }) => ({
|
||||
dataset: {
|
||||
enabled: true,
|
||||
id,
|
||||
}
|
||||
}))
|
||||
|
||||
// new model config data struct
|
||||
const data: BackendModelConfig = {
|
||||
pre_prompt: promptTemplate,
|
||||
user_input_form: promptVariablesToUserInputsForm(promptVariables),
|
||||
opening_statement: introduction || '',
|
||||
more_like_this: moreLikeThisConifg,
|
||||
suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
|
||||
agent_mode: {
|
||||
enabled: true,
|
||||
tools: [...postDatasets]
|
||||
},
|
||||
model: {
|
||||
provider: modelConfig.provider,
|
||||
name: modelId,
|
||||
completion_params: completionParams as any,
|
||||
},
|
||||
}
|
||||
|
||||
await updateAppModelConfig({ url: `/apps/${appId}/model-config`, body: data })
|
||||
setPusblisedConfig({
|
||||
modelConfig,
|
||||
completionParams,
|
||||
})
|
||||
notify({ type: 'success', message: t('common.api.success'), duration: 3000 })
|
||||
}
|
||||
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
const resetAppConfig = () => {
|
||||
// debugger
|
||||
syncToPublishedConfig(pusblisedConfig)
|
||||
setShowConfirm(false)
|
||||
}
|
||||
|
||||
const [showUseGPT4Confirm, setShowUseGPT4Confirm] = useState(false)
|
||||
const [showSetAPIKeyModal, setShowSetAPIKeyModal] = useState(false)
|
||||
|
||||
if (isLoading) {
|
||||
return <div className='flex h-full items-center justify-center'>
|
||||
<Loading type='area' />
|
||||
</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider value={{
|
||||
appId,
|
||||
hasSetAPIKEY,
|
||||
isTrailFinished,
|
||||
mode,
|
||||
conversationId,
|
||||
introduction,
|
||||
setIntroduction,
|
||||
setConversationId,
|
||||
controlClearChatMessage,
|
||||
setControlClearChatMessage,
|
||||
prevPromptConfig,
|
||||
setPrevPromptConfig,
|
||||
moreLikeThisConifg,
|
||||
setMoreLikeThisConifg,
|
||||
suggestedQuestionsAfterAnswerConfig,
|
||||
setSuggestedQuestionsAfterAnswerConfig,
|
||||
formattingChanged,
|
||||
setFormattingChanged,
|
||||
inputs,
|
||||
setInputs,
|
||||
query,
|
||||
setQuery,
|
||||
completionParams,
|
||||
setCompletionParams,
|
||||
modelConfig,
|
||||
setModelConfig,
|
||||
dataSets,
|
||||
setDataSets
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<div className="flex flex-col h-full">
|
||||
<div className='flex items-center justify-between px-6 border-b shrink-0 h-14 boder-gray-100'>
|
||||
<div className='text-xl text-gray-900'>{t('appDebug.pageTitle')}</div>
|
||||
<div className='flex items-center'>
|
||||
{/* Model and Parameters */}
|
||||
<ConfigModel
|
||||
mode={mode}
|
||||
completionParams={completionParams}
|
||||
modelId={modelConfig.model_id}
|
||||
setModelId={setModelId}
|
||||
onCompletionParamsChange={(newParams: CompletionParams) => {
|
||||
setCompletionParams(newParams)
|
||||
}}
|
||||
disabled={!hasSetAPIKEY}
|
||||
canUseGPT4={hasSetCustomAPIKEY}
|
||||
onShowUseGPT4Confirm={() => {
|
||||
setShowUseGPT4Confirm(true)
|
||||
}}
|
||||
/>
|
||||
<div className='mx-3 w-[1px] h-[14px] bg-gray-200'></div>
|
||||
<Button onClick={() => setShowConfirm(true)} className='shrink-0 mr-2 w-[70px] !h-8 !text-[13px] font-medium'>{t('appDebug.operation.resetConfig')}</Button>
|
||||
<Button type='primary' onClick={saveAppConfig} className='shrink-0 w-[70px] !h-8 !text-[13px] font-medium'>{t('appDebug.operation.applyConfig')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex grow h-[200px]'>
|
||||
<div className="w-[574px] shrink-0 h-full overflow-y-auto border-r border-gray-100 py-4 px-6">
|
||||
<Config />
|
||||
</div>
|
||||
<div className="relative grow h-full overflow-y-auto py-4 px-6 bg-gray-50 flex flex-col">
|
||||
<Debug hasSetAPIKEY={hasSetAPIKEY} onSetting={showSetAPIKey} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showConfirm && (
|
||||
<Confirm
|
||||
title={t('appDebug.resetConfig.title')}
|
||||
content={t('appDebug.resetConfig.message')}
|
||||
isShow={showConfirm}
|
||||
onClose={() => setShowConfirm(false)}
|
||||
onConfirm={resetAppConfig}
|
||||
onCancel={() => setShowConfirm(false)}
|
||||
/>
|
||||
)}
|
||||
{showUseGPT4Confirm && (
|
||||
<Confirm
|
||||
title={t('appDebug.trailUseGPT4Info.title')}
|
||||
content={t('appDebug.trailUseGPT4Info.description')}
|
||||
isShow={showUseGPT4Confirm}
|
||||
onClose={() => setShowUseGPT4Confirm(false)}
|
||||
onConfirm={() => {
|
||||
setShowSetAPIKeyModal(true)
|
||||
setShowUseGPT4Confirm(false)
|
||||
}}
|
||||
onCancel={() => setShowUseGPT4Confirm(false)}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
showSetAPIKeyModal && (
|
||||
<AccountSetting activeTab="provider" onCancel={async () => {
|
||||
setShowSetAPIKeyModal(false)
|
||||
}} />
|
||||
)
|
||||
}
|
||||
{isShowSetAPIKey && <AccountSetting activeTab="provider" onCancel={async () => {
|
||||
await checkAPIKey()
|
||||
hideSetAPIkey()
|
||||
}} />}
|
||||
</>
|
||||
</ConfigContext.Provider>
|
||||
)
|
||||
}
|
||||
export default React.memo(Configuration)
|
||||
@@ -0,0 +1,210 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import {
|
||||
PlayIcon,
|
||||
} from '@heroicons/react/24/solid'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
import { AppType } from '@/types/app'
|
||||
import Select from '@/app/components/base/select'
|
||||
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
|
||||
import VarIcon from '../base/icons/var-icon'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
export type IPromptValuePanelProps = {
|
||||
appType: AppType
|
||||
value?: string
|
||||
onChange?: (value: string) => void
|
||||
onSend?: () => void
|
||||
}
|
||||
|
||||
const starIcon = (
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.75 1C2.75 0.723858 2.52614 0.5 2.25 0.5C1.97386 0.5 1.75 0.723858 1.75 1V1.75H1C0.723858 1.75 0.5 1.97386 0.5 2.25C0.5 2.52614 0.723858 2.75 1 2.75H1.75V3.5C1.75 3.77614 1.97386 4 2.25 4C2.52614 4 2.75 3.77614 2.75 3.5V2.75H3.5C3.77614 2.75 4 2.52614 4 2.25C4 1.97386 3.77614 1.75 3.5 1.75H2.75V1Z" fill="#444CE7" />
|
||||
<path d="M2.75 8.5C2.75 8.22386 2.52614 8 2.25 8C1.97386 8 1.75 8.22386 1.75 8.5V9.25H1C0.723858 9.25 0.5 9.47386 0.5 9.75C0.5 10.0261 0.723858 10.25 1 10.25H1.75V11C1.75 11.2761 1.97386 11.5 2.25 11.5C2.52614 11.5 2.75 11.2761 2.75 11V10.25H3.5C3.77614 10.25 4 10.0261 4 9.75C4 9.47386 3.77614 9.25 3.5 9.25H2.75V8.5Z" fill="#444CE7" />
|
||||
<path d="M6.96667 1.32051C6.8924 1.12741 6.70689 1 6.5 1C6.29311 1 6.10759 1.12741 6.03333 1.32051L5.16624 3.57494C5.01604 3.96546 4.96884 4.078 4.90428 4.1688C4.8395 4.2599 4.7599 4.3395 4.6688 4.40428C4.578 4.46884 4.46546 4.51604 4.07494 4.66624L1.82051 5.53333C1.62741 5.60759 1.5 5.79311 1.5 6C1.5 6.20689 1.62741 6.39241 1.82051 6.46667L4.07494 7.33376C4.46546 7.48396 4.578 7.53116 4.6688 7.59572C4.7599 7.6605 4.8395 7.7401 4.90428 7.8312C4.96884 7.922 5.01604 8.03454 5.16624 8.42506L6.03333 10.6795C6.1076 10.8726 6.29311 11 6.5 11C6.70689 11 6.89241 10.8726 6.96667 10.6795L7.83376 8.42506C7.98396 8.03454 8.03116 7.922 8.09572 7.8312C8.1605 7.7401 8.2401 7.6605 8.3312 7.59572C8.422 7.53116 8.53454 7.48396 8.92506 7.33376L11.1795 6.46667C11.3726 6.39241 11.5 6.20689 11.5 6C11.5 5.79311 11.3726 5.60759 11.1795 5.53333L8.92506 4.66624C8.53454 4.51604 8.422 4.46884 8.3312 4.40428C8.2401 4.3395 8.1605 4.2599 8.09572 4.1688C8.03116 4.078 7.98396 3.96546 7.83376 3.57494L6.96667 1.32051Z" fill="#444CE7" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
appType,
|
||||
value,
|
||||
onChange,
|
||||
onSend,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { modelConfig, inputs, setInputs } = useContext(ConfigContext)
|
||||
const promptTemplate = modelConfig.configs.prompt_template
|
||||
const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => {
|
||||
return key && key?.trim() && name && name?.trim()
|
||||
})
|
||||
|
||||
const promptVariableObj = (() => {
|
||||
const obj: Record<string, boolean> = {}
|
||||
promptVariables.forEach((input) => {
|
||||
obj[input.key] = true
|
||||
})
|
||||
return obj
|
||||
})()
|
||||
|
||||
const handleInputValueChange = (key: string, value: string) => {
|
||||
if (!(key in promptVariableObj))
|
||||
return
|
||||
|
||||
const newInputs = { ...inputs }
|
||||
promptVariables.forEach((input) => {
|
||||
if (input.key === key)
|
||||
newInputs[key] = value
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}
|
||||
|
||||
const promptPreview = (
|
||||
<div className='pt-3 pb-4 rounded-t-xl bg-indigo-25'>
|
||||
<div className="px-4">
|
||||
<div className="flex items-center space-x-1">
|
||||
{starIcon}
|
||||
<div className="text-xs font-medium text-indigo-600 uppercase">{t('appDebug.inputs.previewTitle')}</div>
|
||||
</div>
|
||||
<div className='mt-2 leading-normal'>
|
||||
{
|
||||
(promptTemplate && promptTemplate?.trim()) ? (
|
||||
<div
|
||||
className="max-h-48 overflow-y-auto text-sm text-gray-700"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: format(replaceStringWithValuesWithFormat(promptTemplate, promptVariables, inputs)),
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
) : (
|
||||
<div className='text-xs text-gray-500'>{t('appDebug.inputs.noPrompt')}</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="pb-5 border border-gray-200 bg-white rounded-xl" style={{
|
||||
boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
|
||||
}}>
|
||||
{promptPreview}
|
||||
|
||||
<div className="mt-5 px-4">
|
||||
<div className='mb-4 '>
|
||||
<div className='flex items-center space-x-1'>
|
||||
<div className='flex items-center justify-center w-4 h-4'><VarIcon /></div>
|
||||
<div className='text-sm font-semibold text-gray-800'>{t('appDebug.inputs.userInputField')}</div>
|
||||
</div>
|
||||
{appType === AppType.completion && promptVariables.length > 0 && (
|
||||
<div className="mt-1 text-xs leading-normal text-gray-500">{t('appDebug.inputs.completionVarTip')}</div>
|
||||
)}
|
||||
</div>
|
||||
{
|
||||
promptVariables.length > 0 ? (
|
||||
<div className="space-y-3 ">
|
||||
{promptVariables.map(({ key, name, type, options, max_length, required }) => (
|
||||
<div key={key} className="flex items-center justify-between">
|
||||
<div className="mr-1 shrink-0 w-[120px] text-sm text-gray-900">{name || key}</div>
|
||||
{type === 'select' ? (
|
||||
<Select
|
||||
className='w-full'
|
||||
defaultValue={inputs[key] as string}
|
||||
onSelect={(i) => { handleInputValueChange(key, i.value as string) }}
|
||||
items={(options || []).map(i => ({ name: i, value: i }))}
|
||||
allowSearch={false}
|
||||
bgClassName='bg-gray-50'
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
|
||||
placeholder={`${name}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||
type="text"
|
||||
value={inputs[key] ? `${inputs[key]}` : ''}
|
||||
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
|
||||
maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className='text-xs text-gray-500'>{t('appDebug.inputs.noVar')}</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
appType === AppType.completion && (
|
||||
<div className='px-4'>
|
||||
<div className="mt-5 border-b border-gray-100"></div>
|
||||
<div className="mt-4">
|
||||
<div>
|
||||
<div className="text-[13px] text-gray-900 font-medium">{t('appDebug.inputs.queryTitle')}</div>
|
||||
<div className="mt-2 mb-4 overflow-hidden border border-gray-200 rounded-lg grow bg-gray-50 ">
|
||||
<div className="px-4 py-2 rounded-t-lg bg-gray-50">
|
||||
<textarea
|
||||
rows={4}
|
||||
className="w-full px-0 text-sm text-gray-900 border-0 bg-gray-50 focus:outline-none placeholder:bg-gray-50"
|
||||
placeholder={t('appDebug.inputs.queryPlaceholder') as string}
|
||||
value={value}
|
||||
onChange={e => onChange && onChange(e.target.value)}
|
||||
></textarea>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-gray-50">
|
||||
<div className="flex pl-0 space-x-1 sm:pl-2">
|
||||
<span className="bg-gray-100 text-gray-500 text-xs font-medium mr-2 px-2.5 py-0.5 rounded cursor-pointer">{value?.length}</span>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => onSend && onSend()}
|
||||
className="w-[80px] !h-8">
|
||||
<PlayIcon className="shrink-0 w-4 h-4 mr-1" aria-hidden="true" />
|
||||
<span className='uppercase text-[13px]'>{t('appDebug.inputs.run')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(PromptValuePanel)
|
||||
|
||||
function replaceStringWithValuesWithFormat(str: string, promptVariables: PromptVariable[], inputs: Record<string, any>) {
|
||||
return str.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
||||
const name = inputs[key]
|
||||
if (name) { // has set value
|
||||
return `<div class='inline-block px-1 rounded-md text-gray-900' style='background: rgba(16, 24, 40, 0.1)'>${name}</div>`
|
||||
}
|
||||
|
||||
const valueObj: PromptVariable | undefined = promptVariables.find(v => v.key === key)
|
||||
return `<div class='inline-block px-1 rounded-md text-gray-500' style='background: rgba(16, 24, 40, 0.05)'>${valueObj ? valueObj.name : match}</div>`
|
||||
})
|
||||
}
|
||||
|
||||
export function replaceStringWithValues(str: string, promptVariables: PromptVariable[], inputs: Record<string, any>) {
|
||||
return str.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
||||
const name = inputs[key]
|
||||
if (name) { // has set value
|
||||
return name
|
||||
}
|
||||
|
||||
const valueObj: PromptVariable | undefined = promptVariables.find(v => v.key === key)
|
||||
return valueObj ? `{{${valueObj.name}}}` : match
|
||||
})
|
||||
}
|
||||
|
||||
// \n -> br
|
||||
function format(str: string) {
|
||||
return str.replaceAll('\n', '<br>')
|
||||
}
|
||||
26
web/app/components/app/configuration/toolbox/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import GroupName from '../base/group-name'
|
||||
|
||||
export interface IToolboxProps {
|
||||
searchToolConfig: any
|
||||
sensitiveWordAvoidanceConifg: any
|
||||
}
|
||||
|
||||
/*
|
||||
* Include
|
||||
* 1. Search Tool
|
||||
* 2. Sensitive word avoidance
|
||||
*/
|
||||
const Toolbox: FC<IToolboxProps> = ({ searchToolConfig, sensitiveWordAvoidanceConifg }) => {
|
||||
return (
|
||||
<div>
|
||||
<GroupName name='Toolbox' />
|
||||
<div>
|
||||
{searchToolConfig?.enabled && <div>Search Tool</div>}
|
||||
{sensitiveWordAvoidanceConifg?.enabled && <div>Sensitive word avoidance</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Toolbox)
|
||||
83
web/app/components/app/log/filter.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
MagnifyingGlassIcon,
|
||||
} from '@heroicons/react/24/solid'
|
||||
import useSWR from 'swr'
|
||||
import dayjs from 'dayjs'
|
||||
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
|
||||
import type { QueryParam } from './index'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import { fetchAnnotationsCount } from '@/service/log'
|
||||
dayjs.extend(quarterOfYear)
|
||||
|
||||
const today = dayjs()
|
||||
|
||||
export const TIME_PERIOD_LIST = [
|
||||
{ value: 0, name: 'today' },
|
||||
{ value: 7, name: 'last7days' },
|
||||
{ value: 28, name: 'last4weeks' },
|
||||
{ value: today.diff(today.subtract(3, 'month'), 'day'), name: 'last3months' },
|
||||
{ value: today.diff(today.subtract(12, 'month'), 'day'), name: 'last12months' },
|
||||
{ value: today.diff(today.startOf('month'), 'day'), name: 'monthToDate' },
|
||||
{ value: today.diff(today.startOf('quarter'), 'day'), name: 'quarterToDate' },
|
||||
{ value: today.diff(today.startOf('year'), 'day'), name: 'yearToDate' },
|
||||
{ value: 'all', name: 'allTime' },
|
||||
]
|
||||
|
||||
type IFilterProps = {
|
||||
appId: string
|
||||
queryParams: QueryParam
|
||||
setQueryParams: (v: QueryParam) => void
|
||||
}
|
||||
|
||||
const Filter: FC<IFilterProps> = ({ appId, queryParams, setQueryParams }: IFilterProps) => {
|
||||
const { data } = useSWR({ url: `/apps/${appId}/annotations/count` }, fetchAnnotationsCount)
|
||||
const { t } = useTranslation()
|
||||
if (!data)
|
||||
return null
|
||||
return (
|
||||
<div className='flex flex-row items-center mb-4 text-gray-900 text-base'>
|
||||
<SimpleSelect
|
||||
items={TIME_PERIOD_LIST.map(item => ({ value: item.value, name: t(`appLog.filter.period.${item.name}`) }))}
|
||||
className='mt-0 !w-40'
|
||||
onSelect={(item) => {
|
||||
setQueryParams({ ...queryParams, period: item.value })
|
||||
}}
|
||||
defaultValue={queryParams.period} />
|
||||
<div className="relative ml-4 rounded-md mr-4">
|
||||
<SimpleSelect
|
||||
defaultValue={'all'}
|
||||
className='!w-[300px]'
|
||||
onSelect={
|
||||
(item) => {
|
||||
setQueryParams({ ...queryParams, annotation_status: item.value as string })
|
||||
}
|
||||
}
|
||||
items={[{ value: 'all', name: t('appLog.filter.annotation.all') },
|
||||
{ value: 'annotated', name: t('appLog.filter.annotation.annotated', { count: data?.count }) },
|
||||
{ value: 'not_annotated', name: t('appLog.filter.annotation.not_annotated') }]}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="query"
|
||||
className="block w-[240px] bg-gray-100 shadow-sm rounded-md border-0 py-1.5 pl-10 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none sm:text-sm sm:leading-6"
|
||||
placeholder={t('common.operation.search')}
|
||||
value={queryParams.keyword}
|
||||
onChange={(e) => {
|
||||
setQueryParams({ ...queryParams, keyword: e.target.value })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Filter
|
||||
146
web/app/components/app/log/index.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { Pagination } from 'react-headless-pagination'
|
||||
import { omit } from 'lodash-es'
|
||||
import dayjs from 'dayjs'
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import Link from 'next/link'
|
||||
import List from './list'
|
||||
import Filter from './filter'
|
||||
import s from './style.module.css'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { fetchChatConversations, fetchCompletionConversations } from '@/service/log'
|
||||
import { fetchAppDetail } from '@/service/apps'
|
||||
|
||||
export type ILogsProps = {
|
||||
appId: string
|
||||
}
|
||||
|
||||
export type QueryParam = {
|
||||
period?: number | string
|
||||
annotation_status?: string
|
||||
keyword?: string
|
||||
}
|
||||
|
||||
// Custom page count is not currently supported.
|
||||
const limit = 10
|
||||
|
||||
const ThreeDotsIcon: FC<{ className?: string }> = ({ className }) => {
|
||||
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
|
||||
<path d="M5 6.5V5M8.93934 7.56066L10 6.5M10.0103 11.5H11.5103" stroke="#374151" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
const EmptyElement: FC<{ appUrl: string }> = ({ appUrl }) => {
|
||||
const { t } = useTranslation()
|
||||
const pathname = usePathname()
|
||||
const pathSegments = pathname.split('/')
|
||||
pathSegments.pop()
|
||||
return <div className='flex items-center justify-center h-full'>
|
||||
<div className='bg-gray-50 w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
|
||||
<span className='text-gray-700 font-semibold'>{t('appLog.table.empty.element.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
|
||||
<div className='mt-2 text-gray-500 text-sm font-normal'>
|
||||
<Trans
|
||||
i18nKey="appLog.table.empty.element.content"
|
||||
components={{ shareLink: <Link href={`${pathSegments.join('/')}/overview`} className='text-primary-600' />, testLink: <Link href={appUrl} className='text-primary-600' target='_blank' /> }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const Logs: FC<ILogsProps> = ({ appId }) => {
|
||||
const { t } = useTranslation()
|
||||
const [queryParams, setQueryParams] = useState<QueryParam>({ period: 7, annotation_status: 'all' })
|
||||
const [currPage, setCurrPage] = React.useState<number>(0)
|
||||
|
||||
const query = {
|
||||
page: currPage + 1,
|
||||
limit,
|
||||
...(queryParams.period !== 'all'
|
||||
? {
|
||||
start: dayjs().subtract(queryParams.period as number, 'day').format('YYYY-MM-DD HH:mm'),
|
||||
end: dayjs().format('YYYY-MM-DD HH:mm'),
|
||||
}
|
||||
: {}),
|
||||
...omit(queryParams, ['period']),
|
||||
}
|
||||
|
||||
// Get the app type first
|
||||
const { data: appDetail } = useSWR({ url: '/apps', id: appId }, fetchAppDetail)
|
||||
const isChatMode = appDetail?.mode === 'chat'
|
||||
|
||||
// When the details are obtained, proceed to the next request
|
||||
const { data: chatConversations, mutate: mutateChatList } = useSWR(() => isChatMode
|
||||
? {
|
||||
url: `/apps/${appId}/chat-conversations`,
|
||||
params: query,
|
||||
}
|
||||
: null, fetchChatConversations)
|
||||
|
||||
const { data: completionConversations, mutate: mutateCompletionList } = useSWR(() => !isChatMode
|
||||
? {
|
||||
url: `/apps/${appId}/completion-conversations`,
|
||||
params: query,
|
||||
}
|
||||
: null, fetchCompletionConversations)
|
||||
|
||||
const total = isChatMode ? chatConversations?.total : completionConversations?.total
|
||||
|
||||
return (
|
||||
<div className='flex flex-col h-full'>
|
||||
<div className='flex flex-col justify-center px-6 pt-4'>
|
||||
<h1 className='flex text-xl font-medium text-gray-900'>{t('appLog.title')}</h1>
|
||||
<p className='flex text-sm font-normal text-gray-500'>{t('appLog.description')}</p>
|
||||
</div>
|
||||
<div className='flex flex-col px-6 py-4 flex-1'>
|
||||
<Filter appId={appId} queryParams={queryParams} setQueryParams={setQueryParams} />
|
||||
{total === undefined
|
||||
? <Loading type='app' />
|
||||
: total > 0
|
||||
? <List logs={isChatMode ? chatConversations : completionConversations} appDetail={appDetail} onRefresh={isChatMode ? mutateChatList : mutateCompletionList} />
|
||||
: <EmptyElement appUrl={`${appDetail?.site.app_base_url}/${appDetail?.mode}/${appDetail?.site.access_token}`} />
|
||||
}
|
||||
{/* Show Pagination only if the total is more than the limit */}
|
||||
{(total && total > limit)
|
||||
? <Pagination
|
||||
className="flex items-center w-full h-10 text-sm select-none mt-8"
|
||||
currentPage={currPage}
|
||||
edgePageCount={2}
|
||||
middlePagesSiblingCount={1}
|
||||
setCurrentPage={setCurrPage}
|
||||
totalPages={Math.ceil(total / limit)}
|
||||
truncableClassName="w-8 px-0.5 text-center"
|
||||
truncableText="..."
|
||||
>
|
||||
<Pagination.PrevButton
|
||||
disabled={currPage === 0}
|
||||
className={`flex items-center mr-2 text-gray-500 focus:outline-none ${currPage === 0 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600 dark:hover:text-gray-200'}`} >
|
||||
<ArrowLeftIcon className="mr-3 h-3 w-3" />
|
||||
{t('appLog.table.pagination.previous')}
|
||||
</Pagination.PrevButton>
|
||||
<div className={`flex items-center justify-center flex-grow ${s.pagination}`}>
|
||||
<Pagination.PageButton
|
||||
activeClassName="bg-primary-50 dark:bg-opacity-0 text-primary-600 dark:text-white"
|
||||
className="flex items-center justify-center h-8 w-8 rounded-full cursor-pointer"
|
||||
inactiveClassName="text-gray-500"
|
||||
/>
|
||||
</div>
|
||||
<Pagination.NextButton
|
||||
disabled={currPage === Math.ceil(total / limit) - 1}
|
||||
className={`flex items-center mr-2 text-gray-500 focus:outline-none ${currPage === Math.ceil(total / limit) - 1 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600 dark:hover:text-gray-200'}`} >
|
||||
{t('appLog.table.pagination.next')}
|
||||
<ArrowRightIcon className="ml-3 h-3 w-3" />
|
||||
</Pagination.NextButton>
|
||||
</Pagination>
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Logs
|
||||
477
web/app/components/app/log/list.tsx
Normal file
9
web/app/components/app/log/style.module.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.logTable td {
|
||||
padding: 7px 8px;
|
||||
box-sizing: border-box;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.pagination li {
|
||||
list-style: none;
|
||||
}
|
||||
211
web/app/components/app/overview/appCard.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
Cog8ToothIcon,
|
||||
DocumentTextIcon,
|
||||
RocketLaunchIcon,
|
||||
ShareIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
import { SparklesIcon } from '@heroicons/react/24/solid'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SettingsModal from './settings'
|
||||
import ShareLink from './share-link'
|
||||
import CustomizeModal from './customize'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import AppBasic, { randomString } from '@/app/components/app-sidebar/basic'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Tag from '@/app/components/base/tag'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import type { AppDetailResponse } from '@/models/app'
|
||||
|
||||
export type IAppCardProps = {
|
||||
className?: string
|
||||
appInfo: AppDetailResponse
|
||||
cardType?: 'app' | 'api'
|
||||
customBgColor?: string
|
||||
onChangeStatus: (val: boolean) => Promise<any>
|
||||
onSaveSiteConfig?: (params: any) => Promise<any>
|
||||
onGenerateCode?: () => Promise<any>
|
||||
}
|
||||
|
||||
// todo: get image url from appInfo
|
||||
const defaultUrl = 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80'
|
||||
|
||||
function AppCard({
|
||||
appInfo,
|
||||
cardType = 'app',
|
||||
customBgColor,
|
||||
onChangeStatus,
|
||||
onSaveSiteConfig,
|
||||
onGenerateCode,
|
||||
className,
|
||||
}: IAppCardProps) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const [showSettingsModal, setShowSettingsModal] = useState(false)
|
||||
const [showShareModal, setShowShareModal] = useState(false)
|
||||
const [showCustomizeModal, setShowCustomizeModal] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const OPERATIONS_MAP = {
|
||||
app: [
|
||||
{ opName: t('appOverview.overview.appInfo.preview'), opIcon: RocketLaunchIcon },
|
||||
{ opName: t('appOverview.overview.appInfo.share.entry'), opIcon: ShareIcon },
|
||||
{ opName: t('appOverview.overview.appInfo.settings.entry'), opIcon: Cog8ToothIcon },
|
||||
],
|
||||
api: [{ opName: t('appOverview.overview.apiInfo.doc'), opIcon: DocumentTextIcon }],
|
||||
}
|
||||
|
||||
const isApp = cardType === 'app'
|
||||
const basicName = isApp ? appInfo?.site?.title : t('appOverview.overview.apiInfo.title')
|
||||
const runningStatus = isApp ? appInfo.enable_site : appInfo.enable_api
|
||||
const { app_base_url, access_token } = appInfo.site ?? {}
|
||||
const appUrl = `${app_base_url}/${appInfo.mode}/${access_token}`
|
||||
const apiUrl = appInfo?.api_base_url
|
||||
|
||||
let bgColor = 'bg-primary-50 bg-opacity-40'
|
||||
if (cardType === 'api')
|
||||
bgColor = 'bg-purple-50'
|
||||
|
||||
const genClickFuncByName = (opName: string) => {
|
||||
switch (opName) {
|
||||
case t('appOverview.overview.appInfo.preview'):
|
||||
return () => {
|
||||
window.open(appUrl, '_blank')
|
||||
}
|
||||
case t('appOverview.overview.appInfo.share.entry'):
|
||||
return () => {
|
||||
setShowShareModal(true)
|
||||
}
|
||||
case t('appOverview.overview.appInfo.settings.entry'):
|
||||
return () => {
|
||||
setShowSettingsModal(true)
|
||||
}
|
||||
default:
|
||||
// jump to page develop
|
||||
return () => {
|
||||
const pathSegments = pathname.split('/')
|
||||
pathSegments.pop()
|
||||
router.push(`${pathSegments.join('/')}/develop`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onClickCustomize = () => {
|
||||
setShowCustomizeModal(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col w-full shadow-sm border-[0.5px] rounded-lg border-gray-200 ${className ?? ''}`}
|
||||
>
|
||||
<div className={`px-6 py-4 ${customBgColor ?? bgColor} rounded-lg`}>
|
||||
<div className="mb-2.5 flex flex-row items-start justify-between">
|
||||
<AppBasic
|
||||
iconType={isApp ? 'app' : 'api'}
|
||||
iconUrl={defaultUrl}
|
||||
name={basicName}
|
||||
type={
|
||||
isApp
|
||||
? t('appOverview.overview.appInfo.explanation')
|
||||
: t('appOverview.overview.apiInfo.explanation')
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-row items-center h-9">
|
||||
<Tag className="mr-2" color={runningStatus ? 'green' : 'yellow'}>
|
||||
{runningStatus ? t('appOverview.overview.status.running') : t('appOverview.overview.status.disable')}
|
||||
</Tag>
|
||||
<Switch defaultValue={runningStatus} onChange={onChangeStatus} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center py-2">
|
||||
<div className="py-1">
|
||||
<div className="pb-1 text-xs text-gray-500">
|
||||
{isApp ? t('appOverview.overview.appInfo.accessibleAddress') : t('appOverview.overview.apiInfo.accessibleAddress')}
|
||||
</div>
|
||||
<div className="text-sm text-gray-800">
|
||||
{isApp ? appUrl : apiUrl}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`pt-2 flex flex-row items-center ${!isApp ? 'mb-[calc(2rem_+_1px)]' : ''
|
||||
}`}
|
||||
>
|
||||
{OPERATIONS_MAP[cardType].map((op) => {
|
||||
return (
|
||||
<Button
|
||||
className="mr-2 text-gray-800"
|
||||
key={op.opName}
|
||||
onClick={genClickFuncByName(op.opName)}
|
||||
disabled={
|
||||
[t('appOverview.overview.appInfo.preview'), t('appOverview.overview.appInfo.share.entry')].includes(op.opName) && !runningStatus
|
||||
}
|
||||
>
|
||||
<Tooltip
|
||||
content={t('appOverview.overview.appInfo.preUseReminder') ?? ''}
|
||||
selector={`op-btn-${randomString(16)}`}
|
||||
className={
|
||||
([t('appOverview.overview.appInfo.preview'), t('appOverview.overview.appInfo.share.entry')].includes(op.opName) && !runningStatus)
|
||||
? 'mt-[-8px]'
|
||||
: '!hidden'
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<op.opIcon className="h-4 w-4 mr-1.5" />
|
||||
<span className="text-xs">{op.opName}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{isApp
|
||||
? (
|
||||
<div
|
||||
className={
|
||||
'flex items-center px-6 py-2 box-border text-xs text-gray-500 bg-opacity-50 bg-white border-t-[0.5px] border-primary-50'
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="flex items-center hover:text-primary-600 hover:cursor-pointer"
|
||||
onClick={onClickCustomize}
|
||||
>
|
||||
<SparklesIcon className="w-4 h-4 mr-1" />
|
||||
{t('appOverview.overview.appInfo.customize.entry')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: null}
|
||||
{isApp
|
||||
? (
|
||||
<div>
|
||||
<ShareLink
|
||||
isShow={showShareModal}
|
||||
onClose={() => setShowShareModal(false)}
|
||||
linkUrl={appUrl}
|
||||
onGenerateCode={onGenerateCode}
|
||||
/>
|
||||
<SettingsModal
|
||||
appInfo={appInfo}
|
||||
isShow={showSettingsModal}
|
||||
onClose={() => setShowSettingsModal(false)}
|
||||
onSave={onSaveSiteConfig}
|
||||
/>
|
||||
<CustomizeModal
|
||||
isShow={showCustomizeModal}
|
||||
linkUrl=""
|
||||
onClose={() => setShowCustomizeModal(false)}
|
||||
appId={appInfo.id}
|
||||
mode={appInfo.mode}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppCard
|
||||
291
web/app/components/app/overview/appChart.tsx
Normal file
@@ -0,0 +1,291 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import ReactECharts from 'echarts-for-react'
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import useSWR from 'swr'
|
||||
import dayjs from 'dayjs'
|
||||
import { get } from 'lodash-es'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Basic from '@/app/components/app-sidebar/basic'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import type { AppDailyConversationsResponse, AppDailyEndUsersResponse, AppTokenCostsResponse } from '@/models/app'
|
||||
import { getAppDailyConversations, getAppDailyEndUsers, getAppTokenCosts } from '@/service/apps'
|
||||
const valueFormatter = (v: string | number) => v
|
||||
|
||||
const COLOR_TYPE_MAP = {
|
||||
green: {
|
||||
lineColor: 'rgba(6, 148, 162, 1)',
|
||||
bgColor: ['rgba(6, 148, 162, 0.2)', 'rgba(67, 174, 185, 0.08)'],
|
||||
},
|
||||
orange: {
|
||||
lineColor: 'rgba(255, 138, 76, 1)',
|
||||
bgColor: ['rgba(254, 145, 87, 0.2)', 'rgba(255, 138, 76, 0.1)'],
|
||||
},
|
||||
blue: {
|
||||
lineColor: 'rgba(28, 100, 242, 1)',
|
||||
bgColor: ['rgba(28, 100, 242, 0.3)', 'rgba(28, 100, 242, 0.1)'],
|
||||
},
|
||||
}
|
||||
|
||||
const COMMON_COLOR_MAP = {
|
||||
label: '#9CA3AF',
|
||||
splitLineLight: '#F3F4F6',
|
||||
splitLineDark: '#E5E7EB',
|
||||
}
|
||||
|
||||
type IColorType = 'green' | 'orange' | 'blue'
|
||||
type IChartType = 'conversations' | 'endUsers' | 'costs'
|
||||
type IChartConfigType = { colorType: IColorType; showTokens?: boolean }
|
||||
|
||||
const commonDateFormat = 'MMM D, YYYY'
|
||||
|
||||
const CHART_TYPE_CONFIG: Record<string, IChartConfigType> = {
|
||||
conversations: {
|
||||
colorType: 'green',
|
||||
},
|
||||
endUsers: {
|
||||
colorType: 'orange',
|
||||
},
|
||||
costs: {
|
||||
colorType: 'blue',
|
||||
showTokens: true,
|
||||
},
|
||||
}
|
||||
|
||||
const sum = (arr: number[]): number => {
|
||||
return arr.reduce((acr, cur) => {
|
||||
return acr + cur
|
||||
})
|
||||
}
|
||||
|
||||
export type PeriodParams = {
|
||||
name: string
|
||||
query: {
|
||||
start: string
|
||||
end: string
|
||||
}
|
||||
}
|
||||
|
||||
export type IBizChartProps = {
|
||||
period: PeriodParams
|
||||
id: string
|
||||
}
|
||||
|
||||
export type IChartProps = {
|
||||
className?: string
|
||||
basicInfo: { title: string; explanation: string; timePeriod: string }
|
||||
yMax?: number
|
||||
chartType: IChartType
|
||||
chartData: AppDailyConversationsResponse | AppDailyEndUsersResponse | AppTokenCostsResponse | { data: Array<{ date: string; count: number }> }
|
||||
}
|
||||
|
||||
const Chart: React.FC<IChartProps> = ({
|
||||
basicInfo: { title, explanation, timePeriod },
|
||||
chartType = 'conversations',
|
||||
chartData,
|
||||
yMax,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const statistics = chartData.data
|
||||
const statisticsLen = statistics.length
|
||||
const extraDataForMarkLine = new Array(statisticsLen >= 2 ? statisticsLen - 2 : statisticsLen).fill('1')
|
||||
extraDataForMarkLine.push('')
|
||||
extraDataForMarkLine.unshift('')
|
||||
|
||||
const xData = statistics.map(({ date }) => date)
|
||||
const yField = Object.keys(statistics[0]).find(name => name.includes('count')) || ''
|
||||
const yData = statistics.map((item) => {
|
||||
// @ts-expect-error field is valid
|
||||
return item[yField] || 0
|
||||
})
|
||||
|
||||
const options: EChartsOption = {
|
||||
dataset: {
|
||||
dimensions: ['date', yField],
|
||||
source: statistics,
|
||||
},
|
||||
grid: { top: 8, right: 36, bottom: 0, left: 0, containLabel: true },
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
position: 'top',
|
||||
borderWidth: 0,
|
||||
},
|
||||
xAxis: [{
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
axisLabel: {
|
||||
color: COMMON_COLOR_MAP.label,
|
||||
hideOverlap: true,
|
||||
overflow: 'break',
|
||||
formatter(value) {
|
||||
return dayjs(value).format(commonDateFormat)
|
||||
},
|
||||
},
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: COMMON_COLOR_MAP.splitLineLight,
|
||||
width: 1,
|
||||
type: [10, 10],
|
||||
},
|
||||
interval(index) {
|
||||
return index === 0 || index === xData.length - 1
|
||||
},
|
||||
},
|
||||
}, {
|
||||
position: 'bottom',
|
||||
boundaryGap: false,
|
||||
data: extraDataForMarkLine,
|
||||
axisLabel: { show: false },
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: COMMON_COLOR_MAP.splitLineDark,
|
||||
},
|
||||
interval(index, value) {
|
||||
return !!value
|
||||
},
|
||||
},
|
||||
}],
|
||||
yAxis: {
|
||||
max: yMax ?? 'dataMax',
|
||||
type: 'value',
|
||||
axisLabel: { color: COMMON_COLOR_MAP.label, hideOverlap: true },
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: COMMON_COLOR_MAP.splitLineLight,
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'line',
|
||||
showSymbol: true,
|
||||
// symbol: 'circle',
|
||||
// triggerLineEvent: true,
|
||||
symbolSize: 4,
|
||||
lineStyle: {
|
||||
color: COLOR_TYPE_MAP[CHART_TYPE_CONFIG[chartType].colorType].lineColor,
|
||||
width: 2,
|
||||
},
|
||||
itemStyle: {
|
||||
color: COLOR_TYPE_MAP[CHART_TYPE_CONFIG[chartType].colorType].lineColor,
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [{
|
||||
offset: 0, color: COLOR_TYPE_MAP[CHART_TYPE_CONFIG[chartType].colorType].bgColor[0],
|
||||
}, {
|
||||
offset: 1, color: COLOR_TYPE_MAP[CHART_TYPE_CONFIG[chartType].colorType].bgColor[1],
|
||||
}],
|
||||
global: false,
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
padding: [8, 12, 8, 12],
|
||||
formatter(params) {
|
||||
return `<div style='color:#6B7280;font-size:12px'>${params.name}</div>
|
||||
<div style='font-size:14px;color:#1F2A37'>${valueFormatter((params.data as any)[yField])}
|
||||
${!CHART_TYPE_CONFIG[chartType].showTokens
|
||||
? ''
|
||||
: `<span style='font-size:12px'>
|
||||
<span style='margin-left:4px;color:#6B7280'>(</span>
|
||||
<span style='color:#FF8A4C'>~$${get(params.data, 'total_price', 0)}</span>
|
||||
<span style='color:#6B7280'>)</span>
|
||||
</span>`}
|
||||
</div>`
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const sumData = sum(yData)
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col w-full px-6 py-4 border-[0.5px] rounded-lg border-gray-200 shadow-sm ${className ?? ''}`}>
|
||||
<div className='mb-3'>
|
||||
<Basic name={title} type={timePeriod} hoverTip={explanation} />
|
||||
</div>
|
||||
<div className='mb-4'>
|
||||
<Basic
|
||||
name={chartType !== 'costs' ? sumData.toLocaleString() : `${sumData < 1000 ? sumData : (formatNumber(Math.round(sumData / 1000)) + 'k')}`}
|
||||
type={!CHART_TYPE_CONFIG[chartType].showTokens
|
||||
? ''
|
||||
: <span>{t('appOverview.analysis.tokenUsage.consumed')} Tokens<span className='text-sm'>
|
||||
<span className='ml-1 text-gray-500'>(</span>
|
||||
<span className='text-orange-400'>~{sum(statistics.map(item => parseFloat(get(item, 'total_price', '0')))).toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 4 })}</span>
|
||||
<span className='text-gray-500'>)</span>
|
||||
</span></span>}
|
||||
textStyle={{ main: `!text-3xl !font-normal ${sumData === 0 ? '!text-gray-300' : ''}` }} />
|
||||
</div>
|
||||
<ReactECharts option={options} style={{ height: 160 }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const getDefaultChartData = ({ start, end }: { start: string; end: string }) => {
|
||||
const diffDays = dayjs(end).diff(dayjs(start), 'day')
|
||||
return Array.from({ length: diffDays || 1 }, () => ({ date: '', count: 0 })).map((item, index) => {
|
||||
item.date = dayjs(start).add(index, 'day').format(commonDateFormat)
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
export const ConversationsChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const { t } = useTranslation()
|
||||
const { data: response } = useSWR({ url: `/apps/${id}/statistics/daily-conversations`, params: period.query }, getAppDailyConversations)
|
||||
if (!response)
|
||||
return <Loading />
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
basicInfo={{ title: t('appOverview.analysis.totalMessages.title'), explanation: t('appOverview.analysis.totalMessages.explanation'), timePeriod: period.name }}
|
||||
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query) }}
|
||||
chartType='conversations'
|
||||
{...(noDataFlag && { yMax: 500 })}
|
||||
/>
|
||||
}
|
||||
|
||||
export const EndUsersChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { data: response } = useSWR({ url: `/apps/${id}/statistics/daily-end-users`, id, params: period.query }, getAppDailyEndUsers)
|
||||
if (!response)
|
||||
return <Loading />
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
basicInfo={{ title: t('appOverview.analysis.activeUsers.title'), explanation: t('appOverview.analysis.activeUsers.explanation'), timePeriod: period.name }}
|
||||
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query) }}
|
||||
chartType='endUsers'
|
||||
{...(noDataFlag && { yMax: 500 })}
|
||||
/>
|
||||
}
|
||||
|
||||
export const CostChart: FC<IBizChartProps> = ({ id, period }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { data: response } = useSWR({ url: `/apps/${id}/statistics/token-costs`, params: period.query }, getAppTokenCosts)
|
||||
if (!response)
|
||||
return <Loading />
|
||||
const noDataFlag = !response.data || response.data.length === 0
|
||||
return <Chart
|
||||
basicInfo={{ title: t('appOverview.analysis.tokenUsage.title'), explanation: t('appOverview.analysis.tokenUsage.explanation'), timePeriod: period.name }}
|
||||
chartData={!noDataFlag ? response : { data: getDefaultChartData(period.query) }}
|
||||
chartType='costs'
|
||||
{...(noDataFlag && { yMax: 100 })}
|
||||
/>
|
||||
}
|
||||
|
||||
export default Chart
|
||||
108
web/app/components/app/overview/customize/index.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { AppMode } from '@/types/app'
|
||||
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import I18n from '@/context/i18n'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Tag from '@/app/components/base/tag'
|
||||
|
||||
type IShareLinkProps = {
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
linkUrl: string
|
||||
appId: string
|
||||
mode: AppMode
|
||||
}
|
||||
|
||||
const StepNum: FC<{ children: React.ReactNode }> = ({ children }) =>
|
||||
<div className='h-7 w-7 flex justify-center items-center flex-shrink-0 mr-3 text-primary-600 bg-primary-50 rounded-2xl'>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
const GithubIcon = ({ className }: { className: string }) => {
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<path d="M5.80078 13.7109C5.80078 13.6406 5.73047 13.5703 5.625 13.5703C5.51953 13.5703 5.44922 13.6406 5.44922 13.7109C5.44922 13.7812 5.51953 13.8516 5.625 13.8164C5.73047 13.8164 5.80078 13.7812 5.80078 13.7109ZM4.71094 13.5352C4.71094 13.6055 4.78125 13.7109 4.88672 13.7109C4.95703 13.7461 5.0625 13.7109 5.09766 13.6406C5.09766 13.5703 5.0625 13.5 4.95703 13.4648C4.85156 13.4297 4.74609 13.4648 4.71094 13.5352ZM6.29297 13.5C6.1875 13.5 6.11719 13.5703 6.11719 13.6758C6.11719 13.7461 6.22266 13.7812 6.32812 13.7461C6.43359 13.7109 6.50391 13.6758 6.46875 13.6055C6.46875 13.5352 6.36328 13.4648 6.29297 13.5ZM8.57812 0C3.72656 0 0 3.72656 0 8.57812C0 12.4805 2.42578 15.8203 5.94141 17.0156C6.39844 17.0859 6.53906 16.8047 6.53906 16.5938C6.53906 16.3477 6.53906 15.1523 6.53906 14.4141C6.53906 14.4141 4.07812 14.9414 3.55078 13.3594C3.55078 13.3594 3.16406 12.3398 2.60156 12.0938C2.60156 12.0938 1.79297 11.5312 2.63672 11.5312C2.63672 11.5312 3.51562 11.6016 4.00781 12.4453C4.78125 13.8164 6.04688 13.4297 6.57422 13.1836C6.64453 12.6211 6.85547 12.2344 7.13672 11.9883C5.16797 11.7773 3.16406 11.4961 3.16406 8.12109C3.16406 7.13672 3.44531 6.67969 4.00781 6.04688C3.90234 5.80078 3.62109 4.88672 4.11328 3.65625C4.81641 3.44531 6.53906 4.60547 6.53906 4.60547C7.24219 4.39453 7.98047 4.32422 8.71875 4.32422C9.49219 4.32422 10.2305 4.39453 10.9336 4.60547C10.9336 4.60547 12.6211 3.41016 13.3594 3.65625C13.8516 4.88672 13.5352 5.80078 13.4648 6.04688C14.0273 6.67969 14.3789 7.13672 14.3789 8.12109C14.3789 11.4961 12.3047 11.7773 10.3359 11.9883C10.6523 12.2695 10.9336 12.7969 10.9336 13.6406C10.9336 14.8008 10.8984 16.2773 10.8984 16.5586C10.8984 16.8047 11.0742 17.0859 11.5312 16.9805C15.0469 15.8203 17.4375 12.4805 17.4375 8.57812C17.4375 3.72656 13.4648 0 8.57812 0ZM3.41016 12.1289C3.33984 12.1641 3.375 12.2695 3.41016 12.3398C3.48047 12.375 3.55078 12.4102 3.62109 12.375C3.65625 12.3398 3.65625 12.2344 3.58594 12.1641C3.51562 12.1289 3.44531 12.0938 3.41016 12.1289ZM3.02344 11.8477C2.98828 11.918 3.02344 11.9531 3.09375 11.9883C3.16406 12.0234 3.23438 12.0234 3.26953 11.9531C3.26953 11.918 3.23438 11.8828 3.16406 11.8477C3.09375 11.8125 3.05859 11.8125 3.02344 11.8477ZM4.14844 13.1133C4.11328 13.1484 4.11328 13.2539 4.21875 13.3242C4.28906 13.3945 4.39453 13.4297 4.42969 13.3594C4.46484 13.3242 4.46484 13.2188 4.39453 13.1484C4.32422 13.0781 4.21875 13.043 4.14844 13.1133ZM3.76172 12.5859C3.69141 12.6211 3.69141 12.7266 3.76172 12.7969C3.83203 12.8672 3.90234 12.9023 3.97266 12.8672C4.00781 12.832 4.00781 12.7266 3.97266 12.6562C3.90234 12.5859 3.83203 12.5508 3.76172 12.5859Z" fill="#1F2A37" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const prefixCustomize = 'appOverview.overview.appInfo.customize'
|
||||
|
||||
const CustomizeModal: FC<IShareLinkProps> = ({
|
||||
isShow,
|
||||
onClose,
|
||||
appId,
|
||||
mode,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const isChatApp = mode === 'chat'
|
||||
|
||||
return <Modal
|
||||
title={t(`${prefixCustomize}.title`)}
|
||||
description={t(`${prefixCustomize}.explanation`)}
|
||||
isShow={isShow}
|
||||
onClose={onClose}
|
||||
className='!max-w-2xl w-[640px]'
|
||||
closable={true}
|
||||
>
|
||||
<div className='w-full mt-4 px-6 py-5 border-gray-200 rounded-lg border-[0.5px]'>
|
||||
<Tag bordered={true} hideBg={true} className='text-primary-600 border-primary-600 uppercase'>{t(`${prefixCustomize}.way`)} 1</Tag>
|
||||
<p className='my-2 text-base font-medium text-gray-800'>{t(`${prefixCustomize}.way1.name`)}</p>
|
||||
<div className='flex py-4'>
|
||||
<StepNum>1</StepNum>
|
||||
<div className='flex flex-col'>
|
||||
<div className='text-gray-900'>{t(`${prefixCustomize}.way1.step1`)}</div>
|
||||
<div className='text-gray-500 text-xs mt-1 mb-2'>{t(`${prefixCustomize}.way1.step1Tip`)}</div>
|
||||
<a href={`https://github.com/langgenius/${isChatApp ? 'webapp-conversation' : 'webapp-text-generator'}`} target='_blank'>
|
||||
<Button className='text-gray-800 text-sm w-fit'><GithubIcon className='text-gray-800 mr-2' />{t(`${prefixCustomize}.way1.step1Operation`)}</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex py-4'>
|
||||
<StepNum>2</StepNum>
|
||||
<div className='flex flex-col w-full'>
|
||||
<div className='text-gray-900'>{t(`${prefixCustomize}.way1.step2`)}</div>
|
||||
<div className='text-gray-500 text-xs mt-1 mb-2'>{t(`${prefixCustomize}.way1.step2Tip`)}</div>
|
||||
<pre className='box-border py-3 px-4 bg-gray-100 text-xs font-medium rounded-lg'>
|
||||
export const APP_ID = '{appId}'<br />
|
||||
export const API_KEY = {`'<Web API Key From Dify>'`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex pt-4'>
|
||||
<StepNum>3</StepNum>
|
||||
<div className='flex flex-col'>
|
||||
<div className='text-gray-900'>{t(`${prefixCustomize}.way1.step3`)}</div>
|
||||
<div className='text-gray-500 text-xs mt-1 mb-2'>{t(`${prefixCustomize}.way1.step3Tip`)}</div>
|
||||
<a href="https://vercel.com/docs/concepts/deployments/git/vercel-for-github" target='_blank'>
|
||||
<Button className='text-gray-800 text-sm w-fit'>
|
||||
<div className='mr-1.5 border-solid border-t-0 border-r-[7px] border-l-[7px] border-b-[12px] border-r-transparent border-b-black border-l-transparent border-t-transparent'></div>
|
||||
<span>{t(`${prefixCustomize}.way1.step3Operation`)}</span>
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-full mt-4 px-6 py-5 border-gray-200 rounded-lg border-[0.5px]'>
|
||||
<Tag bordered={true} hideBg={true} className='text-primary-600 border-primary-600 uppercase'>{t(`${prefixCustomize}.way`)} 2</Tag>
|
||||
<p className='mt-2 text-base font-medium text-gray-800'>{t(`${prefixCustomize}.way2.name`)}</p>
|
||||
<Button
|
||||
className='w-36 mt-2'
|
||||
onClick={() => window.open(`https://docs.dify.ai/${locale.toLowerCase()}/application/developing-with-apis`, '_blank')}
|
||||
>
|
||||
<span className='text-sm text-gray-800'>{t(`${prefixCustomize}.way2.operation`)}</span>
|
||||
<ArrowTopRightOnSquareIcon className='w-4 h-4 ml-1 text-gray-800 shrink-0' />
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
|
||||
export default CustomizeModal
|
||||
145
web/app/components/app/overview/settings/index.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { ChevronRightIcon } from '@heroicons/react/20/solid'
|
||||
import Link from 'next/link'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import s from './style.module.css'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import type { AppDetailResponse } from '@/models/app'
|
||||
import type { Language } from '@/types/app'
|
||||
|
||||
export type ISettingsModalProps = {
|
||||
appInfo: AppDetailResponse
|
||||
isShow: boolean
|
||||
defaultValue?: string
|
||||
onClose: () => void
|
||||
onSave: (params: ConfigParams) => Promise<any>
|
||||
}
|
||||
|
||||
export type ConfigParams = {
|
||||
title: string
|
||||
description: string
|
||||
default_language: string
|
||||
prompt_public: boolean
|
||||
}
|
||||
|
||||
const LANGUAGE_MAP: Record<Language, string> = {
|
||||
'en-US': 'English(United States)',
|
||||
'zh-Hans': '简体中文',
|
||||
}
|
||||
|
||||
const prefixSettings = 'appOverview.overview.appInfo.settings'
|
||||
|
||||
const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
appInfo,
|
||||
isShow = false,
|
||||
onClose,
|
||||
onSave,
|
||||
}) => {
|
||||
const [isShowMore, setIsShowMore] = useState(false)
|
||||
const { title, description, copyright, privacy_policy, default_language } = appInfo.site
|
||||
const [inputInfo, setInputInfo] = useState({ title, desc: description, copyright, privacyPolicy: privacy_policy })
|
||||
const [language, setLanguage] = useState(default_language)
|
||||
const [saveLoading, setSaveLoading] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onHide = () => {
|
||||
onClose()
|
||||
setTimeout(() => {
|
||||
setIsShowMore(false)
|
||||
}, 200)
|
||||
}
|
||||
|
||||
const onClickSave = async () => {
|
||||
setSaveLoading(true)
|
||||
const params = {
|
||||
title: inputInfo.title,
|
||||
description: inputInfo.desc,
|
||||
default_language: language,
|
||||
prompt_public: false,
|
||||
copyright: inputInfo.copyright,
|
||||
privacy_policy: inputInfo.privacyPolicy,
|
||||
}
|
||||
await onSave(params)
|
||||
setSaveLoading(false)
|
||||
onHide()
|
||||
}
|
||||
|
||||
const onChange = (field: string) => {
|
||||
return (e: any) => {
|
||||
setInputInfo(item => ({ ...item, [field]: e.target.value }))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t(`${prefixSettings}.title`)}
|
||||
isShow={isShow}
|
||||
onClose={onHide}
|
||||
className={`${s.settingsModal}`}
|
||||
>
|
||||
<div className={`mt-6 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.webName`)}</div>
|
||||
<div className='flex mt-2'>
|
||||
<AppIcon className='!mr-3 self-center' />
|
||||
<input className={`flex-grow rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
|
||||
value={inputInfo.title}
|
||||
onChange={onChange('title')} />
|
||||
</div>
|
||||
<div className={`mt-6 font-medium ${s.settingTitle} text-gray-900 `}>{t(`${prefixSettings}.webDesc`)}</div>
|
||||
<p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.webDescTip`)}</p>
|
||||
<textarea
|
||||
rows={3}
|
||||
className={`mt-2 pt-2 pb-2 px-3 rounded-lg bg-gray-100 w-full ${s.settingsTip} text-gray-900`}
|
||||
value={inputInfo.desc}
|
||||
onChange={onChange('desc')}
|
||||
placeholder={t(`${prefixSettings}.webDescPlaceholder`) as string}
|
||||
/>
|
||||
<div className={`mt-6 mb-2 font-medium ${s.settingTitle} text-gray-900 `}>{t(`${prefixSettings}.language`)}</div>
|
||||
<SimpleSelect
|
||||
items={Object.keys(LANGUAGE_MAP).map(lang => ({ name: LANGUAGE_MAP[lang as Language], value: lang }))}
|
||||
defaultValue={language}
|
||||
onSelect={item => setLanguage(item.value as Language)}
|
||||
/>
|
||||
{!isShowMore && <div className='w-full cursor-pointer mt-8' onClick={() => setIsShowMore(true)}>
|
||||
<div className='flex justify-between'>
|
||||
<div className={`font-medium ${s.settingTitle} flex-grow text-gray-900`}>{t(`${prefixSettings}.more.entry`)}</div>
|
||||
<div className='flex-shrink-0 w-4 h-4 text-gray-500'>
|
||||
<ChevronRightIcon />
|
||||
</div>
|
||||
</div>
|
||||
<p className={`mt-1 ${s.policy} text-gray-500`}>{t(`${prefixSettings}.more.copyright`)} & {t(`${prefixSettings}.more.privacyPolicy`)}</p>
|
||||
</div>}
|
||||
{isShowMore && <>
|
||||
<hr className='w-full mt-6' />
|
||||
<div className={`mt-6 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.copyright`)}</div>
|
||||
<input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
|
||||
value={inputInfo.copyright}
|
||||
onChange={onChange('copyright')}
|
||||
placeholder={t(`${prefixSettings}.more.copyRightPlaceholder`) as string}
|
||||
/>
|
||||
<div className={`mt-8 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.privacyPolicy`)}</div>
|
||||
<p className={`mt-1 ${s.settingsTip} text-gray-500`}>
|
||||
<Trans
|
||||
i18nKey={`${prefixSettings}.more.privacyPolicyTip`}
|
||||
components={{ privacyPolicyLink: <Link href={'https://langgenius.ai/privacy-policy'} target='_blank' className='text-primary-600' /> }}
|
||||
/>
|
||||
</p>
|
||||
<input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
|
||||
value={inputInfo.privacyPolicy}
|
||||
onChange={onChange('privacyPolicy')}
|
||||
placeholder={t(`${prefixSettings}.more.privacyPolicyPlaceholder`) as string}
|
||||
/>
|
||||
</>}
|
||||
<div className='mt-10 flex justify-end'>
|
||||
<Button className='mr-2 flex-shrink-0' onClick={onHide}>{t('common.operation.cancel')}</Button>
|
||||
<Button type='primary' className='flex-shrink-0' onClick={onClickSave} loading={saveLoading}>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</Modal >
|
||||
)
|
||||
}
|
||||
export default React.memo(SettingsModal)
|
||||
23
web/app/components/app/overview/settings/style.module.css
Normal file
@@ -0,0 +1,23 @@
|
||||
.settingsModal {
|
||||
max-width: 32.5rem !important;
|
||||
}
|
||||
|
||||
.settingTitle {
|
||||
line-height: 21px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.settingsTip {
|
||||
line-height: 1.125rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.policy {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.125rem;
|
||||
}
|
||||
|
||||
.projectName {
|
||||
font-size: 0.875rem;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
63
web/app/components/app/overview/share-link.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
LinkIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import useCopyToClipboard from '@/hooks/use-copy-to-clipboard'
|
||||
|
||||
import './style.css'
|
||||
|
||||
type IShareLinkProps = {
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
onGenerateCode: () => Promise<void>
|
||||
linkUrl: string
|
||||
}
|
||||
const ShareLinkModal: FC<IShareLinkProps> = ({
|
||||
linkUrl,
|
||||
isShow,
|
||||
onClose,
|
||||
onGenerateCode,
|
||||
}) => {
|
||||
const [_, copy] = useCopyToClipboard()
|
||||
const [genLoading, setGenLoading] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
return <Modal
|
||||
title={t('appOverview.overview.appInfo.share.explanation')}
|
||||
isShow={isShow}
|
||||
onClose={onClose}
|
||||
>
|
||||
{/* share url */}
|
||||
<p className='mt-5 text-xs font-medium text-gray-500'>{t('appOverview.overview.appInfo.share.shareUrl')}</p>
|
||||
{/* input share url */}
|
||||
<input disabled type='text' value={linkUrl} className='mt-1 w-full bg-gray-50 p-2 text-primary-600 text-xs font-normal outline-gray-50 hover:outline-gray-50 cursor-pointer' />
|
||||
{/* button copy link/ button regenerate */}
|
||||
<div className='mt-4 flex gap-3'>
|
||||
<Button
|
||||
type="primary"
|
||||
className='w-32'
|
||||
onClick={() => {
|
||||
copy(linkUrl)
|
||||
}}
|
||||
>
|
||||
<LinkIcon className='w-4 h-4 mr-2' />
|
||||
{t('appOverview.overview.appInfo.share.copyLink')}
|
||||
</Button>
|
||||
<Button className='w-32' onClick={async () => {
|
||||
setGenLoading(true)
|
||||
await onGenerateCode()
|
||||
setGenLoading(false)
|
||||
}}>
|
||||
<ArrowPathIcon className={`w-4 h-4 mr-2 ${genLoading ? 'generateLogo' : ''}`} />
|
||||
{t('appOverview.overview.appInfo.share.regenerate')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
|
||||
export default ShareLinkModal
|
||||
13
web/app/components/app/overview/style.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.generateLogo {
|
||||
animation: 2s rotate linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
26
web/app/components/app/text-generate/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { format } from '@/service/base'
|
||||
import React from 'react'
|
||||
|
||||
export type ITextGenerationProps = {
|
||||
value: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const TextGeneration: FC<ITextGenerationProps> = ({
|
||||
value,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: format(value)
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(TextGeneration)
|
||||
242
web/app/components/app/text-generate/item/index.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
'use client'
|
||||
import React, { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { Feedbacktype } from '@/app/components/app/chat'
|
||||
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { fetcMoreLikeThis, updateFeedback } from '@/service/share'
|
||||
|
||||
const MAX_DEPTH = 3
|
||||
export interface IGenerationItemProps {
|
||||
className?: string
|
||||
content: string
|
||||
messageId?: string | null
|
||||
isLoading?: boolean
|
||||
isInWebApp?: boolean
|
||||
moreLikeThis?: boolean
|
||||
depth?: number
|
||||
feedback?: Feedbacktype
|
||||
onFeedback?: (feedback: Feedbacktype) => void
|
||||
onSave?: (messageId: string) => void
|
||||
isMobile?: boolean
|
||||
}
|
||||
|
||||
export const SimpleBtn = ({ className, onClick, children }: {
|
||||
className?: string
|
||||
onClick?: () => void,
|
||||
children: React.ReactNode
|
||||
}) => (
|
||||
<div
|
||||
className={cn(className, 'flex items-center h-7 px-3 rounded-md border border-gray-200 text-xs text-gray-700 font-medium cursor-pointer hover:shadow-sm hover:border-gray-300')}
|
||||
onClick={() => onClick?.()}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
export const copyIcon = (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.3335 2.33341C9.87598 2.33341 10.1472 2.33341 10.3698 2.39304C10.9737 2.55486 11.4454 3.02657 11.6072 3.63048C11.6668 3.85302 11.6668 4.12426 11.6668 4.66675V10.0334C11.6668 11.0135 11.6668 11.5036 11.4761 11.8779C11.3083 12.2072 11.0406 12.4749 10.7113 12.6427C10.337 12.8334 9.84692 12.8334 8.86683 12.8334H5.1335C4.1534 12.8334 3.66336 12.8334 3.28901 12.6427C2.95973 12.4749 2.69201 12.2072 2.52423 11.8779C2.3335 11.5036 2.3335 11.0135 2.3335 10.0334V4.66675C2.3335 4.12426 2.3335 3.85302 2.39313 3.63048C2.55494 3.02657 3.02665 2.55486 3.63056 2.39304C3.8531 2.33341 4.12435 2.33341 4.66683 2.33341M5.60016 3.50008H8.40016C8.72686 3.50008 8.89021 3.50008 9.01499 3.4365C9.12475 3.38058 9.21399 3.29134 9.26992 3.18158C9.3335 3.05679 9.3335 2.89345 9.3335 2.56675V2.10008C9.3335 1.77338 9.3335 1.61004 9.26992 1.48525C9.21399 1.37549 9.12475 1.28625 9.01499 1.23033C8.89021 1.16675 8.72686 1.16675 8.40016 1.16675H5.60016C5.27347 1.16675 5.11012 1.16675 4.98534 1.23033C4.87557 1.28625 4.78634 1.37549 4.73041 1.48525C4.66683 1.61004 4.66683 1.77338 4.66683 2.10008V2.56675C4.66683 2.89345 4.66683 3.05679 4.73041 3.18158C4.78634 3.29134 4.87557 3.38058 4.98534 3.4365C5.11012 3.50008 5.27347 3.50008 5.60016 3.50008Z" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const moreLikeThisIcon = (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#clip0_4138_1944)">
|
||||
<path d="M2.62533 12.8337V9.91699M2.62533 4.08366V1.16699M1.16699 2.62533H4.08366M1.16699 11.3753H4.08366M7.58366 1.75033L6.57206 4.38049C6.40755 4.80821 6.32529 5.02207 6.19738 5.20196C6.08402 5.36139 5.94472 5.50069 5.78529 5.61405C5.6054 5.74196 5.39155 5.82421 4.96383 5.98872L2.33366 7.00033L4.96383 8.01193C5.39155 8.17644 5.60541 8.25869 5.78529 8.3866C5.94472 8.49996 6.08402 8.63926 6.19738 8.79869C6.32529 8.97858 6.40755 9.19244 6.57206 9.62016L7.58366 12.2503L8.59526 9.62015C8.75977 9.19244 8.84202 8.97858 8.96993 8.79869C9.0833 8.63926 9.22259 8.49996 9.38203 8.3866C9.56191 8.25869 9.77577 8.17644 10.2035 8.01193L12.8337 7.00033L10.2035 5.98872C9.77577 5.82421 9.56191 5.74196 9.38203 5.61405C9.22259 5.50069 9.0833 5.36139 8.96993 5.20196C8.84202 5.02207 8.75977 4.80821 8.59526 4.38049L7.58366 1.75033Z" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4138_1944">
|
||||
<rect width="14" height="14" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const saveIcon = (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.0837 12.25L7.00033 9.33333L2.91699 12.25V2.91667C2.91699 2.60725 3.03991 2.3105 3.2587 2.09171C3.47749 1.87292 3.77424 1.75 4.08366 1.75H9.91699C10.2264 1.75 10.5232 1.87292 10.7419 2.09171C10.9607 2.3105 11.0837 2.60725 11.0837 2.91667V12.25Z" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const GenerationItem: FC<IGenerationItemProps> = ({
|
||||
className,
|
||||
content,
|
||||
messageId,
|
||||
isLoading,
|
||||
moreLikeThis,
|
||||
isInWebApp = false,
|
||||
feedback,
|
||||
onFeedback,
|
||||
onSave,
|
||||
depth = 1,
|
||||
isMobile
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isTop = depth === 1
|
||||
|
||||
const [completionRes, setCompletionRes] = useState('')
|
||||
const [childMessageId, setChildMessageId] = useState<string | null>(null)
|
||||
const hasChild = !!childMessageId
|
||||
const [childFeedback, setChildFeedback] = useState<Feedbacktype>({
|
||||
rating: null
|
||||
})
|
||||
|
||||
const handleFeedback = async (childFeedback: Feedbacktype) => {
|
||||
await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } })
|
||||
setChildFeedback(childFeedback)
|
||||
}
|
||||
|
||||
const [isQuerying, { setTrue: startQuerying, setFalse: stopQuerying }] = useBoolean(false)
|
||||
|
||||
const childProps = {
|
||||
isInWebApp: true,
|
||||
content: completionRes,
|
||||
messageId: childMessageId,
|
||||
depth: depth + 1,
|
||||
moreLikeThis: true,
|
||||
onFeedback: handleFeedback,
|
||||
isLoading: isQuerying,
|
||||
feedback: childFeedback,
|
||||
onSave,
|
||||
isMobile
|
||||
}
|
||||
|
||||
const handleMoreLikeThis = async () => {
|
||||
if (isQuerying || !messageId) {
|
||||
Toast.notify({ type: 'warning', message: t('appDebug.errorMessage.waitForResponse') })
|
||||
return
|
||||
}
|
||||
startQuerying()
|
||||
const res: any = await fetcMoreLikeThis(messageId as string)
|
||||
setCompletionRes(res.answer)
|
||||
setChildMessageId(res.id)
|
||||
stopQuerying()
|
||||
}
|
||||
|
||||
const mainStyle = (() => {
|
||||
const res: any = !isTop ? {
|
||||
background: depth % 2 === 0 ? 'linear-gradient(90.07deg, #F9FAFB 0.05%, rgba(249, 250, 251, 0) 99.93%)' : '#fff'
|
||||
} : {}
|
||||
|
||||
if (hasChild) {
|
||||
res.boxShadow = '0px 1px 2px rgba(16, 24, 40, 0.05)'
|
||||
}
|
||||
return res
|
||||
})()
|
||||
return (
|
||||
<div className={cn(className, isTop ? 'rounded-xl border border-gray-200 bg-white' : 'rounded-br-xl !mt-0')}
|
||||
style={isTop ? {
|
||||
boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)'
|
||||
} : {}}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className='flex items-center h-10'><Loading type='area' /></div>
|
||||
) : (
|
||||
<div
|
||||
className={cn(!isTop && 'rounded-br-xl border-l-2 border-primary-400', 'p-4')}
|
||||
style={mainStyle}
|
||||
>
|
||||
<Markdown content={content} />
|
||||
{messageId && (
|
||||
<div className='flex items-center justify-between mt-3'>
|
||||
<div className='flex items-center'>
|
||||
<SimpleBtn
|
||||
className={cn(isMobile && '!px-1.5', 'space-x-1')}
|
||||
onClick={() => {
|
||||
copy(content)
|
||||
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
|
||||
}}>
|
||||
{copyIcon}
|
||||
{!isMobile && <div>{t('common.operation.copy')}</div>}
|
||||
</SimpleBtn>
|
||||
{isInWebApp && (
|
||||
<>
|
||||
<SimpleBtn
|
||||
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
|
||||
onClick={() => { onSave?.(messageId as string) }}
|
||||
>
|
||||
{saveIcon}
|
||||
{!isMobile && <div>{t('common.operation.save')}</div>}
|
||||
</SimpleBtn>
|
||||
{(moreLikeThis && depth < MAX_DEPTH) && (
|
||||
<SimpleBtn
|
||||
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
|
||||
onClick={handleMoreLikeThis}
|
||||
>
|
||||
{moreLikeThisIcon}
|
||||
{!isMobile && <div>{t('appDebug.feature.moreLikeThis.title')}</div>}
|
||||
</SimpleBtn>)}
|
||||
<div className="mx-3 w-[1px] h-[14px] bg-gray-200"></div>
|
||||
{!feedback?.rating && (
|
||||
<SimpleBtn className="!px-0">
|
||||
<>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback?.({
|
||||
rating: 'like'
|
||||
})
|
||||
}}
|
||||
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
|
||||
<HandThumbUpIcon width={16} height={16} />
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback?.({
|
||||
rating: 'dislike'
|
||||
})
|
||||
}}
|
||||
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
|
||||
<HandThumbDownIcon width={16} height={16} />
|
||||
</div>
|
||||
</>
|
||||
</SimpleBtn>
|
||||
)}
|
||||
{feedback?.rating === 'like' && (
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback?.({
|
||||
rating: null
|
||||
})
|
||||
}}
|
||||
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-primary-600 border border-primary-200 bg-primary-100 hover:border-primary-300 hover:bg-primary-200'>
|
||||
<HandThumbUpIcon width={16} height={16} />
|
||||
</div>
|
||||
)}
|
||||
{feedback?.rating === 'dislike' && (
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback?.({
|
||||
rating: null
|
||||
})
|
||||
}}
|
||||
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-red-600 border border-red-200 bg-red-100 hover:border-red-300 hover:bg-red-200'>
|
||||
<HandThumbDownIcon width={16} height={16} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className='text-xs text-gray-500'>{content?.length} {t('common.unit.char')}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{((childMessageId || isQuerying) && depth < 3) && (
|
||||
<div className='pl-4'>
|
||||
<GenerationItem {...childProps} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(GenerationItem)
|
||||
79
web/app/components/app/text-generate/saved-items/index.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import type { SavedMessage } from '@/models/debug'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import { SimpleBtn, copyIcon } from '@/app/components/app/text-generate/item'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import NoData from './no-data'
|
||||
|
||||
export interface ISavedItemsProps {
|
||||
className?: string
|
||||
list: SavedMessage[],
|
||||
onRemove: (id: string) => void
|
||||
onStartCreateContent: () => void
|
||||
}
|
||||
|
||||
const removeIcon = (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.25 1.75H8.75M1.75 3.5H12.25M11.0833 3.5L10.6742 9.63625C10.6129 10.5569 10.5822 11.0172 10.3833 11.3663C10.2083 11.6735 9.94422 11.9206 9.62597 12.0748C9.26448 12.25 8.80314 12.25 7.88045 12.25H6.11955C5.19686 12.25 4.73552 12.25 4.37403 12.0748C4.05577 11.9206 3.79172 11.6735 3.61666 11.3663C3.41781 11.0172 3.38713 10.5569 3.32575 9.63625L2.91667 3.5M5.83333 6.125V9.04167M8.16667 6.125V9.04167" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const SavedItems: FC<ISavedItemsProps> = ({
|
||||
className,
|
||||
list,
|
||||
onRemove,
|
||||
onStartCreateContent
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'space-y-3')}>
|
||||
{list.length === 0 ? (
|
||||
<div className='px-6'>
|
||||
<NoData onStartCreateContent={onStartCreateContent} />
|
||||
</div>
|
||||
) : (<>
|
||||
{list.map(({ id, answer }) => (
|
||||
<div
|
||||
key={id}
|
||||
className='p-4 rounded-xl bg-gray-50'
|
||||
style={{
|
||||
boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)'
|
||||
}}
|
||||
>
|
||||
<Markdown content={answer} />
|
||||
<div className='flex items-center justify-between mt-3'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<SimpleBtn
|
||||
className='space-x-1'
|
||||
onClick={() => {
|
||||
copy(answer)
|
||||
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
|
||||
}}>
|
||||
{copyIcon}
|
||||
<div>{t('common.operation.copy')}</div>
|
||||
</SimpleBtn>
|
||||
|
||||
<SimpleBtn
|
||||
className='space-x-1'
|
||||
onClick={() => {
|
||||
onRemove(id)
|
||||
}}>
|
||||
{removeIcon}
|
||||
<div>{t('common.operation.remove')}</div>
|
||||
</SimpleBtn>
|
||||
</div>
|
||||
<div className='text-xs text-gray-500'>{answer?.length} {t('common.unit.char')}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(SavedItems)
|
||||
@@ -0,0 +1,50 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { PlusIcon } from '@heroicons/react/24/outline'
|
||||
export interface INoDataProps {
|
||||
onStartCreateContent: () => void
|
||||
}
|
||||
|
||||
const markIcon = (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.16699 6.5C4.16699 5.09987 4.16699 4.3998 4.43948 3.86502C4.67916 3.39462 5.06161 3.01217 5.53202 2.77248C6.0668 2.5 6.76686 2.5 8.16699 2.5H11.8337C13.2338 2.5 13.9339 2.5 14.4686 2.77248C14.939 3.01217 15.3215 3.39462 15.5612 3.86502C15.8337 4.3998 15.8337 5.09987 15.8337 6.5V17.5L10.0003 14.1667L4.16699 17.5V6.5Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const lightIcon = (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className="inline relative -top-3 -left-1.5"><path d="M5 6.5V5M8.93934 7.56066L10 6.5M10.0103 11.5H11.5103" stroke="#374151" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
||||
)
|
||||
|
||||
const NoData: FC<INoDataProps> = ({
|
||||
onStartCreateContent
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='mt-[60px] px-5 py-4 rounded-2xl bg-gray-50 '>
|
||||
<div className='flex items-center justify-center w-11 h-11 border border-gray-100 rounded-lg'>
|
||||
{markIcon}
|
||||
</div>
|
||||
<div className='mt-2'>
|
||||
<span className='text-gray-700 font-semibold'>{t('share.generation.savedNoData.title')}</span>
|
||||
{lightIcon}
|
||||
</div>
|
||||
<div className='mt-2 text-gray-500 text-[13px] font-normal'>
|
||||
{t('share.generation.savedNoData.description')}
|
||||
</div>
|
||||
<Button
|
||||
className='mt-4 !h-8 !px-3'
|
||||
onClick={onStartCreateContent}
|
||||
>
|
||||
<div className='flex items-center space-x-2 text-primary-600 text-[13px] font-medium'>
|
||||
<PlusIcon className='w-4 h-4' />
|
||||
<span>{t('share.generation.savedNoData.startCreateContent')}</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(NoData)
|
||||
38
web/app/components/base/app-icon/index.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { FC } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import style from './style.module.css'
|
||||
|
||||
export type AppIconProps = {
|
||||
size?: 'tiny' | 'small' | 'medium' | 'large'
|
||||
rounded?: boolean
|
||||
icon?: string
|
||||
background?: string
|
||||
className?: string
|
||||
innerIcon?: React.ReactNode
|
||||
}
|
||||
|
||||
const AppIcon: FC<AppIconProps> = ({
|
||||
size = 'medium',
|
||||
rounded = false,
|
||||
background,
|
||||
className,
|
||||
innerIcon,
|
||||
}) => {
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
style.appIcon,
|
||||
size !== 'medium' && style[size],
|
||||
rounded && style.rounded,
|
||||
className ?? '',
|
||||
)}
|
||||
style={{
|
||||
background,
|
||||
}}
|
||||
>
|
||||
{innerIcon ? innerIcon : <>🤖</>}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppIcon
|
||||
15
web/app/components/base/app-icon/style.module.css
Normal file
@@ -0,0 +1,15 @@
|
||||
.appIcon {
|
||||
@apply flex items-center justify-center relative w-9 h-9 text-lg bg-teal-100 rounded-lg grow-0 shrink-0;
|
||||
}
|
||||
.appIcon.large {
|
||||
@apply w-10 h-10;
|
||||
}
|
||||
.appIcon.small {
|
||||
@apply w-8 h-8;
|
||||
}
|
||||
.appIcon.tiny {
|
||||
@apply w-6 h-6 text-base;
|
||||
}
|
||||
.appIcon.rounded {
|
||||
@apply rounded-full;
|
||||
}
|
||||
28
web/app/components/base/app-unavailable.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface IAppUnavailableProps {
|
||||
code?: number
|
||||
isUnknwonReason?: boolean
|
||||
unknownReason?: string
|
||||
}
|
||||
|
||||
const AppUnavailable: FC<IAppUnavailableProps> = ({
|
||||
code = 404,
|
||||
isUnknwonReason,
|
||||
unknownReason,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='flex items-center justify-center w-screen h-screen'>
|
||||
<h1 className='mr-5 h-[50px] leading-[50px] pr-5 text-[24px] font-medium'
|
||||
style={{
|
||||
borderRight: '1px solid rgba(0,0,0,.3)',
|
||||
}}>{code}</h1>
|
||||
<div className='text-sm'>{unknownReason || (isUnknwonReason ? t('share.common.appUnkonwError') : t('share.common.appUnavailable'))}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(AppUnavailable)
|
||||
75
web/app/components/base/auto-height-textarea/index.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { forwardRef, useEffect, useRef } from 'react'
|
||||
import cn from 'classnames'
|
||||
|
||||
type IProps = {
|
||||
placeholder?: string
|
||||
value: string
|
||||
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
|
||||
className?: string
|
||||
minHeight?: number
|
||||
maxHeight?: number
|
||||
autoFocus?: boolean
|
||||
controlFocus?: number
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void
|
||||
onKeyUp?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void
|
||||
}
|
||||
|
||||
const AutoHeightTextarea = forwardRef(
|
||||
(
|
||||
{ value, onChange, placeholder, className, minHeight = 36, maxHeight = 96, autoFocus, controlFocus, onKeyDown, onKeyUp }: IProps,
|
||||
outerRef: any,
|
||||
) => {
|
||||
const ref = outerRef || useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
const doFocus = () => {
|
||||
if (ref.current) {
|
||||
// console.log('focus')
|
||||
ref.current.setSelectionRange(value.length, value.length)
|
||||
ref.current.focus()
|
||||
return true
|
||||
}
|
||||
// console.log(autoFocus, 'not focus')
|
||||
return false
|
||||
}
|
||||
|
||||
const focus = () => {
|
||||
if (!doFocus()) {
|
||||
let hasFocus = false
|
||||
const runId = setInterval(() => {
|
||||
hasFocus = doFocus()
|
||||
if (hasFocus)
|
||||
clearInterval(runId)
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (autoFocus)
|
||||
focus()
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (controlFocus)
|
||||
focus()
|
||||
}, [controlFocus])
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<div className={cn(className, 'invisible whitespace-pre-wrap break-all overflow-y-auto')} style={{ minHeight, maxHeight }}>
|
||||
{!value ? placeholder : value.replace(/\n$/, '\n ')}
|
||||
</div>
|
||||
<textarea
|
||||
ref={ref}
|
||||
autoFocus={autoFocus}
|
||||
className={cn(className, 'absolute inset-0 resize-none overflow-hidden')}
|
||||
placeholder={placeholder}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export default AutoHeightTextarea
|
||||
45
web/app/components/base/avatar/index.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
'use client'
|
||||
import cn from 'classnames'
|
||||
|
||||
interface IAvatarProps {
|
||||
name: string
|
||||
avatar?: string
|
||||
size?: number
|
||||
className?: string
|
||||
}
|
||||
const Avatar = ({
|
||||
name,
|
||||
avatar,
|
||||
size = 30,
|
||||
className
|
||||
}: IAvatarProps) => {
|
||||
const avatarClassName = `shrink-0 flex items-center rounded-full bg-primary-600`
|
||||
const style = { width: `${size}px`, height:`${size}px`, fontSize: `${size}px`, lineHeight: `${size}px` }
|
||||
|
||||
if (avatar) {
|
||||
return (
|
||||
<img
|
||||
className={cn(avatarClassName, className)}
|
||||
style={style}
|
||||
alt={name}
|
||||
src={avatar}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(avatarClassName, className)}
|
||||
style={style}
|
||||
>
|
||||
<div
|
||||
className={`text-center text-white scale-[0.4]`}
|
||||
style={style}
|
||||
>
|
||||
{name[0].toLocaleUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Avatar
|
||||
174
web/app/components/base/block-input/index.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
'use client'
|
||||
|
||||
import type { ChangeEvent, FC } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { checkKeys } from '@/utils/var'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Toast from '../toast'
|
||||
import { varHighlightHTML } from '../../app/configuration/base/var-highlight'
|
||||
|
||||
// regex to match the {{}} and replace it with a span
|
||||
const regex = /\{\{([^}]+)\}\}/g
|
||||
|
||||
export const getInputKeys = (value: string) => {
|
||||
const keys = value.match(regex)?.map((item) => {
|
||||
return item.replace('{{', '').replace('}}', '')
|
||||
}) || []
|
||||
const keyObj: Record<string, boolean> = {}
|
||||
// remove duplicate keys
|
||||
const res: string[] = []
|
||||
keys.forEach((key) => {
|
||||
if (keyObj[key])
|
||||
return
|
||||
|
||||
keyObj[key] = true
|
||||
res.push(key)
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
export type IBlockInputProps = {
|
||||
value: string
|
||||
className?: string // wrapper class
|
||||
highLightClassName?: string // class for the highlighted text default is text-blue-500
|
||||
onConfirm?: (value: string, keys: string[]) => void
|
||||
}
|
||||
|
||||
const BlockInput: FC<IBlockInputProps> = ({
|
||||
value = '',
|
||||
className,
|
||||
onConfirm,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
// current is used to store the current value of the contentEditable element
|
||||
const [currentValue, setCurrentValue] = useState<string>(value)
|
||||
useEffect(() => {
|
||||
setCurrentValue(value)
|
||||
}, [value])
|
||||
|
||||
const isContentChanged = value !== currentValue
|
||||
|
||||
const contentEditableRef = useRef<HTMLTextAreaElement>(null)
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false)
|
||||
useEffect(() => {
|
||||
if (isEditing && contentEditableRef.current) {
|
||||
// TODO: Focus at the click positon
|
||||
if (currentValue) {
|
||||
contentEditableRef.current.setSelectionRange(currentValue.length, currentValue.length)
|
||||
}
|
||||
contentEditableRef.current.focus()
|
||||
}
|
||||
}, [isEditing])
|
||||
|
||||
const style = classNames({
|
||||
'block px-4 py-1 w-full h-full text-sm text-gray-900 outline-0 border-0': true,
|
||||
'block-input--editing': isEditing,
|
||||
})
|
||||
|
||||
const coloredContent = (currentValue || '')
|
||||
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
|
||||
.replace(/\n/g, '<br />')
|
||||
|
||||
// Not use useCallback. That will cause out callback get old data.
|
||||
const handleSubmit = () => {
|
||||
if (onConfirm) {
|
||||
const value = currentValue
|
||||
const keys = getInputKeys(value)
|
||||
const { isValid, errorKey, errorMessageKey } = checkKeys(keys)
|
||||
if (!isValid) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey })
|
||||
})
|
||||
return
|
||||
}
|
||||
onConfirm(value, keys)
|
||||
setIsEditing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
setIsEditing(false)
|
||||
setCurrentValue(value)
|
||||
}, [value])
|
||||
|
||||
const onValueChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setCurrentValue(e.target.value)
|
||||
}, [])
|
||||
|
||||
// Prevent rerendering caused cursor to jump to the start of the contentEditable element
|
||||
const TextAreaContentView = () => {
|
||||
return <div
|
||||
className={classNames(style, className)}
|
||||
dangerouslySetInnerHTML={{ __html: coloredContent }}
|
||||
suppressContentEditableWarning={true}
|
||||
/>
|
||||
}
|
||||
|
||||
const placeholder = ''
|
||||
const editAreaClassName = 'focus:outline-none bg-transparent text-sm'
|
||||
|
||||
const textAreaContent = (
|
||||
<div className='h-[180px] overflow-y-auto' onClick={() => setIsEditing(true)}>
|
||||
{isEditing
|
||||
? <div className='h-full px-4 py-1'>
|
||||
<textarea
|
||||
ref={contentEditableRef}
|
||||
className={classNames(editAreaClassName, 'block w-full h-full absolut3e resize-none')}
|
||||
placeholder={placeholder}
|
||||
onChange={onValueChange}
|
||||
value={currentValue}
|
||||
onBlur={() => {
|
||||
blur()
|
||||
if (!isContentChanged) {
|
||||
setIsEditing(false)
|
||||
}
|
||||
// click confirm also make blur. Then outter value is change. So below code has problem.
|
||||
// setTimeout(() => {
|
||||
// handleCancel()
|
||||
// }, 1000)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
: <TextAreaContentView />}
|
||||
</div>)
|
||||
|
||||
return (
|
||||
<div className={classNames('block-input w-full overflow-y-auto border-none rounded-lg')}>
|
||||
{textAreaContent}
|
||||
{/* footer */}
|
||||
<div className='flex item-center h-14 px-4'>
|
||||
{isContentChanged ? (
|
||||
<div className='flex items-center justify-between w-full'>
|
||||
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{currentValue.length}</div>
|
||||
<div className='flex space-x-2'>
|
||||
<Button
|
||||
onClick={handleCancel}
|
||||
className='w-20 !h-8 !text-[13px]'
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
type="primary"
|
||||
className='w-20 !h-8 !text-[13px]'
|
||||
>
|
||||
{t('common.operation.confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
) : (
|
||||
<p className="leading-5 text-xs text-gray-500">
|
||||
{t('appDebug.promptTip')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(BlockInput)
|
||||
47
web/app/components/base/button/index.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { FC, MouseEventHandler } from 'react'
|
||||
import React from 'react'
|
||||
import Spinner from '../spinner'
|
||||
|
||||
export type IButtonProps = {
|
||||
type?: string
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
children: React.ReactNode
|
||||
onClick?: MouseEventHandler<HTMLDivElement>
|
||||
}
|
||||
|
||||
const Button: FC<IButtonProps> = ({
|
||||
type,
|
||||
disabled,
|
||||
children,
|
||||
className,
|
||||
onClick,
|
||||
loading = false,
|
||||
}) => {
|
||||
let style = 'cursor-pointer'
|
||||
switch (type) {
|
||||
case 'primary':
|
||||
style = (disabled || loading) ? 'bg-primary-200 cursor-not-allowed text-white' : 'bg-primary-600 hover:bg-primary-600/75 hover:shadow-md cursor-pointer text-white hover:shadow-sm'
|
||||
break
|
||||
case 'warning':
|
||||
style = (disabled || loading) ? 'bg-red-600/75 cursor-not-allowed text-white' : 'bg-red-600 hover:bg-red-600/75 hover:shadow-md cursor-pointer text-white hover:shadow-sm'
|
||||
break
|
||||
default:
|
||||
style = disabled ? 'border-solid border border-gray-200 bg-gray-200 cursor-not-allowed text-gray-800' : 'border-solid border border-gray-200 cursor-pointer text-gray-500 hover:bg-white hover:shadow-sm hover:border-gray-300'
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`inline-flex justify-center items-center content-center h-9 leading-5 rounded-lg px-4 py-2 text-base ${style} ${className && className}`}
|
||||
onClick={disabled ? undefined : onClick}
|
||||
>
|
||||
{children}
|
||||
{/* Spinner is hidden when loading is false */}
|
||||
<Spinner loading={loading} className='!text-white !h-3 !w-3 !border-2 !ml-1' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Button)
|
||||
51
web/app/components/base/confirm-ui/index.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export type IConfirmUIProps = {
|
||||
type: 'info' | 'warning'
|
||||
title: string
|
||||
content: string
|
||||
confirmText?: string
|
||||
onConfirm: () => void
|
||||
cancelText?: string
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
const ConfirmUI: FC<IConfirmUIProps> = ({
|
||||
type,
|
||||
title,
|
||||
content,
|
||||
confirmText,
|
||||
cancelText,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="w-[420px] rounded-lg p-7 bg-white">
|
||||
<div className='flex items-center'>
|
||||
{type === 'info' && (<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.3333 21.3333H16V16H14.6667M16 10.6667H16.0133M28 16C28 17.5759 27.6896 19.1363 27.0866 20.5922C26.4835 22.0481 25.5996 23.371 24.4853 24.4853C23.371 25.5996 22.0481 26.4835 20.5922 27.0866C19.1363 27.6896 17.5759 28 16 28C14.4241 28 12.8637 27.6896 11.4078 27.0866C9.95189 26.4835 8.62902 25.5996 7.51472 24.4853C6.40042 23.371 5.5165 22.0481 4.91345 20.5922C4.31039 19.1363 4 17.5759 4 16C4 12.8174 5.26428 9.76516 7.51472 7.51472C9.76516 5.26428 12.8174 4 16 4C19.1826 4 22.2348 5.26428 24.4853 7.51472C26.7357 9.76516 28 12.8174 28 16Z" stroke="#9CA3AF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>)}
|
||||
{type === 'warning' && (<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 10.6667V16M16 21.3333H16.0133M28 16C28 17.5759 27.6896 19.1363 27.0866 20.5922C26.4835 22.0481 25.5996 23.371 24.4853 24.4853C23.371 25.5996 22.0481 26.4835 20.5922 27.0866C19.1363 27.6896 17.5759 28 16 28C14.4241 28 12.8637 27.6896 11.4078 27.0866C9.95189 26.4835 8.62902 25.5996 7.51472 24.4853C6.40042 23.371 5.5165 22.0481 4.91345 20.5922C4.31039 19.1363 4 17.5759 4 16C4 12.8174 5.26428 9.76516 7.51472 7.51472C9.76516 5.26428 12.8174 4 16 4C19.1826 4 22.2348 5.26428 24.4853 7.51472C26.7357 9.76516 28 12.8174 28 16Z" stroke="#FACA15" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)}
|
||||
<div className='ml-4 text-lg text-gray-900'>{title}</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-1 ml-12'>
|
||||
<div className='text-sm leading-normal text-gray-500'>{content}</div>
|
||||
</div>
|
||||
|
||||
<div className='flex gap-3 mt-4 ml-12'>
|
||||
<div onClick={onConfirm} className='w-20 leading-9 text-center text-white border rounded-lg cursor-pointer h-9 border-color-primary-700 bg-primary-700'>{confirmText || t('common.operation.confirm')}</div>
|
||||
<div onClick={onCancel} className='w-20 leading-9 text-center text-gray-500 border rounded-lg cursor-pointer h-9 border-color-gray-200'>{cancelText || t('common.operation.cancel')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(ConfirmUI)
|
||||
79
web/app/components/base/confirm/index.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Dialog, Transition } from '@headlessui/react'
|
||||
import { Fragment } from 'react'
|
||||
import ConfirmUI from '../confirm-ui'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
// https://headlessui.com/react/dialog
|
||||
|
||||
type IConfirm = {
|
||||
className?: string
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
type?: 'info' | 'warning'
|
||||
title: string
|
||||
content: string
|
||||
confirmText?: string
|
||||
onConfirm: () => void
|
||||
cancelText?: string
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export default function Confirm({
|
||||
isShow,
|
||||
onClose,
|
||||
type = 'warning',
|
||||
title,
|
||||
content,
|
||||
confirmText,
|
||||
cancelText,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: IConfirm) {
|
||||
const { t } = useTranslation()
|
||||
const confirmTxt = confirmText || `${t('common.operation.confirm')}`
|
||||
const cancelTxt = cancelText || `${t('common.operation.cancel')}`
|
||||
return (
|
||||
<Transition appear show={isShow} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={onClose} onClick={e => e.preventDefault()}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex items-center justify-center min-h-full p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className={'w-full max-w-md transform overflow-hidden rounded-2xl bg-white text-left align-middle shadow-xl transition-all'}>
|
||||
<ConfirmUI
|
||||
type={type}
|
||||
title={title}
|
||||
content={content}
|
||||
confirmText={confirmTxt}
|
||||
cancelText={cancelTxt}
|
||||
onConfirm={onConfirm}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
16
web/app/components/base/custom-icon/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
type IconProps = {
|
||||
icon: any
|
||||
className?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const Icon: FC<IconProps> = ({ icon, className, ...other }) => {
|
||||
return (
|
||||
<img src={icon} className={`h-3 w-3 ${className}`} {...other} alt="icon" />
|
||||
)
|
||||
}
|
||||
|
||||
export default Icon
|
||||
86
web/app/components/base/dialog/index.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Fragment, useCallback } from 'react'
|
||||
import type { ElementType, ReactNode } from 'react'
|
||||
import { Dialog, Transition } from '@headlessui/react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
// https://headlessui.com/react/dialog
|
||||
|
||||
type DialogProps = {
|
||||
className?: string
|
||||
titleClassName?: string
|
||||
bodyClassName?: string
|
||||
footerClassName?: string
|
||||
titleAs?: ElementType
|
||||
title?: ReactNode
|
||||
children: ReactNode
|
||||
footer?: ReactNode
|
||||
show: boolean
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
const CustomDialog = ({
|
||||
className,
|
||||
titleClassName,
|
||||
bodyClassName,
|
||||
footerClassName,
|
||||
titleAs,
|
||||
title,
|
||||
children,
|
||||
footer,
|
||||
show,
|
||||
onClose,
|
||||
}: DialogProps) => {
|
||||
const close = useCallback(() => onClose?.(), [onClose])
|
||||
return (
|
||||
<Transition appear show={show} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={close}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex items-center justify-center min-h-full p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className={classNames('w-full max-w-[800px] p-0 overflow-hidden text-left text-gray-900 align-middle transition-all transform bg-white shadow-xl rounded-2xl', className)}>
|
||||
{Boolean(title) && (
|
||||
<Dialog.Title
|
||||
as={titleAs || 'h3'}
|
||||
className={classNames('px-8 py-6 text-lg font-medium leading-6 text-gray-900', titleClassName)}
|
||||
>
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
)}
|
||||
<div className={classNames('px-8 text-lg font-medium leading-6', bodyClassName)}>
|
||||
{children}
|
||||
</div>
|
||||
{Boolean(footer) && (
|
||||
<div className={classNames('flex items-center justify-end gap-2 px-8 py-6', footerClassName)}>
|
||||
{footer}
|
||||
</div>
|
||||
)}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition >
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomDialog
|
||||
18
web/app/components/base/divider/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { CSSProperties, FC } from 'react'
|
||||
import React from 'react'
|
||||
import s from './style.module.css'
|
||||
|
||||
type Props = {
|
||||
type?: 'horizontal' | 'vertical'
|
||||
// orientation?: 'left' | 'right' | 'center'
|
||||
className?: string
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
const Divider: FC<Props> = ({ type = 'horizontal', className = '', style }) => {
|
||||
return (
|
||||
<div className={`${s.divider} ${s[type]} ${className}`} style={style}></div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Divider
|
||||
9
web/app/components/base/divider/style.module.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.divider {
|
||||
@apply bg-gray-200;
|
||||
}
|
||||
.horizontal {
|
||||
@apply w-full h-[0.5px] my-2;
|
||||
}
|
||||
.vertical {
|
||||
@apply w-[1px] h-full mx-2;
|
||||
}
|
||||
75
web/app/components/base/drawer/index.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
'use client'
|
||||
import { Dialog } from '@headlessui/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '../button'
|
||||
|
||||
type DrawerProps = {
|
||||
title?: string
|
||||
description?: string
|
||||
panelClassname?: string
|
||||
children: React.ReactNode
|
||||
footer?: React.ReactNode
|
||||
mask?: boolean
|
||||
isOpen: boolean
|
||||
// closable: boolean
|
||||
onClose: () => void
|
||||
onCancel?: () => void
|
||||
onOk?: () => void
|
||||
}
|
||||
|
||||
export default function Drawer({
|
||||
title = '',
|
||||
description = '',
|
||||
panelClassname = '',
|
||||
children,
|
||||
footer,
|
||||
mask = true,
|
||||
isOpen,
|
||||
onClose,
|
||||
onCancel,
|
||||
onOk,
|
||||
}: DrawerProps) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Dialog
|
||||
unmount={false}
|
||||
open={isOpen}
|
||||
onClose={() => onClose()}
|
||||
className="fixed z-30 inset-0 overflow-y-auto"
|
||||
>
|
||||
<div className="flex w-screen h-screen justify-end">
|
||||
{/* mask */}
|
||||
<Dialog.Overlay
|
||||
className={`z-40 fixed inset-0 ${!mask ? '' : 'bg-black bg-opacity-30'}`}
|
||||
/>
|
||||
<div className={`z-50 flex flex-col justify-between bg-white w-full
|
||||
max-w-sm p-6 overflow-hidden text-left align-middle
|
||||
shadow-xl ${panelClassname}`}>
|
||||
<>
|
||||
{title && <Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-900"
|
||||
>
|
||||
{title}
|
||||
</Dialog.Title>}
|
||||
{description && <Dialog.Description className='text-gray-500 text-xs font-normal mt-2'>{description}</Dialog.Description>}
|
||||
{children}
|
||||
</>
|
||||
{footer || (footer === null
|
||||
? null
|
||||
: <div className="mt-10 flex flex-row justify-end">
|
||||
<Button
|
||||
className='mr-2'
|
||||
onClick={() => {
|
||||
onCancel && onCancel()
|
||||
}}>{t('common.operation.cancel')}</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onOk && onOk()
|
||||
}}>{t('common.operation.save')}</Button>
|
||||
</div>)}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
37
web/app/components/base/ga/index.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React, { FC } from 'react'
|
||||
import Script from 'next/script'
|
||||
|
||||
export enum GaType {
|
||||
admin = 'admin',
|
||||
webapp = 'webapp',
|
||||
}
|
||||
|
||||
const gaIdMaps = {
|
||||
[GaType.admin]: 'G-DM9497FN4V',
|
||||
[GaType.webapp]: 'G-2MFWXK7WYT',
|
||||
}
|
||||
|
||||
export interface IGAProps {
|
||||
gaType: GaType
|
||||
}
|
||||
|
||||
|
||||
const GA: FC<IGAProps> = ({
|
||||
gaType
|
||||
}) => {
|
||||
return (
|
||||
<Script
|
||||
id="gtag-base"
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer', '${gaIdMaps[gaType]}');
|
||||
`,
|
||||
}} />
|
||||
)
|
||||
}
|
||||
export default React.memo(GA)
|
||||