feat: move anwser related code

This commit is contained in:
Joel
2024-01-26 18:06:04 +08:00
parent 3de8abdd7a
commit ed839618a7
19 changed files with 768 additions and 15 deletions

View File

@ -6,10 +6,13 @@ import { useTranslation } from 'react-i18next'
import LoadingAnim from '../loading-anim'
import type { FeedbackFunc, IChatItem } from '../type'
import s from '../style.module.css'
import ImageGallery from '../../base/image-gallery'
import Thought from '../thought'
import { randomString } from '@/utils/string'
import type { MessageRating } from '@/types/app'
import type { MessageRating, VisionFile } from '@/types/app'
import Tooltip from '@/app/components/base/tooltip'
import { Markdown } from '@/app/components/base/markdown'
import type { Emoji } from '@/types/tools'
const OperationBtn = ({ innerContent, onClick, className }: { innerContent: React.ReactNode; onClick?: () => void; className?: string }) => (
<div
@ -55,11 +58,20 @@ type IAnswerProps = {
feedbackDisabled: boolean
onFeedback?: FeedbackFunc
isResponsing?: boolean
allToolIcons?: Record<string, string | Emoji>
}
// The component needs to maintain its own state to control whether to display input component
const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, onFeedback, isResponsing }) => {
const { id, content, feedback } = item
const Answer: FC<IAnswerProps> = ({
item,
feedbackDisabled = false,
onFeedback,
isResponsing,
allToolIcons,
}) => {
const { id, content, feedback, agent_thoughts } = item
const isAgentMode = !!agent_thoughts && agent_thoughts.length > 0
const { t } = useTranslation()
/**
@ -121,6 +133,37 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, onFeedback,
)
}
const getImgs = (list?: VisionFile[]) => {
if (!list)
return []
return list.filter(file => file.type === 'image' && file.belongs_to === 'assistant')
}
const agentModeAnswer = (
<div>
{agent_thoughts?.map((item, index) => (
<div key={index}>
{item.thought && (
<Markdown content={item.thought} />
)}
{/* {item.tool} */}
{/* perhaps not use tool */}
{!!item.tool && (
<Thought
thought={item}
allToolIcons={allToolIcons || {}}
isFinished={!!item.observation || !isResponsing}
/>
)}
{getImgs(item.message_files).length > 0 && (
<ImageGallery srcs={getImgs(item.message_files).map(item => item.url)} />
)}
</div>
))}
</div>
)
return (
<div key={id}>
<div className='flex items-start'>
@ -134,21 +177,17 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, onFeedback,
<div className={`${s.answerWrap}`}>
<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('app.chat.openingStatementTitle')}</div>
</div>
)}
{(isResponsing && !content)
{(isResponsing && (isAgentMode ? (!content && (agent_thoughts || []).filter(item => !!item.thought || !!item.tool).length === 0) : !content))
? (
<div className='flex items-center justify-center w-6 h-5'>
<LoadingAnim type='text' />
</div>
)
: (
<Markdown content={content} />
)}
: (isAgentMode
? agentModeAnswer
: (
<Markdown content={content} />
))}
</div>
<div className='absolute top-[-14px] right-[-14px] flex flex-row justify-end gap-1'>
{!feedbackDisabled && !item.feedbackDisabled && renderItemOperation()}

View File

@ -0,0 +1,61 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import type { ThoughtItem, ToolInfoInThought } from '../type'
import Tool from './tool'
import type { Emoji } from '@/types/tools'
export type IThoughtProps = {
thought: ThoughtItem
allToolIcons: Record<string, string | Emoji>
isFinished: boolean
}
function getValue(value: string, isValueArray: boolean, index: number) {
if (isValueArray) {
try {
return JSON.parse(value)[index]
}
catch (e) {
}
}
return value
}
const Thought: FC<IThoughtProps> = ({
thought,
allToolIcons,
isFinished,
}) => {
const [toolNames, isValueArray]: [string[], boolean] = (() => {
try {
if (Array.isArray(JSON.parse(thought.tool)))
return [JSON.parse(thought.tool), true]
}
catch (e) {
}
return [[thought.tool], false]
})()
const toolThoughtList = toolNames.map((toolName, index) => {
return {
name: toolName,
input: getValue(thought.tool_input, isValueArray, index),
output: getValue(thought.observation, isValueArray, index),
isFinished,
}
})
return (
<div className='my-2 space-y-2'>
{toolThoughtList.map((item: ToolInfoInThought, index) => (
<Tool
key={index}
payload={item}
allToolIcons={allToolIcons}
/>
))}
</div>
)
}
export default React.memo(Thought)

