Unverified Commit 3de8abdd authored by Joel's avatar Joel Committed by GitHub

Merge pull request #40 from langgenius/chore/split-question-answer-comp

chore: split question and and answer to components
parents c8f73020 4a75e377
...@@ -9,7 +9,8 @@ export async function GET(request: NextRequest) { ...@@ -9,7 +9,8 @@ export async function GET(request: NextRequest) {
return NextResponse.json(data as object, { return NextResponse.json(data as object, {
headers: setSession(sessionId), headers: setSession(sessionId),
}) })
} catch (error) { }
return NextResponse.json([]); catch (error) {
return NextResponse.json([])
} }
} }
'use client'
import type { FC } from 'react'
import React from 'react'
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
import { useTranslation } from 'react-i18next'
import LoadingAnim from '../loading-anim'
import type { FeedbackFunc, IChatItem } from '../type'
import s from '../style.module.css'
import { randomString } from '@/utils/string'
import type { MessageRating } from '@/types/app'
import Tooltip from '@/app/components/base/tooltip'
import { Markdown } from '@/app/components/base/markdown'
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 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" clip-rule="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" clip-rule="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 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
onFeedback?: FeedbackFunc
isResponsing?: boolean
}
// 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 { 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
* @returns comp
*/
const renderFeedbackRating = (rating: MessageRating | undefined) => {
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'
// The tooltip is always displayed, but the content is different for different scenarios.
return (
<Tooltip
selector={`user-feedback-${randomString(16)}`}
content={isLike ? '取消赞同' : '取消反对'}
>
<div
className={'relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-gray-500 hover:text-gray-800'}
style={{ boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.05)' }}
onClick={async () => {
await onFeedback?.(id, { rating: null })
}}
>
<div className={`${ratingIconClassname} rounded-lg h-6 w-6 flex items-center justify-center`}>
<RatingIcon isLike={isLike} />
</div>
</div>
</Tooltip>
)
}
/**
* Different scenarios have different operation items.
* @returns comp
*/
const renderItemOperation = () => {
const userOperation = () => {
return feedback?.rating
? null
: <div className='flex gap-1'>
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('common.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('common.operation.dislike') as string}>
{OperationBtn({ innerContent: <IconWrapper><RatingIcon isLike={false} /></IconWrapper>, onClick: () => onFeedback?.(id, { rating: 'dislike' }) })}
</Tooltip>
</div>
}
return (
<div className={`${s.itemOperation} flex gap-2`}>
{userOperation()}
</div>
)
}
return (
<div key={id}>
<div className='flex items-start'>
<div className={`${s.answerIcon} w-10 h-10 shrink-0`}>
{isResponsing
&& <div className={s.typeingIcon}>
<LoadingAnim type='avatar' />
</div>
}
</div>
<div className={`${s.answerWrap}`}>
<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)
? (
<div className='flex items-center justify-center w-6 h-5'>
<LoadingAnim type='text' />
</div>
)
: (
<Markdown content={content} />
)}
</div>
<div className='absolute top-[-14px] right-[-14px] flex flex-row justify-end gap-1'>
{!feedbackDisabled && !item.feedbackDisabled && renderItemOperation()}
{/* User feedback must be displayed */}
{!feedbackDisabled && renderFeedbackRating(feedback?.rating)}
</div>
</div>
</div>
</div>
</div>
)
}
export default React.memo(Answer)
This diff is collapsed.
'use client'
import type { FC } from 'react'
import React from 'react'
import type { IChatItem } from '../type'
import s from '../style.module.css'
import { Markdown } from '@/app/components/base/markdown'
import ImageGallery from '@/app/components/base/image-gallery'
type IQuestionProps = Pick<IChatItem, 'id' | 'content' | 'useCurrentUserAvatar'> & {
imgSrcs?: string[]
}
const Question: FC<IQuestionProps> = ({ id, content, useCurrentUserAvatar, imgSrcs }) => {
const userName = ''
return (
<div className='flex items-start justify-end' key={id}>
<div>
<div className={`${s.question} relative text-sm text-gray-900`}>
<div
className={'mr-2 py-3 px-4 bg-blue-500 rounded-tl-2xl rounded-b-2xl'}
>
{imgSrcs && imgSrcs.length > 0 && (
<ImageGallery srcs={imgSrcs} />
)}
<Markdown content={content} />
</div>
</div>
</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)
import type { VisionFile } from '@/types/app'
export type LogAnnotation = {
content: string
account: {
id: string
name: string
email: string
}
created_at: number
}
export type Annotation = {
id: string
authorName: string
logAnnotation?: LogAnnotation
created_at?: number
}
export const MessageRatings = ['like', 'dislike', null] as const
export type MessageRating = typeof MessageRatings[number]
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 ToolInfoInThought = {
name: string
input: string
output: string
isFinished: boolean
}
export type ThoughtItem = {
id: string
tool: string // plugin or dataset. May has multi.
thought: string
tool_input: string
message_id: string
observation: string
position: number
files?: string[]
message_files?: VisionFile[]
}
export type CitationItem = {
content: string
data_source_type: string
dataset_name: string
dataset_id: string
document_id: string
document_name: string
hit_count: number
index_node_hash: string
segment_id: string
segment_position: number
score: number
word_count: number
}
export type IChatItem = {
id: string
content: string
citation?: CitationItem[]
/**
* 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
suggestedQuestions?: string[]
log?: { role: string; text: string }[]
agent_thoughts?: ThoughtItem[]
message_files?: VisionFile[]
}
export type MessageEnd = {
id: string
metadata: {
retriever_resources?: CitationItem[]
annotation_reply: {
id: string
account: {
id: string
name: string
}
}
}
}
export type MessageReplace = {
id: string
task_id: string
answer: string
conversation_id: string
}
export type AnnotationReply = {
id: string
task_id: string
answer: string
conversation_id: string
annotation_id: string
annotation_author_name: string
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment