feat: chat in explore support agent (#647)
Co-authored-by: StyleZhang <jasonapring2015@outlook.com>
13
web/app/(commonLayout)/explore/chat/page.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import UniversalChat from '@/app/components/explore/universal-chat'
|
||||
|
||||
const Chat: FC = () => {
|
||||
return (
|
||||
<div className='h-full p-2'>
|
||||
<UniversalChat />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Chat)
|
|
@ -5,6 +5,8 @@ import { AppContextProvider } from '@/context/app-context'
|
|||
import GA, { GaType } from '@/app/components/base/ga'
|
||||
import HeaderWrapper from '@/app/components/header/HeaderWrapper'
|
||||
import Header from '@/app/components/header'
|
||||
import { EventEmitterContextProvider } from '@/context/event-emitter'
|
||||
import { ProviderContextProvider } from '@/context/provider-context'
|
||||
|
||||
const Layout = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
|
@ -12,10 +14,14 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
|||
<GA gaType={GaType.admin} />
|
||||
<SwrInitor>
|
||||
<AppContextProvider>
|
||||
<EventEmitterContextProvider>
|
||||
<ProviderContextProvider>
|
||||
<HeaderWrapper>
|
||||
<Header />
|
||||
</HeaderWrapper>
|
||||
{children}
|
||||
</ProviderContextProvider>
|
||||
</EventEmitterContextProvider>
|
||||
</AppContextProvider>
|
||||
</SwrInitor>
|
||||
</>
|
||||
|
|
262
web/app/components/app/chat/answer/index.tsx
Normal file
|
@ -0,0 +1,262 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { UserCircleIcon } from '@heroicons/react/24/solid'
|
||||
import cn from 'classnames'
|
||||
import type { DisplayScene, FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc, ThoughtItem } from '../type'
|
||||
import { randomString } from '../../../app-sidebar/basic'
|
||||
import OperationBtn from '../operation'
|
||||
import LoadingAnim from '../loading-anim'
|
||||
import { EditIcon, EditIconSolid, OpeningStatementIcon, RatingIcon } from '../icon-component'
|
||||
import s from '../style.module.css'
|
||||
import MoreInfo from '../more-info'
|
||||
import CopyBtn from '../copy-btn'
|
||||
import Thought from '../thought'
|
||||
import type { Annotation, MessageRating } from '@/models/log'
|
||||
import AppContext from '@/context/app-context'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import AutoHeightTextarea from '@/app/components/base/auto-height-textarea'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
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>
|
||||
}
|
||||
export type IAnswerProps = {
|
||||
item: IChatItem
|
||||
feedbackDisabled: boolean
|
||||
isHideFeedbackEdit: boolean
|
||||
onFeedback?: FeedbackFunc
|
||||
onSubmitAnnotation?: SubmitAnnotationFunc
|
||||
displayScene: DisplayScene
|
||||
isResponsing?: boolean
|
||||
answerIconClassName?: string
|
||||
thoughts?: ThoughtItem[]
|
||||
isThinking?: boolean
|
||||
dataSets?: DataSet[]
|
||||
}
|
||||
// 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, answerIconClassName, thoughts, isThinking, dataSets }) => {
|
||||
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 ? t('appDebug.operation.cancelAgree') : t('appDebug.operation.cancelDisagree') : (!isWebScene && isUserFeedback) ? `${t('appDebug.operation.userAction')}${isLike ? t('appDebug.operation.agree') : t('appDebug.operation.disagree')}` : '') as 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 ${(!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} ${answerIconClassName} w-10 h-10 shrink-0`}>
|
||||
{isResponsing
|
||||
&& <div className={s.typeingIcon}>
|
||||
<LoadingAnim type='avatar' />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className={s.answerWrapWrap}>
|
||||
<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>
|
||||
)}
|
||||
{(thoughts && thoughts.length > 0) && (
|
||||
<Thought
|
||||
list={thoughts || []}
|
||||
isThinking={isThinking}
|
||||
dataSets={dataSets}
|
||||
/>
|
||||
)}
|
||||
{(isResponsing && !content)
|
||||
? (
|
||||
<div className='flex items-center justify-center w-6 h-5'>
|
||||
<LoadingAnim type='text' />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div>
|
||||
<Markdown content={content} />
|
||||
</div>
|
||||
)}
|
||||
{!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'>
|
||||
{!item.isOpeningStatement && (
|
||||
<CopyBtn
|
||||
value={content}
|
||||
className={cn(s.copyBtn, 'mr-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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Answer)
|
37
web/app/components/app/chat/icon-component/index.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import type { FC } from 'react'
|
||||
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
|
||||
|
||||
export const stopIcon = (
|
||||
<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.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 const OpeningStatementIcon: FC<{ className?: string }> = ({ className }) => (
|
||||
<svg className={className} 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>
|
||||
)
|
||||
|
||||
export const RatingIcon: FC<{ isLike: boolean }> = ({ isLike }) => {
|
||||
return isLike ? <HandThumbUpIcon className='w-4 h-4' /> : <HandThumbDownIcon className='w-4 h-4' />
|
||||
}
|
||||
|
||||
export 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 fillRule="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 fillRule="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>
|
||||
}
|
||||
|
||||
export 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>
|
||||
)
|
|
@ -4,43 +4,25 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|||
import { useContext } from 'use-context-selector'
|
||||
import cn from 'classnames'
|
||||
import Recorder from 'js-audio-recorder'
|
||||
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 LoadingAnim from './loading-anim'
|
||||
import CopyBtn from './copy-btn'
|
||||
import type { DisplayScene, FeedbackFunc, IChatItem, SubmitAnnotationFunc } from './type'
|
||||
import { TryToAskIcon, stopIcon } from './icon-component'
|
||||
import Answer from './answer'
|
||||
import Question from './question'
|
||||
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 { formatNumber } from '@/utils/format'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import VoiceInput from '@/app/components/base/voice-input'
|
||||
import { Microphone01 } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { Microphone01 as Microphone01Solid } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
|
||||
const stopIcon = (
|
||||
<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.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'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
|
||||
export type IChatProps = {
|
||||
configElem?: React.ReactNode
|
||||
chatList: IChatItem[]
|
||||
/**
|
||||
* Whether to display the editing area and rating status
|
||||
|
@ -66,352 +48,12 @@ export type IChatProps = {
|
|||
suggestionList?: string[]
|
||||
isShowSpeechToText?: boolean
|
||||
answerIconClassName?: 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')} ${formatNumber(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 fillRule="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 fillRule="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
|
||||
answerIconClassName?: string
|
||||
}
|
||||
|
||||
// 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, answerIconClassName }) => {
|
||||
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 ? t('appDebug.operation.cancelAgree') : t('appDebug.operation.cancelDisagree') : (!isWebScene && isUserFeedback) ? `${t('appDebug.operation.userAction')}${isLike ? t('appDebug.operation.agree') : t('appDebug.operation.disagree')}` : '') as 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 ${(!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} ${answerIconClassName} w-10 h-10 shrink-0`}>
|
||||
{isResponsing
|
||||
&& <div className={s.typeingIcon}>
|
||||
<LoadingAnim type='avatar' />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className={s.answerWrapWrap}>
|
||||
<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'>
|
||||
{!item.isOpeningStatement && (
|
||||
<CopyBtn
|
||||
value={content}
|
||||
className={cn(s.copyBtn, 'mr-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>
|
||||
</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 className={s.questionWrapWrap}>
|
||||
<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>
|
||||
)
|
||||
isShowConfigElem?: boolean
|
||||
dataSets?: DataSet[]
|
||||
}
|
||||
|
||||
const Chat: FC<IChatProps> = ({
|
||||
configElem,
|
||||
chatList,
|
||||
feedbackDisabled = false,
|
||||
isHideFeedbackEdit = false,
|
||||
|
@ -431,6 +73,8 @@ const Chat: FC<IChatProps> = ({
|
|||
suggestionList,
|
||||
isShowSpeechToText,
|
||||
answerIconClassName,
|
||||
isShowConfigElem,
|
||||
dataSets,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
|
@ -509,11 +153,14 @@ const Chat: FC<IChatProps> = ({
|
|||
|
||||
return (
|
||||
<div className={cn('px-3.5', 'h-full')}>
|
||||
{isShowConfigElem && (configElem || null)}
|
||||
{/* Chat List */}
|
||||
<div className="h-full space-y-[30px]">
|
||||
<div className={cn((isShowConfigElem && configElem) ? 'h-0' : 'h-full', 'space-y-[30px]')}>
|
||||
{chatList.map((item) => {
|
||||
if (item.isAnswer) {
|
||||
const isLast = item.id === chatList[chatList.length - 1].id
|
||||
const thoughts = item.agent_thoughts?.filter(item => item.thought !== '[DONE]')
|
||||
const isThinking = !item.content && item.agent_thoughts && item.agent_thoughts?.length > 0 && !item.agent_thoughts.some(item => item.thought === '[DONE]')
|
||||
return <Answer
|
||||
key={item.id}
|
||||
item={item}
|
||||
|
@ -524,6 +171,9 @@ const Chat: FC<IChatProps> = ({
|
|||
displayScene={displayScene ?? 'web'}
|
||||
isResponsing={isResponsing && isLast}
|
||||
answerIconClassName={answerIconClassName}
|
||||
thoughts={thoughts}
|
||||
isThinking={isThinking}
|
||||
dataSets={dataSets}
|
||||
/>
|
||||
}
|
||||
return <Question key={item.id} id={item.id} content={item.content} more={item.more} useCurrentUserAvatar={useCurrentUserAvatar} />
|
||||
|
@ -532,7 +182,8 @@ const Chat: FC<IChatProps> = ({
|
|||
{
|
||||
!isHideSendInput && (
|
||||
<div className={cn(!feedbackDisabled && '!left-3.5 !right-3.5', 'absolute z-10 bottom-0 left-0 right-0')}>
|
||||
{(isResponsing && canStopResponsing) && (
|
||||
{/* Thinking is sync and can not be stopped */}
|
||||
{(isResponsing && canStopResponsing && !!chatList[chatList.length - 1]?.content) && (
|
||||
<div className='flex justify-center mb-4'>
|
||||
<Button className='flex items-center space-x-1 bg-white' onClick={() => abortResponsing?.()}>
|
||||
{stopIcon}
|
||||
|
@ -560,7 +211,7 @@ const Chat: FC<IChatProps> = ({
|
|||
{/* has scrollbar would hide part of first item */}
|
||||
<div ref={suggestionListRef} className={cn(!hasScrollbar && 'justify-center', 'flex overflow-x-auto pb-2')}>
|
||||
{suggestionList?.map((item, index) => (
|
||||
<div className='shrink-0 flex justify-center mr-2'>
|
||||
<div key={item} className='shrink-0 flex justify-center mr-2'>
|
||||
<Button
|
||||
key={index}
|
||||
onClick={() => setQuery(item)}
|
||||
|
|
19
web/app/components/app/chat/more-info/index.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { MessageMore } from '../type'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
|
||||
export type IMoreInfoProps = { more: MessageMore; isQuestion: boolean }
|
||||
|
||||
const MoreInfo: FC<IMoreInfoProps> = ({ 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')} ${formatNumber(more.tokens)}`}</span>
|
||||
<span>· </span>
|
||||
<span>{more.time} </span>
|
||||
</div>)
|
||||
}
|
||||
export default React.memo(MoreInfo)
|
14
web/app/components/app/chat/operation/index.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
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>
|
||||
)
|
||||
|
||||
export default OperationBtn
|
40
web/app/components/app/chat/question/index.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import s from '../style.module.css'
|
||||
import type { IChatItem } from '../type'
|
||||
import MoreInfo from '../more-info'
|
||||
import AppContext from '@/context/app-context'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
|
||||
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 className={s.questionWrapWrap}>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
export default React.memo(Question)
|
86
web/app/components/app/chat/thought/index.tsx
Normal file
|
@ -0,0 +1,86 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ThoughtItem } from '../type'
|
||||
import s from './style.module.css'
|
||||
import { DataSet as DataSetIcon, Loading as LodingIcon, Search, ThoughtList, WebReader } from '@/app/components/base/icons/src/public/thought'
|
||||
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
|
||||
export type IThoughtProps = {
|
||||
list: ThoughtItem[]
|
||||
isThinking?: boolean
|
||||
dataSets?: DataSet[]
|
||||
}
|
||||
|
||||
const getIcon = (toolId: string) => {
|
||||
switch (toolId) {
|
||||
case 'dataset':
|
||||
return <DataSetIcon />
|
||||
case 'web_reader':
|
||||
return <WebReader />
|
||||
default:
|
||||
return <Search />
|
||||
}
|
||||
}
|
||||
|
||||
const Thought: FC<IThoughtProps> = ({
|
||||
list,
|
||||
isThinking,
|
||||
dataSets,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isShowDetail, setIsShowDetail] = React.useState(false)
|
||||
|
||||
const getThoughtText = (item: ThoughtItem) => {
|
||||
try {
|
||||
const input = JSON.parse(item.tool_input)
|
||||
|
||||
switch (item.tool) {
|
||||
case 'dataset':
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const datasetName = dataSets?.find(item => item.id === input.dataset_id)?.name || 'unknown dataset'
|
||||
return t('explore.universalChat.thought.res.dataset').replace('{datasetName}', `<span class="text-gray-700">${datasetName}</span>`)
|
||||
case 'web_reader':
|
||||
return t(`explore.universalChat.thought.res.webReader.${!input.cursor ? 'normal' : 'hasPageInfo'}`).replace('{url}', `<a href="${input.url}" class="text-[#155EEF]">${input.url}</a>`)
|
||||
default: // google, wikipedia
|
||||
return t('explore.universalChat.thought.res.search', { query: input.query })
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
return item
|
||||
}
|
||||
}
|
||||
const renderItem = (item: ThoughtItem) => (
|
||||
<div className='flex space-x-1 py-[3px] leading-[18px]' key={item.id}>
|
||||
<div className='flex items-center h-[18px] shrink-0'>{getIcon(item.tool)}</div>
|
||||
<div dangerouslySetInnerHTML={{
|
||||
__html: getThoughtText(item),
|
||||
// item.thought.replace(urlRegex, (url) => {
|
||||
// return `<a href="${url}" class="text-[#155EEF]">${url}</a>`
|
||||
// }),
|
||||
}}></div>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className={cn(s.wrap, !isShowDetail && s.wrapHoverEffect, 'inline-block mb-2 px-2 py-0.5 rounded-md text-xs text-gray-500 font-medium')} >
|
||||
<div className='flex items-center h-6 space-x-1 cursor-pointer' onClick={() => setIsShowDetail(!isShowDetail)} >
|
||||
{!isThinking ? <ThoughtList /> : <div className='animate-spin'><LodingIcon /></div>}
|
||||
<div dangerouslySetInnerHTML= {{
|
||||
__html: isThinking ? getThoughtText(list[list.length - 1]) : (t(`explore.universalChat.thought.${isShowDetail ? 'hide' : 'show'}`) + t('explore.universalChat.thought.processOfThought')),
|
||||
}}
|
||||
></div>
|
||||
<ChevronDown className={isShowDetail ? 'rotate-180' : '' } />
|
||||
</div>
|
||||
{isShowDetail && (
|
||||
<div>
|
||||
{list.map(item => renderItem(item))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Thought)
|
7
web/app/components/app/chat/thought/style.module.css
Normal file
|
@ -0,0 +1,7 @@
|
|||
.wrap {
|
||||
background-color: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.wrapHoverEffect:hover{
|
||||
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.06), 0px 1px 3px 0px rgba(16, 24, 40, 0.1);
|
||||
}
|
53
web/app/components/app/chat/type.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import type { Annotation, MessageRating } from '@/models/log'
|
||||
|
||||
export type MessageMore = {
|
||||
time: string
|
||||
tokens: number
|
||||
latency: number | string
|
||||
}
|
||||
|
||||
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 ThoughtItem = {
|
||||
id: string
|
||||
tool: string // plugin or dataset
|
||||
thought: string
|
||||
tool_input: string
|
||||
message_id: string
|
||||
}
|
||||
export type IChatItem = {
|
||||
id: string
|
||||
content: string
|
||||
agent_thoughts?: ThoughtItem[]
|
||||
/**
|
||||
* 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
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
'use client'
|
||||
import React, { FC, ReactNode } from 'react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
|
||||
export interface IFeaturePanelProps {
|
||||
export type IFeaturePanelProps = {
|
||||
className?: string
|
||||
headerIcon: ReactNode
|
||||
headerIcon?: ReactNode
|
||||
title: ReactNode
|
||||
headerRight: ReactNode
|
||||
headerRight?: ReactNode
|
||||
hasHeaderBottomBorder?: boolean
|
||||
isFocus?: boolean
|
||||
noBodySpacing?: boolean
|
||||
|
@ -26,15 +27,17 @@ const FeaturePanel: FC<IFeaturePanelProps> = ({
|
|||
return (
|
||||
<div
|
||||
className={cn(className, isFocus && 'border border-[#2D0DEE]', 'rounded-xl bg-gray-50 pt-2 pb-3', noBodySpacing && '!pb-0')}
|
||||
style={isFocus ? {
|
||||
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>
|
||||
{headerIcon && <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>
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { ProviderType } from '@/types/app'
|
||||
import { MODEL_LIST } from '@/config'
|
||||
import { Anthropic, Gpt3, Gpt4 } from '@/app/components/base/icons/src/public/llm'
|
||||
|
||||
export type IModelIconProps = { modelId: string; className?: string }
|
||||
|
||||
const ModelIcon: FC<IModelIconProps> = ({ modelId, className }) => {
|
||||
const resClassName = `w-4 h-4 ${className}`
|
||||
const model = MODEL_LIST.find(item => item.id === modelId)
|
||||
if (model?.id === 'gpt-4')
|
||||
return <Gpt4 className={resClassName} />
|
||||
|
||||
if (model?.provider === ProviderType.anthropic) {
|
||||
return (
|
||||
<Anthropic className={resClassName} />
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Gpt3 className={resClassName} />
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ModelIcon)
|
|
@ -4,7 +4,6 @@ import React from 'react'
|
|||
import { useContext } from 'use-context-selector'
|
||||
import produce from 'immer'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import useSWR from 'swr'
|
||||
import DatasetConfig from '../dataset-config'
|
||||
import ChatGroup from '../features/chat-group'
|
||||
import ExperienceEnchanceGroup from '../features/experience-enchance-group'
|
||||
|
@ -20,7 +19,7 @@ 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 { fetchTenantInfo } from '@/service/common'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
|
||||
const Config: FC = () => {
|
||||
const {
|
||||
|
@ -39,8 +38,7 @@ const Config: FC = () => {
|
|||
setSpeechToTextConfig,
|
||||
} = useContext(ConfigContext)
|
||||
const isChatApp = mode === AppType.chat
|
||||
const { data: userInfo } = useSWR({ url: '/info' }, fetchTenantInfo)
|
||||
const openaiProvider = userInfo?.providers?.find(({ token_is_set, is_valid, provider_name }) => token_is_set && is_valid && provider_name === 'openai')
|
||||
const { currentProvider } = useProviderContext()
|
||||
|
||||
const promptTemplate = modelConfig.configs.prompt_template
|
||||
const promptVariables = modelConfig.configs.prompt_variables
|
||||
|
@ -92,7 +90,7 @@ const Config: FC = () => {
|
|||
},
|
||||
})
|
||||
|
||||
const hasChatConfig = isChatApp && (featureConfig.openingStatement || featureConfig.suggestedQuestionsAfterAnswer || (featureConfig.speechToText && openaiProvider))
|
||||
const hasChatConfig = isChatApp && (featureConfig.openingStatement || featureConfig.suggestedQuestionsAfterAnswer || (featureConfig.speechToText && currentProvider?.provider_name === 'openai'))
|
||||
const hasToolbox = false
|
||||
|
||||
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
|
||||
|
@ -122,7 +120,7 @@ const Config: FC = () => {
|
|||
isChatApp={isChatApp}
|
||||
config={featureConfig}
|
||||
onChange={handleFeatureChange}
|
||||
showSpeechToTextItem={!!openaiProvider}
|
||||
showSpeechToTextItem={currentProvider?.provider_name === 'openai'}
|
||||
/>
|
||||
)}
|
||||
{showAutomatic && (
|
||||
|
@ -162,7 +160,7 @@ const Config: FC = () => {
|
|||
}
|
||||
}
|
||||
isShowSuggestedQuestionsAfterAnswer={featureConfig.suggestedQuestionsAfterAnswer}
|
||||
isShowSpeechText={featureConfig.speechToText}
|
||||
isShowSpeechText={featureConfig.speechToText && currentProvider?.provider_name === 'openai'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import TypeIcon from '../type-icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import TypeIcon from '../type-icon'
|
||||
import RemoveIcon from '../../base/icons/remove-icon'
|
||||
import s from './style.module.css'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
|
||||
export interface ICardItemProps {
|
||||
export type ICardItemProps = {
|
||||
className?: string
|
||||
config: any
|
||||
onRemove: (id: string) => void
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 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" />
|
||||
|
@ -24,7 +24,8 @@ export interface ICardItemProps {
|
|||
const CardItem: FC<ICardItemProps> = ({
|
||||
className,
|
||||
config,
|
||||
onRemove
|
||||
onRemove,
|
||||
readonly,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
@ -44,7 +45,7 @@ const CardItem: FC<ICardItemProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<RemoveIcon className={`${s.deleteBtn} shrink-0`} onClick={() => onRemove(config.id)} />
|
||||
{!readonly && <RemoveIcon className={`${s.deleteBtn} shrink-0`} onClick={() => onRemove(config.id)} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -128,8 +128,12 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
|
|||
<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 className='flex text-xs text-gray-500 overflow-hidden whitespace-nowrap'>
|
||||
<span className='max-w-[100px] overflow-hidden text-ellipsis whitespace-nowrap'>{formatNumber(item.word_count)}</span>
|
||||
{t('appDebug.feature.dataSet.words')}
|
||||
<span className='px-0.5'>·</span>
|
||||
<span className='max-w-[100px] min-w-[8px] overflow-hidden text-ellipsis whitespace-nowrap'>{formatNumber(item.document_count)} </span>
|
||||
{t('appDebug.feature.dataSet.textBlocks')}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
@ -22,6 +22,7 @@ import type { ModelConfig as BackendModelConfig } from '@/types/app'
|
|||
import { promptVariablesToUserInputsForm } from '@/utils/model-config'
|
||||
import TextGeneration from '@/app/components/app/text-generate/item'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
|
||||
type IDebug = {
|
||||
hasSetAPIKEY: boolean
|
||||
|
@ -51,7 +52,7 @@ const Debug: FC<IDebug> = ({
|
|||
modelConfig,
|
||||
completionParams,
|
||||
} = useContext(ConfigContext)
|
||||
|
||||
const { currentProvider } = useProviderContext()
|
||||
const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([])
|
||||
const chatListDomRef = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
|
@ -389,7 +390,7 @@ const Debug: FC<IDebug> = ({
|
|||
}}
|
||||
isShowSuggestion={doShowSuggestion}
|
||||
suggestionList={suggestQuestions}
|
||||
isShowSpeechToText={speechToTextConfig.enabled}
|
||||
isShowSpeechToText={speechToTextConfig.enabled && currentProvider?.provider_name === 'openai'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -16,10 +16,10 @@ import dayjs from 'dayjs'
|
|||
import { createContext, useContext } from 'use-context-selector'
|
||||
import classNames from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { EditIconSolid } from '../chat'
|
||||
import { randomString } from '../../app-sidebar/basic'
|
||||
import s from './style.module.css'
|
||||
import type { FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc } from '@/app/components/app/chat'
|
||||
import { EditIconSolid } from '@/app/components/app/chat/icon-component'
|
||||
import type { FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc } from '@/app/components/app/chat/type'
|
||||
import type { Annotation, ChatConversationFullDetailResponse, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationFullDetailResponse, CompletionConversationGeneralDetail, CompletionConversationsResponse } from '@/models/log'
|
||||
import type { App } from '@/types/app'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="24" height="24" rx="6" fill="#CA9F7B"/>
|
||||
<g clip-path="url(#clip0_7672_55906)">
|
||||
<path d="M15.3843 6.43457H12.9687L17.3739 17.565H19.7896L15.3843 6.43457ZM8.40522 6.43457L4 17.565H6.4633L7.36417 15.2276H11.9729L12.8737 17.565H15.337L10.9318 6.43457H8.40522ZM8.16104 13.1605L9.66852 9.24883L11.176 13.1605H8.16104Z" fill="#191918"/>
|
||||
</g>
|
||||
<rect x="0.5" y="0.5" width="23" height="23" rx="5.5" stroke="black" stroke-opacity="0.05"/>
|
||||
<defs>
|
||||
<clipPath id="clip0_7672_55906">
|
||||
<rect width="16" height="11.1304" fill="white" transform="translate(4 6.43457)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 686 B |
After Width: | Height: | Size: 7.0 KiB |
After Width: | Height: | Size: 7.0 KiB |
|
@ -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="M13.3332 4L5.99984 11.3333L2.6665 8" stroke="#155EEF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 232 B |
|
@ -0,0 +1,6 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.501 12.2331C22.501 11.3698 22.4296 10.7398 22.2748 10.0864H12.2153V13.983H18.12C18.001 14.9514 17.3582 16.4097 15.9296 17.3897L15.9096 17.5202L19.0902 19.9349L19.3106 19.9564C21.3343 18.1247 22.501 15.4297 22.501 12.2331Z" fill="#4285F4"/>
|
||||
<path d="M12.2147 22.5001C15.1075 22.5001 17.5361 21.5667 19.3099 19.9567L15.929 17.39C15.0242 18.0083 13.8099 18.44 12.2147 18.44C9.38142 18.44 6.97669 16.6083 6.11947 14.0767L5.99382 14.0871L2.68656 16.5955L2.64331 16.7133C4.40519 20.1433 8.02423 22.5001 12.2147 22.5001Z" fill="#34A853"/>
|
||||
<path d="M6.11997 14.0765C5.89379 13.4232 5.76289 12.7231 5.76289 11.9998C5.76289 11.2764 5.89379 10.5765 6.10807 9.92313L6.10208 9.78398L2.75337 7.23535L2.64381 7.28642C1.91765 8.70977 1.50098 10.3081 1.50098 11.9998C1.50098 13.6915 1.91765 15.2897 2.64381 16.7131L6.11997 14.0765Z" fill="#FBBC05"/>
|
||||
<path d="M12.2148 5.55997C14.2267 5.55997 15.5838 6.41163 16.3576 7.12335L19.3814 4.23C17.5243 2.53834 15.1076 1.5 12.2148 1.5C8.02426 1.5 4.4052 3.85665 2.64331 7.28662L6.10759 9.92332C6.97672 7.39166 9.38146 5.55997 12.2148 5.55997Z" fill="#EB4335"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,4 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.59235 3.32566C10.3587 3.11341 11.1661 3 12 3C13.962 3 15.7773 3.62779 17.2561 4.69345C16.4693 5.21349 15.8824 5.77819 15.4756 6.38193C14.854 7.30445 14.6947 8.25844 14.8234 9.12887C14.9484 9.97416 15.3366 10.696 15.7446 11.2301C16.1402 11.7479 16.6256 12.181 17.0531 12.3946C18.1294 12.9327 19.3714 13.2022 20.2999 13.341C21.1399 13.4667 22.9206 13.8871 22.9865 12.5492C22.9955 12.3672 23 12.1841 23 12C23 5.92487 18.0751 1 12 1C5.92487 1 1 5.92487 1 12C1 18.0751 5.92487 23 12 23C12.1841 23 12.3672 22.9955 12.5492 22.9865C13.1008 22.9593 13.526 22.4902 13.4988 21.9385C13.4716 21.3869 13.0024 20.9618 12.4508 20.9889C12.3015 20.9963 12.1512 21 12 21C8.49063 21 5.45038 18.9914 3.96619 16.0611L4.93474 15.502L8.50745 16.1706C9.43309 16.3439 10.2876 15.6313 10.2834 14.6896L10.2694 11.5365L12.0952 8.41051C12.3911 7.90404 12.3646 7.27161 12.0274 6.79167L9.59235 3.32566Z" fill="#444CE7"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.9456 12.6561C13.5777 12.5165 13.1621 12.6057 12.8839 12.884C12.6056 13.1623 12.5164 13.5778 12.656 13.9458L15.8228 22.2945C15.969 22.68 16.3367 22.9362 16.7489 22.9399C17.1611 22.9435 17.5333 22.6938 17.6863 22.3111L19.007 19.0071L22.311 17.6865C22.6937 17.5334 22.9434 17.1612 22.9397 16.749C22.9361 16.3368 22.6799 15.9691 22.2944 15.8229L13.9456 12.6561Z" fill="#444CE7"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.8431 5.0001H19.2179H19.0609V5.15706V5.66001V5.81696H19.2179H19.5393C19.9131 5.81696 20.2502 6.00882 20.4411 6.33021C20.632 6.65161 20.6392 7.0394 20.4603 7.36765L15.3174 16.8077L12.9751 11.2238L15.1813 7.17527C15.6379 6.33743 16.5143 5.81696 17.4684 5.81696H17.5726H17.7296V5.66001V5.15706V5.0001H17.5726H12.9474H12.7905V5.15706V5.66001V5.81696H12.9474H13.2688C13.6426 5.81696 13.9797 6.00882 14.1706 6.33021C14.3615 6.65161 14.3687 7.0394 14.1899 7.36765L12.5896 10.305L11.1634 6.9051C11.0601 6.65867 11.0856 6.38965 11.2336 6.16714C11.3816 5.94462 11.6197 5.81696 11.887 5.81696H12.2526H12.4095V5.66001V5.15706V5.0001H12.2526H6.72092H6.56396V5.15706V5.66001V5.81696H6.72092H6.79699C7.88821 5.81696 8.866 6.46719 9.28817 7.47344L11.3954 12.497L9.04698 16.8077L4.89304 6.9051C4.78966 6.65867 4.81525 6.38965 4.9632 6.16714C5.11116 5.94462 5.34932 5.81696 5.61657 5.81696H6.17832H6.33527V5.66001V5.15706V5.0001H6.17832H0.156957H0V5.15706V5.66001V5.81696H0.156957H0.52654C1.61776 5.81696 2.59561 6.46719 3.01772 7.47344L7.80628 18.889C7.89004 19.0887 8.08425 19.2177 8.30111 19.2177C8.50014 19.2177 8.67588 19.1131 8.77125 18.9381L9.39589 17.7918L11.7807 13.4155L14.0767 18.889C14.1604 19.0886 14.3547 19.2176 14.5715 19.2176C14.7705 19.2176 14.9463 19.1131 15.0417 18.938L15.6663 17.7917L21.4517 7.17517C21.9083 6.33733 22.7847 5.81686 23.7388 5.81686H23.843H24V5.6599V5.15696V5H23.8431V5.0001Z" fill="#222A30"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,10 @@
|
|||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7847_32895)">
|
||||
<path d="M10.5 2.5C10.5 3.32843 8.48528 4 6 4C3.51472 4 1.5 3.32843 1.5 2.5M10.5 2.5C10.5 1.67157 8.48528 1 6 1C3.51472 1 1.5 1.67157 1.5 2.5M10.5 2.5V9.5C10.5 10.33 8.5 11 6 11C3.5 11 1.5 10.33 1.5 9.5V2.5M10.5 6C10.5 6.83 8.5 7.5 6 7.5C3.5 7.5 1.5 6.83 1.5 6" stroke="#667085" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7847_32895">
|
||||
<rect width="12" height="12" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 599 B |
|
@ -0,0 +1,10 @@
|
|||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7998_4025)">
|
||||
<path d="M6 1.125V2.375M6 9V11M2.875 6H1.125M10.625 6H9.875M9.22855 9.22855L8.875 8.875M9.33211 2.70789L8.625 3.415M2.46079 9.53921L3.875 8.125M2.56434 2.60434L3.625 3.665" stroke="#667085" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7998_4025">
|
||||
<rect width="12" height="12" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 508 B |
|
@ -0,0 +1,10 @@
|
|||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7847_32899)">
|
||||
<path d="M10.5 10.5L8.75005 8.75M10 5.75C10 8.09721 8.09721 10 5.75 10C3.40279 10 1.5 8.09721 1.5 5.75C1.5 3.40279 3.40279 1.5 5.75 1.5C8.09721 1.5 10 3.40279 10 5.75Z" stroke="#667085" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7847_32899">
|
||||
<rect width="12" height="12" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 506 B |
|
@ -0,0 +1,8 @@
|
|||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 6C4 5.72386 4.22386 5.5 4.5 5.5L10.5 5.5C10.7761 5.5 11 5.72386 11 6C11 6.27614 10.7761 6.5 10.5 6.5L4.5 6.5C4.22386 6.5 4 6.27614 4 6Z" fill="#667085"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 3C4 2.72386 4.22386 2.5 4.5 2.5L10.5 2.5C10.7761 2.5 11 2.72386 11 3C11 3.27614 10.7761 3.5 10.5 3.5L4.5 3.5C4.22386 3.5 4 3.27614 4 3Z" fill="#667085"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 9C4 8.72386 4.22386 8.5 4.5 8.5L10.5 8.5C10.7761 8.5 11 8.72386 11 9C11 9.27614 10.7761 9.5 10.5 9.5L4.5 9.5C4.22386 9.5 4 9.27614 4 9Z" fill="#667085"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 6C1 5.44772 1.44772 5 2 5C2.55228 5 3 5.44772 3 6C3 6.55228 2.55228 7 2 7C1.44772 7 1 6.55228 1 6Z" fill="#667085"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 3C1 2.44772 1.44772 2 2 2C2.55228 2 3 2.44772 3 3C3 3.55228 2.55228 4 2 4C1.44772 4 1 3.55228 1 3Z" fill="#667085"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 9C1 8.44772 1.44772 8 2 8C2.55228 8 3 8.44772 3 9C3 9.55228 2.55228 10 2 10C1.44772 10 1 9.55228 1 9Z" fill="#667085"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,10 @@
|
|||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_7847_32887)">
|
||||
<path d="M4.5 1.75V1M2.53033 2.53033L2 2M2.53033 6.5L2 7.03033M6.5 2.53033L7.03033 2M1.75 4.5H1M7.93224 8.09479L6.68637 10.4085C6.54404 10.6728 6.47287 10.805 6.38725 10.8384C6.31295 10.8674 6.22926 10.8592 6.16199 10.8164C6.08447 10.767 6.04028 10.6235 5.95191 10.3366L4.22259 4.72263C4.1504 4.48825 4.1143 4.37107 4.14335 4.29192C4.16865 4.22298 4.22298 4.16865 4.29192 4.14335C4.37107 4.1143 4.48825 4.1504 4.72262 4.2226L10.3366 5.95192C10.6235 6.0403 10.767 6.08449 10.8164 6.16201C10.8592 6.22928 10.8674 6.31297 10.8384 6.38727C10.805 6.47289 10.6728 6.54406 10.4085 6.68639L8.09479 7.93224C8.05551 7.95339 8.03587 7.96396 8.01868 7.97755C8.00341 7.98961 7.98961 8.00341 7.97755 8.01868C7.96396 8.03587 7.95339 8.05551 7.93224 8.09479Z" stroke="#667085" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7847_32887">
|
||||
<rect width="12" height="12" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,5 @@
|
|||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="link-external-02">
|
||||
<path id="Icon" d="M10.5 4.5L10.5 1.5M10.5 1.5H7.49999M10.5 1.5L6 6M5 1.5H3.9C3.05992 1.5 2.63988 1.5 2.31901 1.66349C2.03677 1.8073 1.8073 2.03677 1.66349 2.31901C1.5 2.63988 1.5 3.05992 1.5 3.9V8.1C1.5 8.94008 1.5 9.36012 1.66349 9.68099C1.8073 9.96323 2.03677 10.1927 2.31901 10.3365C2.63988 10.5 3.05992 10.5 3.9 10.5H8.1C8.94008 10.5 9.36012 10.5 9.68099 10.3365C9.96323 10.1927 10.1927 9.96323 10.3365 9.68099C10.5 9.36012 10.5 8.94008 10.5 8.1V7" stroke="#155EEF" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 654 B |
|
@ -0,0 +1,5 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="alert-circle">
|
||||
<path id="Solid" fill-rule="evenodd" clip-rule="evenodd" d="M8 0.666626C3.94992 0.666626 0.666672 3.94987 0.666672 7.99996C0.666672 12.05 3.94992 15.3333 8 15.3333C12.0501 15.3333 15.3333 12.05 15.3333 7.99996C15.3333 3.94987 12.0501 0.666626 8 0.666626ZM8.66667 5.33329C8.66667 4.9651 8.36819 4.66663 8 4.66663C7.63181 4.66663 7.33334 4.9651 7.33334 5.33329V7.99996C7.33334 8.36815 7.63181 8.66663 8 8.66663C8.36819 8.66663 8.66667 8.36815 8.66667 7.99996V5.33329ZM8 9.99996C7.63181 9.99996 7.33334 10.2984 7.33334 10.6666C7.33334 11.0348 7.63181 11.3333 8 11.3333H8.00667C8.37486 11.3333 8.67334 11.0348 8.67334 10.6666C8.67334 10.2984 8.37486 9.99996 8.00667 9.99996H8Z" fill="#D92D20"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 821 B |
|
@ -0,0 +1,5 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="check-circle">
|
||||
<path id="Solid" fill-rule="evenodd" clip-rule="evenodd" d="M8 0.666626C3.94992 0.666626 0.666672 3.94987 0.666672 7.99996C0.666672 12.05 3.94992 15.3333 8 15.3333C12.0501 15.3333 15.3333 12.05 15.3333 7.99996C15.3333 3.94987 12.0501 0.666626 8 0.666626ZM11.4714 6.47136C11.7318 6.21101 11.7318 5.7889 11.4714 5.52855C11.2111 5.26821 10.7889 5.26821 10.5286 5.52855L7 9.05715L5.47141 7.52855C5.21106 7.2682 4.78895 7.2682 4.5286 7.52855C4.26825 7.7889 4.26825 8.21101 4.5286 8.47136L6.5286 10.4714C6.78895 10.7317 7.21106 10.7317 7.47141 10.4714L11.4714 6.47136Z" fill="#039855"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 711 B |
87
web/app/components/base/icons/src/public/llm/Anthropic.json
Normal file
|
@ -0,0 +1,87 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "rect",
|
||||
"attributes": {
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"rx": "6",
|
||||
"fill": "#CA9F7B"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"clip-path": "url(#clip0_7672_55906)"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M15.3843 6.43457H12.9687L17.3739 17.565H19.7896L15.3843 6.43457ZM8.40522 6.43457L4 17.565H6.4633L7.36417 15.2276H11.9729L12.8737 17.565H15.337L10.9318 6.43457H8.40522ZM8.16104 13.1605L9.66852 9.24883L11.176 13.1605H8.16104Z",
|
||||
"fill": "#191918"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "rect",
|
||||
"attributes": {
|
||||
"x": "0.5",
|
||||
"y": "0.5",
|
||||
"width": "23",
|
||||
"height": "23",
|
||||
"rx": "5.5",
|
||||
"stroke": "black",
|
||||
"stroke-opacity": "0.05"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "defs",
|
||||
"attributes": {},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "clipPath",
|
||||
"attributes": {
|
||||
"id": "clip0_7672_55906"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "rect",
|
||||
"attributes": {
|
||||
"width": "16",
|
||||
"height": "11.1304",
|
||||
"fill": "white",
|
||||
"transform": "translate(4 6.43457)"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Anthropic"
|
||||
}
|
14
web/app/components/base/icons/src/public/llm/Anthropic.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Anthropic.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
export default Icon
|
51
web/app/components/base/icons/src/public/llm/Gpt3.json
Normal file
14
web/app/components/base/icons/src/public/llm/Gpt3.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Gpt3.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
export default Icon
|
51
web/app/components/base/icons/src/public/llm/Gpt4.json
Normal file
14
web/app/components/base/icons/src/public/llm/Gpt4.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Gpt4.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
export default Icon
|
3
web/app/components/base/icons/src/public/llm/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export { default as Anthropic } from './Anthropic'
|
||||
export { default as Gpt3 } from './Gpt3'
|
||||
export { default as Gpt4 } from './Gpt4'
|
29
web/app/components/base/icons/src/public/model/Checked.json
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "16",
|
||||
"height": "16",
|
||||
"viewBox": "0 0 16 16",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M13.3332 4L5.99984 11.3333L2.6665 8",
|
||||
"stroke": "#155EEF",
|
||||
"stroke-width": "2",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Checked"
|
||||
}
|
14
web/app/components/base/icons/src/public/model/Checked.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Checked.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
export default Icon
|
1
web/app/components/base/icons/src/public/model/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { default as Checked } from './Checked'
|
53
web/app/components/base/icons/src/public/plugins/Google.json
Normal file
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M22.501 12.2331C22.501 11.3698 22.4296 10.7398 22.2748 10.0864H12.2153V13.983H18.12C18.001 14.9514 17.3582 16.4097 15.9296 17.3897L15.9096 17.5202L19.0902 19.9349L19.3106 19.9564C21.3343 18.1247 22.501 15.4297 22.501 12.2331Z",
|
||||
"fill": "#4285F4"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M12.2147 22.5001C15.1075 22.5001 17.5361 21.5667 19.3099 19.9567L15.929 17.39C15.0242 18.0083 13.8099 18.44 12.2147 18.44C9.38142 18.44 6.97669 16.6083 6.11947 14.0767L5.99382 14.0871L2.68656 16.5955L2.64331 16.7133C4.40519 20.1433 8.02423 22.5001 12.2147 22.5001Z",
|
||||
"fill": "#34A853"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M6.11997 14.0765C5.89379 13.4232 5.76289 12.7231 5.76289 11.9998C5.76289 11.2764 5.89379 10.5765 6.10807 9.92313L6.10208 9.78398L2.75337 7.23535L2.64381 7.28642C1.91765 8.70977 1.50098 10.3081 1.50098 11.9998C1.50098 13.6915 1.91765 15.2897 2.64381 16.7131L6.11997 14.0765Z",
|
||||
"fill": "#FBBC05"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M12.2148 5.55997C14.2267 5.55997 15.5838 6.41163 16.3576 7.12335L19.3814 4.23C17.5243 2.53834 15.1076 1.5 12.2148 1.5C8.02426 1.5 4.4052 3.85665 2.64331 7.28662L6.10759 9.92332C6.97672 7.39166 9.38146 5.55997 12.2148 5.55997Z",
|
||||
"fill": "#EB4335"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Google"
|
||||
}
|
14
web/app/components/base/icons/src/public/plugins/Google.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Google.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
export default Icon
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M9.59235 3.32566C10.3587 3.11341 11.1661 3 12 3C13.962 3 15.7773 3.62779 17.2561 4.69345C16.4693 5.21349 15.8824 5.77819 15.4756 6.38193C14.854 7.30445 14.6947 8.25844 14.8234 9.12887C14.9484 9.97416 15.3366 10.696 15.7446 11.2301C16.1402 11.7479 16.6256 12.181 17.0531 12.3946C18.1294 12.9327 19.3714 13.2022 20.2999 13.341C21.1399 13.4667 22.9206 13.8871 22.9865 12.5492C22.9955 12.3672 23 12.1841 23 12C23 5.92487 18.0751 1 12 1C5.92487 1 1 5.92487 1 12C1 18.0751 5.92487 23 12 23C12.1841 23 12.3672 22.9955 12.5492 22.9865C13.1008 22.9593 13.526 22.4902 13.4988 21.9385C13.4716 21.3869 13.0024 20.9618 12.4508 20.9889C12.3015 20.9963 12.1512 21 12 21C8.49063 21 5.45038 18.9914 3.96619 16.0611L4.93474 15.502L8.50745 16.1706C9.43309 16.3439 10.2876 15.6313 10.2834 14.6896L10.2694 11.5365L12.0952 8.41051C12.3911 7.90404 12.3646 7.27161 12.0274 6.79167L9.59235 3.32566Z",
|
||||
"fill": "#444CE7"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M13.9456 12.6561C13.5777 12.5165 13.1621 12.6057 12.8839 12.884C12.6056 13.1623 12.5164 13.5778 12.656 13.9458L15.8228 22.2945C15.969 22.68 16.3367 22.9362 16.7489 22.9399C17.1611 22.9435 17.5333 22.6938 17.6863 22.3111L19.007 19.0071L22.311 17.6865C22.6937 17.5334 22.9434 17.1612 22.9397 16.749C22.9361 16.3368 22.6799 15.9691 22.2944 15.8229L13.9456 12.6561Z",
|
||||
"fill": "#444CE7"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "WebReader"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './WebReader.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
export default Icon
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M23.8431 5.0001H19.2179H19.0609V5.15706V5.66001V5.81696H19.2179H19.5393C19.9131 5.81696 20.2502 6.00882 20.4411 6.33021C20.632 6.65161 20.6392 7.0394 20.4603 7.36765L15.3174 16.8077L12.9751 11.2238L15.1813 7.17527C15.6379 6.33743 16.5143 5.81696 17.4684 5.81696H17.5726H17.7296V5.66001V5.15706V5.0001H17.5726H12.9474H12.7905V5.15706V5.66001V5.81696H12.9474H13.2688C13.6426 5.81696 13.9797 6.00882 14.1706 6.33021C14.3615 6.65161 14.3687 7.0394 14.1899 7.36765L12.5896 10.305L11.1634 6.9051C11.0601 6.65867 11.0856 6.38965 11.2336 6.16714C11.3816 5.94462 11.6197 5.81696 11.887 5.81696H12.2526H12.4095V5.66001V5.15706V5.0001H12.2526H6.72092H6.56396V5.15706V5.66001V5.81696H6.72092H6.79699C7.88821 5.81696 8.866 6.46719 9.28817 7.47344L11.3954 12.497L9.04698 16.8077L4.89304 6.9051C4.78966 6.65867 4.81525 6.38965 4.9632 6.16714C5.11116 5.94462 5.34932 5.81696 5.61657 5.81696H6.17832H6.33527V5.66001V5.15706V5.0001H6.17832H0.156957H0V5.15706V5.66001V5.81696H0.156957H0.52654C1.61776 5.81696 2.59561 6.46719 3.01772 7.47344L7.80628 18.889C7.89004 19.0887 8.08425 19.2177 8.30111 19.2177C8.50014 19.2177 8.67588 19.1131 8.77125 18.9381L9.39589 17.7918L11.7807 13.4155L14.0767 18.889C14.1604 19.0886 14.3547 19.2176 14.5715 19.2176C14.7705 19.2176 14.9463 19.1131 15.0417 18.938L15.6663 17.7917L21.4517 7.17517C21.9083 6.33733 22.7847 5.81686 23.7388 5.81686H23.843H24V5.6599V5.15696V5H23.8431V5.0001Z",
|
||||
"fill": "#222A30"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Wikipedia"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Wikipedia.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
export default Icon
|
|
@ -0,0 +1,3 @@
|
|||
export { default as Google } from './Google'
|
||||
export { default as WebReader } from './WebReader'
|
||||
export { default as Wikipedia } from './Wikipedia'
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "12",
|
||||
"height": "12",
|
||||
"viewBox": "0 0 12 12",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"clip-path": "url(#clip0_7847_32895)"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M10.5 2.5C10.5 3.32843 8.48528 4 6 4C3.51472 4 1.5 3.32843 1.5 2.5M10.5 2.5C10.5 1.67157 8.48528 1 6 1C3.51472 1 1.5 1.67157 1.5 2.5M10.5 2.5V9.5C10.5 10.33 8.5 11 6 11C3.5 11 1.5 10.33 1.5 9.5V2.5M10.5 6C10.5 6.83 8.5 7.5 6 7.5C3.5 7.5 1.5 6.83 1.5 6",
|
||||
"stroke": "#667085",
|
||||
"stroke-width": "1.25",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "defs",
|
||||
"attributes": {},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "clipPath",
|
||||
"attributes": {
|
||||
"id": "clip0_7847_32895"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "rect",
|
||||
"attributes": {
|
||||
"width": "12",
|
||||
"height": "12",
|
||||
"fill": "white"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "DataSet"
|
||||
}
|
14
web/app/components/base/icons/src/public/thought/DataSet.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './DataSet.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
export default Icon
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "12",
|
||||
"height": "12",
|
||||
"viewBox": "0 0 12 12",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"clip-path": "url(#clip0_7998_4025)"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M6 1.125V2.375M6 9V11M2.875 6H1.125M10.625 6H9.875M9.22855 9.22855L8.875 8.875M9.33211 2.70789L8.625 3.415M2.46079 9.53921L3.875 8.125M2.56434 2.60434L3.625 3.665",
|
||||
"stroke": "#667085",
|
||||
"stroke-width": "1.25",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "defs",
|
||||
"attributes": {},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "clipPath",
|
||||
"attributes": {
|
||||
"id": "clip0_7998_4025"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "rect",
|
||||
"attributes": {
|
||||
"width": "12",
|
||||
"height": "12",
|
||||
"fill": "white"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Loading"
|
||||
}
|
14
web/app/components/base/icons/src/public/thought/Loading.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Loading.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
export default Icon
|
64
web/app/components/base/icons/src/public/thought/Search.json
Normal file
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "12",
|
||||
"height": "12",
|
||||
"viewBox": "0 0 12 12",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"clip-path": "url(#clip0_7847_32899)"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M10.5 10.5L8.75005 8.75M10 5.75C10 8.09721 8.09721 10 5.75 10C3.40279 10 1.5 8.09721 1.5 5.75C1.5 3.40279 3.40279 1.5 5.75 1.5C8.09721 1.5 10 3.40279 10 5.75Z",
|
||||
"stroke": "#667085",
|
||||
"stroke-width": "1.25",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "defs",
|
||||
"attributes": {},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "clipPath",
|
||||
"attributes": {
|
||||
"id": "clip0_7847_32899"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "rect",
|
||||
"attributes": {
|
||||
"width": "12",
|
||||
"height": "12",
|
||||
"fill": "white"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Search"
|
||||
}
|
14
web/app/components/base/icons/src/public/thought/Search.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Search.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
export default Icon
|
|
@ -0,0 +1,83 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "12",
|
||||
"height": "12",
|
||||
"viewBox": "0 0 12 12",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M4 6C4 5.72386 4.22386 5.5 4.5 5.5L10.5 5.5C10.7761 5.5 11 5.72386 11 6C11 6.27614 10.7761 6.5 10.5 6.5L4.5 6.5C4.22386 6.5 4 6.27614 4 6Z",
|
||||
"fill": "#667085"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M4 3C4 2.72386 4.22386 2.5 4.5 2.5L10.5 2.5C10.7761 2.5 11 2.72386 11 3C11 3.27614 10.7761 3.5 10.5 3.5L4.5 3.5C4.22386 3.5 4 3.27614 4 3Z",
|
||||
"fill": "#667085"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M4 9C4 8.72386 4.22386 8.5 4.5 8.5L10.5 8.5C10.7761 8.5 11 8.72386 11 9C11 9.27614 10.7761 9.5 10.5 9.5L4.5 9.5C4.22386 9.5 4 9.27614 4 9Z",
|
||||
"fill": "#667085"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M1 6C1 5.44772 1.44772 5 2 5C2.55228 5 3 5.44772 3 6C3 6.55228 2.55228 7 2 7C1.44772 7 1 6.55228 1 6Z",
|
||||
"fill": "#667085"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M1 3C1 2.44772 1.44772 2 2 2C2.55228 2 3 2.44772 3 3C3 3.55228 2.55228 4 2 4C1.44772 4 1 3.55228 1 3Z",
|
||||
"fill": "#667085"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M1 9C1 8.44772 1.44772 8 2 8C2.55228 8 3 8.44772 3 9C3 9.55228 2.55228 10 2 10C1.44772 10 1 9.55228 1 9Z",
|
||||
"fill": "#667085"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "ThoughtList"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './ThoughtList.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
export default Icon
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "12",
|
||||
"height": "12",
|
||||
"viewBox": "0 0 12 12",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"clip-path": "url(#clip0_7847_32887)"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M4.5 1.75V1M2.53033 2.53033L2 2M2.53033 6.5L2 7.03033M6.5 2.53033L7.03033 2M1.75 4.5H1M7.93224 8.09479L6.68637 10.4085C6.54404 10.6728 6.47287 10.805 6.38725 10.8384C6.31295 10.8674 6.22926 10.8592 6.16199 10.8164C6.08447 10.767 6.04028 10.6235 5.95191 10.3366L4.22259 4.72263C4.1504 4.48825 4.1143 4.37107 4.14335 4.29192C4.16865 4.22298 4.22298 4.16865 4.29192 4.14335C4.37107 4.1143 4.48825 4.1504 4.72262 4.2226L10.3366 5.95192C10.6235 6.0403 10.767 6.08449 10.8164 6.16201C10.8592 6.22928 10.8674 6.31297 10.8384 6.38727C10.805 6.47289 10.6728 6.54406 10.4085 6.68639L8.09479 7.93224C8.05551 7.95339 8.03587 7.96396 8.01868 7.97755C8.00341 7.98961 7.98961 8.00341 7.97755 8.01868C7.96396 8.03587 7.95339 8.05551 7.93224 8.09479Z",
|
||||
"stroke": "#667085",
|
||||
"stroke-width": "1.25",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "defs",
|
||||
"attributes": {},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "clipPath",
|
||||
"attributes": {
|
||||
"id": "clip0_7847_32887"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "rect",
|
||||
"attributes": {
|
||||
"width": "12",
|
||||
"height": "12",
|
||||
"fill": "white"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "WebReader"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './WebReader.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
export default Icon
|
|
@ -0,0 +1,5 @@
|
|||
export { default as DataSet } from './DataSet'
|
||||
export { default as Loading } from './Loading'
|
||||
export { default as Search } from './Search'
|
||||
export { default as ThoughtList } from './ThoughtList'
|
||||
export { default as WebReader } from './WebReader'
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "12",
|
||||
"height": "12",
|
||||
"viewBox": "0 0 12 12",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "link-external-02"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Icon",
|
||||
"d": "M10.5 4.5L10.5 1.5M10.5 1.5H7.49999M10.5 1.5L6 6M5 1.5H3.9C3.05992 1.5 2.63988 1.5 2.31901 1.66349C2.03677 1.8073 1.8073 2.03677 1.66349 2.31901C1.5 2.63988 1.5 3.05992 1.5 3.9V8.1C1.5 8.94008 1.5 9.36012 1.66349 9.68099C1.8073 9.96323 2.03677 10.1927 2.31901 10.3365C2.63988 10.5 3.05992 10.5 3.9 10.5H8.1C8.94008 10.5 9.36012 10.5 9.68099 10.3365C9.96323 10.1927 10.1927 9.96323 10.3365 9.68099C10.5 9.36012 10.5 8.94008 10.5 8.1V7",
|
||||
"stroke": "currentColor",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "LinkExternal02"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './LinkExternal02.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
export default Icon
|
|
@ -1,4 +1,5 @@
|
|||
export { default as Check } from './Check'
|
||||
export { default as LinkExternal02 } from './LinkExternal02'
|
||||
export { default as Loading02 } from './Loading02'
|
||||
export { default as LogOut01 } from './LogOut01'
|
||||
export { default as Trash03 } from './Trash03'
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "16",
|
||||
"height": "16",
|
||||
"viewBox": "0 0 16 16",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "alert-circle"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Solid",
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M8 0.666626C3.94992 0.666626 0.666672 3.94987 0.666672 7.99996C0.666672 12.05 3.94992 15.3333 8 15.3333C12.0501 15.3333 15.3333 12.05 15.3333 7.99996C15.3333 3.94987 12.0501 0.666626 8 0.666626ZM8.66667 5.33329C8.66667 4.9651 8.36819 4.66663 8 4.66663C7.63181 4.66663 7.33334 4.9651 7.33334 5.33329V7.99996C7.33334 8.36815 7.63181 8.66663 8 8.66663C8.36819 8.66663 8.66667 8.36815 8.66667 7.99996V5.33329ZM8 9.99996C7.63181 9.99996 7.33334 10.2984 7.33334 10.6666C7.33334 11.0348 7.63181 11.3333 8 11.3333H8.00667C8.37486 11.3333 8.67334 11.0348 8.67334 10.6666C8.67334 10.2984 8.37486 9.99996 8.00667 9.99996H8Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "AlertCircle"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './AlertCircle.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
export default Icon
|
|
@ -1 +1,2 @@
|
|||
export { default as AlertCircle } from './AlertCircle'
|
||||
export { default as AlertTriangle } from './AlertTriangle'
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "16",
|
||||
"height": "16",
|
||||
"viewBox": "0 0 16 16",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "check-circle"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Solid",
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M8 0.666626C3.94992 0.666626 0.666672 3.94987 0.666672 7.99996C0.666672 12.05 3.94992 15.3333 8 15.3333C12.0501 15.3333 15.3333 12.05 15.3333 7.99996C15.3333 3.94987 12.0501 0.666626 8 0.666626ZM11.4714 6.47136C11.7318 6.21101 11.7318 5.7889 11.4714 5.52855C11.2111 5.26821 10.7889 5.26821 10.5286 5.52855L7 9.05715L5.47141 7.52855C5.21106 7.2682 4.78895 7.2682 4.5286 7.52855C4.26825 7.7889 4.26825 8.21101 4.5286 8.47136L6.5286 10.4714C6.78895 10.7317 7.21106 10.7317 7.47141 10.4714L11.4714 6.47136Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "CheckCircle"
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './CheckCircle.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
export default Icon
|
|
@ -1,2 +1,3 @@
|
|||
export { default as CheckCircle } from './CheckCircle'
|
||||
export { default as Download02 } from './Download02'
|
||||
export { default as XCircle } from './XCircle'
|
||||
|
|
|
@ -1,16 +1,26 @@
|
|||
import ReactMarkdown from "react-markdown";
|
||||
import "katex/dist/katex.min.css";
|
||||
import RemarkMath from "remark-math";
|
||||
import RemarkBreaks from "remark-breaks";
|
||||
import RehypeKatex from "rehype-katex";
|
||||
import RemarkGfm from "remark-gfm";
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import 'katex/dist/katex.min.css'
|
||||
import RemarkMath from 'remark-math'
|
||||
import RemarkBreaks from 'remark-breaks'
|
||||
import RehypeKatex from 'rehype-katex'
|
||||
import RemarkGfm from 'remark-gfm'
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter'
|
||||
import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs'
|
||||
import { useRef, useState, RefObject, useEffect } from "react";
|
||||
import type { RefObject } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
// import { copyToClipboard } from "../utils";
|
||||
// https://txtfiddle.com/~hlshwya/extract-urls-from-text
|
||||
// const urlRegex = /\b((https?|ftp|file):\/\/|(www|ftp)\.)[-A-Z0-9+&@#\/%?=~_|$!:,.;]*[A-Z0-9+&@#\/%=~_|$]/ig
|
||||
|
||||
// function highlightURL(content: string) {
|
||||
// return content.replace(urlRegex, (url) => {
|
||||
// // fix http:// in [] will be parsed to link agin
|
||||
// const res = `[${url.replace('://', '://')}](${url})`
|
||||
// return res
|
||||
// })
|
||||
// }
|
||||
export function PreCode(props: { children: any }) {
|
||||
const ref = useRef<HTMLPreElement>(null);
|
||||
const ref = useRef<HTMLPreElement>(null)
|
||||
|
||||
return (
|
||||
<pre ref={ref}>
|
||||
|
@ -18,38 +28,37 @@ export function PreCode(props: { children: any }) {
|
|||
className="copy-code-button"
|
||||
onClick={() => {
|
||||
if (ref.current) {
|
||||
const code = ref.current.innerText;
|
||||
const code = ref.current.innerText
|
||||
// copyToClipboard(code);
|
||||
}
|
||||
}}
|
||||
></span>
|
||||
{props.children}
|
||||
</pre>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
const useLazyLoad = (ref: RefObject<Element>): boolean => {
|
||||
const [isIntersecting, setIntersecting] = useState<boolean>(false);
|
||||
const [isIntersecting, setIntersecting] = useState<boolean>(false)
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
setIntersecting(true);
|
||||
observer.disconnect();
|
||||
setIntersecting(true)
|
||||
observer.disconnect()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current);
|
||||
}
|
||||
if (ref.current)
|
||||
observer.observe(ref.current)
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [ref]);
|
||||
observer.disconnect()
|
||||
}
|
||||
}, [ref])
|
||||
|
||||
return isIntersecting;
|
||||
};
|
||||
return isIntersecting
|
||||
}
|
||||
|
||||
export function Markdown(props: { content: string }) {
|
||||
return (
|
||||
|
@ -62,7 +71,8 @@ export function Markdown(props: { content: string }) {
|
|||
components={{
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
return !inline && match ? (
|
||||
return (!inline && match)
|
||||
? (
|
||||
<SyntaxHighlighter
|
||||
{...props}
|
||||
children={String(children).replace(/\n$/, '')}
|
||||
|
@ -71,17 +81,19 @@ export function Markdown(props: { content: string }) {
|
|||
showLineNumbers
|
||||
PreTag="div"
|
||||
/>
|
||||
) : (
|
||||
)
|
||||
: (
|
||||
<code {...props} className={className}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
}
|
||||
},
|
||||
}}
|
||||
linkTarget={"_blank"}
|
||||
linkTarget={'_blank'}
|
||||
>
|
||||
{/* Markdown detect has problem. */}
|
||||
{props.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
InformationCircleIcon,
|
||||
XCircleIcon,
|
||||
} from '@heroicons/react/20/solid'
|
||||
import { createContext } from 'use-context-selector'
|
||||
import { createContext, useContext } from 'use-context-selector'
|
||||
|
||||
export type IToastProps = {
|
||||
type?: 'success' | 'error' | 'warning' | 'info'
|
||||
|
@ -24,6 +24,7 @@ type IToastContext = {
|
|||
const defaultDuring = 3000
|
||||
|
||||
export const ToastContext = createContext<IToastContext>({} as IToastContext)
|
||||
export const useToastContext = () => useContext(ToastContext)
|
||||
const Toast = ({
|
||||
type = 'info',
|
||||
duration,
|
||||
|
@ -31,9 +32,9 @@ const Toast = ({
|
|||
children,
|
||||
}: IToastProps) => {
|
||||
// sometimes message is react node array. Not handle it.
|
||||
if (typeof message !== 'string') {
|
||||
if (typeof message !== 'string')
|
||||
return null
|
||||
}
|
||||
|
||||
return <div className={classNames(
|
||||
'fixed rounded-md p-4 my-4 mx-8 z-50',
|
||||
'top-0',
|
||||
|
|
|
@ -86,7 +86,7 @@ const VoiceInput = ({
|
|||
const formData = new FormData()
|
||||
formData.append('file', mp3File)
|
||||
|
||||
let url = ''
|
||||
let url = '/universal-chat/audio-to-text'
|
||||
let isPublic = false
|
||||
|
||||
if (params.token) {
|
||||
|
|
|
@ -24,6 +24,18 @@ const DiscoveryIcon = () => (
|
|||
</svg>
|
||||
)
|
||||
|
||||
const SelectedChatIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M8.00016 1.3335C4.31826 1.3335 1.3335 4.31826 1.3335 8.00016C1.3335 8.88571 1.50651 9.7325 1.8212 10.5074C1.84962 10.5773 1.86597 10.6178 1.87718 10.6476L1.88058 10.6568L1.88016 10.66C1.87683 10.6846 1.87131 10.7181 1.86064 10.7821L1.46212 13.1732C1.44424 13.2803 1.42423 13.4001 1.41638 13.5041C1.40782 13.6176 1.40484 13.7981 1.48665 13.9888C1.58779 14.2246 1.77569 14.4125 2.0115 14.5137C2.20224 14.5955 2.38274 14.5925 2.49619 14.5839C2.60025 14.5761 2.72006 14.5561 2.82715 14.5382L5.2182 14.1397C5.28222 14.129 5.31576 14.1235 5.34036 14.1202L5.34353 14.1197L5.35274 14.1231C5.38258 14.1344 5.42298 14.1507 5.49297 14.1791C6.26783 14.4938 7.11462 14.6668 8.00016 14.6668C11.6821 14.6668 14.6668 11.6821 14.6668 8.00016C14.6668 4.31826 11.6821 1.3335 8.00016 1.3335ZM4.00016 8.00016C4.00016 7.44788 4.44788 7.00016 5.00016 7.00016C5.55245 7.00016 6.00016 7.44788 6.00016 8.00016C6.00016 8.55245 5.55245 9.00016 5.00016 9.00016C4.44788 9.00016 4.00016 8.55245 4.00016 8.00016ZM7.00016 8.00016C7.00016 7.44788 7.44788 7.00016 8.00016 7.00016C8.55245 7.00016 9.00016 7.44788 9.00016 8.00016C9.00016 8.55245 8.55245 9.00016 8.00016 9.00016C7.44788 9.00016 7.00016 8.55245 7.00016 8.00016ZM11.0002 7.00016C10.4479 7.00016 10.0002 7.44788 10.0002 8.00016C10.0002 8.55245 10.4479 9.00016 11.0002 9.00016C11.5524 9.00016 12.0002 8.55245 12.0002 8.00016C12.0002 7.44788 11.5524 7.00016 11.0002 7.00016Z" fill="#155EEF"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const ChatIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 8H5.00667M8 8H8.00667M11 8H11.0067M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 8.7981 2.15582 9.5598 2.43871 10.2563C2.49285 10.3897 2.51992 10.4563 2.532 10.5102C2.54381 10.5629 2.54813 10.6019 2.54814 10.6559C2.54814 10.7111 2.53812 10.7713 2.51807 10.8916L2.12275 13.2635C2.08135 13.5119 2.06065 13.6361 2.09917 13.7259C2.13289 13.8045 2.19552 13.8671 2.27412 13.9008C2.36393 13.9393 2.48812 13.9186 2.73651 13.8772L5.10843 13.4819C5.22872 13.4619 5.28887 13.4519 5.34409 13.4519C5.3981 13.4519 5.43711 13.4562 5.48981 13.468C5.54369 13.4801 5.61035 13.5072 5.74366 13.5613C6.4402 13.8442 7.2019 14 8 14ZM5.33333 8C5.33333 8.1841 5.1841 8.33333 5 8.33333C4.81591 8.33333 4.66667 8.1841 4.66667 8C4.66667 7.81591 4.81591 7.66667 5 7.66667C5.1841 7.66667 5.33333 7.81591 5.33333 8ZM8.33333 8C8.33333 8.1841 8.1841 8.33333 8 8.33333C7.81591 8.33333 7.66667 8.1841 7.66667 8C7.66667 7.81591 7.81591 7.66667 8 7.66667C8.1841 7.66667 8.33333 7.81591 8.33333 8ZM11.3333 8C11.3333 8.1841 11.1841 8.33333 11 8.33333C10.8159 8.33333 10.6667 8.1841 10.6667 8C10.6667 7.81591 10.8159 7.66667 11 7.66667C11.1841 7.66667 11.3333 7.81591 11.3333 8Z" stroke="#344054" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const SideBar: FC<{
|
||||
controlUpdateInstalledApps: number
|
||||
}> = ({
|
||||
|
@ -33,6 +45,7 @@ const SideBar: FC<{
|
|||
const segments = useSelectedLayoutSegments()
|
||||
const lastSegment = segments.slice(-1)[0]
|
||||
const isDiscoverySelected = lastSegment === 'apps'
|
||||
const isChatSelected = lastSegment === 'chat'
|
||||
const { installedApps, setInstalledApps } = useContext(ExploreContext)
|
||||
|
||||
const fetchInstalledAppList = async () => {
|
||||
|
@ -81,6 +94,14 @@ const SideBar: FC<{
|
|||
{isDiscoverySelected ? <SelectedDiscoveryIcon /> : <DiscoveryIcon />}
|
||||
<div className='text-sm'>{t('explore.sidebar.discovery')}</div>
|
||||
</Link>
|
||||
<Link
|
||||
href='/explore/chat'
|
||||
className={cn(isChatSelected ? 'text-primary-600 bg-white font-semibold' : 'text-gray-700 font-medium', 'flex items-center h-9 pl-3 space-x-2 rounded-lg')}
|
||||
style={isChatSelected ? { boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)' } : {}}
|
||||
>
|
||||
{isChatSelected ? <SelectedChatIcon /> : <ChatIcon />}
|
||||
<div className='text-sm'>{t('explore.sidebar.chat')}</div>
|
||||
</Link>
|
||||
</div>
|
||||
{installedApps.length > 0 && (
|
||||
<div className='mt-10'>
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import s from './style.module.css'
|
||||
import Config from '@/app/components/explore/universal-chat/config'
|
||||
|
||||
type Props = {
|
||||
modelId: string
|
||||
plugins: Record<string, boolean>
|
||||
dataSets: any[]
|
||||
}
|
||||
const ConfigViewPanel: FC<Props> = ({
|
||||
modelId,
|
||||
plugins,
|
||||
dataSets,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className={cn('absolute top-9 right-0 z-20 p-4 bg-white rounded-2xl shadow-md', s.panelBorder)}>
|
||||
<div className='w-[368px]'>
|
||||
<Config
|
||||
readonly
|
||||
modelId={modelId}
|
||||
plugins={plugins}
|
||||
dataSets={dataSets}
|
||||
/>
|
||||
<div className='mt-3 text-xs leading-[18px] text-500 font-normal'>{t('explore.universalChat.viewConfigDetailTip')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ConfigViewPanel)
|
|
@ -0,0 +1,9 @@
|
|||
.btn {
|
||||
background: url(~@/app/components/datasets/documents/assets/action.svg) center center no-repeat transparent;
|
||||
background-size: 16px 16px;
|
||||
/* mask-image: ; */
|
||||
}
|
||||
|
||||
.panelBorder {
|
||||
border: 0.5px solid rgba(0, 0, 0, .05);
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useBoolean, useClickAway } from 'ahooks'
|
||||
import s from './style.module.css'
|
||||
import ModelIcon from '@/app/components/app/configuration/config-model/model-icon'
|
||||
import { Google, WebReader, Wikipedia } from '@/app/components/base/icons/src/public/plugins'
|
||||
import ConfigDetail from '@/app/components/explore/universal-chat/config-view/detail'
|
||||
|
||||
export type ISummaryProps = {
|
||||
modelId: string
|
||||
plugins: Record<string, boolean>
|
||||
dataSets: any[]
|
||||
}
|
||||
|
||||
const getColorInfo = (modelId: string) => {
|
||||
if (modelId === 'gpt-4')
|
||||
return s.gpt4
|
||||
|
||||
if (modelId === 'claude-2')
|
||||
return s.claude
|
||||
|
||||
return s.gpt3
|
||||
}
|
||||
|
||||
const getPlugIcon = (pluginId: string) => {
|
||||
const className = 'w-4 h-4'
|
||||
switch (pluginId) {
|
||||
case 'google_search':
|
||||
return <Google className={className} />
|
||||
case 'web_reader':
|
||||
return <WebReader className={className} />
|
||||
case 'wikipedia':
|
||||
return <Wikipedia className={className} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const Summary: FC<ISummaryProps> = ({
|
||||
modelId,
|
||||
plugins,
|
||||
dataSets,
|
||||
}) => {
|
||||
const pluginIds = Object.keys(plugins).filter(key => plugins[key])
|
||||
const [isShowConfig, { setFalse: hideConfig, toggle: toggleShowConfig }] = useBoolean(false)
|
||||
const configContentRef = React.useRef(null)
|
||||
|
||||
useClickAway(() => {
|
||||
hideConfig()
|
||||
}, configContentRef)
|
||||
return (
|
||||
<div ref={configContentRef} className='relative'>
|
||||
<div onClick={toggleShowConfig} className={cn(getColorInfo(modelId), 'flex items-center px-1 h-8 rounded-lg border cursor-pointer')}>
|
||||
<ModelIcon modelId={modelId} className='!w-6 !h-6' />
|
||||
<div className='ml-2 text-[13px] font-medium text-gray-900'>{modelId}</div>
|
||||
{
|
||||
pluginIds.length > 0 && (
|
||||
<div className='ml-1.5 flex items-center'>
|
||||
<div className='mr-1 h-3 w-[1px] bg-[#000] opacity-[0.05]'></div>
|
||||
<div className='flex space-x-1'>
|
||||
{pluginIds.map(pluginId => (
|
||||
<div
|
||||
key={pluginId}
|
||||
className={`flex items-center justify-center w-6 h-6 rounded-md ${s.border} bg-white`}
|
||||
>
|
||||
{getPlugIcon(pluginId)}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{isShowConfig && (
|
||||
<ConfigDetail
|
||||
modelId={modelId} plugins={plugins} dataSets={dataSets}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(Summary)
|
|
@ -0,0 +1,21 @@
|
|||
.border {
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.gpt3 {
|
||||
background: linear-gradient(0deg, #D3F8DF, #D3F8DF),
|
||||
linear-gradient(0deg, #EDFCF2, #EDFCF2);
|
||||
border: 1px solid rgba(211, 248, 223, 1)
|
||||
}
|
||||
|
||||
.gpt4 {
|
||||
background: linear-gradient(0deg, #EBE9FE, #EBE9FE),
|
||||
linear-gradient(0deg, #F4F3FF, #F4F3FF);
|
||||
border: 1px solid rgba(235, 233, 254, 1)
|
||||
}
|
||||
|
||||
.claude {
|
||||
background: linear-gradient(0deg, #F9EBDF, #F9EBDF),
|
||||
linear-gradient(0deg, #FCF3EB, #FCF3EB);
|
||||
border: 1px solid rgba(249, 235, 223, 1)
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { isEqual } from 'lodash-es'
|
||||
import produce from 'immer'
|
||||
import FeaturePanel from '@/app/components/app/configuration/base/feature-panel'
|
||||
import OperationBtn from '@/app/components/app/configuration/base/operation-btn'
|
||||
import CardItem from '@/app/components/app/configuration/dataset-config/card-item'
|
||||
import SelectDataSet from '@/app/components/app/configuration/dataset-config/select-dataset'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
|
||||
type Props = {
|
||||
readonly?: boolean
|
||||
dataSets: DataSet[]
|
||||
onChange?: (data: DataSet[]) => void
|
||||
}
|
||||
|
||||
const DatasetConfig: FC<Props> = ({
|
||||
readonly,
|
||||
dataSets,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const selectedIds = dataSets.map(item => item.id)
|
||||
|
||||
const hasData = dataSets.length > 0
|
||||
const [isShowSelectDataSet, { setTrue: showSelectDataSet, setFalse: hideSelectDataSet }] = useBoolean(false)
|
||||
const handleSelect = (data: DataSet[]) => {
|
||||
if (isEqual(data.map(item => item.id), dataSets.map(item => item.id))) {
|
||||
hideSelectDataSet()
|
||||
return
|
||||
}
|
||||
|
||||
if (data.find(item => !item.name)) { // has not loaded selected dataset
|
||||
const newSelected = produce(data, (draft) => {
|
||||
data.forEach((item, index) => {
|
||||
if (!item.name) { // not fetched database
|
||||
const newItem = dataSets.find(i => i.id === item.id)
|
||||
if (newItem)
|
||||
draft[index] = newItem
|
||||
}
|
||||
})
|
||||
})
|
||||
onChange?.(newSelected)
|
||||
}
|
||||
else {
|
||||
onChange?.(data)
|
||||
}
|
||||
hideSelectDataSet()
|
||||
}
|
||||
const onRemove = (id: string) => {
|
||||
onChange?.(dataSets.filter(item => item.id !== id))
|
||||
}
|
||||
|
||||
return (
|
||||
<FeaturePanel
|
||||
className='mt-3'
|
||||
title={t('appDebug.feature.dataSet.title')}
|
||||
headerRight={!readonly && <OperationBtn type="add" onClick={showSelectDataSet} />}
|
||||
hasHeaderBottomBorder={!hasData}
|
||||
>
|
||||
{hasData
|
||||
? (
|
||||
<div className='max-h-[220px] overflow-y-auto'>
|
||||
{dataSets.map(item => (
|
||||
<CardItem
|
||||
className="mb-2 !w-full"
|
||||
key={item.id}
|
||||
config={item}
|
||||
onRemove={onRemove}
|
||||
readonly={readonly}
|
||||
// TODO: readonly remove btn
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className='pt-2 pb-1 text-xs text-gray-500'>{t('appDebug.feature.dataSet.noData')}</div>
|
||||
)}
|
||||
|
||||
{isShowSelectDataSet && (
|
||||
<SelectDataSet
|
||||
isShow={isShowSelectDataSet}
|
||||
onClose={hideSelectDataSet}
|
||||
selectedIds={selectedIds}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
)}
|
||||
</FeaturePanel>
|
||||
)
|
||||
}
|
||||
export default React.memo(DatasetConfig)
|
51
web/app/components/explore/universal-chat/config/index.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import ModelConfig from './model-config'
|
||||
import DataConfig from './data-config'
|
||||
import PluginConfig from './plugins-config'
|
||||
|
||||
export type IConfigProps = {
|
||||
className?: string
|
||||
readonly?: boolean
|
||||
modelId: string
|
||||
onModelChange?: (modelId: string) => void
|
||||
plugins: Record<string, boolean>
|
||||
onPluginChange?: (key: string, value: boolean) => void
|
||||
dataSets: any[]
|
||||
onDataSetsChange?: (contexts: any[]) => void
|
||||
}
|
||||
|
||||
const Config: FC<IConfigProps> = ({
|
||||
className,
|
||||
readonly,
|
||||
modelId,
|
||||
onModelChange,
|
||||
plugins,
|
||||
onPluginChange,
|
||||
dataSets,
|
||||
onDataSetsChange,
|
||||
}) => {
|
||||
return (
|
||||
<div className={className}>
|
||||
<ModelConfig
|
||||
readonly={readonly}
|
||||
modelId={modelId}
|
||||
onChange={onModelChange}
|
||||
/>
|
||||
<PluginConfig
|
||||
readonly={readonly}
|
||||
config={plugins}
|
||||
onChange={onPluginChange}
|
||||
/>
|
||||
{(!readonly || (readonly && dataSets.length > 0)) && (
|
||||
<DataConfig
|
||||
readonly={readonly}
|
||||
dataSets={dataSets}
|
||||
onChange={onDataSetsChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Config)
|
|
@ -0,0 +1,61 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useBoolean, useClickAway } from 'ahooks'
|
||||
import { ChevronDownIcon } from '@heroicons/react/24/outline'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ModelIcon from '@/app/components/app/configuration/config-model/model-icon'
|
||||
import { UNIVERSAL_CHAT_MODEL_LIST as MODEL_LIST } from '@/config'
|
||||
import { Checked as CheckedIcon } from '@/app/components/base/icons/src/public/model'
|
||||
export type IModelConfigProps = {
|
||||
modelId: string
|
||||
onChange?: (model: string) => void
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
const ModelConfig: FC<IModelConfigProps> = ({
|
||||
modelId,
|
||||
onChange,
|
||||
readonly,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const currModel = MODEL_LIST.find(item => item.id === modelId)
|
||||
const [isShowOption, { setFalse: hideOption, toggle: toogleOption }] = useBoolean(false)
|
||||
const triggerRef = React.useRef(null)
|
||||
useClickAway(() => {
|
||||
hideOption()
|
||||
}, triggerRef)
|
||||
|
||||
return (
|
||||
<div className='flex items-center justify-between h-[52px] px-3 rounded-xl bg-gray-50'>
|
||||
<div className='text-sm font-semibold text-gray-800'>{t('explore.universalChat.model')}</div>
|
||||
<div className="relative z-10">
|
||||
<div
|
||||
ref={triggerRef}
|
||||
onClick={() => !readonly && toogleOption()}
|
||||
className={cn(
|
||||
readonly ? 'cursor-not-allowed' : 'cursor-pointer', 'flex items-center h-9 px-3 space-x-2 rounded-lg',
|
||||
isShowOption && 'bg-gray-100',
|
||||
)}>
|
||||
<ModelIcon modelId={currModel?.id as string} />
|
||||
<div className="text-sm gray-900">{currModel?.name}</div>
|
||||
{!readonly && <ChevronDownIcon className={cn(isShowOption && 'rotate-180', 'w-[14px] h-[14px] text-gray-500')} />}
|
||||
</div>
|
||||
{isShowOption && (
|
||||
<div className={cn('absolute top-10 right-0 bg-white rounded-lg shadow')}>
|
||||
{MODEL_LIST.map(item => (
|
||||
<div key={item.id} onClick={() => onChange?.(item.id)} className="w-[232px] flex items-center h-9 px-4 rounded-lg cursor-pointer hover:bg-gray-100">
|
||||
<ModelIcon className='shrink-0 mr-2' modelId={item?.id} />
|
||||
<div className="text-sm gray-900 whitespace-nowrap">{item.name}</div>
|
||||
{(item.id === currModel?.id) && <CheckedIcon className='absolute right-4' />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ModelConfig)
|
|
@ -0,0 +1,111 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Item from './item'
|
||||
import FeaturePanel from '@/app/components/app/configuration/base/feature-panel'
|
||||
import { Google, WebReader, Wikipedia } from '@/app/components/base/icons/src/public/plugins'
|
||||
import { getToolProviders } from '@/service/explore'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import AccountSetting from '@/app/components/header/account-setting'
|
||||
|
||||
export type IPluginsProps = {
|
||||
readonly?: boolean
|
||||
config: Record<string, boolean>
|
||||
onChange?: (key: string, value: boolean) => void
|
||||
}
|
||||
|
||||
const plugins = [
|
||||
{ key: 'google_search', icon: <Google /> },
|
||||
{ key: 'web_reader', icon: <WebReader /> },
|
||||
{ key: 'wikipedia', icon: <Wikipedia /> },
|
||||
]
|
||||
const Plugins: FC<IPluginsProps> = ({
|
||||
readonly,
|
||||
config,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isLoading, setIsLoading] = React.useState(!readonly)
|
||||
const [isSerpApiValid, setIsSerpApiValid] = React.useState(false)
|
||||
const checkSerpApiKey = async () => {
|
||||
if (readonly)
|
||||
return
|
||||
|
||||
const provides: any = await getToolProviders()
|
||||
const isSerpApiValid = !!provides.find((v: any) => v.tool_name === 'serpapi' && v.is_enabled)
|
||||
setIsSerpApiValid(isSerpApiValid)
|
||||
setIsLoading(false)
|
||||
}
|
||||
useEffect(() => {
|
||||
checkSerpApiKey()
|
||||
}, [])
|
||||
|
||||
const [showSetSerpAPIKeyModal, setShowSetAPIKeyModal] = React.useState(false)
|
||||
|
||||
const itemConfigs = plugins.map((plugin) => {
|
||||
const res: Record<string, any> = { ...plugin }
|
||||
const { key } = plugin
|
||||
res.name = t(`explore.universalChat.plugins.${key}.name`)
|
||||
if (key === 'web_reader')
|
||||
res.description = t(`explore.universalChat.plugins.${key}.description`)
|
||||
|
||||
if (key === 'google_search' && !isSerpApiValid && !readonly) {
|
||||
res.readonly = true
|
||||
res.more = (
|
||||
<div className='border-t border-[#FEF0C7] flex items-center h-[34px] pl-2 bg-[#FFFAEB] text-gray-700 text-xs '>
|
||||
<span className='whitespace-pre'>{t('explore.universalChat.plugins.google_search.more.left')}</span>
|
||||
<span className='cursor-pointer text-[#155EEF]' onClick={() => setShowSetAPIKeyModal(true)}>{t('explore.universalChat.plugins.google_search.more.link')}</span>
|
||||
<span className='whitespace-pre'>{t('explore.universalChat.plugins.google_search.more.right')}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return res
|
||||
})
|
||||
|
||||
const enabledPluginNum = Object.values(config).filter(v => v).length
|
||||
|
||||
return (
|
||||
<>
|
||||
<FeaturePanel
|
||||
className='mt-3'
|
||||
title={
|
||||
<div className='flex space-x-1'>
|
||||
<div>{t('explore.universalChat.plugins.name')}</div>
|
||||
<div className='text-[13px] font-normal text-gray-500'>({enabledPluginNum}/{plugins.length})</div>
|
||||
</div>}
|
||||
hasHeaderBottomBorder={false}
|
||||
>
|
||||
{isLoading
|
||||
? (
|
||||
<div className='flex items-center h-[166px]'>
|
||||
<Loading type='area' />
|
||||
</div>
|
||||
)
|
||||
: (<div className='space-y-2'>
|
||||
{itemConfigs.map(item => (
|
||||
<Item
|
||||
key={item.key}
|
||||
icon={item.icon}
|
||||
name={item.name}
|
||||
description={item.description}
|
||||
more={item.more}
|
||||
enabled={config[item.key]}
|
||||
onChange={enabled => onChange?.(item.key, enabled)}
|
||||
readonly={readonly || item.readonly}
|
||||
/>
|
||||
))}
|
||||
</div>)}
|
||||
</FeaturePanel>
|
||||
{
|
||||
showSetSerpAPIKeyModal && (
|
||||
<AccountSetting activeTab="plugin" onCancel={async () => {
|
||||
setShowSetAPIKeyModal(false)
|
||||
await checkSerpApiKey()
|
||||
}} />
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(Plugins)
|
|
@ -0,0 +1,3 @@
|
|||
.shadow {
|
||||
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import s from './item.module.css'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
|
||||
export type IItemProps = {
|
||||
icon: React.ReactNode
|
||||
name: string
|
||||
description?: string
|
||||
more?: React.ReactNode
|
||||
enabled: boolean
|
||||
onChange: (enabled: boolean) => void
|
||||
readonly?: boolean
|
||||
}
|
||||
|
||||
const Item: FC<IItemProps> = ({
|
||||
icon,
|
||||
name,
|
||||
description,
|
||||
more,
|
||||
enabled,
|
||||
onChange,
|
||||
readonly,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn('bg-white rounded-xl border border-gray-200 overflow-hidden', s.shadow)}>
|
||||
<div className='flex justify-between items-center min-h-[48px] px-2'>
|
||||
<div className='flex items-center space-x-2'>
|
||||
{icon}
|
||||
<div className='leading-[18px]'>
|
||||
<div className='text-[13px] font-medium text-gray-800'>{name}</div>
|
||||
{description && <div className='text-xs leading-[18px] text-gray-500'>{description}</div>}
|
||||
</div>
|
||||
</div>
|
||||
<Switch size='md' defaultValue={enabled} onChange={onChange} disabled={readonly} />
|
||||
</div>
|
||||
{more}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Item)
|
|
@ -0,0 +1,72 @@
|
|||
import { useState } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useGetState } from 'ahooks'
|
||||
import type { ConversationItem } from '@/models/share'
|
||||
|
||||
const storageConversationIdKey = 'conversationIdInfo'
|
||||
|
||||
type ConversationInfoType = Omit<ConversationItem, 'inputs' | 'id'>
|
||||
function useConversation() {
|
||||
const [conversationList, setConversationList] = useState<ConversationItem[]>([])
|
||||
const [pinnedConversationList, setPinnedConversationList] = useState<ConversationItem[]>([])
|
||||
const [currConversationId, doSetCurrConversationId, getCurrConversationId] = useGetState<string>('-1')
|
||||
// when set conversation id, we do not have set appId
|
||||
const setCurrConversationId = (id: string, appId: string, isSetToLocalStroge = true, newConversationName = '') => {
|
||||
doSetCurrConversationId(id)
|
||||
if (isSetToLocalStroge && id !== '-1') {
|
||||
// conversationIdInfo: {[appId1]: conversationId1, [appId2]: conversationId2}
|
||||
const conversationIdInfo = globalThis.localStorage?.getItem(storageConversationIdKey) ? JSON.parse(globalThis.localStorage?.getItem(storageConversationIdKey) || '') : {}
|
||||
conversationIdInfo[appId] = id
|
||||
globalThis.localStorage?.setItem(storageConversationIdKey, JSON.stringify(conversationIdInfo))
|
||||
}
|
||||
}
|
||||
|
||||
const getConversationIdFromStorage = (appId: string) => {
|
||||
const conversationIdInfo = globalThis.localStorage?.getItem(storageConversationIdKey) ? JSON.parse(globalThis.localStorage?.getItem(storageConversationIdKey) || '') : {}
|
||||
const id = conversationIdInfo[appId]
|
||||
return id
|
||||
}
|
||||
|
||||
const isNewConversation = currConversationId === '-1'
|
||||
// input can be updated by user
|
||||
const [newConversationInputs, setNewConversationInputs] = useState<Record<string, any> | null>(null)
|
||||
const resetNewConversationInputs = () => {
|
||||
if (!newConversationInputs)
|
||||
return
|
||||
setNewConversationInputs(produce(newConversationInputs, (draft) => {
|
||||
Object.keys(draft).forEach((key) => {
|
||||
draft[key] = ''
|
||||
})
|
||||
}))
|
||||
}
|
||||
const [existConversationInputs, setExistConversationInputs] = useState<Record<string, any> | null>(null)
|
||||
const currInputs = isNewConversation ? newConversationInputs : existConversationInputs
|
||||
const setCurrInputs = isNewConversation ? setNewConversationInputs : setExistConversationInputs
|
||||
|
||||
// info is muted
|
||||
const [newConversationInfo, setNewConversationInfo] = useState<ConversationInfoType | null>(null)
|
||||
const [existConversationInfo, setExistConversationInfo] = useState<ConversationInfoType | null>(null)
|
||||
const currConversationInfo = isNewConversation ? newConversationInfo : existConversationInfo
|
||||
|
||||
return {
|
||||
conversationList,
|
||||
setConversationList,
|
||||
pinnedConversationList,
|
||||
setPinnedConversationList,
|
||||
currConversationId,
|
||||
getCurrConversationId,
|
||||
setCurrConversationId,
|
||||
getConversationIdFromStorage,
|
||||
isNewConversation,
|
||||
currInputs,
|
||||
newConversationInputs,
|
||||
existConversationInputs,
|
||||
resetNewConversationInputs,
|
||||
setCurrInputs,
|
||||
currConversationInfo,
|
||||
setNewConversationInfo,
|
||||
setExistConversationInfo,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConversation
|
725
web/app/components/explore/universal-chat/index.tsx
Normal file
|
@ -0,0 +1,725 @@
|
|||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import produce from 'immer'
|
||||
import { useBoolean, useGetState } from 'ahooks'
|
||||
import AppUnavailable from '../../base/app-unavailable'
|
||||
import useConversation from './hooks/use-conversation'
|
||||
import s from './style.module.css'
|
||||
import Init from './init'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import Sidebar from '@/app/components/share/chat/sidebar'
|
||||
import {
|
||||
delConversation,
|
||||
fetchAppParams,
|
||||
fetchChatList,
|
||||
fetchConversations,
|
||||
fetchSuggestedQuestions,
|
||||
pinConversation,
|
||||
sendChatMessage,
|
||||
stopChatMessageResponding,
|
||||
unpinConversation,
|
||||
updateFeedback,
|
||||
} from '@/service/universal-chat'
|
||||
import type { ConversationItem, SiteInfo } from '@/models/share'
|
||||
import type { PromptConfig, SuggestedQuestionsAfterAnswerConfig } from '@/models/debug'
|
||||
import type { Feedbacktype, IChatItem } from '@/app/components/app/chat/type'
|
||||
import Chat from '@/app/components/app/chat'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
|
||||
import { userInputsFormToPromptVariables } from '@/utils/model-config'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import ConfigSummary from '@/app/components/explore/universal-chat/config-view/summary'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
import ItemOperation from '@/app/components/explore/item-operation'
|
||||
|
||||
const APP_ID = 'universal-chat'
|
||||
const DEFAULT_MODEL_ID = 'gpt-3.5-turbo' // gpt-4, claude-2
|
||||
const DEFAULT_PLUGIN = {
|
||||
google_search: false,
|
||||
web_reader: true,
|
||||
wikipedia: true,
|
||||
}
|
||||
export type IMainProps = {}
|
||||
|
||||
const Main: FC<IMainProps> = () => {
|
||||
const { t } = useTranslation()
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
|
||||
/*
|
||||
* app info
|
||||
*/
|
||||
const [appUnavailable, setAppUnavailable] = useState<boolean>(false)
|
||||
const [isUnknwonReason, setIsUnknwonReason] = useState<boolean>(false)
|
||||
const siteInfo: SiteInfo = (
|
||||
{
|
||||
title: 'universal Chatbot',
|
||||
icon: '',
|
||||
icon_background: '',
|
||||
description: '',
|
||||
default_language: 'en', // TODO
|
||||
prompt_public: true,
|
||||
}
|
||||
)
|
||||
const [promptConfig, setPromptConfig] = useState<PromptConfig | null>(null)
|
||||
const [inited, setInited] = useState<boolean>(false)
|
||||
// in mobile, show sidebar by click button
|
||||
const [isShowSidebar, { setTrue: showSidebar, setFalse: hideSidebar }] = useBoolean(false)
|
||||
/*
|
||||
* conversation info
|
||||
*/
|
||||
const [allConversationList, setAllConversationList] = useState<ConversationItem[]>([])
|
||||
const [isClearConversationList, { setTrue: clearConversationListTrue, setFalse: clearConversationListFalse }] = useBoolean(false)
|
||||
const [isClearPinnedConversationList, { setTrue: clearPinnedConversationListTrue, setFalse: clearPinnedConversationListFalse }] = useBoolean(false)
|
||||
const {
|
||||
conversationList,
|
||||
setConversationList,
|
||||
pinnedConversationList,
|
||||
setPinnedConversationList,
|
||||
currConversationId,
|
||||
getCurrConversationId,
|
||||
setCurrConversationId,
|
||||
getConversationIdFromStorage,
|
||||
isNewConversation,
|
||||
currConversationInfo,
|
||||
currInputs,
|
||||
newConversationInputs,
|
||||
// existConversationInputs,
|
||||
resetNewConversationInputs,
|
||||
setCurrInputs,
|
||||
setNewConversationInfo,
|
||||
setExistConversationInfo,
|
||||
} = useConversation()
|
||||
const [hasMore, setHasMore] = useState<boolean>(true)
|
||||
const [hasPinnedMore, setHasPinnedMore] = useState<boolean>(true)
|
||||
const onMoreLoaded = ({ data: conversations, has_more }: any) => {
|
||||
setHasMore(has_more)
|
||||
if (isClearConversationList) {
|
||||
setConversationList(conversations)
|
||||
clearConversationListFalse()
|
||||
}
|
||||
else {
|
||||
setConversationList([...conversationList, ...conversations])
|
||||
}
|
||||
}
|
||||
const onPinnedMoreLoaded = ({ data: conversations, has_more }: any) => {
|
||||
setHasPinnedMore(has_more)
|
||||
if (isClearPinnedConversationList) {
|
||||
setPinnedConversationList(conversations)
|
||||
clearPinnedConversationListFalse()
|
||||
}
|
||||
else {
|
||||
setPinnedConversationList([...pinnedConversationList, ...conversations])
|
||||
}
|
||||
}
|
||||
const [controlUpdateConversationList, setControlUpdateConversationList] = useState(0)
|
||||
const noticeUpdateList = () => {
|
||||
setHasMore(true)
|
||||
clearConversationListTrue()
|
||||
|
||||
setHasPinnedMore(true)
|
||||
clearPinnedConversationListTrue()
|
||||
|
||||
setControlUpdateConversationList(Date.now())
|
||||
}
|
||||
const handlePin = async (id: string) => {
|
||||
await pinConversation(id)
|
||||
setControlItemOpHide(Date.now())
|
||||
notify({ type: 'success', message: t('common.api.success') })
|
||||
noticeUpdateList()
|
||||
}
|
||||
|
||||
const handleUnpin = async (id: string) => {
|
||||
await unpinConversation(id)
|
||||
setControlItemOpHide(Date.now())
|
||||
notify({ type: 'success', message: t('common.api.success') })
|
||||
noticeUpdateList()
|
||||
}
|
||||
const [isShowConfirm, { setTrue: showConfirm, setFalse: hideConfirm }] = useBoolean(false)
|
||||
const [toDeleteConversationId, setToDeleteConversationId] = useState('')
|
||||
const handleDelete = (id: string) => {
|
||||
setToDeleteConversationId(id)
|
||||
hideSidebar() // mobile
|
||||
showConfirm()
|
||||
}
|
||||
|
||||
const didDelete = async () => {
|
||||
await delConversation(toDeleteConversationId)
|
||||
setControlItemOpHide(Date.now())
|
||||
notify({ type: 'success', message: t('common.api.success') })
|
||||
hideConfirm()
|
||||
if (currConversationId === toDeleteConversationId)
|
||||
handleConversationIdChange('-1')
|
||||
|
||||
noticeUpdateList()
|
||||
}
|
||||
|
||||
const [suggestedQuestionsAfterAnswerConfig, setSuggestedQuestionsAfterAnswerConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null)
|
||||
const [speechToTextConfig, setSpeechToTextConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null)
|
||||
|
||||
const [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = useGetState(false)
|
||||
|
||||
const conversationName = currConversationInfo?.name || t('share.chat.newChatDefaultName') as string
|
||||
const conversationIntroduction = currConversationInfo?.introduction || ''
|
||||
|
||||
const handleConversationSwitch = async () => {
|
||||
if (!inited)
|
||||
return
|
||||
|
||||
// update inputs of current conversation
|
||||
let notSyncToStateIntroduction = ''
|
||||
let notSyncToStateInputs: Record<string, any> | undefined | null = {}
|
||||
// debugger
|
||||
if (!isNewConversation) {
|
||||
const item = allConversationList.find(item => item.id === currConversationId) as any
|
||||
notSyncToStateInputs = item?.inputs || {}
|
||||
// setCurrInputs(notSyncToStateInputs)
|
||||
notSyncToStateIntroduction = item?.introduction || ''
|
||||
setExistConversationInfo({
|
||||
name: item?.name || '',
|
||||
introduction: notSyncToStateIntroduction,
|
||||
})
|
||||
const modelConfig = item?.model_config
|
||||
if (modelConfig) {
|
||||
setModeId(modelConfig.model_id)
|
||||
const pluginConfig: Record<string, boolean> = {}
|
||||
const datasetIds: string[] = []
|
||||
modelConfig.agent_mode.tools.forEach((item: any) => {
|
||||
const pluginName = Object.keys(item)[0]
|
||||
if (pluginName === 'dataset')
|
||||
datasetIds.push(item.dataset.id)
|
||||
else
|
||||
pluginConfig[pluginName] = item[pluginName].enabled
|
||||
})
|
||||
setPlugins(pluginConfig)
|
||||
if (datasetIds.length > 0) {
|
||||
const { data } = await fetchDatasets({ url: '/datasets', params: { page: 1, ids: datasetIds } })
|
||||
setDateSets(data)
|
||||
}
|
||||
else {
|
||||
setDateSets([])
|
||||
}
|
||||
}
|
||||
else {
|
||||
configSetDefaultValue()
|
||||
}
|
||||
}
|
||||
else {
|
||||
configSetDefaultValue()
|
||||
notSyncToStateInputs = newConversationInputs
|
||||
setCurrInputs(notSyncToStateInputs)
|
||||
}
|
||||
|
||||
// update chat list of current conversation
|
||||
if (!isNewConversation && !conversationIdChangeBecauseOfNew) {
|
||||
fetchChatList(currConversationId).then((res: any) => {
|
||||
const { data } = res
|
||||
const newChatList: IChatItem[] = generateNewChatListWithOpenstatement(notSyncToStateIntroduction, notSyncToStateInputs)
|
||||
|
||||
data.forEach((item: any) => {
|
||||
newChatList.push({
|
||||
id: `question-${item.id}`,
|
||||
content: item.query,
|
||||
isAnswer: false,
|
||||
})
|
||||
newChatList.push({
|
||||
...item,
|
||||
id: item.id,
|
||||
content: item.answer,
|
||||
feedback: item.feedback,
|
||||
isAnswer: true,
|
||||
})
|
||||
})
|
||||
setChatList(newChatList)
|
||||
setErrorHappened(false)
|
||||
})
|
||||
}
|
||||
|
||||
if (isNewConversation) {
|
||||
setChatList(generateNewChatListWithOpenstatement())
|
||||
setErrorHappened(false)
|
||||
}
|
||||
|
||||
setControlFocus(Date.now())
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
handleConversationSwitch()
|
||||
}, [currConversationId, inited])
|
||||
|
||||
const handleConversationIdChange = (id: string) => {
|
||||
if (id === '-1') {
|
||||
createNewChat()
|
||||
setConversationIdChangeBecauseOfNew(true)
|
||||
}
|
||||
else {
|
||||
setConversationIdChangeBecauseOfNew(false)
|
||||
}
|
||||
// trigger handleConversationSwitch
|
||||
setCurrConversationId(id, APP_ID)
|
||||
setIsShowSuggestion(false)
|
||||
hideSidebar()
|
||||
}
|
||||
|
||||
/*
|
||||
* chat info. chat is under conversation.
|
||||
*/
|
||||
const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([])
|
||||
const chatListDomRef = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
// scroll to bottom
|
||||
if (chatListDomRef.current)
|
||||
chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight
|
||||
}, [chatList, currConversationId])
|
||||
|
||||
// user can not edit inputs if user had send message
|
||||
const createNewChat = async () => {
|
||||
// if new chat is already exist, do not create new chat
|
||||
abortController?.abort()
|
||||
setResponsingFalse()
|
||||
if (conversationList.some(item => item.id === '-1'))
|
||||
return
|
||||
|
||||
setConversationList(produce(conversationList, (draft) => {
|
||||
draft.unshift({
|
||||
id: '-1',
|
||||
name: t('share.chat.newChatDefaultName'),
|
||||
inputs: newConversationInputs,
|
||||
introduction: conversationIntroduction,
|
||||
})
|
||||
}))
|
||||
configSetDefaultValue()
|
||||
}
|
||||
|
||||
// sometime introduction is not applied to state
|
||||
const generateNewChatListWithOpenstatement = (introduction?: string, inputs?: Record<string, any> | null) => {
|
||||
let caculatedIntroduction = introduction || conversationIntroduction || ''
|
||||
const caculatedPromptVariables = inputs || currInputs || null
|
||||
if (caculatedIntroduction && caculatedPromptVariables)
|
||||
caculatedIntroduction = replaceStringWithValues(caculatedIntroduction, promptConfig?.prompt_variables || [], caculatedPromptVariables)
|
||||
|
||||
const openstatement = {
|
||||
id: `${Date.now()}`,
|
||||
content: caculatedIntroduction,
|
||||
isAnswer: true,
|
||||
feedbackDisabled: true,
|
||||
isOpeningStatement: true,
|
||||
}
|
||||
if (caculatedIntroduction)
|
||||
return [openstatement]
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
const fetchAllConversations = () => {
|
||||
return fetchConversations(undefined, undefined, 100)
|
||||
}
|
||||
|
||||
const fetchInitData = async () => {
|
||||
return Promise.all([fetchAllConversations(), fetchAppParams()])
|
||||
}
|
||||
|
||||
// init
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const [conversationData, appParams]: any = await fetchInitData()
|
||||
const prompt_template = ''
|
||||
// handle current conversation id
|
||||
const { data: allConversations } = conversationData as { data: ConversationItem[]; has_more: boolean }
|
||||
const _conversationId = getConversationIdFromStorage(APP_ID)
|
||||
const isNotNewConversation = allConversations.some(item => item.id === _conversationId)
|
||||
setAllConversationList(allConversations)
|
||||
// fetch new conversation info
|
||||
const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text }: any = appParams
|
||||
const prompt_variables = userInputsFormToPromptVariables(user_input_form)
|
||||
|
||||
setNewConversationInfo({
|
||||
name: t('share.chat.newChatDefaultName'),
|
||||
introduction,
|
||||
})
|
||||
setPromptConfig({
|
||||
prompt_template,
|
||||
prompt_variables,
|
||||
} as PromptConfig)
|
||||
setSuggestedQuestionsAfterAnswerConfig(suggested_questions_after_answer)
|
||||
setSpeechToTextConfig(speech_to_text)
|
||||
|
||||
if (isNotNewConversation)
|
||||
setCurrConversationId(_conversationId, APP_ID, false)
|
||||
|
||||
setInited(true)
|
||||
}
|
||||
catch (e: any) {
|
||||
if (e.status === 404) {
|
||||
setAppUnavailable(true)
|
||||
}
|
||||
else {
|
||||
setIsUnknwonReason(true)
|
||||
setAppUnavailable(true)
|
||||
}
|
||||
}
|
||||
})()
|
||||
}, [])
|
||||
|
||||
const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false)
|
||||
const [abortController, setAbortController] = useState<AbortController | null>(null)
|
||||
const { notify } = useContext(ToastContext)
|
||||
const logError = (message: string) => {
|
||||
notify({ type: 'error', message })
|
||||
}
|
||||
|
||||
const checkCanSend = () => {
|
||||
if (currConversationId !== '-1')
|
||||
return true
|
||||
|
||||
const prompt_variables = promptConfig?.prompt_variables
|
||||
const inputs = currInputs
|
||||
if (!inputs || !prompt_variables || prompt_variables?.length === 0)
|
||||
return true
|
||||
|
||||
let hasEmptyInput = false
|
||||
const requiredVars = prompt_variables?.filter(({ key, name, required }) => {
|
||||
const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
|
||||
return res
|
||||
}) || [] // compatible with old version
|
||||
requiredVars.forEach(({ key }) => {
|
||||
if (hasEmptyInput)
|
||||
return
|
||||
|
||||
if (!inputs?.[key])
|
||||
hasEmptyInput = true
|
||||
})
|
||||
|
||||
if (hasEmptyInput) {
|
||||
logError(t('appDebug.errorMessage.valueOfVarRequired'))
|
||||
return false
|
||||
}
|
||||
return !hasEmptyInput
|
||||
}
|
||||
|
||||
const [controlFocus, setControlFocus] = useState(0)
|
||||
const [isShowSuggestion, setIsShowSuggestion] = useState(false)
|
||||
const doShowSuggestion = isShowSuggestion && !isResponsing
|
||||
const [suggestQuestions, setSuggestQuestions] = useState<string[]>([])
|
||||
const [messageTaskId, setMessageTaskId] = useState('')
|
||||
const [hasStopResponded, setHasStopResponded, getHasStopResponded] = useGetState(false)
|
||||
const [errorHappened, setErrorHappened] = useState(false)
|
||||
const [isResponsingConIsCurrCon, setIsResponsingConCurrCon, getIsResponsingConIsCurrCon] = useGetState(true)
|
||||
const handleSend = async (message: string) => {
|
||||
if (isResponsing) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
|
||||
return
|
||||
}
|
||||
const formattedPlugins = Object.keys(plugins).map(key => ({
|
||||
[key]: {
|
||||
enabled: plugins[key],
|
||||
},
|
||||
}))
|
||||
const formattedDataSets = dataSets.map(({ id }) => {
|
||||
return {
|
||||
dataset: {
|
||||
enabled: true,
|
||||
id,
|
||||
},
|
||||
}
|
||||
})
|
||||
const data = {
|
||||
query: message,
|
||||
conversation_id: isNewConversation ? null : currConversationId,
|
||||
model: modelId,
|
||||
tools: [...formattedPlugins, ...formattedDataSets],
|
||||
}
|
||||
|
||||
// qustion
|
||||
const questionId = `question-${Date.now()}`
|
||||
const questionItem = {
|
||||
id: questionId,
|
||||
content: message,
|
||||
agent_thoughts: [],
|
||||
isAnswer: false,
|
||||
}
|
||||
|
||||
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
|
||||
const placeholderAnswerItem = {
|
||||
id: placeholderAnswerId,
|
||||
content: '',
|
||||
isAnswer: true,
|
||||
}
|
||||
|
||||
const newList = [...getChatList(), questionItem, placeholderAnswerItem]
|
||||
setChatList(newList)
|
||||
|
||||
// answer
|
||||
const responseItem: IChatItem = {
|
||||
id: `${Date.now()}`,
|
||||
content: '',
|
||||
agent_thoughts: [],
|
||||
isAnswer: true,
|
||||
}
|
||||
|
||||
const prevTempNewConversationId = getCurrConversationId() || '-1'
|
||||
let tempNewConversationId = prevTempNewConversationId
|
||||
|
||||
setHasStopResponded(false)
|
||||
setResponsingTrue()
|
||||
setErrorHappened(false)
|
||||
setIsShowSuggestion(false)
|
||||
setIsResponsingConCurrCon(true)
|
||||
|
||||
sendChatMessage(data, {
|
||||
getAbortController: (abortController) => {
|
||||
setAbortController(abortController)
|
||||
},
|
||||
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => {
|
||||
responseItem.content = responseItem.content + message
|
||||
responseItem.id = messageId
|
||||
if (isFirstMessage && newConversationId)
|
||||
tempNewConversationId = newConversationId
|
||||
|
||||
setMessageTaskId(taskId)
|
||||
// has switched to other conversation
|
||||
if (prevTempNewConversationId !== getCurrConversationId()) {
|
||||
setIsResponsingConCurrCon(false)
|
||||
return
|
||||
}
|
||||
|
||||
// closesure new list is outdated.
|
||||
const newListWithAnswer = produce(
|
||||
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
|
||||
(draft) => {
|
||||
if (!draft.find(item => item.id === questionId))
|
||||
draft.push({ ...questionItem } as any)
|
||||
|
||||
draft.push({ ...responseItem })
|
||||
})
|
||||
|
||||
setChatList(newListWithAnswer)
|
||||
},
|
||||
async onCompleted(hasError?: boolean) {
|
||||
if (hasError) {
|
||||
setResponsingFalse()
|
||||
return
|
||||
}
|
||||
|
||||
if (getConversationIdChangeBecauseOfNew()) {
|
||||
const { data: allConversations }: any = await fetchAllConversations()
|
||||
setAllConversationList(allConversations)
|
||||
noticeUpdateList()
|
||||
}
|
||||
setConversationIdChangeBecauseOfNew(false)
|
||||
resetNewConversationInputs()
|
||||
setCurrConversationId(tempNewConversationId, APP_ID, true)
|
||||
if (getIsResponsingConIsCurrCon() && suggestedQuestionsAfterAnswerConfig?.enabled && !getHasStopResponded()) {
|
||||
const { data }: any = await fetchSuggestedQuestions(responseItem.id)
|
||||
setSuggestQuestions(data)
|
||||
setIsShowSuggestion(true)
|
||||
}
|
||||
setResponsingFalse()
|
||||
},
|
||||
onThought(thought) {
|
||||
// thought finished then start to return message. Warning: use push agent_thoughts.push would caused problem when the thought is more then 2
|
||||
responseItem.id = thought.message_id;
|
||||
(responseItem as any).agent_thoughts = [...(responseItem as any).agent_thoughts, thought] // .push(thought)
|
||||
// has switched to other conversation
|
||||
|
||||
if (prevTempNewConversationId !== getCurrConversationId()) {
|
||||
setIsResponsingConCurrCon(false)
|
||||
return
|
||||
}
|
||||
const newListWithAnswer = produce(
|
||||
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
|
||||
(draft) => {
|
||||
if (!draft.find(item => item.id === questionId))
|
||||
draft.push({ ...questionItem })
|
||||
draft.push({ ...responseItem })
|
||||
})
|
||||
setChatList(newListWithAnswer)
|
||||
},
|
||||
onError() {
|
||||
setErrorHappened(true)
|
||||
// role back placeholder answer
|
||||
setChatList(produce(getChatList(), (draft) => {
|
||||
draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1)
|
||||
}))
|
||||
setResponsingFalse()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleFeedback = async (messageId: string, feedback: Feedbacktype) => {
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } })
|
||||
const newChatList = chatList.map((item) => {
|
||||
if (item.id === messageId) {
|
||||
return {
|
||||
...item,
|
||||
feedback,
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
setChatList(newChatList)
|
||||
notify({ type: 'success', message: t('common.api.success') })
|
||||
}
|
||||
|
||||
const renderSidebar = () => {
|
||||
if (!APP_ID || !promptConfig)
|
||||
return null
|
||||
return (
|
||||
<Sidebar
|
||||
list={conversationList}
|
||||
isClearConversationList={isClearConversationList}
|
||||
pinnedList={pinnedConversationList}
|
||||
isClearPinnedConversationList={isClearPinnedConversationList}
|
||||
onMoreLoaded={onMoreLoaded}
|
||||
onPinnedMoreLoaded={onPinnedMoreLoaded}
|
||||
isNoMore={!hasMore}
|
||||
isPinnedNoMore={!hasPinnedMore}
|
||||
onCurrentIdChange={handleConversationIdChange}
|
||||
currentId={currConversationId}
|
||||
copyRight={''}
|
||||
isInstalledApp={false}
|
||||
isUniversalChat
|
||||
installedAppId={''}
|
||||
siteInfo={siteInfo}
|
||||
onPin={handlePin}
|
||||
onUnpin={handleUnpin}
|
||||
controlUpdateList={controlUpdateConversationList}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const [modelId, setModeId] = useState(DEFAULT_MODEL_ID)
|
||||
// const currModel = MODEL_LIST.find(item => item.id === modelId)
|
||||
|
||||
const [plugins, setPlugins] = useState<Record<string, boolean>>(DEFAULT_PLUGIN)
|
||||
const handlePluginsChange = (key: string, value: boolean) => {
|
||||
setPlugins({
|
||||
...plugins,
|
||||
[key]: value,
|
||||
})
|
||||
}
|
||||
const [dataSets, setDateSets] = useState<DataSet[]>([])
|
||||
const configSetDefaultValue = () => {
|
||||
setModeId(DEFAULT_MODEL_ID)
|
||||
setPlugins(DEFAULT_PLUGIN)
|
||||
setDateSets([])
|
||||
}
|
||||
const isCurrConversationPinned = !!pinnedConversationList.find(item => item.id === currConversationId)
|
||||
const [controlItemOpHide, setControlItemOpHide] = useState(0)
|
||||
if (appUnavailable)
|
||||
return <AppUnavailable isUnknwonReason={isUnknwonReason} />
|
||||
|
||||
if (!promptConfig)
|
||||
return <Loading type='app' />
|
||||
|
||||
return (
|
||||
<div className='bg-gray-100'>
|
||||
<div
|
||||
className={cn(
|
||||
'flex rounded-t-2xl bg-white overflow-hidden rounded-b-2xl',
|
||||
)}
|
||||
style={{
|
||||
boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)',
|
||||
}}
|
||||
>
|
||||
{/* sidebar */}
|
||||
{!isMobile && renderSidebar()}
|
||||
{isMobile && isShowSidebar && (
|
||||
<div className='fixed inset-0 z-50'
|
||||
style={{ backgroundColor: 'rgba(35, 56, 118, 0.2)' }}
|
||||
onClick={hideSidebar}
|
||||
>
|
||||
<div className='inline-block' onClick={e => e.stopPropagation()}>
|
||||
{renderSidebar()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* main */}
|
||||
<div className={cn(
|
||||
s.installedApp,
|
||||
'flex-grow flex flex-col overflow-y-auto',
|
||||
)
|
||||
}>
|
||||
{(!isNewConversation || isResponsing || errorHappened) && (
|
||||
<div className='mb-5 antialiased font-sans shrink-0 relative mobile:min-h-[48px] tablet:min-h-[64px]'>
|
||||
<div className='absolute z-10 top-0 left-0 right-0 flex items-center justify-between border-b border-gray-100 mobile:h-12 tablet:h-16 px-8 bg-white'>
|
||||
<div className='text-gray-900'>{conversationName}</div>
|
||||
<div className='flex items-center shrink-0 ml-2 space-x-2'>
|
||||
<ConfigSummary
|
||||
modelId={modelId}
|
||||
plugins={plugins}
|
||||
dataSets={dataSets}
|
||||
/>
|
||||
<div className={cn('flex w-8 h-8 justify-center items-center shrink-0 rounded-lg border border-gray-200')} onClick={e => e.stopPropagation()}>
|
||||
<ItemOperation
|
||||
key={controlItemOpHide}
|
||||
className='!w-8 !h-8'
|
||||
isPinned={isCurrConversationPinned}
|
||||
togglePin={() => isCurrConversationPinned ? handleUnpin(currConversationId) : handlePin(currConversationId)}
|
||||
isShowDelete
|
||||
onDelete={() => handleDelete(currConversationId)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={cn(doShowSuggestion ? 'pb-[140px]' : (isResponsing ? 'pb-[113px]' : 'pb-[76px]'), 'relative grow h-[200px] pc:w-[794px] max-w-full mobile:w-full mx-auto mb-3.5 overflow-hidden')}>
|
||||
<div className={cn('pc:w-[794px] max-w-full mobile:w-full mx-auto h-full overflow-y-auto')} ref={chatListDomRef}>
|
||||
<Chat
|
||||
isShowConfigElem={isNewConversation && chatList.length === 0}
|
||||
configElem={<Init
|
||||
modelId={modelId}
|
||||
onModelChange={setModeId}
|
||||
plugins={plugins}
|
||||
onPluginChange={handlePluginsChange}
|
||||
dataSets={dataSets}
|
||||
onDataSetsChange={setDateSets}
|
||||
/>}
|
||||
chatList={chatList}
|
||||
onSend={handleSend}
|
||||
isHideFeedbackEdit
|
||||
onFeedback={handleFeedback}
|
||||
isResponsing={isResponsing}
|
||||
canStopResponsing={!!messageTaskId && isResponsingConIsCurrCon}
|
||||
abortResponsing={async () => {
|
||||
await stopChatMessageResponding(messageTaskId)
|
||||
setHasStopResponded(true)
|
||||
setResponsingFalse()
|
||||
}}
|
||||
checkCanSend={checkCanSend}
|
||||
controlFocus={controlFocus}
|
||||
isShowSuggestion={doShowSuggestion}
|
||||
suggestionList={suggestQuestions}
|
||||
isShowSpeechToText={speechToTextConfig?.enabled}
|
||||
dataSets={dataSets}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isShowConfirm && (
|
||||
<Confirm
|
||||
title={t('share.chat.deleteConversation.title')}
|
||||
content={t('share.chat.deleteConversation.content')}
|
||||
isShow={isShowConfirm}
|
||||
onClose={hideConfirm}
|
||||
onConfirm={didDelete}
|
||||
onCancel={hideConfirm}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Main)
|
43
web/app/components/explore/universal-chat/init/index.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import type { IConfigProps } from '../config'
|
||||
import Config from '../config'
|
||||
import s from './style.module.css'
|
||||
|
||||
const Line = (
|
||||
<svg width="720" height="1" viewBox="0 0 720 1" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line y1="0.5" x2="720" y2="0.5" stroke="url(#paint0_linear_6845_53470)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_6845_53470" x1="0" y1="1" x2="720" y2="1" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#F2F4F7" stopOpacity="0"/>
|
||||
<stop offset="0.491667" stopColor="#F2F4F7"/>
|
||||
<stop offset="1" stopColor="#F2F4F7" stopOpacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const Init: FC<IConfigProps> = ({
|
||||
...configProps
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='h-full flex items-center'>
|
||||
<div>
|
||||
<div className='w-[480px] mx-auto text-center'>
|
||||
<div className={cn(s.textGradient, 'mb-2 leading-[32px] font-semibold text-[24px]')}>{t('explore.universalChat.welcome')}</div>
|
||||
<div className='mb-2 font-normal text-sm text-gray-500'>{t('explore.universalChat.welcomeDescribe')}</div>
|
||||
</div>
|
||||
<div className='flex mb-2 mx-auto h-8 items-center'>
|
||||
{Line}
|
||||
</div>
|
||||
<Config className='w-[480px] mx-auto' {...configProps} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Init)
|
|
@ -0,0 +1,9 @@
|
|||
.textGradient {
|
||||
background: linear-gradient(to right, rgba(16, 74, 225, 1) 0, rgba(0, 152, 238, 1) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-fill-color: transparent;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.installedApp {
|
||||
height: calc(100vh - 74px);
|
||||
}
|
|
@ -9,11 +9,12 @@ import MembersPage from './members-page'
|
|||
import IntegrationsPage from './Integrations-page'
|
||||
import LanguagePage from './language-page'
|
||||
import ProviderPage from './provider-page'
|
||||
import PluginPage from './plugin-page'
|
||||
import DataSourcePage from './data-source-page'
|
||||
import s from './index.module.css'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { Database03 } from '@/app/components/base/icons/src/vender/line/development'
|
||||
import { Database03 as Database03Solid } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { Database03, PuzzlePiece01 } from '@/app/components/base/icons/src/vender/line/development'
|
||||
import { Database03 as Database03Solid, PuzzlePiece01 as PuzzlePiece01Solid } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
|
||||
const iconClassName = `
|
||||
w-4 h-4 ml-3 mr-2
|
||||
|
@ -80,6 +81,12 @@ export default function AccountSetting({
|
|||
icon: <Database03 className={iconClassName} />,
|
||||
activeIcon: <Database03Solid className={iconClassName} />,
|
||||
},
|
||||
{
|
||||
key: 'plugin',
|
||||
name: t('common.settings.plugin'),
|
||||
icon: <PuzzlePiece01 className={iconClassName} />,
|
||||
activeIcon: <PuzzlePiece01Solid className={iconClassName} />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
@ -148,6 +155,7 @@ export default function AccountSetting({
|
|||
{activeMenu === 'language' && <LanguagePage />}
|
||||
{activeMenu === 'provider' && <ProviderPage />}
|
||||
{activeMenu === 'data-source' && <DataSourcePage />}
|
||||
{activeMenu === 'plugin' && <PluginPage />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
import type { ChangeEvent } from 'react'
|
||||
import {
|
||||
ValidatedErrorIcon,
|
||||
ValidatedErrorMessage,
|
||||
ValidatedSuccessIcon,
|
||||
ValidatingTip,
|
||||
} from './ValidateStatus'
|
||||
import { ValidatedStatus } from './declarations'
|
||||
import type { ValidatedStatusState } from './declarations'
|
||||
|
||||
type KeyInputProps = {
|
||||
value?: string
|
||||
name: string
|
||||
placeholder: string
|
||||
className?: string
|
||||
onChange: (v: string) => void
|
||||
onFocus?: () => void
|
||||
validating: boolean
|
||||
validatedStatusState: ValidatedStatusState
|
||||
}
|
||||
|
||||
const KeyInput = ({
|
||||
value,
|
||||
name,
|
||||
placeholder,
|
||||
className,
|
||||
onChange,
|
||||
onFocus,
|
||||
validating,
|
||||
validatedStatusState,
|
||||
}: KeyInputProps) => {
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const inputValue = e.target.value
|
||||
onChange(inputValue)
|
||||
}
|
||||
|
||||
const getValidatedIcon = () => {
|
||||
if (validatedStatusState.status === ValidatedStatus.Error || validatedStatusState.status === ValidatedStatus.Exceed)
|
||||
return <ValidatedErrorIcon />
|
||||
|
||||
if (validatedStatusState.status === ValidatedStatus.Success)
|
||||
return <ValidatedSuccessIcon />
|
||||
}
|
||||
const getValidatedTip = () => {
|
||||
if (validating)
|
||||
return <ValidatingTip />
|
||||
|
||||
if (validatedStatusState.status === ValidatedStatus.Error)
|
||||
return <ValidatedErrorMessage errorMessage={validatedStatusState.message ?? ''} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="mb-2 text-[13px] font-medium text-gray-800">{name}</div>
|
||||
<div className='
|
||||
flex items-center px-3 bg-white rounded-lg
|
||||
shadow-[0_1px_2px_rgba(16,24,40,0.05)]
|
||||
'>
|
||||
<input
|
||||
className='
|
||||
w-full py-[9px] mr-2
|
||||
text-xs font-medium text-gray-700 leading-[18px]
|
||||
appearance-none outline-none bg-transparent
|
||||
'
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={handleChange}
|
||||
onFocus={onFocus}
|
||||
/>
|
||||
{getValidatedIcon()}
|
||||
</div>
|
||||
{getValidatedTip()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default KeyInput
|
|
@ -0,0 +1,85 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import Indicator from '../../indicator'
|
||||
import type { Status } from './declarations'
|
||||
|
||||
type OperateProps = {
|
||||
isOpen: boolean
|
||||
status: Status
|
||||
onCancel: () => void
|
||||
onSave: () => void
|
||||
onAdd: () => void
|
||||
onEdit: () => void
|
||||
}
|
||||
|
||||
const Operate = ({
|
||||
isOpen,
|
||||
status,
|
||||
onCancel,
|
||||
onSave,
|
||||
onAdd,
|
||||
onEdit,
|
||||
}: OperateProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (isOpen) {
|
||||
return (
|
||||
<div className='flex items-center'>
|
||||
<div className='
|
||||
flex items-center
|
||||
mr-[5px] px-3 h-7 rounded-md cursor-pointer
|
||||
text-xs font-medium text-gray-700
|
||||
' onClick={onCancel} >
|
||||
{t('common.operation.cancel')}
|
||||
</div>
|
||||
<div className='
|
||||
flex items-center
|
||||
px-3 h-7 rounded-md cursor-pointer bg-primary-700
|
||||
text-xs font-medium text-white
|
||||
' onClick={onSave}>
|
||||
{t('common.operation.save')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (status === 'add') {
|
||||
return (
|
||||
<div className='
|
||||
px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer
|
||||
text-xs font-medium text-gray-700 flex items-center
|
||||
' onClick={onAdd}>
|
||||
{t('common.provider.addKey')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (status === 'fail' || status === 'success') {
|
||||
return (
|
||||
<div className='flex items-center'>
|
||||
{
|
||||
status === 'fail' && (
|
||||
<div className='flex items-center mr-4'>
|
||||
<div className='text-xs text-[#D92D20]'>{t('common.provider.invalidApiKey')}</div>
|
||||
<Indicator color='red' className='ml-2' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
status === 'success' && (
|
||||
<Indicator color='green' className='mr-4' />
|
||||
)
|
||||
}
|
||||
<div className='
|
||||
px-3 h-[28px] bg-white border border-gray-200 rounded-md cursor-pointer
|
||||
text-xs font-medium text-gray-700 flex items-center
|
||||
' onClick={onEdit}>
|
||||
{t('common.provider.editKey')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default Operate
|
|
@ -0,0 +1,30 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { AlertCircle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
|
||||
import { CheckCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
|
||||
export const ValidatedErrorIcon = () => {
|
||||
return <AlertCircle className='w-4 h-4 text-[#D92D20]' />
|
||||
}
|
||||
|
||||
export const ValidatedSuccessIcon = () => {
|
||||
return <CheckCircle className='w-4 h-4 text-[#039855]' />
|
||||
}
|
||||
|
||||
export const ValidatingTip = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className={'mt-2 text-primary-600 text-xs font-normal'}>
|
||||
{t('common.provider.validating')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ValidatedErrorMessage = ({ errorMessage }: { errorMessage: string }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={'mt-2 text-[#D92D20] text-xs font-normal'}>
|
||||
{t('common.provider.validatedError')}{errorMessage}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import type { Dispatch, SetStateAction } from 'react'
|
||||
|
||||
export enum ValidatedStatus {
|
||||
Success = 'success',
|
||||
Error = 'error',
|
||||
Exceed = 'exceed',
|
||||
}
|
||||
|
||||
export type ValidatedStatusState = {
|
||||
status?: ValidatedStatus
|
||||
message?: string
|
||||
}
|
||||
|
||||
export type Status = 'add' | 'fail' | 'success'
|
||||
|
||||
export type ValidateValue = Record<string, string | undefined>
|
||||
|
||||
export type ValidateCallback = {
|
||||
before: (v?: ValidateValue) => boolean | undefined
|
||||
run?: (v?: ValidateValue) => Promise<ValidatedStatusState>
|
||||
}
|
||||
|
||||
export type Form = {
|
||||
key: string
|
||||
title: string
|
||||
placeholder: string
|
||||
value?: string
|
||||
validate?: ValidateCallback
|
||||
handleFocus?: (v: ValidateValue, dispatch: Dispatch<SetStateAction<ValidateValue>>) => void
|
||||
}
|
||||
|
||||
export type KeyFrom = {
|
||||
text: string
|
||||
link: string
|
||||
}
|
||||
|
||||
export type KeyValidatorProps = {
|
||||
type: string
|
||||
title: React.ReactNode
|
||||
status: Status
|
||||
forms: Form[]
|
||||
keyFrom: KeyFrom
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import { useState } from 'react'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import type { DebouncedFunc } from 'lodash-es'
|
||||
import { ValidatedStatus } from './declarations'
|
||||
import type { ValidateCallback, ValidateValue, ValidatedStatusState } from './declarations'
|
||||
|
||||
export const useValidate: (value: ValidateValue) => [DebouncedFunc<(validateCallback: ValidateCallback) => Promise<void>>, boolean, ValidatedStatusState] = (value) => {
|
||||
const [validating, setValidating] = useState(false)
|
||||
const [validatedStatus, setValidatedStatus] = useState<ValidatedStatusState>({})
|
||||
|
||||
const { run } = useDebounceFn(async (validateCallback: ValidateCallback) => {
|
||||
if (!validateCallback.before(value)) {
|
||||
setValidating(false)
|
||||
setValidatedStatus({})
|
||||
return
|
||||
}
|
||||
|
||||
setValidating(true)
|
||||
|
||||
if (validateCallback.run) {
|
||||
const res = await validateCallback?.run(value)
|
||||
setValidatedStatus(
|
||||
res.status === 'success'
|
||||
? { status: ValidatedStatus.Success }
|
||||
: { status: ValidatedStatus.Error, message: res.message })
|
||||
|
||||
setValidating(false)
|
||||
}
|
||||
}, { wait: 500 })
|
||||
|
||||
return [run, validating, validatedStatus]
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
import { useState } from 'react'
|
||||
import Operate from './Operate'
|
||||
import KeyInput from './KeyInput'
|
||||
import { useValidate } from './hooks'
|
||||
import type { Form, KeyFrom, Status, ValidateValue } from './declarations'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
export type KeyValidatorProps = {
|
||||
type: string
|
||||
title: React.ReactNode
|
||||
status: Status
|
||||
forms: Form[]
|
||||
keyFrom: KeyFrom
|
||||
onSave: (v: ValidateValue) => Promise<boolean | undefined>
|
||||
}
|
||||
|
||||
const KeyValidator = ({
|
||||
type,
|
||||
title,
|
||||
status,
|
||||
forms,
|
||||
keyFrom,
|
||||
onSave,
|
||||
}: KeyValidatorProps) => {
|
||||
const triggerKey = `plugins/${type}`
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const prevValue = forms.reduce((prev: ValidateValue, next: Form) => {
|
||||
prev[next.key] = next.value
|
||||
return prev
|
||||
}, {})
|
||||
const [value, setValue] = useState(prevValue)
|
||||
const [validate, validating, validatedStatusState] = useValidate(value)
|
||||
|
||||
eventEmitter?.useSubscription((v) => {
|
||||
if (v !== triggerKey) {
|
||||
setIsOpen(false)
|
||||
setValue(prevValue)
|
||||
validate({ before: () => false })
|
||||
}
|
||||
})
|
||||
|
||||
const handleCancel = () => {
|
||||
eventEmitter?.emit('')
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (await onSave(value))
|
||||
eventEmitter?.emit('')
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
setIsOpen(true)
|
||||
eventEmitter?.emit(triggerKey)
|
||||
}
|
||||
|
||||
const handleEdit = () => {
|
||||
setIsOpen(true)
|
||||
eventEmitter?.emit(triggerKey)
|
||||
}
|
||||
|
||||
const handleChange = (form: Form, val: string) => {
|
||||
setValue({ ...value, [form.key]: val })
|
||||
|
||||
if (form.validate)
|
||||
validate(form.validate)
|
||||
}
|
||||
|
||||
const handleFocus = (form: Form) => {
|
||||
if (form.handleFocus)
|
||||
form.handleFocus(value, setValue)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mb-2 border-[0.5px] border-gray-200 bg-gray-50 rounded-md'>
|
||||
<div className={
|
||||
`flex items-center justify-between px-4 h-[52px] cursor-pointer ${isOpen && 'border-b-[0.5px] border-b-gray-200'}`
|
||||
}>
|
||||
{title}
|
||||
<Operate
|
||||
isOpen={isOpen}
|
||||
status={status}
|
||||
onCancel={handleCancel}
|
||||
onSave={handleSave}
|
||||
onAdd={handleAdd}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
isOpen && (
|
||||
<div className='px-4 py-3'>
|
||||
{
|
||||
forms.map(form => (
|
||||
<KeyInput
|
||||
key={form.key}
|
||||
className='mb-4'
|
||||
name={form.title}
|
||||
placeholder={form.placeholder}
|
||||
value={value[form.key] || ''}
|
||||
onChange={v => handleChange(form, v)}
|
||||
onFocus={() => handleFocus(form)}
|
||||
validating={validating}
|
||||
validatedStatusState={validatedStatusState}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<a className="flex items-center text-xs cursor-pointer text-primary-600" href={keyFrom.link} target={'_blank'}>
|
||||
{keyFrom.text}
|
||||
<LinkExternal02 className='w-3 h-3 ml-1 text-primary-600' />
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default KeyValidator
|
|
@ -0,0 +1,77 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import Image from 'next/image'
|
||||
import SerpapiLogo from '../../assets/serpapi.png'
|
||||
import KeyValidator from '../key-validator'
|
||||
import type { Form, ValidateValue } from '../key-validator/declarations'
|
||||
import { updatePluginKey, validatePluginKey } from './utils'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import type { PluginProvider } from '@/models/common'
|
||||
|
||||
type SerpapiPluginProps = {
|
||||
plugin: PluginProvider
|
||||
onUpdate: () => void
|
||||
}
|
||||
const SerpapiPlugin = ({
|
||||
plugin,
|
||||
onUpdate,
|
||||
}: SerpapiPluginProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
|
||||
const forms: Form[] = [{
|
||||
key: 'api_key',
|
||||
title: t('common.plugin.serpapi.apiKey'),
|
||||
placeholder: t('common.plugin.serpapi.apiKeyPlaceholder'),
|
||||
value: plugin.credentials?.api_key,
|
||||
validate: {
|
||||
before: (v) => {
|
||||
if (v?.api_key)
|
||||
return true
|
||||
},
|
||||
run: async (v) => {
|
||||
return validatePluginKey('serpapi', {
|
||||
credentials: {
|
||||
api_key: v?.api_key,
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
handleFocus: (v, dispatch) => {
|
||||
if (v.api_key === plugin.credentials?.api_key)
|
||||
dispatch({ ...v, api_key: '' })
|
||||
},
|
||||
}]
|
||||
|
||||
const handleSave = async (v: ValidateValue) => {
|
||||
if (!v?.api_key || v?.api_key === plugin.credentials?.api_key)
|
||||
return
|
||||
|
||||
const res = await updatePluginKey('serpapi', {
|
||||
credentials: {
|
||||
api_key: v?.api_key,
|
||||
},
|
||||
})
|
||||
|
||||
if (res.status === 'success') {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
onUpdate()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyValidator
|
||||
type='serpapi'
|
||||
title={<Image alt='serpapi logo' src={SerpapiLogo} width={64} />}
|
||||
status={plugin.credentials?.api_key ? 'success' : 'add'}
|
||||
forms={forms}
|
||||
keyFrom={{
|
||||
text: t('common.plugin.serpapi.keyFrom'),
|
||||
link: 'https://serpapi.com/manage-api-key',
|
||||
}}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default SerpapiPlugin
|
|
@ -0,0 +1,38 @@
|
|||
import useSWR from 'swr'
|
||||
import { LockClosedIcon } from '@heroicons/react/24/solid'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Link from 'next/link'
|
||||
import SerpapiPlugin from './SerpapiPlugin'
|
||||
import { fetchPluginProviders } from '@/service/common'
|
||||
import type { PluginProvider } from '@/models/common'
|
||||
|
||||
const PluginPage = () => {
|
||||
const { t } = useTranslation()
|
||||
const { data: plugins, mutate } = useSWR('/workspaces/current/tool-providers', fetchPluginProviders)
|
||||
|
||||
const Plugin_MAP: Record<string, any> = {
|
||||
serpapi: (plugin: PluginProvider) => <SerpapiPlugin key='serpapi' plugin={plugin} onUpdate={() => mutate()} />,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='pb-7'>
|
||||
<div>
|
||||
{plugins?.map(plugin => Plugin_MAP[plugin.tool_name](plugin))}
|
||||
</div>
|
||||
<div className='fixed bottom-0 w-[472px] h-[42px] flex items-center bg-white text-xs text-gray-500'>
|
||||
<LockClosedIcon className='w-3 h-3 mr-1' />
|
||||
{t('common.provider.encrypted.front')}
|
||||
<Link
|
||||
className='text-primary-600 mx-1'
|
||||
target={'_blank'}
|
||||
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
|
||||
>
|
||||
PKCS1_OAEP
|
||||
</Link>
|
||||
{t('common.provider.encrypted.back')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PluginPage
|
|
@ -0,0 +1,34 @@
|
|||
import { ValidatedStatus } from '../key-validator/declarations'
|
||||
import { updatePluginProviderAIKey, validatePluginProviderKey } from '@/service/common'
|
||||
|
||||
export const validatePluginKey = async (pluginType: string, body: any) => {
|
||||
try {
|
||||
const res = await validatePluginProviderKey({
|
||||
url: `/workspaces/current/tool-providers/${pluginType}/credentials-validate`,
|
||||
body,
|
||||
})
|
||||
if (res.result === 'success')
|
||||
return Promise.resolve({ status: ValidatedStatus.Success })
|
||||
else
|
||||
return Promise.resolve({ status: ValidatedStatus.Error, message: res.error })
|
||||
}
|
||||
catch (e: any) {
|
||||
return Promise.resolve({ status: ValidatedStatus.Error, message: e.message })
|
||||
}
|
||||
}
|
||||
|
||||
export const updatePluginKey = async (pluginType: string, body: any) => {
|
||||
try {
|
||||
const res = await updatePluginProviderAIKey({
|
||||
url: `/workspaces/current/tool-providers/${pluginType}/credentials`,
|
||||
body,
|
||||
})
|
||||
if (res.result === 'success')
|
||||
return Promise.resolve({ status: ValidatedStatus.Success })
|
||||
else
|
||||
return Promise.resolve({ status: ValidatedStatus.Error, message: res.error })
|
||||
}
|
||||
catch (e: any) {
|
||||
return Promise.resolve({ status: ValidatedStatus.Error, message: e.message })
|
||||
}
|
||||
}
|