View File

@ -0,0 +1,28 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
type Props = {
isRequest: boolean
toolName: string
content: string
}
const Panel: FC<Props> = ({
isRequest,
toolName,
content,
}) => {
const { t } = useTranslation()
return (
<div className='rounded-md bg-gray-100 overflow-hidden border border-black/5'>
<div className='flex items-center px-2 py-1 leading-[18px] bg-gray-50 uppercase text-xs font-medium text-gray-500'>
{t(`tools.thought.${isRequest ? 'requestTitle' : 'responseTitle'}`)} {toolName}
</div>
<div className='p-2 border-t border-black/5 leading-4 text-xs text-gray-700'>{content}</div>
</div>
)
}
export default React.memo(Panel)

View 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);
}

View File

@ -0,0 +1,103 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import type { ToolInfoInThought } from '../type'
import Panel from './panel'
import Loading02 from '@/app/components/base/icons/line/loading-02'
import ChevronDown from '@/app/components/base/icons/line/arrows/chevron-down'
import CheckCircle from '@/app/components/base/icons/solid/general/check-circle'
import DataSetIcon from '@/app/components/base/icons/public/data-set'
import type { Emoji } from '@/types/tools'
import AppIcon from '@/app/components/base/app-icon'
type Props = {
payload: ToolInfoInThought
allToolIcons?: Record<string, string | Emoji>
}
const getIcon = (toolName: string, allToolIcons: Record<string, string | Emoji>) => {
if (toolName.startsWith('dataset-'))
return <DataSetIcon className='shrink-0'></DataSetIcon>
const icon = allToolIcons[toolName]
if (!icon)
return null
return (
typeof icon === 'string'
? (
<div
className='w-3 h-3 bg-cover bg-center rounded-[3px] shrink-0'
style={{
backgroundImage: `url(${icon})`,
}}
></div>
)
: (
<AppIcon
className='rounded-[3px] shrink-0'
size='xs'
icon={icon?.content}
background={icon?.background}
/>
))
}
const Tool: FC<Props> = ({
payload,
allToolIcons = {},
}) => {
const { t } = useTranslation()
const { name, input, isFinished, output } = payload
const toolName = name.startsWith('dataset-') ? t('dataset.knowledge') : name
const [isShowDetail, setIsShowDetail] = useState(false)
const icon = getIcon(toolName, allToolIcons) as any
return (
<div>
<div className={cn(!isShowDetail && 'shadow-sm', !isShowDetail && 'inline-block', 'max-w-full overflow-x-auto bg-white rounded-md')}>
<div
className={cn('flex items-center h-7 px-2 cursor-pointer')}
onClick={() => setIsShowDetail(!isShowDetail)}
>
{!isFinished && (
<Loading02 className='w-3 h-3 text-gray-500 animate-spin shrink-0' />
)}
{isFinished && !isShowDetail && (
<CheckCircle className='w-3 h-3 text-[#12B76A] shrink-0' />
)}
{isFinished && isShowDetail && (
icon
)}
<span className='mx-1 text-xs font-medium text-gray-500 shrink-0'>
{t(`tools.thought.${isFinished ? 'used' : 'using'}`)}
</span>
<span
className='text-xs font-medium text-gray-700 truncate'
title={toolName}
>
{toolName}
</span>
<ChevronDown
className={cn(isShowDetail && 'rotate-180', 'ml-1 w-3 h-3 text-gray-500 select-none cursor-pointer shrink-0')}
/>
</div>
{isShowDetail && (
<div className='border-t border-black/5 p-2 space-y-2 '>
<Panel
isRequest={true}
toolName={toolName}
content={input} />
{output && (
<Panel
isRequest={false}
toolName={toolName}
content={output as string} />
)}
</div>
)}
</div>
</div>
)
}
export default React.memo(Tool)