mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-10 11:26:52 +08:00
Initial commit
This commit is contained in:
100
web/app/components/share/text-generation/config-scence/index.tsx
Normal file
100
web/app/components/share/text-generation/config-scence/index.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
PlayIcon,
|
||||
} from '@heroicons/react/24/solid'
|
||||
import Select from '@/app/components/base/select'
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
import type { PromptConfig } from '@/models/debug'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
|
||||
|
||||
export type IConfigSenceProps = {
|
||||
siteInfo: SiteInfo
|
||||
promptConfig: PromptConfig
|
||||
inputs: Record<string, any>
|
||||
onInputsChange: (inputs: Record<string, any>) => void
|
||||
query: string
|
||||
onQueryChange: (query: string) => void
|
||||
onSend: () => void
|
||||
}
|
||||
const ConfigSence: FC<IConfigSenceProps> = ({
|
||||
promptConfig,
|
||||
inputs,
|
||||
onInputsChange,
|
||||
query,
|
||||
onQueryChange,
|
||||
onSend,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<section>
|
||||
{/* input form */}
|
||||
<form>
|
||||
{promptConfig.prompt_variables.map(item => (
|
||||
<div className='w-full mt-4' key={item.key}>
|
||||
<label className='text-gray-900 text-sm font-medium'>{item.name}</label>
|
||||
<div className='mt-2'>
|
||||
{item.type === 'select' ? (
|
||||
<Select
|
||||
className='w-full'
|
||||
defaultValue={inputs[item.key]}
|
||||
onSelect={(i) => { onInputsChange({ ...inputs, [item.key]: i.value }) }}
|
||||
items={(item.options || []).map(i => ({ name: i, value: i }))}
|
||||
allowSearch={false}
|
||||
bgClassName='bg-gray-50'
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
className="block w-full p-2 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 sm:text-xs focus:ring-blue-500 focus:border-blue-500 "
|
||||
placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||
value={inputs[item.key]}
|
||||
onChange={(e) => { onInputsChange({ ...inputs, [item.key]: e.target.value }) }}
|
||||
maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className='mt-6 h-[1px] bg-gray-100'></div>
|
||||
<div className='w-full mt-5'>
|
||||
<label className='text-gray-900 text-sm font-medium'>{t('share.generation.queryTitle')}</label>
|
||||
<div className="mt-2 overflow-hidden rounded-lg bg-gray-50 ">
|
||||
<div className="px-4 py-2 bg-gray-50 rounded-t-lg">
|
||||
<textarea
|
||||
value={query}
|
||||
onChange={(e) => { onQueryChange(e.target.value) }}
|
||||
rows={4}
|
||||
className="w-full px-0 text-sm text-gray-900 border-0 bg-gray-50 focus:outline-none placeholder:bg-gray-50"
|
||||
placeholder={t('share.generation.queryPlaceholder') as string}
|
||||
required
|
||||
>
|
||||
</textarea>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-3 py-2">
|
||||
<div className="flex pl-0 space-x-1 sm:pl-2">
|
||||
<span className="bg-gray-100 text-gray-500 text-xs font-medium mr-2 px-2.5 py-0.5 rounded cursor-pointer">{query?.length}</span>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
className='w-[80px] !h-8'
|
||||
onClick={onSend}
|
||||
disabled={!query || query === ''}
|
||||
>
|
||||
<PlayIcon className="shrink-0 w-4 h-4 mr-1" aria-hidden="true" />
|
||||
<span className='uppercase text-[13px]'>{t('share.generation.run')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ConfigSence)
|
||||
79
web/app/components/share/text-generation/history/index.tsx
Normal file
79
web/app/components/share/text-generation/history/index.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronUpIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
import { fetchHistories } from '@/models/history'
|
||||
import type { History as HistoryItem } from '@/models/history'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { mockAPI } from '@/test/test_util'
|
||||
|
||||
mockAPI()
|
||||
|
||||
export type IHistoryProps = {
|
||||
dictionary: any
|
||||
}
|
||||
|
||||
const HistoryCard = (
|
||||
{ history }: { history: HistoryItem },
|
||||
) => {
|
||||
return (
|
||||
<div className='p-4 h-32 bg-gray-50 border-gray-200 rounded-lg relative flex flex-col justify-between items-center cursor-pointer'>
|
||||
<div className='text-gray-700 text-sm'>
|
||||
{history.source}
|
||||
</div>
|
||||
<div className="absolute inset-0 flex items-center m-4" aria-hidden="true">
|
||||
<div className="w-full border-t border-gray-100" />
|
||||
</div>
|
||||
<div className='text-gray-700 text-sm'>
|
||||
{history.target}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const History = ({
|
||||
dictionary,
|
||||
}: IHistoryProps) => {
|
||||
const { data, error } = useSWR('http://localhost:3000/api/histories', fetchHistories)
|
||||
const [showHistory, setShowHistory] = useState(false)
|
||||
|
||||
const DivideLine = () => {
|
||||
return <div className="mt-6 relative">
|
||||
{/* divider line */}
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className="w-full border-t border-gray-300" />
|
||||
</div>
|
||||
<div className="relative flex justify-center flex-col items-center">
|
||||
{!showHistory ? <ChevronUpIcon className="h-3 w-3 text-gray-500" aria-hidden="true" /> : <div className='h-3 w-3' />}
|
||||
<span className="px-2 bg-white text-sm font-medium text-gray-600 cursor-pointer">{dictionary.app.textGeneration.history}</span>
|
||||
{!showHistory ? <div className='h-3 w-3' /> : <ChevronDownIcon className="h-3 w-3 text-gray-500" aria-hidden="true" />}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
if (error)
|
||||
return <div>failed to load</div>
|
||||
if (!data)
|
||||
return <Loading />
|
||||
return showHistory
|
||||
? <div className='w-1/2 block fixed bottom-0 right-0 px-10 py-4' onClick={
|
||||
() => setShowHistory(v => !v)
|
||||
}>
|
||||
<DivideLine />
|
||||
<div
|
||||
className='mt-4 grid grid-cols-3 space-x-4 h-[400px] overflow-auto'
|
||||
>
|
||||
{data.histories.map((item: HistoryItem) =>
|
||||
<HistoryCard key={item.id} history={item} />)}
|
||||
</div>
|
||||
</div>
|
||||
: <div className='w-1/2 block fixed bottom-0 right-0 px-10 py-4' onClick={
|
||||
() => setShowHistory(true)
|
||||
}>
|
||||
<DivideLine />
|
||||
</div>
|
||||
}
|
||||
export default History
|
||||
22
web/app/components/share/text-generation/icons/app-icon.svg
Normal file
22
web/app/components/share/text-generation/icons/app-icon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 26 KiB |
5
web/app/components/share/text-generation/icons/star.svg
Normal file
5
web/app/components/share/text-generation/icons/star.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.66699 1.33366C3.66699 0.965469 3.36852 0.666992 3.00033 0.666992C2.63214 0.666992 2.33366 0.965469 2.33366 1.33366V2.33366H1.33366C0.965469 2.33366 0.666992 2.63214 0.666992 3.00033C0.666992 3.36852 0.965469 3.66699 1.33366 3.66699H2.33366V4.66699C2.33366 5.03518 2.63214 5.33366 3.00033 5.33366C3.36852 5.33366 3.66699 5.03518 3.66699 4.66699V3.66699H4.66699C5.03518 3.66699 5.33366 3.36852 5.33366 3.00033C5.33366 2.63214 5.03518 2.33366 4.66699 2.33366H3.66699V1.33366Z" fill="#444CE7"/>
|
||||
<path d="M3.66699 11.3337C3.66699 10.9655 3.36852 10.667 3.00033 10.667C2.63214 10.667 2.33366 10.9655 2.33366 11.3337V12.3337H1.33366C0.965469 12.3337 0.666992 12.6321 0.666992 13.0003C0.666992 13.3685 0.965469 13.667 1.33366 13.667H2.33366V14.667C2.33366 15.0352 2.63214 15.3337 3.00033 15.3337C3.36852 15.3337 3.66699 15.0352 3.66699 14.667V13.667H4.66699C5.03518 13.667 5.33366 13.3685 5.33366 13.0003C5.33366 12.6321 5.03518 12.3337 4.66699 12.3337H3.66699V11.3337Z" fill="#444CE7"/>
|
||||
<path d="M9.28922 1.76101C9.1902 1.50354 8.94284 1.33366 8.66699 1.33366C8.39114 1.33366 8.14378 1.50354 8.04476 1.76101L6.88864 4.76691C6.68837 5.28761 6.62544 5.43766 6.53936 5.55872C6.45299 5.68019 6.34686 5.78632 6.22539 5.87269C6.10432 5.95878 5.95428 6.02171 5.43358 6.22198L2.42767 7.37809C2.17021 7.47712 2.00033 7.72448 2.00033 8.00033C2.00033 8.27617 2.17021 8.52353 2.42767 8.62256L5.43358 9.77867C5.95428 9.97894 6.10432 10.0419 6.22539 10.128C6.34686 10.2143 6.45299 10.3205 6.53936 10.4419C6.62544 10.563 6.68837 10.713 6.88864 11.2337L8.04476 14.2396C8.14379 14.4971 8.39114 14.667 8.66699 14.667C8.94284 14.667 9.1902 14.4971 9.28922 14.2396L10.4453 11.2337C10.6456 10.713 10.7085 10.563 10.7946 10.4419C10.881 10.3205 10.9871 10.2143 11.1086 10.128C11.2297 10.0419 11.3797 9.97894 11.9004 9.77867L14.9063 8.62256C15.1638 8.52353 15.3337 8.27617 15.3337 8.00033C15.3337 7.72448 15.1638 7.47712 14.9063 7.37809L11.9004 6.22198C11.3797 6.02171 11.2297 5.95878 11.1086 5.87269C10.9871 5.78632 10.881 5.68019 10.7946 5.55872C10.7085 5.43766 10.6456 5.28761 10.4453 4.76691L9.28922 1.76101Z" fill="#444CE7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
349
web/app/components/share/text-generation/index.tsx
Normal file
349
web/app/components/share/text-generation/index.tsx
Normal file
@@ -0,0 +1,349 @@
|
||||
'use client'
|
||||
import React, { useEffect, useState, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import cn from 'classnames'
|
||||
import { useBoolean, useClickAway } from 'ahooks'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import ConfigScence from '@/app/components/share/text-generation/config-scence'
|
||||
import NoData from '@/app/components/share/text-generation/no-data'
|
||||
// import History from '@/app/components/share/text-generation/history'
|
||||
import { fetchAppInfo, fetchAppParams, sendCompletionMessage, updateFeedback, saveMessage, fetchSavedMessage as doFetchSavedMessage, removeMessage } from '@/service/share'
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
import type { PromptConfig, MoreLikeThisConfig, SavedMessage } from '@/models/debug'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { Feedbacktype } from '@/app/components/app/chat'
|
||||
import { changeLanguage } from '@/i18n/i18next-config'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { userInputsFormToPromptVariables } from '@/utils/model-config'
|
||||
import TextGenerationRes from '@/app/components/app/text-generate/item'
|
||||
import SavedItems from '@/app/components/app/text-generate/saved-items'
|
||||
import TabHeader from '../../base/tab-header'
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline'
|
||||
import s from './style.module.css'
|
||||
import Button from '../../base/button'
|
||||
|
||||
const TextGeneration = () => {
|
||||
const { t } = useTranslation()
|
||||
const media = useBreakpoints()
|
||||
const isPC = media === MediaType.pc
|
||||
const isTablet = media === MediaType.tablet
|
||||
const isMoble = media === MediaType.mobile
|
||||
|
||||
const [currTab, setCurrTab] = useState<string>('create')
|
||||
|
||||
const [inputs, setInputs] = useState<Record<string, any>>({})
|
||||
const [appId, setAppId] = useState<string>('')
|
||||
const [siteInfo, setSiteInfo] = useState<SiteInfo | null>(null)
|
||||
const [promptConfig, setPromptConfig] = useState<PromptConfig | null>(null)
|
||||
const [moreLikeThisConifg, setMoreLikeThisConifg] = useState<MoreLikeThisConfig | null>(null)
|
||||
const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const [completionRes, setCompletionRes] = useState('')
|
||||
const { notify } = Toast
|
||||
const isNoData = !completionRes
|
||||
|
||||
const [messageId, setMessageId] = useState<string | null>(null)
|
||||
const [feedback, setFeedback] = useState<Feedbacktype>({
|
||||
rating: null
|
||||
})
|
||||
|
||||
const handleFeedback = async (feedback: Feedbacktype) => {
|
||||
await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } })
|
||||
setFeedback(feedback)
|
||||
}
|
||||
|
||||
const [savedMessages, setSavedMessages] = useState<SavedMessage[]>([])
|
||||
|
||||
const fetchSavedMessage = async () => {
|
||||
const res: any = await doFetchSavedMessage()
|
||||
setSavedMessages(res.data)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchSavedMessage()
|
||||
}, [])
|
||||
|
||||
const handleSaveMessage = async (messageId: string) => {
|
||||
await saveMessage(messageId)
|
||||
notify({ type: 'success', message: t('common.api.saved') })
|
||||
fetchSavedMessage()
|
||||
}
|
||||
|
||||
const handleRemoveSavedMessage = async (messageId: string) => {
|
||||
await removeMessage(messageId)
|
||||
notify({ type: 'success', message: t('common.api.remove') })
|
||||
fetchSavedMessage()
|
||||
}
|
||||
|
||||
const logError = (message: string) => {
|
||||
notify({ type: 'error', message })
|
||||
}
|
||||
|
||||
const checkCanSend = () => {
|
||||
const prompt_variables = promptConfig?.prompt_variables
|
||||
if (!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 handleSend = async () => {
|
||||
if (isResponsing) {
|
||||
notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
|
||||
return false
|
||||
}
|
||||
|
||||
if (!checkCanSend())
|
||||
return
|
||||
|
||||
if (!query) {
|
||||
logError(t('appDebug.errorMessage.queryRequired'))
|
||||
return false
|
||||
}
|
||||
|
||||
const data = {
|
||||
inputs,
|
||||
query,
|
||||
}
|
||||
|
||||
setMessageId(null)
|
||||
setFeedback({
|
||||
rating: null
|
||||
})
|
||||
setCompletionRes('')
|
||||
|
||||
const res: string[] = []
|
||||
let tempMessageId = ''
|
||||
|
||||
if (!isPC) {
|
||||
showResSidebar()
|
||||
}
|
||||
setResponsingTrue()
|
||||
sendCompletionMessage(data, {
|
||||
onData: (data: string, _isFirstMessage: boolean, { messageId }: any) => {
|
||||
tempMessageId = messageId
|
||||
res.push(data)
|
||||
setCompletionRes(res.join(''))
|
||||
},
|
||||
onCompleted: () => {
|
||||
setResponsingFalse()
|
||||
setMessageId(tempMessageId)
|
||||
},
|
||||
onError() {
|
||||
setResponsingFalse()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const [appData, appParams]: any = await Promise.all([fetchAppInfo(), fetchAppParams()])
|
||||
const { app_id: appId, site: siteInfo } = appData
|
||||
setAppId(appId)
|
||||
setSiteInfo(siteInfo as SiteInfo)
|
||||
changeLanguage(siteInfo.default_language)
|
||||
|
||||
const { user_input_form, more_like_this }: any = appParams
|
||||
const prompt_variables = userInputsFormToPromptVariables(user_input_form)
|
||||
setPromptConfig({
|
||||
prompt_template: '', // placeholder for feture
|
||||
prompt_variables,
|
||||
} as PromptConfig)
|
||||
setMoreLikeThisConifg(more_like_this)
|
||||
})()
|
||||
}, [])
|
||||
|
||||
// Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client.
|
||||
useEffect(() => {
|
||||
if (siteInfo?.title)
|
||||
document.title = `${siteInfo.title} - Powered by Dify`
|
||||
}, [siteInfo?.title])
|
||||
|
||||
const [isShowResSidebar, { setTrue: showResSidebar, setFalse: hideResSidebar }] = useBoolean(false)
|
||||
const resRef = useRef<HTMLDivElement>(null)
|
||||
useClickAway(() => {
|
||||
hideResSidebar();
|
||||
}, resRef)
|
||||
|
||||
const renderRes = (
|
||||
<div
|
||||
ref={resRef}
|
||||
className={
|
||||
cn("flex flex-col h-full shrink-0",
|
||||
isPC ? 'px-10 py-8' : 'bg-gray-50',
|
||||
isTablet && 'p-6', isMoble && 'p-4')}
|
||||
>
|
||||
<>
|
||||
<div className='shrink-0 flex items-center justify-between'>
|
||||
<div className='flex items-center space-x-3'>
|
||||
<div className={s.starIcon}></div>
|
||||
<div className='text-lg text-gray-800 font-semibold'>{t('share.generation.title')}</div>
|
||||
</div>
|
||||
{!isPC && (
|
||||
<div
|
||||
className='flex items-center justify-center cursor-pointer'
|
||||
onClick={hideResSidebar}
|
||||
>
|
||||
<XMarkIcon className='w-4 h-4 text-gray-800' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='grow'>
|
||||
{(isResponsing && !completionRes) ? (
|
||||
<div className='flex h-full w-full justify-center items-center'>
|
||||
<Loading type='area' />
|
||||
</div>) : (
|
||||
<>
|
||||
{isNoData
|
||||
? <NoData />
|
||||
: (
|
||||
<TextGenerationRes
|
||||
className='mt-3'
|
||||
content={completionRes}
|
||||
messageId={messageId}
|
||||
isInWebApp
|
||||
moreLikeThis={moreLikeThisConifg?.enabled}
|
||||
onFeedback={handleFeedback}
|
||||
feedback={feedback}
|
||||
onSave={handleSaveMessage}
|
||||
isMobile={isMoble}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
|
||||
if (!appId || !siteInfo || !promptConfig)
|
||||
return <Loading type='app' />
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn(isPC && 'flex', 'h-screen bg-gray-50')}>
|
||||
{/* Left */}
|
||||
<div className={cn(isPC ? 'w-[600px] max-w-[50%] p-8' : 'p-4', "shrink-0 relative flex flex-col pb-10 h-full border-r border-gray-100 bg-white")}>
|
||||
<div className='mb-6'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className='flex items-center space-x-3'>
|
||||
<div className={cn(s.appIcon, 'shrink-0')}></div>
|
||||
<div className='text-lg text-gray-800 font-semibold'>{siteInfo.title}</div>
|
||||
</div>
|
||||
{!isPC && (
|
||||
<Button
|
||||
className='shrink-0 !h-8 !px-3 ml-2'
|
||||
onClick={showResSidebar}
|
||||
>
|
||||
<div className='flex items-center space-x-2 text-primary-600 text-[13px] font-medium'>
|
||||
<div className={s.starIcon}></div>
|
||||
<span>{t('share.generation.title')}</span>
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{siteInfo.description && (
|
||||
<div className='mt-2 text-xs text-gray-500'>{siteInfo.description}</div>
|
||||
)}
|
||||
</div>
|
||||
<TabHeader
|
||||
items={[
|
||||
{ id: 'create', name: t('share.generation.tabs.create') },
|
||||
{
|
||||
id: 'saved', name: t('share.generation.tabs.saved'), extra: savedMessages.length > 0 ? (
|
||||
<div className='ml-1 flext items-center h-5 px-1.5 rounded-md border border-gray-200 text-gray-500 text-xs font-medium'>
|
||||
{savedMessages.length}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
]}
|
||||
value={currTab}
|
||||
onChange={setCurrTab}
|
||||
/>
|
||||
<div className='grow h-20 overflow-y-auto'>
|
||||
{currTab === 'create' && (
|
||||
<ConfigScence
|
||||
siteInfo={siteInfo}
|
||||
inputs={inputs}
|
||||
onInputsChange={setInputs}
|
||||
promptConfig={promptConfig}
|
||||
query={query}
|
||||
onQueryChange={setQuery}
|
||||
onSend={handleSend}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currTab === 'saved' && (
|
||||
<SavedItems
|
||||
className='mt-4'
|
||||
list={savedMessages}
|
||||
onRemove={handleRemoveSavedMessage}
|
||||
onStartCreateContent={() => setCurrTab('create')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* copyright */}
|
||||
<div className='fixed left-8 bottom-4 flex space-x-2 text-gray-400 font-normal text-xs'>
|
||||
<div className="">© {siteInfo.copyright || siteInfo.title} {(new Date()).getFullYear()}</div>
|
||||
{siteInfo.privacy_policy && (
|
||||
<>
|
||||
<div>·</div>
|
||||
<div>{t('share.chat.privacyPolicyLeft')}
|
||||
<a
|
||||
className='text-gray-500'
|
||||
href={siteInfo.privacy_policy}
|
||||
target='_blank'>{t('share.chat.privacyPolicyMiddle')}</a>
|
||||
{t('share.chat.privacyPolicyRight')}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Result */}
|
||||
{isPC && (
|
||||
<div className='grow h-full'>
|
||||
{renderRes}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!isPC && isShowResSidebar) && (
|
||||
<div
|
||||
className={cn('fixed z-50 inset-0', isTablet ? 'pl-[128px]' : 'pl-6')}
|
||||
style={{
|
||||
background: 'rgba(35, 56, 118, 0.2)'
|
||||
}}
|
||||
>
|
||||
{renderRes}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TextGeneration
|
||||
26
web/app/components/share/text-generation/no-data/index.tsx
Normal file
26
web/app/components/share/text-generation/no-data/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const StarIcon = (
|
||||
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.50033 48.3337V36.667M7.50033 13.3337V1.66699M1.66699 7.50033H13.3337M1.66699 42.5003H13.3337M27.3337 4.00032L23.2872 14.521C22.6292 16.2319 22.3002 17.0873 21.7886 17.8069C21.3351 18.4446 20.7779 19.0018 20.1402 19.4552C19.4206 19.9669 18.5652 20.2959 16.8543 20.9539L6.33366 25.0003L16.8543 29.0467C18.5652 29.7048 19.4206 30.0338 20.1402 30.5454C20.7779 30.9989 21.3351 31.5561 21.7886 32.1938C22.3002 32.9133 22.6292 33.7688 23.2872 35.4796L27.3337 46.0003L31.3801 35.4796C32.0381 33.7688 32.3671 32.9133 32.8788 32.1938C33.3322 31.5561 33.8894 30.9989 34.5271 30.5454C35.2467 30.0338 36.1021 29.7048 37.813 29.0467L48.3337 25.0003L37.813 20.9539C36.1021 20.2959 35.2467 19.9669 34.5271 19.4552C33.8894 19.0018 33.3322 18.4446 32.8788 17.8069C32.3671 17.0873 32.0381 16.2319 31.3801 14.521L27.3337 4.00032Z" stroke="#EAECF0" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
|
||||
)
|
||||
|
||||
export type INoDataProps = {}
|
||||
const NoData: FC<INoDataProps> = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='flex flex-col h-full w-full justify-center items-center'>
|
||||
{StarIcon}
|
||||
<div
|
||||
className='mt-3 text-gray-300 text-xs leading-3'
|
||||
>
|
||||
{t('share.generation.noData')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(NoData)
|
||||
116
web/app/components/share/text-generation/result/header.tsx
Normal file
116
web/app/components/share/text-generation/result/header.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ClipboardDocumentIcon, HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
|
||||
import { Feedbacktype } from '@/app/components/app/chat'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
// import useCopyToClipboard from '@/hooks/use-copy-to-clipboard'
|
||||
import copy from 'copy-to-clipboard'
|
||||
type IResultHeaderProps = {
|
||||
result: string
|
||||
showFeedback: boolean
|
||||
feedback: Feedbacktype
|
||||
onFeedback: (feedback: Feedbacktype) => void
|
||||
}
|
||||
|
||||
const Header: FC<IResultHeaderProps> = ({
|
||||
feedback,
|
||||
showFeedback,
|
||||
onFeedback,
|
||||
result,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='flex w-full justify-between items-center '>
|
||||
<div className='text-gray-800 text-2xl leading-4 font-normal'>{t('share.generation.resultTitle')}</div>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<Button
|
||||
className='flex items-center !h-7 !p-[2px] !pr-2'
|
||||
onClick={() => {
|
||||
copy(result)
|
||||
Toast.notify({ type: 'success', message: 'copied' })
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<ClipboardDocumentIcon className='text-gray-500 w-4 h-3 mr-1' />
|
||||
<span className='text-gray-500 text-xs leading-3'>{t('share.generation.copy')}</span>
|
||||
</>
|
||||
</Button>
|
||||
|
||||
{showFeedback && feedback.rating && feedback.rating === 'like' && (
|
||||
<Tooltip
|
||||
selector="undo-feedback-like"
|
||||
content="Undo Great Rating"
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback({
|
||||
rating: null
|
||||
})
|
||||
}}
|
||||
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-primary-600 border border-primary-200 bg-primary-100 hover:border-primary-300 hover:bg-primary-200'>
|
||||
<HandThumbUpIcon width={16} height={16} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{showFeedback && feedback.rating && feedback.rating === 'dislike' && (
|
||||
<Tooltip
|
||||
selector="undo-feedback-dislike"
|
||||
content="Undo Undesirable Response"
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback({
|
||||
rating: null
|
||||
})
|
||||
}}
|
||||
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-red-600 border border-red-200 bg-red-100 hover:border-red-300 hover:bg-red-200'>
|
||||
<HandThumbDownIcon width={16} height={16} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{showFeedback && !feedback.rating && (
|
||||
<div className='flex rounded-lg border border-gray-200 p-[1px] space-x-1'>
|
||||
<Tooltip
|
||||
selector="feedback-like"
|
||||
content="Great Rating"
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback({
|
||||
rating: 'like'
|
||||
})
|
||||
}}
|
||||
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
|
||||
<HandThumbUpIcon width={16} height={16} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
selector="feedback-dislike"
|
||||
content="Undesirable Response"
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback({
|
||||
rating: 'dislike'
|
||||
})
|
||||
}}
|
||||
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
|
||||
<HandThumbDownIcon width={16} height={16} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Header)
|
||||
34
web/app/components/share/text-generation/result/index.tsx
Normal file
34
web/app/components/share/text-generation/result/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import Header from './header'
|
||||
import { Feedbacktype } from '@/app/components/app/chat'
|
||||
import { format } from '@/service/base'
|
||||
|
||||
export type IResultProps = {
|
||||
content: string
|
||||
showFeedback: boolean
|
||||
feedback: Feedbacktype
|
||||
onFeedback: (feedback: Feedbacktype) => void
|
||||
}
|
||||
const Result: FC<IResultProps> = ({
|
||||
content,
|
||||
showFeedback,
|
||||
feedback,
|
||||
onFeedback
|
||||
}) => {
|
||||
return (
|
||||
<div className='basis-3/4 h-max'>
|
||||
<Header result={content} showFeedback={showFeedback} feedback={feedback} onFeedback={onFeedback} />
|
||||
<div
|
||||
className='mt-4 w-full flex text-sm leading-5 overflow-scroll font-normal text-gray-900'
|
||||
style={{
|
||||
maxHeight: '70vh'
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: format(content)
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Result)
|
||||
13
web/app/components/share/text-generation/style.module.css
Normal file
13
web/app/components/share/text-generation/style.module.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.appIcon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: url(./icons/app-icon.svg) center center no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.starIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: url(./icons/star.svg) center center no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
Reference in New Issue
Block a user