Initial commit
22
web/.editorconfig
Normal file
@@ -0,0 +1,22 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
|
||||
# Matches multiple files with brace expansion notation
|
||||
# Set default charset
|
||||
[*.{js,tsx}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
|
||||
# Matches the exact files either package.json or .travis.yml
|
||||
[{package.json,.travis.yml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
28
web/.eslintrc.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"extends": [
|
||||
"@antfu",
|
||||
"plugin:react-hooks/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/consistent-type-definitions": [
|
||||
"error",
|
||||
"type"
|
||||
],
|
||||
"no-console": "off",
|
||||
"indent": "off",
|
||||
"@typescript-eslint/indent": [
|
||||
"error",
|
||||
2,
|
||||
{
|
||||
"SwitchCase": 1,
|
||||
"flatTernaryExpressions": false,
|
||||
"ignoredNodes": [
|
||||
"PropertyDefinition[decorators]",
|
||||
"TSUnionType",
|
||||
"FunctionExpression[params]:has(Identifier[decorators])"
|
||||
]
|
||||
}
|
||||
],
|
||||
"react-hooks/exhaustive-deps": "warning"
|
||||
}
|
||||
}
|
||||
49
web/.gitignore
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# npm
|
||||
package-lock.json
|
||||
|
||||
# yarn
|
||||
.pnp.cjs
|
||||
.pnp.loader.mjs
|
||||
.yarn/
|
||||
yarn.lock
|
||||
.yarnrc.yml
|
||||
|
||||
# pmpm
|
||||
pnpm-lock.yaml
|
||||
29
web/Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
||||
FROM langgenius/base:1.0.0-bullseye-slim
|
||||
|
||||
LABEL maintainer="takatost@gmail.com"
|
||||
|
||||
ENV EDITION SELF_HOSTED
|
||||
ENV DEPLOY_ENV PRODUCTION
|
||||
ENV CONSOLE_URL http://127.0.0.1:5001
|
||||
ENV APP_URL http://127.0.0.1:5001
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
WORKDIR /app/web
|
||||
|
||||
COPY package.json /app/web/package.json
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . /app/web/
|
||||
|
||||
RUN npm run build
|
||||
|
||||
COPY docker/pm2.json /app/web/pm2.json
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
ARG COMMIT_SHA
|
||||
ENV COMMIT_SHA ${COMMIT_SHA}
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
39
web/README.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Dify Frontend
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
|
||||
|
||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
85
web/app/(commonLayout)/_layout-client.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { usePathname, useRouter, useSelectedLayoutSegments } from 'next/navigation'
|
||||
import useSWR, { SWRConfig } from 'swr'
|
||||
import Header from '../components/header'
|
||||
import { fetchAppList } from '@/service/apps'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
import { fetchLanggeniusVersion, fetchUserProfile, logout } from '@/service/common'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import AppContext from '@/context/app-context'
|
||||
import DatasetsContext from '@/context/datasets-context'
|
||||
import type { LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
|
||||
|
||||
export type ICommonLayoutProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const CommonLayout: FC<ICommonLayoutProps> = ({ children }) => {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const segments = useSelectedLayoutSegments()
|
||||
const pattern = pathname.replace(/.*\/app\//, '')
|
||||
const [idOrMethod] = pattern.split('/')
|
||||
const isNotDetailPage = idOrMethod === 'list'
|
||||
|
||||
const appId = isNotDetailPage ? '' : idOrMethod
|
||||
|
||||
const { data: appList, mutate: mutateApps } = useSWR({ url: '/apps', params: { page: 1 } }, fetchAppList)
|
||||
const { data: datasetList, mutate: mutateDatasets } = useSWR(segments[0] === 'datasets' ? { url: '/datasets', params: { page: 1 } } : null, fetchDatasets)
|
||||
const { data: userProfileResponse, mutate: mutateUserProfile } = useSWR({ url: '/account/profile', params: {} }, fetchUserProfile)
|
||||
|
||||
const [userProfile, setUserProfile] = useState<UserProfileResponse>()
|
||||
const [langeniusVersionInfo, setLangeniusVersionInfo] = useState<LangGeniusVersionResponse>()
|
||||
const updateUserProfileAndVersion = async () => {
|
||||
if (userProfileResponse && !userProfileResponse.bodyUsed) {
|
||||
const result = await userProfileResponse.json()
|
||||
setUserProfile(result)
|
||||
const current_version = userProfileResponse.headers.get('x-version')
|
||||
const current_env = userProfileResponse.headers.get('x-env')
|
||||
const versionData = await fetchLanggeniusVersion({ url: '/version', params: { current_version } })
|
||||
setLangeniusVersionInfo({ ...versionData, current_version, latest_version: versionData.version, current_env })
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
updateUserProfileAndVersion()
|
||||
}, [userProfileResponse])
|
||||
|
||||
if (!appList || !userProfile || !langeniusVersionInfo)
|
||||
return <Loading type='app' />
|
||||
|
||||
const curApp = appList?.data.find(opt => opt.id === appId)
|
||||
const currentDatasetId = segments[0] === 'datasets' && segments[2]
|
||||
const currentDataset = datasetList?.data?.find(opt => opt.id === currentDatasetId)
|
||||
|
||||
// if (!isNotDetailPage && !curApp) {
|
||||
// alert('app not found') // TODO: use toast. Now can not get toast context here.
|
||||
// // notify({ type: 'error', message: 'App not found' })
|
||||
// router.push('/apps')
|
||||
// }
|
||||
|
||||
const onLogout = async () => {
|
||||
await logout({
|
||||
url: '/logout',
|
||||
params: {},
|
||||
})
|
||||
router.push('/signin')
|
||||
}
|
||||
|
||||
return (
|
||||
<SWRConfig value={{
|
||||
shouldRetryOnError: false
|
||||
}}>
|
||||
<AppContext.Provider value={{ apps: appList.data, mutateApps, userProfile, mutateUserProfile }}>
|
||||
<DatasetsContext.Provider value={{ datasets: datasetList?.data || [], mutateDatasets, currentDataset }}>
|
||||
<div className='relative flex flex-col h-full overflow-scroll bg-gray-100'>
|
||||
<Header isBordered={['/apps', '/datasets'].includes(pathname)} curApp={curApp as any} appItems={appList.data} userProfile={userProfile} onLogout={onLogout} langeniusVersionInfo={langeniusVersionInfo} />
|
||||
{children}
|
||||
</div>
|
||||
</DatasetsContext.Provider>
|
||||
</AppContext.Provider>
|
||||
</SWRConfig>
|
||||
)
|
||||
}
|
||||
export default React.memo(CommonLayout)
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import Configuration from '@/app/components/app/configuration'
|
||||
|
||||
const IConfiguration = async () => {
|
||||
return (
|
||||
<Configuration />
|
||||
)
|
||||
}
|
||||
|
||||
export default IConfiguration
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import { getDictionary } from '@/i18n/server'
|
||||
import { type Locale } from '@/i18n'
|
||||
import DevelopMain from '@/app/components/develop'
|
||||
|
||||
export type IDevelopProps = {
|
||||
params: { locale: Locale; appId: string }
|
||||
}
|
||||
|
||||
const Develop = async ({
|
||||
params: { locale, appId },
|
||||
}: IDevelopProps) => {
|
||||
const dictionary = await getDictionary(locale)
|
||||
|
||||
return <DevelopMain appId={appId} dictionary={dictionary} />
|
||||
}
|
||||
|
||||
export default Develop
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import cn from 'classnames'
|
||||
import useSWR from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
ChartBarSquareIcon,
|
||||
Cog8ToothIcon,
|
||||
CommandLineIcon,
|
||||
DocumentTextIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
import {
|
||||
ChartBarSquareIcon as ChartBarSquareSolidIcon,
|
||||
Cog8ToothIcon as Cog8ToothSolidIcon,
|
||||
CommandLineIcon as CommandLineSolidIcon,
|
||||
DocumentTextIcon as DocumentTextSolidIcon,
|
||||
} from '@heroicons/react/24/solid'
|
||||
import s from './style.module.css'
|
||||
import AppSideBar from '@/app/components/app-sidebar'
|
||||
import { fetchAppDetail } from '@/service/apps'
|
||||
|
||||
export type IAppDetailLayoutProps = {
|
||||
children: React.ReactNode
|
||||
params: { appId: string }
|
||||
}
|
||||
|
||||
const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
const {
|
||||
children,
|
||||
params: { appId }, // get appId in path
|
||||
} = props
|
||||
const { t } = useTranslation()
|
||||
const detailParams = { url: '/apps', id: appId }
|
||||
const { data: response } = useSWR(detailParams, fetchAppDetail)
|
||||
|
||||
const navigation = [
|
||||
{ name: t('common.appMenus.overview'), href: `/app/${appId}/overview`, icon: ChartBarSquareIcon, selectedIcon: ChartBarSquareSolidIcon },
|
||||
{ name: t('common.appMenus.promptEng'), href: `/app/${appId}/configuration`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon },
|
||||
{ name: t('common.appMenus.apiAccess'), href: `/app/${appId}/develop`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon },
|
||||
{ name: t('common.appMenus.logAndAnn'), href: `/app/${appId}/logs`, icon: DocumentTextIcon, selectedIcon: DocumentTextSolidIcon },
|
||||
]
|
||||
const appModeName = response?.mode?.toUpperCase() === 'COMPLETION' ? t('common.appModes.completionApp') : t('common.appModes.chatApp')
|
||||
useEffect(() => {
|
||||
if (response?.name)
|
||||
document.title = `${(response.name || 'App')} - Dify`
|
||||
}, [response])
|
||||
if (!response)
|
||||
return null
|
||||
return (
|
||||
<div className={cn(s.app, 'flex', 'overflow-hidden')}>
|
||||
<AppSideBar title={response.name} desc={appModeName} navigation={navigation} />
|
||||
<div className="bg-white grow">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(AppDetailLayout)
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import Main from '@/app/components/app/log'
|
||||
|
||||
export type IProps = {
|
||||
params: { appId: string }
|
||||
}
|
||||
|
||||
const Logs = async ({
|
||||
params: { appId },
|
||||
}: IProps) => {
|
||||
return (
|
||||
<Main appId={appId} />
|
||||
)
|
||||
}
|
||||
|
||||
export default Logs
|
||||
@@ -0,0 +1,86 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import useSWR, { useSWRConfig } from 'swr'
|
||||
import AppCard from '@/app/components/app/overview/appCard'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { fetchAppDetail, updateAppApiStatus, updateAppSiteAccessToken, updateAppSiteConfig, updateAppSiteStatus } from '@/service/apps'
|
||||
import type { IToastProps } from '@/app/components/base/toast'
|
||||
import type { App } from '@/types/app'
|
||||
|
||||
export type ICardViewProps = {
|
||||
appId: string
|
||||
}
|
||||
|
||||
type IParams = {
|
||||
url: string
|
||||
body?: Record<string, any>
|
||||
}
|
||||
|
||||
export async function asyncRunSafe<T>(func: (val: IParams) => Promise<T>, params: IParams, callback: (props: IToastProps) => void, dict?: any): Promise<[string?, T?]> {
|
||||
try {
|
||||
const res = await func(params)
|
||||
callback && callback({ type: 'success', message: dict('common.actionMsg.modifiedSuccessfully') })
|
||||
return [undefined, res]
|
||||
}
|
||||
catch (err) {
|
||||
callback && callback({ type: 'error', message: dict('common.actionMsg.modificationFailed') })
|
||||
return [(err as Error).message, undefined]
|
||||
}
|
||||
}
|
||||
|
||||
const CardView: FC<ICardViewProps> = ({ appId }) => {
|
||||
const detailParams = { url: '/apps', id: appId }
|
||||
const { data: response } = useSWR(detailParams, fetchAppDetail)
|
||||
const { mutate } = useSWRConfig()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!response)
|
||||
return <Loading />
|
||||
|
||||
const onChangeSiteStatus = async (value: boolean) => {
|
||||
const [err] = await asyncRunSafe<App>(updateAppSiteStatus as any, { url: `/apps/${appId}/site-enable`, body: { enable_site: value } }, notify, t)
|
||||
if (!err)
|
||||
mutate(detailParams)
|
||||
}
|
||||
|
||||
const onChangeApiStatus = async (value: boolean) => {
|
||||
const [err] = await asyncRunSafe<App>(updateAppApiStatus as any, { url: `/apps/${appId}/api-enable`, body: { enable_api: value } }, notify, t)
|
||||
if (!err)
|
||||
mutate(detailParams)
|
||||
}
|
||||
|
||||
const onSaveSiteConfig = async (params: any) => {
|
||||
const [err] = await asyncRunSafe<App>(updateAppSiteConfig as any, { url: `/apps/${appId}/site`, body: params }, notify, t)
|
||||
if (!err)
|
||||
mutate(detailParams)
|
||||
}
|
||||
|
||||
const onGenerateCode = async () => {
|
||||
const [err] = await asyncRunSafe<App>(updateAppSiteAccessToken as any, { url: `/apps/${appId}/site/access-token-reset` }, notify, t)
|
||||
if (!err)
|
||||
mutate(detailParams)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex flex-row justify-between w-full mb-6'>
|
||||
<AppCard
|
||||
className='mr-3 flex-1'
|
||||
appInfo={response}
|
||||
onChangeStatus={onChangeSiteStatus}
|
||||
onGenerateCode={onGenerateCode}
|
||||
onSaveSiteConfig={onSaveSiteConfig} />
|
||||
<AppCard
|
||||
className='ml-3 flex-1'
|
||||
cardType='api'
|
||||
appInfo={response}
|
||||
onChangeStatus={onChangeApiStatus} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CardView
|
||||
@@ -0,0 +1,52 @@
|
||||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import quarterOfYear from 'dayjs/plugin/quarterOfYear'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { PeriodParams } from '@/app/components/app/overview/appChart'
|
||||
import { ConversationsChart, CostChart, EndUsersChart } from '@/app/components/app/overview/appChart'
|
||||
import type { Item } from '@/app/components/base/select'
|
||||
import { SimpleSelect } from '@/app/components/base/select'
|
||||
import { TIME_PERIOD_LIST } from '@/app/components/app/log/filter'
|
||||
|
||||
dayjs.extend(quarterOfYear)
|
||||
|
||||
const today = dayjs()
|
||||
|
||||
const queryDateFormat = 'YYYY-MM-DD HH:mm'
|
||||
|
||||
export type IChartViewProps = {
|
||||
appId: string
|
||||
}
|
||||
|
||||
export default function ChartView({ appId }: IChartViewProps) {
|
||||
const { t } = useTranslation()
|
||||
const [period, setPeriod] = useState<PeriodParams>({ name: t('appLog.filter.period.last7days'), query: { start: today.subtract(7, 'day').format(queryDateFormat), end: today.format(queryDateFormat) } })
|
||||
|
||||
const onSelect = (item: Item) => {
|
||||
setPeriod({ name: item.name, query: { start: today.subtract(item.value as number, 'day').format(queryDateFormat), end: today.format(queryDateFormat) } })
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='flex flex-row items-center mt-8 mb-4 text-gray-900 text-base'>
|
||||
<span className='mr-3'>{t('appOverview.analysis.title')}</span>
|
||||
<SimpleSelect
|
||||
items={TIME_PERIOD_LIST.map(item => ({ value: item.value, name: t(`appLog.filter.period.${item.name}`) }))}
|
||||
className='mt-0 !w-40'
|
||||
onSelect={onSelect}
|
||||
defaultValue={7}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-row w-full mb-6'>
|
||||
<div className='flex-1 mr-3'>
|
||||
<ConversationsChart period={period} id={appId} />
|
||||
</div>
|
||||
<div className='flex-1 ml-3'>
|
||||
<EndUsersChart period={period} id={appId} />
|
||||
</div>
|
||||
</div>
|
||||
<CostChart period={period} id={appId} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import WelcomeBanner, { EditKeyPopover } from './welcome-banner'
|
||||
import ChartView from './chartView'
|
||||
import CardView from './cardView'
|
||||
import { getLocaleOnServer } from '@/i18n/server'
|
||||
import { useTranslation } from '@/i18n/i18next-serverside-config'
|
||||
|
||||
export type IDevelopProps = {
|
||||
params: { appId: string }
|
||||
}
|
||||
|
||||
const Overview = async ({
|
||||
params: { appId },
|
||||
}: IDevelopProps) => {
|
||||
const locale = getLocaleOnServer()
|
||||
const { t } = await useTranslation(locale, 'app-overview')
|
||||
return (
|
||||
<div className="h-full px-16 py-6 overflow-scroll">
|
||||
<WelcomeBanner />
|
||||
<div className='flex flex-row items-center justify-between mb-4 text-xl text-gray-900'>
|
||||
{t('overview.title')}
|
||||
<EditKeyPopover />
|
||||
</div>
|
||||
<CardView appId={appId} />
|
||||
<ChartView appId={appId} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Overview
|
||||
@@ -0,0 +1,200 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Link from 'next/link'
|
||||
import useSWR, { useSWRConfig } from 'swr'
|
||||
import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline'
|
||||
import { ExclamationCircleIcon } from '@heroicons/react/24/solid'
|
||||
import { debounce } from 'lodash-es'
|
||||
import Popover from '@/app/components/base/popover'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Tag from '@/app/components/base/tag'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { updateOpenAIKey, validateOpenAIKey } from '@/service/apps'
|
||||
import { fetchTenantInfo } from '@/service/common'
|
||||
import I18n from '@/context/i18n'
|
||||
|
||||
type IStatusType = 'normal' | 'verified' | 'error' | 'error-api-key-exceed-bill'
|
||||
|
||||
const STATUS_COLOR_MAP = {
|
||||
normal: { color: '', bgColor: 'bg-primary-50', borderColor: 'border-primary-100' },
|
||||
error: { color: 'text-red-600', bgColor: 'bg-red-50', borderColor: 'border-red-100' },
|
||||
verified: { color: '', bgColor: 'bg-green-50', borderColor: 'border-green-100' },
|
||||
'error-api-key-exceed-bill': { color: 'text-red-600', bgColor: 'bg-red-50', borderColor: 'border-red-100' },
|
||||
}
|
||||
|
||||
const CheckCircleIcon: FC<{ className?: string }> = ({ className }) => {
|
||||
return <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
|
||||
<rect width="20" height="20" rx="10" fill="#DEF7EC" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.6947 6.70495C14.8259 6.83622 14.8996 7.01424 14.8996 7.19985C14.8996 7.38547 14.8259 7.56348 14.6947 7.69475L9.0947 13.2948C8.96343 13.426 8.78541 13.4997 8.5998 13.4997C8.41418 13.4997 8.23617 13.426 8.1049 13.2948L5.3049 10.4948C5.17739 10.3627 5.10683 10.1859 5.10842 10.0024C5.11002 9.81883 5.18364 9.64326 5.31342 9.51348C5.44321 9.38369 5.61878 9.31007 5.80232 9.30848C5.98585 9.30688 6.16268 9.37744 6.2947 9.50495L8.5998 11.8101L13.7049 6.70495C13.8362 6.57372 14.0142 6.5 14.1998 6.5C14.3854 6.5 14.5634 6.57372 14.6947 6.70495Z" fill="#046C4E" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
type IEditKeyDiv = {
|
||||
className?: string
|
||||
showInPopover?: boolean
|
||||
onClose?: () => void
|
||||
getTenantInfo?: () => void
|
||||
}
|
||||
|
||||
const EditKeyDiv: FC<IEditKeyDiv> = ({ className = '', showInPopover = false, onClose, getTenantInfo }) => {
|
||||
const [inputValue, setInputValue] = useState<string | undefined>()
|
||||
const [editStatus, setEditStatus] = useState<IStatusType>('normal')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [validating, setValidating] = useState(false)
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
|
||||
// Hide the pop-up window and need to get the latest key again
|
||||
// If the key is valid, the edit button will be hidden later
|
||||
const onClosePanel = () => {
|
||||
getTenantInfo && getTenantInfo()
|
||||
onClose && onClose()
|
||||
}
|
||||
|
||||
const onSaveKey = async () => {
|
||||
if (editStatus === 'verified') {
|
||||
setLoading(true)
|
||||
try {
|
||||
await updateOpenAIKey({ url: '/providers/openai/token', body: { token: inputValue ?? '' } })
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
onClosePanel()
|
||||
}
|
||||
catch (err) {
|
||||
notify({ type: 'error', message: t('common.actionMsg.modificationFailed') })
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const validateKey = async (value: string) => {
|
||||
try {
|
||||
setValidating(true)
|
||||
const res = await validateOpenAIKey({ url: '/providers/openai/token-validate', body: { token: value ?? '' } })
|
||||
setEditStatus(res.result === 'success' ? 'verified' : 'error')
|
||||
}
|
||||
catch (err: any) {
|
||||
if (err.status === 400) {
|
||||
err.json().then(({ code }: any) => {
|
||||
if (code === 'provider_request_failed') {
|
||||
setEditStatus('error-api-key-exceed-bill')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setEditStatus('error')
|
||||
}
|
||||
}
|
||||
finally {
|
||||
setValidating(false)
|
||||
}
|
||||
}
|
||||
const renderErrorMessage = () => {
|
||||
if (validating) {
|
||||
return (
|
||||
<div className={`text-primary-600 mt-2 text-xs`}>
|
||||
{t('common.provider.validating')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (editStatus === 'error-api-key-exceed-bill') {
|
||||
return (
|
||||
<div className={`text-[#D92D20] mt-2 text-xs`}>
|
||||
{t('common.provider.apiKeyExceedBill')}
|
||||
{locale === 'en' ? ' ' : ''}
|
||||
<Link
|
||||
className='underline'
|
||||
href="https://platform.openai.com/account/api-keys"
|
||||
target={'_blank'}>
|
||||
{locale === 'en' ? 'this link' : '这篇文档'}
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (editStatus === 'error') {
|
||||
return (
|
||||
<div className={`text-[#D92D20] mt-2 text-xs`}>
|
||||
{t('common.provider.invalidKey')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col w-full rounded-lg px-8 py-6 border-solid border-[0.5px] ${className} ${Object.values(STATUS_COLOR_MAP[editStatus]).join(' ')}`}>
|
||||
{!showInPopover && <p className='text-xl font-medium text-gray-800'>{t('appOverview.welcome.firstStepTip')}</p>}
|
||||
<p className={`${showInPopover ? 'text-sm' : 'text-xl'} font-medium text-gray-800`}>{t('appOverview.welcome.enterKeyTip')} {showInPopover ? '' : '👇'}</p>
|
||||
<div className='relative mt-2'>
|
||||
<input type="text"
|
||||
className={`h-9 w-96 max-w-full py-2 pl-2 text-gray-900 rounded-lg bg-white sm:text-xs focus:ring-blue-500 focus:border-blue-500 shadow-sm ${editStatus === 'normal' ? 'pr-2' : 'pr-8'}`}
|
||||
placeholder={t('appOverview.welcome.placeholder') || ''}
|
||||
onChange={debounce((e) => {
|
||||
setInputValue(e.target.value)
|
||||
if (!e.target.value) {
|
||||
setEditStatus('normal')
|
||||
return
|
||||
}
|
||||
validateKey(e.target.value)
|
||||
}, 300)}
|
||||
/>
|
||||
{editStatus === 'verified' && <div className="absolute inset-y-0 right-0 flex flex-row-reverse items-center pr-6 pointer-events-none">
|
||||
<CheckCircleIcon className="rounded-lg" />
|
||||
</div>}
|
||||
{(editStatus === 'error' || editStatus === 'error-api-key-exceed-bill') && <div className="absolute inset-y-0 right-0 flex flex-row-reverse items-center pr-6 pointer-events-none">
|
||||
<ExclamationCircleIcon className="w-5 h-5 text-red-800" />
|
||||
</div>}
|
||||
{showInPopover ? null : <Button type='primary' onClick={onSaveKey} className='!h-9 !inline-block ml-2' loading={loading} disabled={editStatus !== 'verified'}>{t('common.operation.save')}</Button>}
|
||||
</div>
|
||||
{renderErrorMessage()}
|
||||
<Link className="inline-flex items-center mt-2 text-xs font-normal cursor-pointer text-primary-600 w-fit" href="https://platform.openai.com/account/api-keys" target={'_blank'}>
|
||||
{t('appOverview.welcome.getKeyTip')}
|
||||
<ArrowTopRightOnSquareIcon className='w-3 h-3 ml-1 text-primary-600' aria-hidden="true" />
|
||||
</Link>
|
||||
{showInPopover && <div className='flex justify-end mt-6'>
|
||||
<Button className='flex-shrink-0 mr-2' onClick={onClosePanel}>{t('common.operation.cancel')}</Button>
|
||||
<Button type='primary' className='flex-shrink-0' onClick={onSaveKey} loading={loading} disabled={editStatus !== 'verified'}>{t('common.operation.save')}</Button>
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const WelcomeBanner: FC = () => {
|
||||
const { data: userInfo } = useSWR({ url: '/info' }, fetchTenantInfo)
|
||||
if (!userInfo)
|
||||
return null
|
||||
return userInfo?.providers?.find(({ token_is_set }) => token_is_set) ? null : <EditKeyDiv className='mb-8' />
|
||||
}
|
||||
|
||||
export const EditKeyPopover: FC = () => {
|
||||
const { data: userInfo } = useSWR({ url: '/info' }, fetchTenantInfo)
|
||||
const { mutate } = useSWRConfig()
|
||||
if (!userInfo)
|
||||
return null
|
||||
|
||||
const getTenantInfo = () => {
|
||||
mutate({ url: '/info' })
|
||||
}
|
||||
// In this case, the edit button is displayed
|
||||
const targetProvider = userInfo?.providers?.some(({ token_is_set, is_valid }) => token_is_set && is_valid)
|
||||
return (
|
||||
!targetProvider
|
||||
? <div className='flex items-center'>
|
||||
<Tag className='mr-2 h-fit' color='red'><ExclamationCircleIcon className='h-3.5 w-3.5 mr-2' />OpenAI API key invalid</Tag>
|
||||
<Popover
|
||||
htmlContent={<EditKeyDiv className='!border-0' showInPopover={true} getTenantInfo={getTenantInfo} />}
|
||||
trigger='click'
|
||||
position='br'
|
||||
btnElement='Edit'
|
||||
btnClassName='text-primary-600 !text-xs px-3 py-1.5'
|
||||
className='!p-0 !w-[464px] h-[200px]'
|
||||
/>
|
||||
</div>
|
||||
: null)
|
||||
}
|
||||
|
||||
export default WelcomeBanner
|
||||
@@ -0,0 +1,5 @@
|
||||
.app {
|
||||
height: calc(100vh - 56px);
|
||||
border-radius: 16px 16px 0px 0px;
|
||||
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.05), 0px 0px 2px -1px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
16
web/app/(commonLayout)/app/(appDetailLayout)/layout.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
export type IAppDetail = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const AppDetail: FC<IAppDetail> = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(AppDetail)
|
||||
76
web/app/(commonLayout)/apps/AppCard.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
'use client'
|
||||
|
||||
import { useContext, useContextSelector } from 'use-context-selector'
|
||||
import Link from 'next/link'
|
||||
import type { MouseEventHandler } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import style from '../list.module.css'
|
||||
import AppModeLabel from './AppModeLabel'
|
||||
import type { App } from '@/types/app'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { deleteApp } from '@/service/apps'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import AppsContext from '@/context/app-context'
|
||||
|
||||
export type AppCardProps = {
|
||||
app: App
|
||||
}
|
||||
|
||||
const AppCard = ({
|
||||
app,
|
||||
}: AppCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
|
||||
const mutateApps = useContextSelector(AppsContext, state => state.mutateApps)
|
||||
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||
const onDeleteClick: MouseEventHandler = useCallback((e) => {
|
||||
e.preventDefault()
|
||||
setShowConfirmDelete(true)
|
||||
}, [])
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
try {
|
||||
await deleteApp(app.id)
|
||||
notify({ type: 'success', message: t('app.appDeleted') })
|
||||
mutateApps()
|
||||
}
|
||||
catch (e: any) {
|
||||
notify({ type: 'error', message: `${t('app.appDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}` })
|
||||
}
|
||||
setShowConfirmDelete(false)
|
||||
}, [app.id])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link href={`/app/${app.id}/overview`} className={style.listItem}>
|
||||
<div className={style.listItemTitle}>
|
||||
<AppIcon size='small' />
|
||||
<div className={style.listItemHeading}>
|
||||
<div className={style.listItemHeadingContent}>{app.name}</div>
|
||||
</div>
|
||||
<span className={style.deleteAppIcon} onClick={onDeleteClick} />
|
||||
</div>
|
||||
<div className={style.listItemDescription}>{app.model_config?.pre_prompt}</div>
|
||||
<div className={style.listItemFooter}>
|
||||
<AppModeLabel mode={app.mode} />
|
||||
</div>
|
||||
|
||||
{showConfirmDelete && (
|
||||
<Confirm
|
||||
title={t('app.deleteAppConfirmTitle')}
|
||||
content={t('app.deleteAppConfirmContent')}
|
||||
isShow={showConfirmDelete}
|
||||
onClose={() => setShowConfirmDelete(false)}
|
||||
onConfirm={onConfirmDelete}
|
||||
onCancel={() => setShowConfirmDelete(false)}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppCard
|
||||
26
web/app/(commonLayout)/apps/AppModeLabel.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
'use client'
|
||||
|
||||
import classNames from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { type AppMode } from '@/types/app'
|
||||
import style from '../list.module.css'
|
||||
|
||||
export type AppModeLabelProps = {
|
||||
mode: AppMode
|
||||
className?: string
|
||||
}
|
||||
|
||||
const AppModeLabel = ({
|
||||
mode,
|
||||
className,
|
||||
}: AppModeLabelProps) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<span className={classNames('flex items-center w-fit h-6 gap-1 px-2 text-gray-500 text-xs border border-gray-100 rounded', className)}>
|
||||
<span className={classNames(style.listItemFooterIcon, mode === 'chat' && style.solidChatIcon, mode === 'completion' && style.solidCompletionIcon)} />
|
||||
{t(`app.modes.${mode}`)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppModeLabel
|
||||
23
web/app/(commonLayout)/apps/Apps.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import AppCard from './AppCard'
|
||||
import NewAppCard from './NewAppCard'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
||||
const Apps = () => {
|
||||
const { apps, mutateApps } = useAppContext()
|
||||
|
||||
useEffect(() => {
|
||||
mutateApps()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-8 sm:grid-cols-2 lg:grid-cols-4 grow shrink-0'>
|
||||
{apps.map(app => (<AppCard key={app.id} app={app} />))}
|
||||
<NewAppCard />
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default Apps
|
||||
29
web/app/(commonLayout)/apps/NewAppCard.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import style from '../list.module.css'
|
||||
import NewAppDialog from './NewAppDialog'
|
||||
|
||||
const CreateAppCard = () => {
|
||||
const { t } = useTranslation()
|
||||
const [showNewAppDialog, setShowNewAppDialog] = useState(false)
|
||||
|
||||
return (
|
||||
<a className={classNames(style.listItem, style.newItemCard)} onClick={() => setShowNewAppDialog(true)}>
|
||||
<div className={style.listItemTitle}>
|
||||
<span className={style.newItemIcon}>
|
||||
<span className={classNames(style.newItemIconImage, style.newItemIconAdd)} />
|
||||
</span>
|
||||
<div className={classNames(style.listItemHeading, style.newItemCardHeading)}>
|
||||
{t('app.createApp')}
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className='text-xs text-gray-500'>{t('app.createFromConfigFile')}</div> */}
|
||||
<NewAppDialog show={showNewAppDialog} onClose={() => setShowNewAppDialog(false)} />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateAppCard
|
||||
193
web/app/(commonLayout)/apps/NewAppDialog.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
'use client'
|
||||
|
||||
import type { MouseEventHandler } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import classNames from 'classnames'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useContext, useContextSelector } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import style from '../list.module.css'
|
||||
import AppModeLabel from './AppModeLabel'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Dialog from '@/app/components/base/dialog'
|
||||
import type { AppMode } from '@/types/app'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { createApp, fetchAppTemplates } from '@/service/apps'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import AppsContext from '@/context/app-context'
|
||||
|
||||
type NewAppDialogProps = {
|
||||
show: boolean
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
const NewAppDialog = ({ show, onClose }: NewAppDialogProps) => {
|
||||
const router = useRouter()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const nameInputRef = useRef<HTMLInputElement>(null)
|
||||
const [newAppMode, setNewAppMode] = useState<AppMode>()
|
||||
const [isWithTemplate, setIsWithTemplate] = useState(false)
|
||||
const [selectedTemplateIndex, setSelectedTemplateIndex] = useState<number>(-1)
|
||||
const mutateApps = useContextSelector(AppsContext, state => state.mutateApps)
|
||||
|
||||
const { data: templates, mutate } = useSWR({ url: '/app-templates' }, fetchAppTemplates)
|
||||
const mutateTemplates = useCallback(
|
||||
() => mutate(),
|
||||
[],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
mutateTemplates()
|
||||
setIsWithTemplate(false)
|
||||
}
|
||||
}, [show])
|
||||
|
||||
const isCreatingRef = useRef(false)
|
||||
const onCreate: MouseEventHandler = useCallback(async () => {
|
||||
const name = nameInputRef.current?.value
|
||||
if (!name) {
|
||||
notify({ type: 'error', message: t('app.newApp.nameNotEmpty') })
|
||||
return
|
||||
}
|
||||
if (!templates || (isWithTemplate && !(selectedTemplateIndex > -1))) {
|
||||
notify({ type: 'error', message: t('app.newApp.appTemplateNotSelected') })
|
||||
return
|
||||
}
|
||||
if (!isWithTemplate && !newAppMode) {
|
||||
notify({ type: 'error', message: t('app.newApp.appTypeRequired') })
|
||||
return
|
||||
}
|
||||
if (isCreatingRef.current)
|
||||
return
|
||||
isCreatingRef.current = true
|
||||
try {
|
||||
const app = await createApp({
|
||||
name,
|
||||
mode: isWithTemplate ? templates.data[selectedTemplateIndex].mode : newAppMode!,
|
||||
config: isWithTemplate ? templates.data[selectedTemplateIndex].model_config : undefined,
|
||||
})
|
||||
if (onClose)
|
||||
onClose()
|
||||
notify({ type: 'success', message: t('app.newApp.appCreated') })
|
||||
mutateApps()
|
||||
router.push(`/app/${app.id}/overview`)
|
||||
}
|
||||
catch (e) {
|
||||
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||
}
|
||||
isCreatingRef.current = false
|
||||
}, [isWithTemplate, newAppMode, notify, router, templates, selectedTemplateIndex])
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
show={show}
|
||||
title={t('app.newApp.startToCreate')}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onClose}>{t('app.newApp.Cancel')}</Button>
|
||||
<Button type="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<h3 className={style.newItemCaption}>{t('app.newApp.captionName')}</h3>
|
||||
|
||||
<div className='flex items-center justify-between gap-3 mb-8'>
|
||||
<AppIcon size='large' />
|
||||
<input ref={nameInputRef} className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow' />
|
||||
</div>
|
||||
|
||||
<div className='h-[247px]'>
|
||||
<div className={style.newItemCaption}>
|
||||
<h3 className='inline'>{t('app.newApp.captionAppType')}</h3>
|
||||
{isWithTemplate && (
|
||||
<>
|
||||
<span className='block ml-[9px] mr-[9px] w-[1px] h-[13px] bg-gray-200' />
|
||||
<span
|
||||
className='inline-flex items-center gap-1 text-xs font-medium cursor-pointer text-primary-600'
|
||||
onClick={() => setIsWithTemplate(false)}
|
||||
>
|
||||
{t('app.newApp.hideTemplates')}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isWithTemplate
|
||||
? (
|
||||
<ul className='grid grid-cols-2 gap-4'>
|
||||
{templates?.data?.map((template, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className={classNames(style.listItem, style.selectable, selectedTemplateIndex === index && style.selected)}
|
||||
onClick={() => setSelectedTemplateIndex(index)}
|
||||
>
|
||||
<div className={style.listItemTitle}>
|
||||
<AppIcon size='small' />
|
||||
<div className={style.listItemHeading}>
|
||||
<div className={style.listItemHeadingContent}>{template.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.listItemDescription}>{template.model_config?.pre_prompt}</div>
|
||||
<AppModeLabel mode={template.mode} className='mt-2' />
|
||||
{/* <AppModeLabel mode='chat' className='mt-2' /> */}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<ul className='grid grid-cols-2 gap-4'>
|
||||
<li
|
||||
className={classNames(style.listItem, style.selectable, newAppMode === 'chat' && style.selected)}
|
||||
onClick={() => setNewAppMode('chat')}
|
||||
>
|
||||
<div className={style.listItemTitle}>
|
||||
<span className={style.newItemIcon}>
|
||||
<span className={classNames(style.newItemIconImage, style.newItemIconChat)} />
|
||||
</span>
|
||||
<div className={style.listItemHeading}>
|
||||
<div className={style.listItemHeadingContent}>{t('app.newApp.chatApp')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.listItemDescription}>{t('app.newApp.chatAppIntro')}</div>
|
||||
<div className={classNames(style.listItemFooter, 'justify-end')}>
|
||||
<a className={style.listItemLink} href='https://udify.app/chat/7CQBa5yyvYLSkZtx' target='_blank'>{t('app.newApp.previewDemo')}<span className={classNames(style.linkIcon, style.grayLinkIcon)} /></a>
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
className={classNames(style.listItem, style.selectable, newAppMode === 'completion' && style.selected)}
|
||||
onClick={() => setNewAppMode('completion')}
|
||||
>
|
||||
<div className={style.listItemTitle}>
|
||||
<span className={style.newItemIcon}>
|
||||
<span className={classNames(style.newItemIconImage, style.newItemIconComplete)} />
|
||||
</span>
|
||||
<div className={style.listItemHeading}>
|
||||
<div className={style.listItemHeadingContent}>{t('app.newApp.completeApp')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.listItemDescription}>{t('app.newApp.completeAppIntro')}</div>
|
||||
<div className={classNames(style.listItemFooter, 'justify-end')}>
|
||||
<a className={style.listItemLink} href='https://udify.app/completion/aeFTj0VCb3Ok3TUE' target='_blank'>{t('app.newApp.previewDemo')}<span className={classNames(style.linkIcon, style.grayLinkIcon)} /></a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div className='flex items-center h-[34px] mt-2'>
|
||||
<span
|
||||
className='inline-flex items-center gap-1 text-xs font-medium cursor-pointer text-primary-600'
|
||||
onClick={() => setIsWithTemplate(true)}
|
||||
>
|
||||
{t('app.newApp.showTemplates')}<span className={style.rightIcon} />
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewAppDialog
|
||||
3
web/app/(commonLayout)/apps/assets/add.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 4V8M8 8V12M8 8H12M8 8H4" stroke="#6B7280" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 206 B |
4
web/app/(commonLayout)/apps/assets/chat-solid.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<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="M0.631586 8.25C0.631586 6.46656 2.04586 5 3.8158 5C5.58573 5 7.00001 6.46656 7.00001 8.25C7.00001 10.0334 5.58573 11.5 3.8158 11.5C3.45197 11.5 3.10149 11.4375 2.77474 11.3222C2.72073 11.3031 2.68723 11.2913 2.66266 11.2832C2.65821 11.2817 2.65456 11.2806 2.65164 11.2796L2.64892 11.2799C2.63177 11.2818 2.60839 11.285 2.56507 11.2909L1.06766 11.4954C0.905637 11.5175 0.743029 11.459 0.632239 11.3387C0.521449 11.2185 0.476481 11.0516 0.511825 10.8919L0.817497 9.51109C0.828118 9.46311 0.833802 9.43722 0.837453 9.41817C0.83766 9.4171 0.838022 9.41517 0.838022 9.41517C0.837114 9.412 0.835963 9.40808 0.834525 9.40332C0.826292 9.37605 0.814183 9.33888 0.794499 9.27863C0.688657 8.95463 0.631586 8.60857 0.631586 8.25Z" fill="#98A2B3"/>
|
||||
<path d="M2.57377 4.1863C2.96256 4.06535 3.37698 4 3.80894 4C6.16566 4 8.00006 5.94534 8.00006 8.24999C8.00006 8.65682 7.9429 9.05245 7.8358 9.42816C8.10681 9.37948 8.36964 9.30678 8.6219 9.21229C8.65748 9.19897 8.69298 9.18534 8.72893 9.17304C8.75795 9.17641 8.78684 9.18093 8.81574 9.18517L10.4222 9.42065C10.498 9.43179 10.5841 9.44444 10.6591 9.4487C10.7422 9.45343 10.8713 9.45292 11.0081 9.39408C11.1789 9.32061 11.3164 9.18628 11.3938 9.01716C11.4558 8.88174 11.4593 8.75269 11.4564 8.66955C11.4539 8.59442 11.4433 8.5081 11.4339 8.43202L11.2309 6.78307C11.2256 6.7402 11.2229 6.71768 11.2213 6.70118C11.23 6.66505 11.2466 6.6301 11.2598 6.59546C11.4492 6.09896 11.5526 5.56093 11.5526 5C11.5526 2.51163 9.52304 0.5 7.02632 0.5C4.80843 0.5 2.95915 2.08742 2.57377 4.1863Z" fill="#98A2B3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
3
web/app/(commonLayout)/apps/assets/chat.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.1667 6.66634H15.8333C16.2754 6.66634 16.6993 6.84194 17.0118 7.1545C17.3244 7.46706 17.5 7.89098 17.5 8.33301V13.333C17.5 13.775 17.3244 14.199 17.0118 14.5115C16.6993 14.8241 16.2754 14.9997 15.8333 14.9997H14.1667V18.333L10.8333 14.9997H7.5C7.28111 14.9999 7.06433 14.9569 6.86211 14.8731C6.6599 14.7893 6.47623 14.6663 6.32167 14.5113M6.32167 14.5113L9.16667 11.6663H12.5C12.942 11.6663 13.366 11.4907 13.6785 11.1782C13.9911 10.8656 14.1667 10.4417 14.1667 9.99967V4.99967C14.1667 4.55765 13.9911 4.13372 13.6785 3.82116C13.366 3.5086 12.942 3.33301 12.5 3.33301H4.16667C3.72464 3.33301 3.30072 3.5086 2.98816 3.82116C2.67559 4.13372 2.5 4.55765 2.5 4.99967V9.99967C2.5 10.4417 2.67559 10.8656 2.98816 11.1782C3.30072 11.4907 3.72464 11.6663 4.16667 11.6663H5.83333V14.9997L6.32167 14.5113Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1002 B |
4
web/app/(commonLayout)/apps/assets/completion-solid.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<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="M6.5 1.00779C6.5 0.994638 6.5 0.988062 6.49943 0.976137C6.48764 0.729248 6.27052 0.51224 6.02363 0.50056C6.01171 0.499996 6.0078 0.499998 6.00001 0.5H4.37933C3.97686 0.499995 3.64468 0.49999 3.37409 0.522098C3.09304 0.545061 2.83469 0.594343 2.59202 0.717989C2.2157 0.909735 1.90973 1.2157 1.71799 1.59202C1.59434 1.83469 1.54506 2.09304 1.5221 2.37409C1.49999 2.64468 1.49999 2.97686 1.5 3.37934V8.62066C1.49999 9.02313 1.49999 9.35532 1.5221 9.62591C1.54506 9.90696 1.59434 10.1653 1.71799 10.408C1.90973 10.7843 2.2157 11.0903 2.59202 11.282C2.83469 11.4057 3.09304 11.4549 3.37409 11.4779C3.64468 11.5 3.97686 11.5 4.37934 11.5H7.62066C8.02314 11.5 8.35532 11.5 8.62591 11.4779C8.90696 11.4549 9.16531 11.4057 9.40798 11.282C9.78431 11.0903 10.0903 10.7843 10.282 10.408C10.4057 10.1653 10.4549 9.90696 10.4779 9.62591C10.5 9.35532 10.5 9.02314 10.5 8.62066V4.99997C10.5 4.9922 10.5 4.98832 10.4994 4.97641C10.4878 4.72949 10.2707 4.51236 10.0238 4.50057C10.0119 4.50001 10.0054 4.50001 9.99225 4.50001L7.78404 4.50001C7.65786 4.50002 7.53496 4.50004 7.43089 4.49153C7.31659 4.48219 7.18172 4.46016 7.04601 4.39101C6.85785 4.29514 6.70487 4.14216 6.609 3.954C6.53985 3.81828 6.51781 3.68342 6.50848 3.56912C6.49997 3.46504 6.49999 3.34215 6.5 3.21596L6.5 1.00779ZM4 6.5C3.72386 6.5 3.5 6.72386 3.5 7C3.5 7.27614 3.72386 7.5 4 7.5H8C8.27614 7.5 8.5 7.27614 8.5 7C8.5 6.72386 8.27614 6.5 8 6.5H4ZM4 8.5C3.72386 8.5 3.5 8.72386 3.5 9C3.5 9.27614 3.72386 9.5 4 9.5H7C7.27614 9.5 7.5 9.27614 7.5 9C7.5 8.72386 7.27614 8.5 7 8.5H4Z" fill="#98A2B3"/>
|
||||
<path d="M9.45398 3.5C9.60079 3.5 9.67419 3.5 9.73432 3.46314C9.81925 3.41107 9.87002 3.28842 9.84674 3.19157C9.83025 3.12299 9.78238 3.07516 9.68665 2.97952L8.02049 1.31336C7.92484 1.21762 7.87701 1.16975 7.80843 1.15326C7.71158 1.12998 7.58893 1.18075 7.53687 1.26567C7.5 1.3258 7.5 1.39921 7.5 1.54602L7.5 3.09998C7.5 3.23999 7.5 3.30999 7.52725 3.36347C7.55122 3.41051 7.58946 3.44876 7.6365 3.47272C7.68998 3.49997 7.75998 3.49997 7.9 3.49998L9.45398 3.5Z" fill="#98A2B3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
3
web/app/(commonLayout)/apps/assets/completion.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.25 11.875V9.6875C16.25 8.1342 14.9908 6.875 13.4375 6.875H12.1875C11.6697 6.875 11.25 6.45527 11.25 5.9375V4.6875C11.25 3.1342 9.9908 1.875 8.4375 1.875H6.875M6.875 12.5H13.125M6.875 15H10M8.75 1.875H4.6875C4.16973 1.875 3.75 2.29473 3.75 2.8125V17.1875C3.75 17.7053 4.16973 18.125 4.6875 18.125H15.3125C15.8303 18.125 16.25 17.7053 16.25 17.1875V9.375C16.25 5.23286 12.8921 1.875 8.75 1.875Z" stroke="#1F2A37" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 595 B |
3
web/app/(commonLayout)/apps/assets/delete.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.6665 7.33333V11.3333M9.33317 7.33333V11.3333M2.6665 4.66667H13.3332M12.6665 4.66667L12.0885 12.7613C12.0646 13.0977 11.914 13.4125 11.6672 13.6424C11.4205 13.8722 11.0957 14 10.7585 14H5.24117C4.90393 14 4.57922 13.8722 4.33243 13.6424C4.08564 13.4125 3.93511 13.0977 3.91117 12.7613L3.33317 4.66667H12.6665ZM9.99984 4.66667V2.66667C9.99984 2.48986 9.9296 2.32029 9.80457 2.19526C9.67955 2.07024 9.50998 2 9.33317 2H6.6665C6.48969 2 6.32012 2.07024 6.1951 2.19526C6.07008 2.32029 5.99984 2.48986 5.99984 2.66667V4.66667H9.99984Z" stroke="#1F2A37" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 711 B |
3
web/app/(commonLayout)/apps/assets/discord.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.0101 4.50191C20.3529 3.74154 18.5759 3.18133 16.7179 2.86048C16.6841 2.85428 16.6503 2.86976 16.6328 2.90071C16.4043 3.30719 16.1511 3.83748 15.9738 4.25429C13.9754 3.95511 11.9873 3.95511 10.0298 4.25429C9.85253 3.82822 9.59019 3.30719 9.36062 2.90071C9.34319 2.87079 9.30939 2.85532 9.27555 2.86048C7.41857 3.18031 5.64152 3.74051 3.98335 4.50191C3.96899 4.5081 3.95669 4.51843 3.94852 4.53183C0.577841 9.56755 -0.345529 14.4795 0.107445 19.3306C0.109495 19.3543 0.122817 19.377 0.141265 19.3914C2.36514 21.0246 4.51935 22.0161 6.63355 22.6732C6.66739 22.6836 6.70324 22.6712 6.72477 22.6433C7.22489 21.9604 7.6707 21.2402 8.05293 20.4829C8.07549 20.4386 8.05396 20.386 8.00785 20.3684C7.30073 20.1002 6.6274 19.7731 5.97971 19.4017C5.92848 19.3718 5.92437 19.2985 5.9715 19.2635C6.1078 19.1613 6.24414 19.0551 6.37428 18.9478C6.39783 18.9282 6.43064 18.924 6.45833 18.9364C10.7134 20.8791 15.32 20.8791 19.5249 18.9364C19.5525 18.923 19.5854 18.9272 19.6099 18.9467C19.7401 19.054 19.8764 19.1613 20.0137 19.2635C20.0609 19.2985 20.0578 19.3718 20.0066 19.4017C19.3589 19.7804 18.6855 20.1002 17.9774 20.3674C17.9313 20.3849 17.9108 20.4386 17.9333 20.4829C18.3238 21.2392 18.7696 21.9593 19.2605 22.6423C19.281 22.6712 19.3179 22.6836 19.3517 22.6732C21.4761 22.0161 23.6303 21.0246 25.8542 19.3914C25.8737 19.377 25.886 19.3553 25.8881 19.3316C26.4302 13.7232 24.98 8.85156 22.0439 4.53286C22.0367 4.51843 22.0245 4.5081 22.0101 4.50191ZM8.68836 16.3768C7.40729 16.3768 6.35173 15.2007 6.35173 13.7563C6.35173 12.3119 7.38682 11.1358 8.68836 11.1358C10.0001 11.1358 11.0455 12.3222 11.025 13.7563C11.025 15.2007 9.98986 16.3768 8.68836 16.3768ZM17.3276 16.3768C16.0466 16.3768 14.991 15.2007 14.991 13.7563C14.991 12.3119 16.0261 11.1358 17.3276 11.1358C18.6394 11.1358 19.6847 12.3222 19.6643 13.7563C19.6643 15.2007 18.6394 16.3768 17.3276 16.3768Z" fill="#5865F2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
17
web/app/(commonLayout)/apps/assets/github.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_131_1011)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.0003 0.5C9.15149 0.501478 6.39613 1.51046 4.22687 3.34652C2.05761 5.18259 0.615903 7.72601 0.159545 10.522C-0.296814 13.318 0.261927 16.1842 1.73587 18.6082C3.20981 21.0321 5.50284 22.8558 8.20493 23.753C8.80105 23.8636 9.0256 23.4941 9.0256 23.18C9.0256 22.8658 9.01367 21.955 9.0097 20.9592C5.6714 21.6804 4.96599 19.5505 4.96599 19.5505C4.42152 18.1674 3.63464 17.8039 3.63464 17.8039C2.54571 17.065 3.71611 17.0788 3.71611 17.0788C4.92227 17.1637 5.55616 18.3097 5.55616 18.3097C6.62521 20.1333 8.36389 19.6058 9.04745 19.2976C9.15475 18.5251 9.46673 17.9995 9.8105 17.7012C7.14383 17.4008 4.34204 16.3774 4.34204 11.8054C4.32551 10.6197 4.76802 9.47305 5.57801 8.60268C5.45481 8.30236 5.04348 7.08923 5.69524 5.44143C5.69524 5.44143 6.7027 5.12135 8.9958 6.66444C10.9627 6.12962 13.0379 6.12962 15.0047 6.66444C17.2958 5.12135 18.3013 5.44143 18.3013 5.44143C18.9551 7.08528 18.5437 8.29841 18.4205 8.60268C19.2331 9.47319 19.6765 10.6218 19.6585 11.8094C19.6585 16.3912 16.8507 17.4008 14.1801 17.6952C14.6093 18.0667 14.9928 18.7918 14.9928 19.9061C14.9928 21.5026 14.9789 22.7868 14.9789 23.18C14.9789 23.4981 15.1955 23.8695 15.8035 23.753C18.5059 22.8557 20.7992 21.0317 22.2731 18.6073C23.747 16.183 24.3055 13.3163 23.8486 10.5201C23.3917 7.7238 21.9493 5.18035 19.7793 3.34461C17.6093 1.50886 14.8533 0.500541 12.0042 0.5H12.0003Z" fill="#191717"/>
|
||||
<path d="M4.54444 17.6321C4.5186 17.6914 4.42322 17.7092 4.34573 17.6677C4.26823 17.6262 4.21061 17.5491 4.23843 17.4879C4.26625 17.4266 4.35964 17.4108 4.43714 17.4523C4.51463 17.4938 4.57424 17.5729 4.54444 17.6321Z" fill="#191717"/>
|
||||
<path d="M5.03123 18.1714C4.99008 18.192 4.943 18.1978 4.89805 18.1877C4.8531 18.1776 4.81308 18.1523 4.78483 18.1161C4.70734 18.0331 4.69143 17.9185 4.75104 17.8671C4.81066 17.8157 4.91797 17.8395 4.99546 17.9224C5.07296 18.0054 5.09084 18.12 5.03123 18.1714Z" fill="#191717"/>
|
||||
<path d="M5.50425 18.857C5.43072 18.9084 5.30553 18.857 5.23598 18.7543C5.21675 18.7359 5.20146 18.7138 5.19101 18.6893C5.18056 18.6649 5.17517 18.6386 5.17517 18.612C5.17517 18.5855 5.18056 18.5592 5.19101 18.5347C5.20146 18.5103 5.21675 18.4882 5.23598 18.4698C5.3095 18.4204 5.4347 18.4698 5.50425 18.5705C5.57379 18.6713 5.57578 18.8057 5.50425 18.857V18.857Z" fill="#191717"/>
|
||||
<path d="M6.14612 19.5207C6.08054 19.5939 5.94741 19.5741 5.83812 19.4753C5.72883 19.3765 5.70299 19.2422 5.76857 19.171C5.83414 19.0999 5.96727 19.1197 6.08054 19.2165C6.1938 19.3133 6.21566 19.4496 6.14612 19.5207V19.5207Z" fill="#191717"/>
|
||||
<path d="M7.04617 19.9081C7.01637 20.001 6.88124 20.0425 6.74612 20.003C6.611 19.9635 6.52158 19.8528 6.54741 19.758C6.57325 19.6631 6.71036 19.6197 6.84747 19.6631C6.98457 19.7066 7.07201 19.8113 7.04617 19.9081Z" fill="#191717"/>
|
||||
<path d="M8.02783 19.9752C8.02783 20.072 7.91656 20.155 7.77349 20.1569C7.63042 20.1589 7.51318 20.0799 7.51318 19.9831C7.51318 19.8863 7.62445 19.8033 7.76752 19.8013C7.91059 19.7993 8.02783 19.8764 8.02783 19.9752Z" fill="#191717"/>
|
||||
<path d="M8.9419 19.8232C8.95978 19.92 8.86042 20.0207 8.71735 20.0445C8.57428 20.0682 8.4491 20.0109 8.43121 19.916C8.41333 19.8212 8.51666 19.7185 8.65576 19.6928C8.79485 19.6671 8.92401 19.7264 8.9419 19.8232Z" fill="#191717"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_131_1011">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
3
web/app/(commonLayout)/apps/assets/link-gray.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="13" height="14" viewBox="0 0 13 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.41663 3.75033H3.24996C2.96264 3.75033 2.68709 3.86446 2.48393 4.06763C2.28076 4.27079 2.16663 4.54634 2.16663 4.83366V10.2503C2.16663 10.5376 2.28076 10.8132 2.48393 11.0164C2.68709 11.2195 2.96264 11.3337 3.24996 11.3337H8.66663C8.95394 11.3337 9.22949 11.2195 9.43266 11.0164C9.63582 10.8132 9.74996 10.5376 9.74996 10.2503V8.08366M7.58329 2.66699H10.8333M10.8333 2.66699V5.91699M10.8333 2.66699L5.41663 8.08366" stroke="#9CA3AF" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 596 B |
3
web/app/(commonLayout)/apps/assets/link.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="13" height="14" viewBox="0 0 13 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.41663 3.75008H3.24996C2.96264 3.75008 2.68709 3.86422 2.48393 4.06738C2.28076 4.27055 2.16663 4.5461 2.16663 4.83341V10.2501C2.16663 10.5374 2.28076 10.8129 2.48393 11.0161C2.68709 11.2193 2.96264 11.3334 3.24996 11.3334H8.66663C8.95394 11.3334 9.22949 11.2193 9.43266 11.0161C9.63582 10.8129 9.74996 10.5374 9.74996 10.2501V8.08341M7.58329 2.66675H10.8333M10.8333 2.66675V5.91675M10.8333 2.66675L5.41663 8.08341" stroke="#1C64F2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 595 B |
3
web/app/(commonLayout)/apps/assets/right-arrow.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 2.5L10.5 6M10.5 6L7 9.5M10.5 6H1.5" stroke="#1C64F2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 217 B |
36
web/app/(commonLayout)/apps/page.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import classNames from 'classnames'
|
||||
import style from '../list.module.css'
|
||||
import Apps from './Apps'
|
||||
import { getLocaleOnServer } from '@/i18n/server'
|
||||
import { useTranslation } from '@/i18n/i18next-serverside-config'
|
||||
|
||||
const AppList = async () => {
|
||||
const locale = getLocaleOnServer()
|
||||
const { t } = await useTranslation(locale, 'app')
|
||||
|
||||
return (
|
||||
<div className='flex flex-col overflow-auto bg-gray-100 shrink-0 grow'>
|
||||
<Apps />
|
||||
<footer className='px-12 py-6 grow-0 shrink-0'>
|
||||
<h3 className='text-xl font-semibold leading-tight text-gradient'>{t('join')}</h3>
|
||||
<p className='mt-1 text-sm font-normal leading-tight text-gray-700'>{t('communityIntro')}</p>
|
||||
{/*<p className='mt-3 text-sm'>*/}
|
||||
{/* <a className='inline-flex items-center gap-1 link' target='_blank' href={`https://docs.dify.ai${locale === 'en' ? '' : '/v/zh-hans'}/community/product-roadmap`}>*/}
|
||||
{/* {t('roadmap')}*/}
|
||||
{/* <span className={style.linkIcon} />*/}
|
||||
{/* </a>*/}
|
||||
{/*</p>*/}
|
||||
<div className='flex items-center gap-2 mt-3'>
|
||||
<a className={style.socialMediaLink} target='_blank' href='https://github.com/langgenius'><span className={classNames(style.socialMediaIcon, style.githubIcon)} /></a>
|
||||
<a className={style.socialMediaLink} target='_blank' href='https://discord.gg/AhzKf7dNgk'><span className={classNames(style.socialMediaIcon, style.discordIcon)} /></a>
|
||||
</div>
|
||||
</footer>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: 'Apps - Dify',
|
||||
}
|
||||
|
||||
export default AppList
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
|
||||
type Props = {}
|
||||
|
||||
const page = (props: Props) => {
|
||||
return (
|
||||
<div>dataset detail api</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default page
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import MainDetail from '@/app/components/datasets/documents/detail'
|
||||
|
||||
export type IDocumentDetailProps = {
|
||||
params: { datasetId: string; documentId: string }
|
||||
}
|
||||
|
||||
const DocumentDetail = async ({
|
||||
params: { datasetId, documentId },
|
||||
}: IDocumentDetailProps) => {
|
||||
return (
|
||||
<MainDetail datasetId={datasetId} documentId={documentId} />
|
||||
)
|
||||
}
|
||||
|
||||
export default DocumentDetail
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import DatasetUpdateForm from '@/app/components/datasets/create'
|
||||
|
||||
export type IProps = {
|
||||
params: { datasetId: string }
|
||||
}
|
||||
|
||||
const Create = async ({
|
||||
params: { datasetId },
|
||||
}: IProps) => {
|
||||
return (
|
||||
<DatasetUpdateForm datasetId={datasetId} />
|
||||
)
|
||||
}
|
||||
|
||||
export default Create
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import Main from '@/app/components/datasets/documents'
|
||||
|
||||
export type IProps = {
|
||||
params: { datasetId: string }
|
||||
}
|
||||
|
||||
const Documents = async ({
|
||||
params: { datasetId },
|
||||
}: IProps) => {
|
||||
return (
|
||||
<Main datasetId={datasetId} />
|
||||
)
|
||||
}
|
||||
|
||||
export default Documents
|
||||
@@ -0,0 +1,9 @@
|
||||
.logTable td {
|
||||
padding: 7px 8px;
|
||||
box-sizing: border-box;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.pagination li {
|
||||
list-style: none;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import Main from '@/app/components/datasets/hit-testing'
|
||||
|
||||
type Props = {
|
||||
params: { datasetId: string }
|
||||
}
|
||||
|
||||
const HitTesting = ({
|
||||
params: { datasetId },
|
||||
}: Props) => {
|
||||
return (
|
||||
<Main datasetId={datasetId} />
|
||||
)
|
||||
}
|
||||
|
||||
export default HitTesting
|
||||
@@ -0,0 +1,169 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { usePathname, useSelectedLayoutSegments } from 'next/navigation'
|
||||
import useSWR from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { getLocaleOnClient } from '@/i18n/client'
|
||||
import {
|
||||
Cog8ToothIcon,
|
||||
// CommandLineIcon,
|
||||
Squares2X2Icon,
|
||||
PuzzlePieceIcon,
|
||||
DocumentTextIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
import {
|
||||
Cog8ToothIcon as Cog8ToothSolidIcon,
|
||||
// CommandLineIcon as CommandLineSolidIcon,
|
||||
DocumentTextIcon as DocumentTextSolidIcon,
|
||||
} from '@heroicons/react/24/solid'
|
||||
import Link from 'next/link'
|
||||
import { fetchDataDetail, fetchDatasetRelatedApps } from '@/service/datasets'
|
||||
import type { RelatedApp } from '@/models/datasets'
|
||||
import s from './style.module.css'
|
||||
import AppSideBar from '@/app/components/app-sidebar'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import DatasetDetailContext from '@/context/dataset-detail'
|
||||
|
||||
// import { fetchDatasetDetail } from '@/service/datasets'
|
||||
|
||||
export type IAppDetailLayoutProps = {
|
||||
children: React.ReactNode
|
||||
params: { datasetId: string }
|
||||
}
|
||||
|
||||
const LikedItem: FC<{ type?: 'plugin' | 'app'; appStatus?: boolean; detail: RelatedApp }> = ({
|
||||
type = 'app',
|
||||
appStatus = true,
|
||||
detail
|
||||
}) => {
|
||||
return (
|
||||
<Link prefetch className={s.itemWrapper} href={`/app/${detail?.id}/overview`}>
|
||||
<div className={s.iconWrapper}>
|
||||
<AppIcon size='tiny' />
|
||||
{type === 'app' && (
|
||||
<div className={s.statusPoint}>
|
||||
<Indicator color={appStatus ? 'green' : 'gray'} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={s.appInfo}>{detail?.name || '--'}</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const TargetIcon: 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 ?? ''}>
|
||||
<g clip-path="url(#clip0_4610_6951)">
|
||||
<path d="M10.6666 5.33325V3.33325L12.6666 1.33325L13.3332 2.66659L14.6666 3.33325L12.6666 5.33325H10.6666ZM10.6666 5.33325L7.9999 7.99988M14.6666 7.99992C14.6666 11.6818 11.6818 14.6666 7.99992 14.6666C4.31802 14.6666 1.33325 11.6818 1.33325 7.99992C1.33325 4.31802 4.31802 1.33325 7.99992 1.33325M11.3333 7.99992C11.3333 9.84087 9.84087 11.3333 7.99992 11.3333C6.15897 11.3333 4.66659 9.84087 4.66659 7.99992C4.66659 6.15897 6.15897 4.66659 7.99992 4.66659" stroke="#344054" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_4610_6951">
|
||||
<rect width="16" height="16" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
}
|
||||
|
||||
const TargetSolidIcon: 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 fill-rule="evenodd" clip-rule="evenodd" d="M12.7733 0.67512C12.9848 0.709447 13.1669 0.843364 13.2627 1.03504L13.83 2.16961L14.9646 2.73689C15.1563 2.83273 15.2902 3.01486 15.3245 3.22639C15.3588 3.43792 15.2894 3.65305 15.1379 3.80458L13.1379 5.80458C13.0128 5.92961 12.8433 5.99985 12.6665 5.99985H10.9426L8.47124 8.47124C8.21089 8.73159 7.78878 8.73159 7.52843 8.47124C7.26808 8.21089 7.26808 7.78878 7.52843 7.52843L9.9998 5.05707V3.33318C9.9998 3.15637 10.07 2.9868 10.1951 2.86177L12.1951 0.861774C12.3466 0.710244 12.5617 0.640794 12.7733 0.67512Z" fill="#155EEF" />
|
||||
<path d="M1.99984 7.99984C1.99984 4.68613 4.68613 1.99984 7.99984 1.99984C8.36803 1.99984 8.6665 1.70136 8.6665 1.33317C8.6665 0.964981 8.36803 0.666504 7.99984 0.666504C3.94975 0.666504 0.666504 3.94975 0.666504 7.99984C0.666504 12.0499 3.94975 15.3332 7.99984 15.3332C12.0499 15.3332 15.3332 12.0499 15.3332 7.99984C15.3332 7.63165 15.0347 7.33317 14.6665 7.33317C14.2983 7.33317 13.9998 7.63165 13.9998 7.99984C13.9998 11.3135 11.3135 13.9998 7.99984 13.9998C4.68613 13.9998 1.99984 11.3135 1.99984 7.99984Z" fill="#155EEF" />
|
||||
<path d="M5.33317 7.99984C5.33317 6.52708 6.52708 5.33317 7.99984 5.33317C8.36803 5.33317 8.6665 5.03469 8.6665 4.6665C8.6665 4.29831 8.36803 3.99984 7.99984 3.99984C5.7907 3.99984 3.99984 5.7907 3.99984 7.99984C3.99984 10.209 5.7907 11.9998 7.99984 11.9998C10.209 11.9998 11.9998 10.209 11.9998 7.99984C11.9998 7.63165 11.7014 7.33317 11.3332 7.33317C10.965 7.33317 10.6665 7.63165 10.6665 7.99984C10.6665 9.4726 9.4726 10.6665 7.99984 10.6665C6.52708 10.6665 5.33317 9.4726 5.33317 7.99984Z" fill="#155EEF" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
const BookOpenIcon: 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 opacity="0.12" d="M1 3.1C1 2.53995 1 2.25992 1.10899 2.04601C1.20487 1.85785 1.35785 1.70487 1.54601 1.60899C1.75992 1.5 2.03995 1.5 2.6 1.5H2.8C3.9201 1.5 4.48016 1.5 4.90798 1.71799C5.28431 1.90973 5.59027 2.21569 5.78201 2.59202C6 3.01984 6 3.5799 6 4.7V10.5L5.94997 10.425C5.60265 9.90398 5.42899 9.64349 5.19955 9.45491C4.99643 9.28796 4.76238 9.1627 4.5108 9.0863C4.22663 9 3.91355 9 3.28741 9H2.6C2.03995 9 1.75992 9 1.54601 8.89101C1.35785 8.79513 1.20487 8.64215 1.10899 8.45399C1 8.24008 1 7.96005 1 7.4V3.1Z" fill="#155EEF" />
|
||||
<path d="M6 10.5L5.94997 10.425C5.60265 9.90398 5.42899 9.64349 5.19955 9.45491C4.99643 9.28796 4.76238 9.1627 4.5108 9.0863C4.22663 9 3.91355 9 3.28741 9H2.6C2.03995 9 1.75992 9 1.54601 8.89101C1.35785 8.79513 1.20487 8.64215 1.10899 8.45399C1 8.24008 1 7.96005 1 7.4V3.1C1 2.53995 1 2.25992 1.10899 2.04601C1.20487 1.85785 1.35785 1.70487 1.54601 1.60899C1.75992 1.5 2.03995 1.5 2.6 1.5H2.8C3.9201 1.5 4.48016 1.5 4.90798 1.71799C5.28431 1.90973 5.59027 2.21569 5.78201 2.59202C6 3.01984 6 3.5799 6 4.7M6 10.5V4.7M6 10.5L6.05003 10.425C6.39735 9.90398 6.57101 9.64349 6.80045 9.45491C7.00357 9.28796 7.23762 9.1627 7.4892 9.0863C7.77337 9 8.08645 9 8.71259 9H9.4C9.96005 9 10.2401 9 10.454 8.89101C10.6422 8.79513 10.7951 8.64215 10.891 8.45399C11 8.24008 11 7.96005 11 7.4V3.1C11 2.53995 11 2.25992 10.891 2.04601C10.7951 1.85785 10.6422 1.70487 10.454 1.60899C10.2401 1.5 9.96005 1.5 9.4 1.5H9.2C8.07989 1.5 7.51984 1.5 7.09202 1.71799C6.71569 1.90973 6.40973 2.21569 6.21799 2.59202C6 3.01984 6 3.5799 6 4.7" stroke="#155EEF" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
const {
|
||||
children,
|
||||
params: { datasetId },
|
||||
} = props
|
||||
const pathname = usePathname()
|
||||
const hideSideBar = /documents\/create$/.test(pathname)
|
||||
const { t } = useTranslation()
|
||||
const { data: datasetRes, error } = useSWR({
|
||||
action: 'fetchDataDetail',
|
||||
datasetId,
|
||||
}, apiParams => fetchDataDetail(apiParams.datasetId))
|
||||
|
||||
const { data: relatedApps } = useSWR({
|
||||
action: 'fetchDatasetRelatedApps',
|
||||
datasetId,
|
||||
}, apiParams => fetchDatasetRelatedApps(apiParams.datasetId))
|
||||
|
||||
const navigation = [
|
||||
{ name: t('common.datasetMenus.documents'), href: `/datasets/${datasetId}/documents`, icon: DocumentTextIcon, selectedIcon: DocumentTextSolidIcon },
|
||||
{ name: t('common.datasetMenus.hitTesting'), href: `/datasets/${datasetId}/hitTesting`, icon: TargetIcon, selectedIcon: TargetSolidIcon },
|
||||
// { name: 'api & webhook', href: `/datasets/${datasetId}/api`, icon: CommandLineIcon, selectedIcon: CommandLineSolidIcon },
|
||||
{ name: t('common.datasetMenus.settings'), href: `/datasets/${datasetId}/settings`, icon: Cog8ToothIcon, selectedIcon: Cog8ToothSolidIcon },
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
if (datasetRes) {
|
||||
document.title = `${datasetRes.name || 'Dataset'} - Dify`
|
||||
}
|
||||
}, [datasetRes])
|
||||
|
||||
const ExtraInfo: FC = () => {
|
||||
const locale = getLocaleOnClient()
|
||||
|
||||
return <div className='w-full'>
|
||||
<Divider className='mt-5' />
|
||||
{relatedApps?.data?.length ? (
|
||||
<>
|
||||
<div className={s.subTitle}>{relatedApps?.total || '--'} {t('common.datasetMenus.relatedApp')}</div>
|
||||
{relatedApps?.data?.map((item) => (<LikedItem detail={item} />))}
|
||||
</>
|
||||
) : (
|
||||
<div className='mt-5 p-3'>
|
||||
<div className='flex items-center justify-start gap-2'>
|
||||
<div className={s.emptyIconDiv}>
|
||||
<Squares2X2Icon className='w-3 h-3 text-gray-500' />
|
||||
</div>
|
||||
<div className={s.emptyIconDiv}>
|
||||
<PuzzlePieceIcon className='w-3 h-3 text-gray-500' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='text-xs text-gray-500 mt-2'>{t('common.datasetMenus.emptyTip')}</div>
|
||||
<a
|
||||
className='inline-flex items-center text-xs text-primary-600 mt-2 cursor-pointer'
|
||||
href={`https://docs.dify.ai/${locale === 'en' ? '' : 'v/zh-hans'}/application/prompt-engineering`}
|
||||
target='_blank'
|
||||
>
|
||||
<BookOpenIcon className='mr-1' />
|
||||
{t('common.datasetMenus.viewDoc')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
if (!datasetRes && !error)
|
||||
return <Loading />
|
||||
|
||||
return (
|
||||
<div className='flex' style={{ height: 'calc(100vh - 56px)' }}>
|
||||
{!hideSideBar && <AppSideBar
|
||||
title={datasetRes?.name || '--'}
|
||||
desc={datasetRes?.description || '--'}
|
||||
navigation={navigation}
|
||||
extraInfo={<ExtraInfo />}
|
||||
iconType='dataset'
|
||||
/>}
|
||||
<DatasetDetailContext.Provider value={{ indexingTechnique: datasetRes?.indexing_technique }}>
|
||||
<div className="bg-white grow">{children}</div>
|
||||
</DatasetDetailContext.Provider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(DatasetDetailLayout)
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
import { getLocaleOnServer } from '@/i18n/server'
|
||||
import { useTranslation } from '@/i18n/i18next-serverside-config'
|
||||
import Form from '@/app/components/datasets/settings/form'
|
||||
|
||||
const Settings = async () => {
|
||||
const locale = getLocaleOnServer()
|
||||
const { t } = await useTranslation(locale, 'dataset-settings')
|
||||
|
||||
return (
|
||||
<div className='bg-white h-full'>
|
||||
<div className='px-6 py-3'>
|
||||
<div className='mb-1 text-lg font-semibold text-gray-900'>{t('title')}</div>
|
||||
<div className='text-sm text-gray-500'>{t('desc')}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Form />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Settings
|
||||
@@ -0,0 +1,18 @@
|
||||
.itemWrapper {
|
||||
@apply flex items-center w-full h-10 px-3 rounded-lg hover:bg-gray-50 cursor-pointer;
|
||||
}
|
||||
.appInfo {
|
||||
@apply truncate text-gray-700 text-sm font-normal;
|
||||
}
|
||||
.iconWrapper {
|
||||
@apply relative w-6 h-6 mr-2 bg-[#D5F5F6] rounded-md;
|
||||
}
|
||||
.statusPoint {
|
||||
@apply flex justify-center items-center absolute -right-0.5 -bottom-0.5 w-2.5 h-2.5 bg-white rounded;
|
||||
}
|
||||
.subTitle {
|
||||
@apply uppercase text-xs text-gray-500 font-medium px-3 pb-2 pt-4;
|
||||
}
|
||||
.emptyIconDiv {
|
||||
@apply h-7 w-7 bg-gray-50 border border-[#EAECF5] inline-flex justify-center items-center rounded-lg;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
export type IDatasetDetail = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const AppDetail: FC<IDatasetDetail> = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(AppDetail)
|
||||
89
web/app/(commonLayout)/datasets/DatasetCard.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
'use client'
|
||||
|
||||
import { useContext, useContextSelector } from 'use-context-selector'
|
||||
import Link from 'next/link'
|
||||
import useSWR from 'swr'
|
||||
import type { MouseEventHandler } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import style from '../list.module.css'
|
||||
import type { App } from '@/types/app'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { deleteDataset, fetchDatasets } from '@/service/datasets'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import AppsContext from '@/context/app-context'
|
||||
import { DataSet } from '@/models/datasets'
|
||||
import classNames from 'classnames'
|
||||
|
||||
export type DatasetCardProps = {
|
||||
dataset: DataSet
|
||||
}
|
||||
|
||||
const DatasetCard = ({
|
||||
dataset,
|
||||
}: DatasetCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
|
||||
const { mutate: mutateDatasets } = useSWR({ url: '/datasets', params: { page: 1 } }, fetchDatasets)
|
||||
|
||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||
const onDeleteClick: MouseEventHandler = useCallback((e) => {
|
||||
e.preventDefault()
|
||||
setShowConfirmDelete(true)
|
||||
}, [])
|
||||
const onConfirmDelete = useCallback(async () => {
|
||||
try {
|
||||
await deleteDataset(dataset.id)
|
||||
notify({ type: 'success', message: t('dataset.datasetDeleted') })
|
||||
mutateDatasets()
|
||||
}
|
||||
catch (e: any) {
|
||||
notify({ type: 'error', message: `${t('dataset.datasetDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}` })
|
||||
}
|
||||
setShowConfirmDelete(false)
|
||||
}, [dataset.id])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link href={`/datasets/${dataset.id}/documents`} className={style.listItem}>
|
||||
<div className={style.listItemTitle}>
|
||||
<AppIcon size='small' />
|
||||
<div className={style.listItemHeading}>
|
||||
<div className={style.listItemHeadingContent}>{dataset.name}</div>
|
||||
</div>
|
||||
<span className={style.deleteAppIcon} onClick={onDeleteClick} />
|
||||
</div>
|
||||
<div className={style.listItemDescription}>{dataset.description}</div>
|
||||
<div className={classNames(style.listItemFooter, style.datasetCardFooter)}>
|
||||
<span className={style.listItemStats}>
|
||||
<span className={classNames(style.listItemFooterIcon, style.docIcon)} />
|
||||
{dataset.document_count}{t('dataset.documentCount')}
|
||||
</span>
|
||||
<span className={style.listItemStats}>
|
||||
<span className={classNames(style.listItemFooterIcon, style.textIcon)} />
|
||||
{Math.round(dataset.word_count / 1000)}{t('dataset.wordCount')}
|
||||
</span>
|
||||
<span className={style.listItemStats}>
|
||||
<span className={classNames(style.listItemFooterIcon, style.applicationIcon)} />
|
||||
{dataset.app_count}{t('dataset.appCount')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{showConfirmDelete && (
|
||||
<Confirm
|
||||
title={t('dataset.deleteDatasetConfirmTitle')}
|
||||
content={t('dataset.deleteDatasetConfirmContent')}
|
||||
isShow={showConfirmDelete}
|
||||
onClose={() => setShowConfirmDelete(false)}
|
||||
onConfirm={onConfirmDelete}
|
||||
onCancel={() => setShowConfirmDelete(false)}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DatasetCard
|
||||
19
web/app/(commonLayout)/datasets/DatasetFooter.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
const DatasetFooter = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<footer className='px-12 py-6 grow-0 shrink-0'>
|
||||
<h3 className='text-xl font-semibold leading-tight text-gradient'>{t('dataset.didYouKnow')}</h3>
|
||||
<p className='mt-1 text-sm font-normal leading-tight text-gray-700'>
|
||||
{t('dataset.intro1')}<a className='inline-flex items-center gap-1 link' target='_blank' href='/'>{t('dataset.intro2')}</a>{t('dataset.intro3')}<br />
|
||||
{t('dataset.intro4')}<a className='inline-flex items-center gap-1 link' target='_blank' href='/'>{t('dataset.intro5')}</a>{t('dataset.intro6')}
|
||||
</p>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export default DatasetFooter
|
||||
27
web/app/(commonLayout)/datasets/Datasets.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { DataSet } from '@/models/datasets';
|
||||
import NewDatasetCard from './NewDatasetCard'
|
||||
import DatasetCard from './DatasetCard';
|
||||
import { fetchDatasets } from '@/service/datasets';
|
||||
|
||||
const Datasets = () => {
|
||||
// const { datasets, mutateDatasets } = useAppContext()
|
||||
const { data: datasetList, mutate: mutateDatasets } = useSWR({ url: '/datasets', params: { page: 1 } }, fetchDatasets)
|
||||
|
||||
useEffect(() => {
|
||||
mutateDatasets()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-8 sm:grid-cols-2 lg:grid-cols-4 grow shrink-0'>
|
||||
{datasetList?.data.map(dataset => (<DatasetCard key={dataset.id} dataset={dataset} />))}
|
||||
<NewDatasetCard />
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
export default Datasets
|
||||
|
||||
28
web/app/(commonLayout)/datasets/NewDatasetCard.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import classNames from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import style from '../list.module.css'
|
||||
|
||||
const CreateAppCard = () => {
|
||||
const { t } = useTranslation()
|
||||
const [showNewAppDialog, setShowNewAppDialog] = useState(false)
|
||||
|
||||
return (
|
||||
<a className={classNames(style.listItem, style.newItemCard)} href='/datasets/create'>
|
||||
<div className={style.listItemTitle}>
|
||||
<span className={style.newItemIcon}>
|
||||
<span className={classNames(style.newItemIconImage, style.newItemIconAdd)} />
|
||||
</span>
|
||||
<div className={classNames(style.listItemHeading, style.newItemCardHeading)}>
|
||||
{t('dataset.createDataset')}
|
||||
</div>
|
||||
</div>
|
||||
<div className={style.listItemDescription}>{t('dataset.createDatasetIntro')}</div>
|
||||
{/* <div className='text-xs text-gray-500'>{t('app.createFromConfigFile')}</div> */}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateAppCard
|
||||
6
web/app/(commonLayout)/datasets/assets/application.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.2 1.5H2.3C2.01997 1.5 1.87996 1.5 1.773 1.5545C1.67892 1.60243 1.60243 1.67892 1.5545 1.773C1.5 1.87996 1.5 2.01997 1.5 2.3V4.2C1.5 4.48003 1.5 4.62004 1.5545 4.727C1.60243 4.82108 1.67892 4.89757 1.773 4.9455C1.87996 5 2.01997 5 2.3 5H4.2C4.48003 5 4.62004 5 4.727 4.9455C4.82108 4.89757 4.89757 4.82108 4.9455 4.727C5 4.62004 5 4.48003 5 4.2V2.3C5 2.01997 5 1.87996 4.9455 1.773C4.89757 1.67892 4.82108 1.60243 4.727 1.5545C4.62004 1.5 4.48003 1.5 4.2 1.5Z" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.7 1.5H7.8C7.51997 1.5 7.37996 1.5 7.273 1.5545C7.17892 1.60243 7.10243 1.67892 7.0545 1.773C7 1.87996 7 2.01997 7 2.3V4.2C7 4.48003 7 4.62004 7.0545 4.727C7.10243 4.82108 7.17892 4.89757 7.273 4.9455C7.37996 5 7.51997 5 7.8 5H9.7C9.98003 5 10.12 5 10.227 4.9455C10.3211 4.89757 10.3976 4.82108 10.4455 4.727C10.5 4.62004 10.5 4.48003 10.5 4.2V2.3C10.5 2.01997 10.5 1.87996 10.4455 1.773C10.3976 1.67892 10.3211 1.60243 10.227 1.5545C10.12 1.5 9.98003 1.5 9.7 1.5Z" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.7 7H7.8C7.51997 7 7.37996 7 7.273 7.0545C7.17892 7.10243 7.10243 7.17892 7.0545 7.273C7 7.37996 7 7.51997 7 7.8V9.7C7 9.98003 7 10.12 7.0545 10.227C7.10243 10.3211 7.17892 10.3976 7.273 10.4455C7.37996 10.5 7.51997 10.5 7.8 10.5H9.7C9.98003 10.5 10.12 10.5 10.227 10.4455C10.3211 10.3976 10.3976 10.3211 10.4455 10.227C10.5 10.12 10.5 9.98003 10.5 9.7V7.8C10.5 7.51997 10.5 7.37996 10.4455 7.273C10.3976 7.17892 10.3211 7.10243 10.227 7.0545C10.12 7 9.98003 7 9.7 7Z" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.2 7H2.3C2.01997 7 1.87996 7 1.773 7.0545C1.67892 7.10243 1.60243 7.17892 1.5545 7.273C1.5 7.37996 1.5 7.51997 1.5 7.8V9.7C1.5 9.98003 1.5 10.12 1.5545 10.227C1.60243 10.3211 1.67892 10.3976 1.773 10.4455C1.87996 10.5 2.01997 10.5 2.3 10.5H4.2C4.48003 10.5 4.62004 10.5 4.727 10.4455C4.82108 10.3976 4.89757 10.3211 4.9455 10.227C5 10.12 5 9.98003 5 9.7V7.8C5 7.51997 5 7.37996 4.9455 7.273C4.89757 7.17892 4.82108 7.10243 4.727 7.0545C4.62004 7 4.48003 7 4.2 7Z" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
3
web/app/(commonLayout)/datasets/assets/doc.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 1H3C2.73478 1 2.48043 1.10536 2.29289 1.29289C2.10536 1.48043 2 1.73478 2 2V10C2 10.2652 2.10536 10.5196 2.29289 10.7071C2.48043 10.8946 2.73478 11 3 11H9C9.26522 11 9.51957 10.8946 9.70711 10.7071C9.89464 10.5196 10 10.2652 10 10V4M7 1L10 4M7 1V4H10M8 6.5H4M8 8.5H4M5 4.5H4" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 457 B |
3
web/app/(commonLayout)/datasets/assets/text.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7 1H3C2.73478 1 2.48043 1.10536 2.29289 1.29289C2.10536 1.48043 2 1.73478 2 2V10C2 10.2652 2.10536 10.5196 2.29289 10.7071C2.48043 10.8946 2.73478 11 3 11H9C9.26522 11 9.51957 10.8946 9.70711 10.7071C9.89464 10.5196 10 10.2652 10 10V4M7 1L10 4M7 1V4H10M8 6.5H4M8 8.5H4M5 4.5H4" stroke="#98A2B3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 457 B |
12
web/app/(commonLayout)/datasets/create/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react'
|
||||
import DatasetUpdateForm from '@/app/components/datasets/create'
|
||||
|
||||
type Props = {}
|
||||
|
||||
const DatasetCreation = async (props: Props) => {
|
||||
return (
|
||||
<DatasetUpdateForm />
|
||||
)
|
||||
}
|
||||
|
||||
export default DatasetCreation
|
||||
23
web/app/(commonLayout)/datasets/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import classNames from 'classnames'
|
||||
import { getLocaleOnServer } from '@/i18n/server'
|
||||
import { useTranslation } from '@/i18n/i18next-serverside-config'
|
||||
import Datasets from './Datasets'
|
||||
import DatasetFooter from './DatasetFooter'
|
||||
|
||||
const AppList = async () => {
|
||||
const locale = getLocaleOnServer()
|
||||
const { t } = await useTranslation(locale, 'dataset')
|
||||
|
||||
return (
|
||||
<div className='flex flex-col overflow-auto bg-gray-100 shrink-0 grow'>
|
||||
<Datasets />
|
||||
<DatasetFooter />
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: 'Datasets - Dify',
|
||||
}
|
||||
|
||||
export default AppList
|
||||
19
web/app/(commonLayout)/layout.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import type { FC } from 'react'
|
||||
import LayoutClient, { ICommonLayoutProps } from "./_layout-client";
|
||||
import GA, { GaType } from '@/app/components/base/ga'
|
||||
|
||||
const Layout: FC<ICommonLayoutProps> = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<GA gaType={GaType.admin} />
|
||||
<LayoutClient children={children}></LayoutClient>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: 'Dify',
|
||||
}
|
||||
|
||||
export default Layout
|
||||
183
web/app/(commonLayout)/list.module.css
Normal file
@@ -0,0 +1,183 @@
|
||||
.listItem {
|
||||
@apply col-span-1 bg-white border-2 border-solid border-transparent rounded-lg shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg;
|
||||
}
|
||||
|
||||
.listItem.newItemCard {
|
||||
@apply outline outline-1 outline-gray-200 -outline-offset-1 hover:shadow-sm hover:bg-white;
|
||||
background-color: rgba(229, 231, 235, 0.5);
|
||||
}
|
||||
|
||||
.listItem.selectable {
|
||||
@apply relative bg-gray-50 outline outline-1 outline-gray-200 -outline-offset-1 shadow-none hover:bg-none hover:shadow-none hover:outline-primary-200 transition-colors;
|
||||
}
|
||||
.listItem.selectable * {
|
||||
@apply relative;
|
||||
}
|
||||
.listItem.selectable::before {
|
||||
content: '';
|
||||
@apply absolute top-0 left-0 block w-full h-full rounded-lg pointer-events-none opacity-0 transition-opacity duration-200 ease-in-out hover:opacity-100;
|
||||
background: linear-gradient(0deg, rgba(235, 245, 255, 0.5), rgba(235, 245, 255, 0.5)), #FFFFFF;
|
||||
}
|
||||
.listItem.selectable:hover::before {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
.listItem.selected {
|
||||
@apply border-primary-600 hover:border-primary-600 border-2;
|
||||
}
|
||||
.listItem.selected::before {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
.appIcon {
|
||||
@apply flex items-center justify-center w-8 h-8 bg-pink-100 rounded-lg grow-0 shrink-0;
|
||||
}
|
||||
.appIcon.medium {
|
||||
@apply w-9 h-9;
|
||||
}
|
||||
.appIcon.large {
|
||||
@apply w-10 h-10;
|
||||
}
|
||||
|
||||
.newItemIcon {
|
||||
@apply flex items-center justify-center w-8 h-8 transition-colors duration-200 ease-in-out border border-gray-200 rounded-lg hover:bg-white grow-0 shrink-0;
|
||||
}
|
||||
.listItem:hover .newItemIcon {
|
||||
@apply bg-gray-50 border-primary-100;
|
||||
}
|
||||
.newItemCard .newItemIcon {
|
||||
@apply bg-gray-100;
|
||||
}
|
||||
.newItemCard:hover .newItemIcon {
|
||||
@apply bg-white;
|
||||
}
|
||||
.selectable .newItemIcon {
|
||||
@apply bg-gray-50;
|
||||
}
|
||||
.selectable:hover .newItemIcon {
|
||||
@apply bg-primary-50;
|
||||
}
|
||||
.newItemIconImage {
|
||||
@apply grow-0 shrink-0 block w-4 h-4 bg-center bg-contain transition-colors duration-200 ease-in-out;
|
||||
color: #1f2a37;
|
||||
}
|
||||
.listItem:hover .newIconImage {
|
||||
@apply text-primary-600;
|
||||
}
|
||||
.newItemIconAdd {
|
||||
background-image: url('./apps/assets/add.svg');
|
||||
}
|
||||
.newItemIconChat {
|
||||
background-image: url('./apps/assets/chat.svg');
|
||||
}
|
||||
.newItemIconComplete {
|
||||
background-image: url('./apps/assets/completion.svg');
|
||||
}
|
||||
|
||||
.listItemTitle {
|
||||
@apply flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0;
|
||||
}
|
||||
|
||||
.listItemHeading {
|
||||
@apply relative h-8 text-sm font-medium leading-8 grow;
|
||||
}
|
||||
|
||||
.listItemHeadingContent {
|
||||
@apply absolute top-0 left-0 w-full h-full overflow-hidden text-ellipsis whitespace-nowrap;
|
||||
}
|
||||
|
||||
.deleteAppIcon {
|
||||
@apply hidden grow-0 shrink-0 basis-8 w-8 h-8 rounded-lg transition-colors duration-200 ease-in-out bg-white border border-gray-200 hover:bg-gray-100 bg-center bg-no-repeat;
|
||||
background-size: 16px;
|
||||
background-image: url('./apps/assets/delete.svg');
|
||||
}
|
||||
.listItem:hover .deleteAppIcon {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
.listItemDescription {
|
||||
@apply mb-3 px-[14px] h-9 text-xs leading-normal text-gray-500 line-clamp-2;
|
||||
}
|
||||
|
||||
.listItemFooter {
|
||||
@apply flex items-center flex-wrap min-h-[42px] px-[14px] pt-2 pb-[10px];
|
||||
}
|
||||
.listItemFooter.datasetCardFooter {
|
||||
@apply flex items-center gap-4 text-xs text-gray-500;
|
||||
}
|
||||
|
||||
.listItemStats {
|
||||
@apply flex items-center gap-1;
|
||||
}
|
||||
|
||||
.listItemFooterIcon {
|
||||
@apply block w-3 h-3 bg-center bg-contain;
|
||||
}
|
||||
.solidChatIcon {
|
||||
background-image: url('./apps/assets/chat-solid.svg');
|
||||
}
|
||||
.solidCompletionIcon {
|
||||
background-image: url('./apps/assets/completion-solid.svg');
|
||||
}
|
||||
.docIcon {
|
||||
background-image: url('./datasets/assets/doc.svg');
|
||||
}
|
||||
.textIcon {
|
||||
background-image: url('./datasets/assets/text.svg');
|
||||
}
|
||||
.applicationIcon {
|
||||
background-image: url('./datasets/assets/application.svg');
|
||||
}
|
||||
|
||||
.newItemCardHeading {
|
||||
@apply transition-colors duration-200 ease-in-out;
|
||||
}
|
||||
.listItem:hover .newItemCardHeading {
|
||||
@apply text-primary-600;
|
||||
}
|
||||
|
||||
.listItemLink {
|
||||
@apply inline-flex items-center gap-1 text-xs text-gray-400 transition-colors duration-200 ease-in-out;
|
||||
}
|
||||
.listItem:hover .listItemLink {
|
||||
@apply text-primary-600
|
||||
}
|
||||
|
||||
.linkIcon {
|
||||
@apply block w-[13px] h-[13px] bg-center bg-contain;
|
||||
background-image: url('./apps/assets/link.svg');
|
||||
}
|
||||
|
||||
.linkIcon.grayLinkIcon {
|
||||
background-image: url('./apps/assets/link-gray.svg');
|
||||
}
|
||||
.listItem:hover .grayLinkIcon {
|
||||
background-image: url('./apps/assets/link.svg');
|
||||
}
|
||||
|
||||
.rightIcon {
|
||||
@apply block w-[13px] h-[13px] bg-center bg-contain;
|
||||
background-image: url('./apps/assets/right-arrow.svg');
|
||||
}
|
||||
|
||||
.socialMediaLink {
|
||||
@apply flex items-center justify-center w-8 h-8 cursor-pointer hover:opacity-80 transition-opacity duration-200 ease-in-out;
|
||||
}
|
||||
|
||||
.socialMediaIcon {
|
||||
@apply block w-6 h-6 bg-center bg-contain;
|
||||
}
|
||||
|
||||
.githubIcon {
|
||||
background-image: url('./apps/assets/github.svg');
|
||||
}
|
||||
|
||||
.discordIcon {
|
||||
background-image: url('./apps/assets/discord.svg');
|
||||
}
|
||||
|
||||
/* #region new app dialog */
|
||||
.newItemCaption {
|
||||
@apply inline-flex items-center mb-2 text-sm font-medium;
|
||||
}
|
||||
/* #endregion new app dialog */
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
BIN
web/app/(commonLayout)/plugins-coming-soon/assets/plugins-bg.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
32
web/app/(commonLayout)/plugins-coming-soon/page.module.css
Normal file
@@ -0,0 +1,32 @@
|
||||
.bg {
|
||||
position: relative;
|
||||
width: 750px;
|
||||
height: 450px;
|
||||
background: #fff url(./assets/plugins-bg.png) center center no-repeat;
|
||||
background-size: contain;
|
||||
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.text {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 48px;
|
||||
width: 526px;
|
||||
background: linear-gradient(91.92deg, #104AE1 -1.74%, #0098EE 75.74%);
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
position: absolute;
|
||||
width: 116.74px;
|
||||
height: 69.3px;
|
||||
left: -18.37px;
|
||||
top: -11.48px;
|
||||
background: url(./assets/coming-soon.png) center center no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
19
web/app/(commonLayout)/plugins-coming-soon/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import s from './page.module.css'
|
||||
import { getLocaleOnServer } from '@/i18n/server'
|
||||
import { useTranslation } from '@/i18n/i18next-serverside-config'
|
||||
|
||||
const PluginsComingSoon = async () => {
|
||||
const locale = getLocaleOnServer()
|
||||
const { t } = await useTranslation(locale, 'common')
|
||||
|
||||
return (
|
||||
<div className='flex justify-center items-center w-full h-full bg-gray-100'>
|
||||
<div className={s.bg}>
|
||||
<div className={s.tag} />
|
||||
<div className={s.text}>{t('menus.pluginsTips')}</div>
|
||||
</div>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
export default PluginsComingSoon
|
||||
16
web/app/(shareLayout)/chat/[token]/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
import type { IMainProps } from '@/app/components/share/chat'
|
||||
import Main from '@/app/components/share/chat'
|
||||
|
||||
const Chat: FC<IMainProps> = ({
|
||||
params,
|
||||
}: any) => {
|
||||
|
||||
return (
|
||||
<Main params={params} />
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Chat)
|
||||
13
web/app/(shareLayout)/completion/[token]/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
import type { IMainProps } from '@/app/components/share/chat'
|
||||
import Main from '@/app/components/share/text-generation'
|
||||
|
||||
const TextGeneration: FC<IMainProps> = () => {
|
||||
return (
|
||||
<Main />
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(TextGeneration)
|
||||
18
web/app/(shareLayout)/layout.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import type { FC } from 'react'
|
||||
import GA, { GaType } from '@/app/components/base/ga'
|
||||
|
||||
const Layout: FC<{
|
||||
children: React.ReactNode
|
||||
}> = ({ children }) => {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<div className="w-screen h-screen min-w-[300px]">
|
||||
<GA gaType={GaType.webapp} />
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Layout
|
||||
3
web/app/api/hello/route.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export async function GET(_request: Request) {
|
||||
return new Response('Hello, Next.js!')
|
||||
}
|
||||
65
web/app/components/app-sidebar/basic.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
InformationCircleIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
import Tooltip from '../base/tooltip'
|
||||
import AppIcon from '../base/app-icon'
|
||||
|
||||
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'
|
||||
|
||||
export function randomString(length: number) {
|
||||
let result = ''
|
||||
for (let i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)]
|
||||
return result
|
||||
}
|
||||
|
||||
export type IAppBasicProps = {
|
||||
iconType?: 'app' | 'api' | 'dataset'
|
||||
iconUrl?: string
|
||||
name: string
|
||||
type: string | React.ReactNode
|
||||
hoverTip?: string
|
||||
textStyle?: { main?: string; extra?: string }
|
||||
}
|
||||
|
||||
const AlgorithmSvg = <svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.5 3.5C8.5 4.60457 9.39543 5.5 10.5 5.5C11.6046 5.5 12.5 4.60457 12.5 3.5C12.5 2.39543 11.6046 1.5 10.5 1.5C9.39543 1.5 8.5 2.39543 8.5 3.5Z" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M12.5 9C12.5 10.1046 13.3954 11 14.5 11C15.6046 11 16.5 10.1046 16.5 9C16.5 7.89543 15.6046 7 14.5 7C13.3954 7 12.5 7.89543 12.5 9Z" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M8.5 3.5H5.5L3.5 6.5" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M8.5 14.5C8.5 15.6046 9.39543 16.5 10.5 16.5C11.6046 16.5 12.5 15.6046 12.5 14.5C12.5 13.3954 11.6046 12.5 10.5 12.5C9.39543 12.5 8.5 13.3954 8.5 14.5Z" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M8.5 14.5H5.5L3.5 11.5" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M12.5 9H1.5" stroke="#5850EC" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
||||
const DatasetSvg = <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.833497 5.13481C0.833483 4.69553 0.83347 4.31654 0.858973 4.0044C0.88589 3.67495 0.94532 3.34727 1.10598 3.03195C1.34567 2.56155 1.72812 2.17909 2.19852 1.93941C2.51384 1.77875 2.84152 1.71932 3.17097 1.6924C3.48312 1.6669 3.86209 1.66691 4.30137 1.66693L7.62238 1.66684C8.11701 1.66618 8.55199 1.66561 8.95195 1.80356C9.30227 1.92439 9.62134 2.12159 9.88607 2.38088C10.1883 2.67692 10.3823 3.06624 10.603 3.50894L11.3484 5.00008H14.3679C15.0387 5.00007 15.5924 5.00006 16.0434 5.03691C16.5118 5.07518 16.9424 5.15732 17.3468 5.36339C17.974 5.68297 18.4839 6.19291 18.8035 6.82011C19.0096 7.22456 19.0917 7.65515 19.13 8.12356C19.1668 8.57455 19.1668 9.12818 19.1668 9.79898V13.5345C19.1668 14.2053 19.1668 14.7589 19.13 15.2099C19.0917 15.6784 19.0096 16.1089 18.8035 16.5134C18.4839 17.1406 17.974 17.6505 17.3468 17.9701C16.9424 18.1762 16.5118 18.2583 16.0434 18.2966C15.5924 18.3334 15.0387 18.3334 14.3679 18.3334H5.63243C4.96163 18.3334 4.40797 18.3334 3.95698 18.2966C3.48856 18.2583 3.05798 18.1762 2.65353 17.9701C2.02632 17.6505 1.51639 17.1406 1.19681 16.5134C0.990734 16.1089 0.908597 15.6784 0.870326 15.2099C0.833478 14.7589 0.833487 14.2053 0.833497 13.5345V5.13481ZM7.51874 3.33359C8.17742 3.33359 8.30798 3.34447 8.4085 3.37914C8.52527 3.41942 8.63163 3.48515 8.71987 3.57158C8.79584 3.64598 8.86396 3.7579 9.15852 4.34704L9.48505 5.00008L2.50023 5.00008C2.50059 4.61259 2.50314 4.34771 2.5201 4.14012C2.5386 3.91374 2.57 3.82981 2.59099 3.7886C2.67089 3.6318 2.79837 3.50432 2.95517 3.42442C2.99638 3.40343 3.08031 3.37203 3.30669 3.35353C3.54281 3.33424 3.85304 3.33359 4.3335 3.33359H7.51874Z" fill="#444CE7" />
|
||||
</svg>
|
||||
|
||||
const ICON_MAP = {
|
||||
'app': <AppIcon className='border !border-[rgba(0,0,0,0.05)]' />,
|
||||
'api': <AppIcon innerIcon={AlgorithmSvg} className='border !bg-purple-50 !border-purple-200' />,
|
||||
'dataset': <AppIcon innerIcon={DatasetSvg} className='!border-[0.5px] !border-indigo-100 !bg-indigo-25' />
|
||||
}
|
||||
|
||||
export default function AppBasic({ iconUrl, name, type, hoverTip, textStyle, iconType = 'app' }: IAppBasicProps) {
|
||||
return (
|
||||
<div className="flex items-start">
|
||||
{iconUrl && (
|
||||
<div className='flex-shrink-0 mr-3'>
|
||||
{/* <img className="inline-block rounded-lg h-9 w-9" src={iconUrl} alt={name} /> */}
|
||||
{ICON_MAP[iconType]}
|
||||
</div>
|
||||
)}
|
||||
<div className="group">
|
||||
<div className={`flex flex-row items-center text-sm font-semibold text-gray-700 group-hover:text-gray-900 ${textStyle?.main}`}>
|
||||
{name}
|
||||
{hoverTip
|
||||
&& <Tooltip content={hoverTip} selector={`a${randomString(16)}`}>
|
||||
<InformationCircleIcon className='w-4 h-4 ml-1 text-gray-400' />
|
||||
</Tooltip>}
|
||||
</div>
|
||||
<div className={`text-xs font-normal text-gray-500 group-hover:text-gray-700 ${textStyle?.extra}`}>{type}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
39
web/app/components/app-sidebar/index.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react'
|
||||
import type { FC } from 'react'
|
||||
import NavLink from './navLink'
|
||||
import AppBasic from './basic'
|
||||
|
||||
export type IAppDetailNavProps = {
|
||||
iconType?: 'app' | 'dataset'
|
||||
title: string
|
||||
desc: string
|
||||
navigation: Array<{
|
||||
name: string
|
||||
href: string
|
||||
icon: any
|
||||
selectedIcon: any
|
||||
}>
|
||||
extraInfo?: React.ReactNode
|
||||
}
|
||||
|
||||
const sampleAppIconUrl = 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80'
|
||||
|
||||
const AppDetailNav: FC<IAppDetailNavProps> = ({ title, desc, navigation, extraInfo, iconType = 'app' }) => {
|
||||
return (
|
||||
<div className="flex flex-col w-56 overflow-y-auto bg-white border-r border-gray-200 shrink-0">
|
||||
<div className="flex flex-shrink-0 p-4">
|
||||
<AppBasic iconType={iconType} iconUrl={sampleAppIconUrl} name={title} type={desc} />
|
||||
</div>
|
||||
<nav className="flex-1 p-4 space-y-1 bg-white">
|
||||
{navigation.map((item, index) => {
|
||||
return (
|
||||
<NavLink key={index} iconMap={{ selected: item.selectedIcon, normal: item.icon }} name={item.name} href={item.href} />
|
||||
)
|
||||
})}
|
||||
{extraInfo ?? null}
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(AppDetailNav)
|
||||
39
web/app/components/app-sidebar/navLink.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
'use client'
|
||||
import { useSelectedLayoutSegment } from 'next/navigation'
|
||||
import classNames from 'classnames'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function NavLink({
|
||||
name,
|
||||
href,
|
||||
iconMap,
|
||||
}: {
|
||||
name: string
|
||||
href: string
|
||||
iconMap: { selected: any; normal: any }
|
||||
}) {
|
||||
const segment = useSelectedLayoutSegment()
|
||||
const isActive = href.toLowerCase().split('/')?.pop() === segment?.toLowerCase()
|
||||
const NavIcon = isActive ? iconMap.selected : iconMap.normal
|
||||
|
||||
return (
|
||||
<Link
|
||||
prefetch
|
||||
key={name}
|
||||
href={href}
|
||||
className={classNames(
|
||||
isActive ? 'bg-primary-50 text-primary-600 font-semibold' : 'text-gray-700 hover:bg-gray-100 hover:text-gray-700',
|
||||
'group flex items-center rounded-md px-2 py-2 text-sm font-normal',
|
||||
)}
|
||||
>
|
||||
<NavIcon
|
||||
className={classNames(
|
||||
'mr-2 h-4 w-4 flex-shrink-0',
|
||||
isActive ? 'text-primary-600' : 'text-gray-700',
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{name}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
3
web/app/components/app-sidebar/style.module.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.sidebar {
|
||||
border-right: 1px solid #F3F4F6;
|
||||
}
|
||||
3
web/app/components/app/chat/icons/answer.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.03647 1.5547C0.59343 0.890144 1.06982 0 1.86852 0H8V12L1.03647 1.5547Z" fill="#F3F4F6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 202 B |
BIN
web/app/components/app/chat/icons/default-avatar.jpg
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
3
web/app/components/app/chat/icons/edit.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 11.9998L13.3332 12.7292C12.9796 13.1159 12.5001 13.3332 12.0001 13.3332C11.5001 13.3332 11.0205 13.1159 10.6669 12.7292C10.3128 12.3432 9.83332 12.1265 9.33345 12.1265C8.83359 12.1265 8.35409 12.3432 7.99998 12.7292M2 13.3332H3.11636C3.44248 13.3332 3.60554 13.3332 3.75899 13.2963C3.89504 13.2637 4.0251 13.2098 4.1444 13.1367C4.27895 13.0542 4.39425 12.9389 4.62486 12.7083L13 4.33316C13.5523 3.78087 13.5523 2.88544 13 2.33316C12.4477 1.78087 11.5523 1.78087 11 2.33316L2.62484 10.7083C2.39424 10.9389 2.27894 11.0542 2.19648 11.1888C2.12338 11.3081 2.0695 11.4381 2.03684 11.5742C2 11.7276 2 11.8907 2 12.2168V13.3332Z" stroke="#6B7280" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 804 B |
3
web/app/components/app/chat/icons/question.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.96353 1.5547C7.40657 0.890144 6.93018 0 6.13148 0H0V12L6.96353 1.5547Z" fill="#E1EFFE"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 202 B |
10
web/app/components/app/chat/icons/robot.svg
Normal file
|
After Width: | Height: | Size: 36 KiB |
3
web/app/components/app/chat/icons/send-active.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.447 16.8939C23.6128 16.8108 23.7523 16.6832 23.8498 16.5253C23.9473 16.3674 23.9989 16.1855 23.9989 15.9999C23.9989 15.8144 23.9473 15.6325 23.8498 15.4746C23.7523 15.3167 23.6128 15.1891 23.447 15.1059L9.44697 8.10595C9.27338 8.01909 9.07827 7.98463 8.88543 8.00677C8.69259 8.02891 8.51036 8.1067 8.36098 8.23064C8.2116 8.35458 8.10151 8.51931 8.04415 8.70475C7.9868 8.89019 7.98465 9.08831 8.03797 9.27495L9.46697 14.2749C9.52674 14.4839 9.65297 14.6677 9.82655 14.7985C10.0001 14.9294 10.2116 15.0001 10.429 14.9999H15C15.2652 14.9999 15.5195 15.1053 15.7071 15.2928C15.8946 15.4804 16 15.7347 16 15.9999C16 16.2652 15.8946 16.5195 15.7071 16.7071C15.5195 16.8946 15.2652 16.9999 15 16.9999H10.429C10.2116 16.9998 10.0001 17.0705 9.82655 17.2013C9.65297 17.3322 9.52674 17.516 9.46697 17.7249L8.03897 22.7249C7.98554 22.9115 7.98756 23.1096 8.04478 23.2951C8.10201 23.4805 8.21195 23.6453 8.36122 23.7693C8.51049 23.8934 8.69263 23.9713 8.88542 23.9936C9.07821 24.0159 9.27332 23.9816 9.44697 23.8949L23.447 16.8949V16.8939Z" fill="#1C64F2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
3
web/app/components/app/chat/icons/send.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.447 16.8939C23.6128 16.8108 23.7523 16.6832 23.8498 16.5253C23.9473 16.3674 23.9989 16.1855 23.9989 15.9999C23.9989 15.8144 23.9473 15.6325 23.8498 15.4746C23.7523 15.3167 23.6128 15.1891 23.447 15.1059L9.44697 8.10595C9.27338 8.01909 9.07827 7.98463 8.88543 8.00677C8.69259 8.02891 8.51036 8.1067 8.36098 8.23064C8.2116 8.35458 8.10151 8.51931 8.04415 8.70475C7.9868 8.89019 7.98465 9.08831 8.03797 9.27495L9.46697 14.2749C9.52674 14.4839 9.65297 14.6677 9.82655 14.7985C10.0001 14.9294 10.2116 15.0001 10.429 14.9999H15C15.2652 14.9999 15.5195 15.1053 15.7071 15.2928C15.8946 15.4804 16 15.7347 16 15.9999C16 16.2652 15.8946 16.5195 15.7071 16.7071C15.5195 16.8946 15.2652 16.9999 15 16.9999H10.429C10.2116 16.9998 10.0001 17.0705 9.82655 17.2013C9.65297 17.3322 9.52674 17.516 9.46697 17.7249L8.03897 22.7249C7.98554 22.9115 7.98756 23.1096 8.04478 23.2951C8.10201 23.4805 8.21195 23.6453 8.36122 23.7693C8.51049 23.8934 8.69263 23.9713 8.88542 23.9936C9.07821 24.0159 9.27332 23.9816 9.44697 23.8949L23.447 16.8949V16.8939Z" fill="#D1D5DB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
19
web/app/components/app/chat/icons/typing.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_2358_1380)">
|
||||
<rect x="2" y="1" width="16" height="16" rx="8" fill="white"/>
|
||||
<path opacity="0.7" d="M13.5 9H13.505M14 9C14 9.13261 13.9473 9.25979 13.8536 9.35355C13.7598 9.44732 13.6326 9.5 13.5 9.5C13.3674 9.5 13.2402 9.44732 13.1464 9.35355C13.0527 9.25979 13 9.13261 13 9C13 8.86739 13.0527 8.74021 13.1464 8.64645C13.2402 8.55268 13.3674 8.5 13.5 8.5C13.6326 8.5 13.7598 8.55268 13.8536 8.64645C13.9473 8.74021 14 8.86739 14 9Z" stroke="#155EEF" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path opacity="0.6" d="M10 9H10.005M10.5 9C10.5 9.13261 10.4473 9.25979 10.3536 9.35355C10.2598 9.44732 10.1326 9.5 10 9.5C9.86739 9.5 9.74021 9.44732 9.64645 9.35355C9.55268 9.25979 9.5 9.13261 9.5 9C9.5 8.86739 9.55268 8.74021 9.64645 8.64645C9.74021 8.55268 9.86739 8.5 10 8.5C10.1326 8.5 10.2598 8.55268 10.3536 8.64645C10.4473 8.74021 10.5 8.86739 10.5 9Z" stroke="#155EEF" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path opacity="0.3" d="M6.5 9H6.505M7 9C7 9.13261 6.94732 9.25979 6.85355 9.35355C6.75979 9.44732 6.63261 9.5 6.5 9.5C6.36739 9.5 6.24021 9.44732 6.14645 9.35355C6.05268 9.25979 6 9.13261 6 9C6 8.86739 6.05268 8.74021 6.14645 8.64645C6.24021 8.55268 6.36739 8.5 6.5 8.5C6.63261 8.5 6.75979 8.55268 6.85355 8.64645C6.94732 8.74021 7 8.86739 7 9Z" stroke="#155EEF" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_2358_1380" x="0" y="0" width="20" height="20" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2358_1380"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2358_1380" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
10
web/app/components/app/chat/icons/user.svg
Normal file
|
After Width: | Height: | Size: 74 KiB |
559
web/app/components/app/chat/index.tsx
Normal file
@@ -0,0 +1,559 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import cn from 'classnames'
|
||||
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
|
||||
import { UserCircleIcon } from '@heroicons/react/24/solid'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { randomString } from '../../app-sidebar/basic'
|
||||
import s from './style.module.css'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import AutoHeightTextarea from '@/app/components/base/auto-height-textarea'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { Annotation, MessageRating } from '@/models/log'
|
||||
import AppContext from '@/context/app-context'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import LoadingAnim from './loading-anim'
|
||||
|
||||
const stopIcon = (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.00004 0.583313C3.45621 0.583313 0.583374 3.45615 0.583374 6.99998C0.583374 10.5438 3.45621 13.4166 7.00004 13.4166C10.5439 13.4166 13.4167 10.5438 13.4167 6.99998C13.4167 3.45615 10.5439 0.583313 7.00004 0.583313ZM4.73029 4.98515C4.66671 5.10993 4.66671 5.27328 4.66671 5.59998V8.39998C4.66671 8.72668 4.66671 8.89003 4.73029 9.01481C4.78621 9.12457 4.87545 9.21381 4.98521 9.26973C5.10999 9.33331 5.27334 9.33331 5.60004 9.33331H8.40004C8.72674 9.33331 8.89009 9.33331 9.01487 9.26973C9.12463 9.21381 9.21387 9.12457 9.2698 9.01481C9.33337 8.89003 9.33337 8.72668 9.33337 8.39998V5.59998C9.33337 5.27328 9.33337 5.10993 9.2698 4.98515C9.21387 4.87539 9.12463 4.78615 9.01487 4.73023C8.89009 4.66665 8.72674 4.66665 8.40004 4.66665H5.60004C5.27334 4.66665 5.10999 4.66665 4.98521 4.73023C4.87545 4.78615 4.78621 4.87539 4.73029 4.98515Z" fill="#667085" />
|
||||
</svg>
|
||||
)
|
||||
export type Feedbacktype = {
|
||||
rating: MessageRating
|
||||
content?: string | null
|
||||
}
|
||||
|
||||
export type FeedbackFunc = (messageId: string, feedback: Feedbacktype) => Promise<any>
|
||||
export type SubmitAnnotationFunc = (messageId: string, content: string) => Promise<any>
|
||||
|
||||
export type DisplayScene = 'web' | 'console'
|
||||
|
||||
export type IChatProps = {
|
||||
chatList: IChatItem[]
|
||||
/**
|
||||
* Whether to display the editing area and rating status
|
||||
*/
|
||||
feedbackDisabled?: boolean
|
||||
/**
|
||||
* Whether to display the input area
|
||||
*/
|
||||
isHideFeedbackEdit?: boolean
|
||||
isHideSendInput?: boolean
|
||||
onFeedback?: FeedbackFunc
|
||||
onSubmitAnnotation?: SubmitAnnotationFunc
|
||||
checkCanSend?: () => boolean
|
||||
onSend?: (message: string) => void
|
||||
displayScene?: DisplayScene
|
||||
useCurrentUserAvatar?: boolean
|
||||
isResponsing?: boolean
|
||||
abortResponsing?: () => void
|
||||
controlClearQuery?: number
|
||||
controlFocus?: number
|
||||
isShowSuggestion?: boolean
|
||||
suggestionList?: string[]
|
||||
}
|
||||
|
||||
export type MessageMore = {
|
||||
time: string
|
||||
tokens: number
|
||||
latency: number | string
|
||||
}
|
||||
|
||||
export type IChatItem = {
|
||||
id: string
|
||||
content: string
|
||||
/**
|
||||
* Specific message type
|
||||
*/
|
||||
isAnswer: boolean
|
||||
/**
|
||||
* The user feedback result of this message
|
||||
*/
|
||||
feedback?: Feedbacktype
|
||||
/**
|
||||
* The admin feedback result of this message
|
||||
*/
|
||||
adminFeedback?: Feedbacktype
|
||||
/**
|
||||
* Whether to hide the feedback area
|
||||
*/
|
||||
feedbackDisabled?: boolean
|
||||
/**
|
||||
* More information about this message
|
||||
*/
|
||||
more?: MessageMore
|
||||
annotation?: Annotation
|
||||
useCurrentUserAvatar?: boolean
|
||||
isOpeningStatement?: boolean
|
||||
}
|
||||
|
||||
const OperationBtn = ({ innerContent, onClick, className }: { innerContent: React.ReactNode; onClick?: () => void; className?: string }) => (
|
||||
<div
|
||||
className={`relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-gray-500 hover:text-gray-800 ${className ?? ''}`}
|
||||
style={{ boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.05)' }}
|
||||
onClick={onClick && onClick}
|
||||
>
|
||||
{innerContent}
|
||||
</div>
|
||||
)
|
||||
|
||||
const MoreInfo: FC<{ more: MessageMore; isQuestion: boolean }> = ({ more, isQuestion }) => {
|
||||
const { t } = useTranslation()
|
||||
return (<div className={`mt-1 space-x-2 text-xs text-gray-400 ${isQuestion ? 'mr-2 text-right ' : 'ml-2 text-left float-right'}`}>
|
||||
<span>{`${t('appLog.detail.timeConsuming')} ${more.latency}${t('appLog.detail.second')}`}</span>
|
||||
<span>{`${t('appLog.detail.tokenCost')} ${more.tokens}`}</span>
|
||||
<span>· </span>
|
||||
<span>{more.time} </span>
|
||||
</div>)
|
||||
}
|
||||
|
||||
const OpeningStatementIcon: FC<{ className?: string }> = ({ className }) => (
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M6.25002 1C3.62667 1 1.50002 3.12665 1.50002 5.75C1.50002 6.28 1.58702 6.79071 1.7479 7.26801C1.7762 7.35196 1.79285 7.40164 1.80368 7.43828L1.80722 7.45061L1.80535 7.45452C1.79249 7.48102 1.77339 7.51661 1.73766 7.58274L0.911727 9.11152C0.860537 9.20622 0.807123 9.30503 0.770392 9.39095C0.733879 9.47635 0.674738 9.63304 0.703838 9.81878C0.737949 10.0365 0.866092 10.2282 1.05423 10.343C1.21474 10.4409 1.38213 10.4461 1.475 10.4451C1.56844 10.444 1.68015 10.4324 1.78723 10.4213L4.36472 10.1549C4.406 10.1506 4.42758 10.1484 4.44339 10.1472L4.44542 10.147L4.45161 10.1492C4.47103 10.1562 4.49738 10.1663 4.54285 10.1838C5.07332 10.3882 5.64921 10.5 6.25002 10.5C8.87338 10.5 11 8.37335 11 5.75C11 3.12665 8.87338 1 6.25002 1ZM4.48481 4.29111C5.04844 3.81548 5.7986 3.9552 6.24846 4.47463C6.69831 3.9552 7.43879 3.82048 8.01211 4.29111C8.58544 4.76175 8.6551 5.562 8.21247 6.12453C7.93825 6.47305 7.24997 7.10957 6.76594 7.54348C6.58814 7.70286 6.49924 7.78255 6.39255 7.81466C6.30103 7.84221 6.19589 7.84221 6.10436 7.81466C5.99767 7.78255 5.90878 7.70286 5.73098 7.54348C5.24694 7.10957 4.55867 6.47305 4.28444 6.12453C3.84182 5.562 3.92117 4.76675 4.48481 4.29111Z" fill="#667085" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const RatingIcon: FC<{ isLike: boolean }> = ({ isLike }) => {
|
||||
return isLike ? <HandThumbUpIcon className='w-4 h-4' /> : <HandThumbDownIcon className='w-4 h-4' />
|
||||
}
|
||||
|
||||
const EditIcon: FC<{ className?: string }> = ({ className }) => {
|
||||
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<path d="M14 11.9998L13.3332 12.7292C12.9796 13.1159 12.5001 13.3332 12.0001 13.3332C11.5001 13.3332 11.0205 13.1159 10.6669 12.7292C10.3128 12.3432 9.83332 12.1265 9.33345 12.1265C8.83359 12.1265 8.35409 12.3432 7.99998 12.7292M2 13.3332H3.11636C3.44248 13.3332 3.60554 13.3332 3.75899 13.2963C3.89504 13.2637 4.0251 13.2098 4.1444 13.1367C4.27895 13.0542 4.39425 12.9389 4.62486 12.7083L13 4.33316C13.5523 3.78087 13.5523 2.88544 13 2.33316C12.4477 1.78087 11.5523 1.78087 11 2.33316L2.62484 10.7083C2.39424 10.9389 2.27894 11.0542 2.19648 11.1888C2.12338 11.3081 2.0695 11.4381 2.03684 11.5742C2 11.7276 2 11.8907 2 12.2168V13.3332Z" stroke="#6B7280" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const EditIconSolid: FC<{ className?: string }> = ({ className }) => {
|
||||
return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<path fill-rule="evenodd" clipRule="evenodd" d="M10.8374 8.63108C11.0412 8.81739 11.0554 9.13366 10.8691 9.33747L10.369 9.88449C10.0142 10.2725 9.52293 10.5001 9.00011 10.5001C8.47746 10.5001 7.98634 10.2727 7.63157 9.8849C7.45561 9.69325 7.22747 9.59515 7.00014 9.59515C6.77271 9.59515 6.54446 9.69335 6.36846 9.88517C6.18177 10.0886 5.86548 10.1023 5.66201 9.91556C5.45853 9.72888 5.44493 9.41259 5.63161 9.20911C5.98678 8.82201 6.47777 8.59515 7.00014 8.59515C7.52251 8.59515 8.0135 8.82201 8.36867 9.20911L8.36924 9.20974C8.54486 9.4018 8.77291 9.50012 9.00011 9.50012C9.2273 9.50012 9.45533 9.40182 9.63095 9.20979L10.131 8.66276C10.3173 8.45895 10.6336 8.44476 10.8374 8.63108Z" fill="#6B7280" />
|
||||
<path fill-rule="evenodd" clipRule="evenodd" d="M7.89651 1.39656C8.50599 0.787085 9.49414 0.787084 10.1036 1.39656C10.7131 2.00604 10.7131 2.99419 10.1036 3.60367L3.82225 9.88504C3.81235 9.89494 3.80254 9.90476 3.79281 9.91451C3.64909 10.0585 3.52237 10.1855 3.3696 10.2791C3.23539 10.3613 3.08907 10.4219 2.93602 10.4587C2.7618 10.5005 2.58242 10.5003 2.37897 10.5001C2.3652 10.5001 2.35132 10.5001 2.33732 10.5001H1.50005C1.22391 10.5001 1.00005 10.2763 1.00005 10.0001V9.16286C1.00005 9.14886 1.00004 9.13497 1.00003 9.1212C0.999836 8.91776 0.999669 8.73838 1.0415 8.56416C1.07824 8.4111 1.13885 8.26479 1.22109 8.13058C1.31471 7.97781 1.44166 7.85109 1.58566 7.70736C1.5954 7.69764 1.60523 7.68783 1.61513 7.67793L7.89651 1.39656Z" fill="#6B7280" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
const TryToAskIcon = (
|
||||
<svg width="11" height="10" viewBox="0 0 11 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.88889 0.683718C5.827 0.522805 5.67241 0.416626 5.5 0.416626C5.3276 0.416626 5.173 0.522805 5.11111 0.683718L4.27279 2.86334C4.14762 3.18877 4.10829 3.28255 4.05449 3.35821C4.00051 3.43413 3.93418 3.50047 3.85826 3.55445C3.78259 3.60825 3.68881 3.64758 3.36338 3.77275L1.18376 4.61106C1.02285 4.67295 0.916668 4.82755 0.916668 4.99996C0.916668 5.17236 1.02285 5.32696 1.18376 5.38885L3.36338 6.22717C3.68881 6.35234 3.78259 6.39167 3.85826 6.44547C3.93418 6.49945 4.00051 6.56578 4.05449 6.6417C4.10829 6.71737 4.14762 6.81115 4.27279 7.13658L5.11111 9.3162C5.173 9.47711 5.3276 9.58329 5.5 9.58329C5.67241 9.58329 5.82701 9.47711 5.8889 9.3162L6.72721 7.13658C6.85238 6.81115 6.89171 6.71737 6.94551 6.6417C6.99949 6.56578 7.06583 6.49945 7.14175 6.44547C7.21741 6.39167 7.31119 6.35234 7.63662 6.22717L9.81624 5.38885C9.97715 5.32696 10.0833 5.17236 10.0833 4.99996C10.0833 4.82755 9.97715 4.67295 9.81624 4.61106L7.63662 3.77275C7.31119 3.64758 7.21741 3.60825 7.14175 3.55445C7.06583 3.50047 6.99949 3.43413 6.94551 3.35821C6.89171 3.28255 6.85238 3.18877 6.72721 2.86334L5.88889 0.683718Z" fill="#667085" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const Divider: FC<{ name: string }> = ({ name }) => {
|
||||
const { t } = useTranslation()
|
||||
return <div className='flex items-center my-2'>
|
||||
<span className='text-xs text-gray-500 inline-flex items-center mr-2'>
|
||||
<EditIconSolid className='mr-1' />{t('appLog.detail.annotationTip', { user: name })}
|
||||
</span>
|
||||
<div className='h-[1px] bg-gray-200 flex-1'></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const IconWrapper: FC<{ children: React.ReactNode | string }> = ({ children }) => {
|
||||
return <div className={'rounded-lg h-6 w-6 flex items-center justify-center hover:bg-gray-100'}>
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
|
||||
type IAnswerProps = {
|
||||
item: IChatItem
|
||||
feedbackDisabled: boolean
|
||||
isHideFeedbackEdit: boolean
|
||||
onFeedback?: FeedbackFunc
|
||||
onSubmitAnnotation?: SubmitAnnotationFunc
|
||||
displayScene: DisplayScene
|
||||
isResponsing?: boolean
|
||||
}
|
||||
|
||||
// The component needs to maintain its own state to control whether to display input component
|
||||
const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, isHideFeedbackEdit = false, onFeedback, onSubmitAnnotation, displayScene = 'web', isResponsing }) => {
|
||||
const { id, content, more, feedback, adminFeedback, annotation: initAnnotation } = item
|
||||
const [showEdit, setShowEdit] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [annotation, setAnnotation] = useState<Annotation | undefined | null>(initAnnotation)
|
||||
const [inputValue, setInputValue] = useState<string>(initAnnotation?.content ?? '')
|
||||
const [localAdminFeedback, setLocalAdminFeedback] = useState<Feedbacktype | undefined | null>(adminFeedback)
|
||||
const { userProfile } = useContext(AppContext)
|
||||
const { t } = useTranslation()
|
||||
|
||||
/**
|
||||
* Render feedback results (distinguish between users and administrators)
|
||||
* User reviews cannot be cancelled in Console
|
||||
* @param rating feedback result
|
||||
* @param isUserFeedback Whether it is user's feedback
|
||||
* @param isWebScene Whether it is web scene
|
||||
* @returns comp
|
||||
*/
|
||||
const renderFeedbackRating = (rating: MessageRating | undefined, isUserFeedback = true, isWebScene = true) => {
|
||||
if (!rating)
|
||||
return null
|
||||
|
||||
const isLike = rating === 'like'
|
||||
const ratingIconClassname = isLike ? 'text-primary-600 bg-primary-100 hover:bg-primary-200' : 'text-red-600 bg-red-100 hover:bg-red-200'
|
||||
const UserSymbol = <UserCircleIcon className='absolute top-[-2px] left-[18px] w-3 h-3 rounded-lg text-gray-400 bg-white' />
|
||||
// The tooltip is always displayed, but the content is different for different scenarios.
|
||||
return (
|
||||
<Tooltip
|
||||
selector={`user-feedback-${randomString(16)}`}
|
||||
content={(isWebScene || (!isUserFeedback && !isWebScene)) ? isLike ? '取消赞同' : '取消反对' : (!isWebScene && isUserFeedback) ? `用户表示${isLike ? '赞同' : '反对'}` : ''}
|
||||
>
|
||||
<div
|
||||
className={`relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-gray-500 hover:text-gray-800 ${(!isWebScene && isUserFeedback) ? '!cursor-default' : ''}`}
|
||||
style={{ boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.05)' }}
|
||||
{...((isWebScene || (!isUserFeedback && !isWebScene))
|
||||
? {
|
||||
onClick: async () => {
|
||||
const res = await onFeedback?.(id, { rating: null })
|
||||
if (res && !isWebScene)
|
||||
setLocalAdminFeedback({ rating: null })
|
||||
},
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
<div className={`${ratingIconClassname} rounded-lg h-6 w-6 flex items-center justify-center`}>
|
||||
<RatingIcon isLike={isLike} />
|
||||
</div>
|
||||
{!isWebScene && isUserFeedback && UserSymbol}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Different scenarios have different operation items.
|
||||
* @param isWebScene Whether it is web scene
|
||||
* @returns comp
|
||||
*/
|
||||
const renderItemOperation = (isWebScene = true) => {
|
||||
const userOperation = () => {
|
||||
return feedback?.rating
|
||||
? null
|
||||
: <div className='flex gap-1'>
|
||||
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('appLog.detail.operation.like') as string}>
|
||||
{OperationBtn({ innerContent: <IconWrapper><RatingIcon isLike={true} /></IconWrapper>, onClick: () => onFeedback?.(id, { rating: 'like' }) })}
|
||||
</Tooltip>
|
||||
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('appLog.detail.operation.dislike') as string}>
|
||||
{OperationBtn({ innerContent: <IconWrapper><RatingIcon isLike={false} /></IconWrapper>, onClick: () => onFeedback?.(id, { rating: 'dislike' }) })}
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
|
||||
const adminOperation = () => {
|
||||
return <div className='flex gap-1'>
|
||||
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('appLog.detail.operation.addAnnotation') as string}>
|
||||
{OperationBtn({
|
||||
innerContent: <IconWrapper><EditIcon className='hover:text-gray-800' /></IconWrapper>,
|
||||
onClick: () => setShowEdit(true),
|
||||
})}
|
||||
</Tooltip>
|
||||
{!localAdminFeedback?.rating && <>
|
||||
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('appLog.detail.operation.like') as string}>
|
||||
{OperationBtn({
|
||||
innerContent: <IconWrapper><RatingIcon isLike={true} /></IconWrapper>,
|
||||
onClick: async () => {
|
||||
const res = await onFeedback?.(id, { rating: 'like' })
|
||||
if (res)
|
||||
setLocalAdminFeedback({ rating: 'like' })
|
||||
},
|
||||
})}
|
||||
</Tooltip>
|
||||
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('appLog.detail.operation.dislike') as string}>
|
||||
{OperationBtn({
|
||||
innerContent: <IconWrapper><RatingIcon isLike={false} /></IconWrapper>,
|
||||
onClick: async () => {
|
||||
const res = await onFeedback?.(id, { rating: 'dislike' })
|
||||
if (res)
|
||||
setLocalAdminFeedback({ rating: 'dislike' })
|
||||
},
|
||||
})}
|
||||
</Tooltip>
|
||||
</>}
|
||||
</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${s.itemOperation} flex gap-2`}>
|
||||
{isWebScene ? userOperation() : adminOperation()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={id}>
|
||||
<div className='flex items-start'>
|
||||
<div className={`${s.answerIcon} w-10 h-10 shrink-0`}>
|
||||
{isResponsing &&
|
||||
<div className={s.typeingIcon}>
|
||||
<LoadingAnim type='avatar' />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className={`${s.answerWrap} ${showEdit ? 'w-full' : ''}`}>
|
||||
<div className={`${s.answer} relative text-sm text-gray-900`}>
|
||||
<div className={'ml-2 py-3 px-4 bg-gray-100 rounded-tr-2xl rounded-b-2xl'}>
|
||||
{item.isOpeningStatement && (
|
||||
<div className='flex items-center mb-1 gap-1'>
|
||||
<OpeningStatementIcon />
|
||||
<div className='text-xs text-gray-500'>{t('appDebug.openingStatement.title')}</div>
|
||||
</div>
|
||||
)}
|
||||
{(isResponsing && !content) ? (
|
||||
<div className='flex items-center justify-center w-6 h-5'>
|
||||
<LoadingAnim type='text' />
|
||||
</div>
|
||||
) : (
|
||||
<Markdown content={content} />
|
||||
)}
|
||||
{!showEdit
|
||||
? (annotation?.content
|
||||
&& <>
|
||||
<Divider name={annotation?.account?.name || userProfile?.name} />
|
||||
{annotation.content}
|
||||
</>)
|
||||
: <>
|
||||
<Divider name={annotation?.account?.name || userProfile?.name} />
|
||||
<AutoHeightTextarea
|
||||
placeholder={t('appLog.detail.operation.annotationPlaceholder') as string}
|
||||
value={inputValue}
|
||||
onChange={e => setInputValue(e.target.value)}
|
||||
minHeight={58}
|
||||
className={`${cn(s.textArea)} !py-2 resize-none block w-full !px-3 bg-gray-50 border border-gray-200 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm text-gray-700 tracking-[0.2px]`}
|
||||
/>
|
||||
<div className="mt-2 flex flex-row">
|
||||
<Button
|
||||
type='primary'
|
||||
className='mr-2'
|
||||
loading={loading}
|
||||
onClick={async () => {
|
||||
if (!inputValue)
|
||||
return
|
||||
setLoading(true)
|
||||
const res = await onSubmitAnnotation?.(id, inputValue)
|
||||
if (res)
|
||||
setAnnotation({ ...annotation, content: inputValue } as any)
|
||||
setLoading(false)
|
||||
setShowEdit(false)
|
||||
}}>{t('common.operation.confirm')}</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setInputValue(annotation?.content ?? '')
|
||||
setShowEdit(false)
|
||||
}}>{t('common.operation.cancel')}</Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
<div className='absolute top-[-14px] right-[-14px] flex flex-row justify-end gap-1'>
|
||||
{!feedbackDisabled && !item.feedbackDisabled && renderItemOperation(displayScene !== 'console')}
|
||||
{/* Admin feedback is displayed only in the background. */}
|
||||
{!feedbackDisabled && renderFeedbackRating(localAdminFeedback?.rating, false, false)}
|
||||
{/* User feedback must be displayed */}
|
||||
{!feedbackDisabled && renderFeedbackRating(feedback?.rating, !isHideFeedbackEdit, displayScene !== 'console')}
|
||||
</div>
|
||||
</div>
|
||||
{more && <MoreInfo more={more} isQuestion={false} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type IQuestionProps = Pick<IChatItem, 'id' | 'content' | 'more' | 'useCurrentUserAvatar'>
|
||||
|
||||
const Question: FC<IQuestionProps> = ({ id, content, more, useCurrentUserAvatar }) => {
|
||||
const { userProfile } = useContext(AppContext)
|
||||
const userName = userProfile?.name
|
||||
return (
|
||||
<div className='flex items-start justify-end' key={id}>
|
||||
<div>
|
||||
<div className={`${s.question} relative text-sm text-gray-900`}>
|
||||
<div
|
||||
className={'mr-2 py-3 px-4 bg-blue-500 rounded-tl-2xl rounded-b-2xl'}
|
||||
>
|
||||
<Markdown content={content} />
|
||||
</div>
|
||||
</div>
|
||||
{more && <MoreInfo more={more} isQuestion={true} />}
|
||||
</div>
|
||||
{useCurrentUserAvatar ? (
|
||||
<div className='w-10 h-10 shrink-0 leading-10 text-center mr-2 rounded-full bg-primary-600 text-white'>
|
||||
{userName?.[0].toLocaleUpperCase()}
|
||||
</div>
|
||||
) : (
|
||||
<div className={`${s.questionIcon} w-10 h-10 shrink-0 `}></div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Chat: FC<IChatProps> = ({
|
||||
chatList,
|
||||
feedbackDisabled = false,
|
||||
isHideFeedbackEdit = false,
|
||||
isHideSendInput = false,
|
||||
onFeedback,
|
||||
onSubmitAnnotation,
|
||||
checkCanSend,
|
||||
onSend = () => { },
|
||||
displayScene,
|
||||
useCurrentUserAvatar,
|
||||
isResponsing,
|
||||
abortResponsing,
|
||||
controlClearQuery,
|
||||
controlFocus,
|
||||
isShowSuggestion,
|
||||
suggestionList
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const isUseInputMethod = useRef(false)
|
||||
|
||||
const [query, setQuery] = React.useState('')
|
||||
const handleContentChange = (e: any) => {
|
||||
const value = e.target.value
|
||||
setQuery(value)
|
||||
}
|
||||
|
||||
const logError = (message: string) => {
|
||||
notify({ type: 'error', message, duration: 3000 })
|
||||
}
|
||||
|
||||
const valid = () => {
|
||||
if (!query || query.trim() === '') {
|
||||
logError('Message cannot be empty')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (controlClearQuery) {
|
||||
setQuery('')
|
||||
}
|
||||
}, [controlClearQuery])
|
||||
|
||||
const handleSend = () => {
|
||||
if (!valid() || (checkCanSend && !checkCanSend()))
|
||||
return
|
||||
onSend(query)
|
||||
if (!isResponsing) {
|
||||
setQuery('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyUp = (e: any) => {
|
||||
if (e.code === 'Enter') {
|
||||
e.preventDefault()
|
||||
// prevent send message when using input method enter
|
||||
if (!e.shiftKey && !isUseInputMethod.current) {
|
||||
handleSend()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const haneleKeyDown = (e: any) => {
|
||||
isUseInputMethod.current = e.nativeEvent.isComposing
|
||||
if (e.code === 'Enter' && !e.shiftKey) {
|
||||
setQuery(query.replace(/\n$/, ''))
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn(!feedbackDisabled && 'px-3.5', 'h-full')}>
|
||||
{/* Chat List */}
|
||||
<div className="h-full space-y-[30px]">
|
||||
{chatList.map((item) => {
|
||||
if (item.isAnswer) {
|
||||
const isLast = item.id === chatList[chatList.length - 1].id
|
||||
return <Answer
|
||||
key={item.id}
|
||||
item={item}
|
||||
feedbackDisabled={feedbackDisabled}
|
||||
isHideFeedbackEdit={isHideFeedbackEdit}
|
||||
onFeedback={onFeedback}
|
||||
onSubmitAnnotation={onSubmitAnnotation}
|
||||
displayScene={displayScene ?? 'web'}
|
||||
isResponsing={isResponsing && isLast}
|
||||
/>
|
||||
}
|
||||
return <Question key={item.id} id={item.id} content={item.content} more={item.more} useCurrentUserAvatar={useCurrentUserAvatar} />
|
||||
})}
|
||||
</div>
|
||||
{
|
||||
!isHideSendInput && (
|
||||
<div className={cn(!feedbackDisabled && '!left-3.5 !right-3.5', 'absolute z-10 bottom-0 left-0 right-0')}>
|
||||
{isResponsing && (
|
||||
<div className='flex justify-center mb-4'>
|
||||
<Button className='flex items-center space-x-1 bg-white' onClick={() => abortResponsing?.()}>
|
||||
{stopIcon}
|
||||
<span className='text-xs text-gray-500 font-normal'>{t('appDebug.operation.stopResponding')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
isShowSuggestion && (
|
||||
<div className='pt-2 mb-2 '>
|
||||
<div className='flex items-center justify-center mb-2.5'>
|
||||
<div className='grow h-[1px]'
|
||||
style={{
|
||||
background: 'linear-gradient(270deg, #F3F4F6 0%, rgba(243, 244, 246, 0) 100%)'
|
||||
}}></div>
|
||||
<div className='shrink-0 flex items-center px-3 space-x-1'>
|
||||
{TryToAskIcon}
|
||||
<span className='text-xs text-gray-500 font-medium'>{t('appDebug.feature.suggestedQuestionsAfterAnswer.tryToAsk')}</span>
|
||||
</div>
|
||||
<div className='grow h-[1px]'
|
||||
style={{
|
||||
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)'
|
||||
}}></div>
|
||||
</div>
|
||||
<div className='flex justify-center overflow-x-scroll pb-2'>
|
||||
{suggestionList?.map((item, index) => (
|
||||
<div className='shrink-0 flex justify-center mr-2'>
|
||||
<Button
|
||||
key={index}
|
||||
onClick={() => setQuery(item)}
|
||||
>
|
||||
<span className='text-primary-600 text-xs font-medium'>{item}</span>
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
<div className="relative">
|
||||
<AutoHeightTextarea
|
||||
value={query}
|
||||
onChange={handleContentChange}
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={haneleKeyDown}
|
||||
minHeight={48}
|
||||
autoFocus
|
||||
controlFocus={controlFocus}
|
||||
className={`${cn(s.textArea)} resize-none block w-full pl-3 bg-gray-50 border border-gray-200 rounded-md focus:outline-none sm:text-sm text-gray-700`}
|
||||
/>
|
||||
<div className="absolute top-0 right-2 flex items-center h-[48px]">
|
||||
<div className={`${s.count} mr-4 h-5 leading-5 text-sm bg-gray-50 text-gray-500`}>{query.trim().length}</div>
|
||||
<Tooltip
|
||||
selector='send-tip'
|
||||
htmlContent={
|
||||
<div>
|
||||
<div>{t('common.operation.send')} Enter</div>
|
||||
<div>{t('common.operation.lineBreak')} Shift Enter</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={`${s.sendBtn} w-8 h-8 cursor-pointer rounded-md`} onClick={handleSend}></div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Chat)
|
||||
16
web/app/components/app/chat/loading-anim/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import s from './style.module.css'
|
||||
|
||||
export interface ILoaidingAnimProps {
|
||||
type: 'text' | 'avatar'
|
||||
}
|
||||
|
||||
const LoaidingAnim: FC<ILoaidingAnimProps> = ({
|
||||
type
|
||||
}) => {
|
||||
return (
|
||||
<div className={`${s['dot-flashing']} ${s[type]}`}></div>
|
||||
)
|
||||
}
|
||||
export default React.memo(LoaidingAnim)
|
||||
82
web/app/components/app/chat/loading-anim/style.module.css
Normal file
@@ -0,0 +1,82 @@
|
||||
.dot-flashing {
|
||||
position: relative;
|
||||
animation: 1s infinite linear alternate;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
.dot-flashing::before,
|
||||
.dot-flashing::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
animation: 1s infinite linear alternate;
|
||||
}
|
||||
|
||||
.dot-flashing::before {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.dot-flashing::after {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes dot-flashing {
|
||||
0% {
|
||||
background-color: #667085;
|
||||
}
|
||||
|
||||
50%,
|
||||
100% {
|
||||
background-color: rgba(102, 112, 133, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dot-flashing-avatar {
|
||||
0% {
|
||||
background-color: #155EEF;
|
||||
}
|
||||
|
||||
50%,
|
||||
100% {
|
||||
background-color: rgba(21, 94, 239, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.text,
|
||||
.text::before,
|
||||
.text::after {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background-color: #667085;
|
||||
color: #667085;
|
||||
animation-name: dot-flashing;
|
||||
}
|
||||
|
||||
.text::before {
|
||||
left: -7px;
|
||||
}
|
||||
|
||||
.text::after {
|
||||
left: 7px;
|
||||
}
|
||||
|
||||
.avatar,
|
||||
.avatar::before,
|
||||
.avatar::after {
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
border-radius: 50%;
|
||||
background-color: #155EEF;
|
||||
color: #155EEF;
|
||||
animation-name: dot-flashing-avatar;
|
||||
}
|
||||
|
||||
.avatar::before {
|
||||
left: -5px;
|
||||
}
|
||||
|
||||
.avatar::after {
|
||||
left: 5px;
|
||||
}
|
||||
91
web/app/components/app/chat/style.module.css
Normal file
@@ -0,0 +1,91 @@
|
||||
.answerIcon {
|
||||
position: relative;
|
||||
background: url(./icons/robot.svg);
|
||||
}
|
||||
|
||||
.typeingIcon {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
|
||||
.questionIcon {
|
||||
background: url(./icons/default-avatar.jpg);
|
||||
background-size: contain;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.answer::before,
|
||||
.question::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 8px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.answer::before {
|
||||
left: 0;
|
||||
background: url(./icons/answer.svg) no-repeat;
|
||||
}
|
||||
|
||||
.answerWrap .itemOperation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.answerWrap:hover .itemOperation {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.question::before {
|
||||
right: 0;
|
||||
background: url(./icons/question.svg) no-repeat;
|
||||
}
|
||||
|
||||
.textArea {
|
||||
padding-top: 13px;
|
||||
padding-bottom: 13px;
|
||||
padding-right: 90px;
|
||||
border-radius: 12px;
|
||||
line-height: 20px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.textArea:hover {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* .textArea:focus {
|
||||
box-shadow: 0px 3px 15px -3px rgba(0, 0, 0, 0.1), 0px 4px 6px rgba(0, 0, 0, 0.05);
|
||||
} */
|
||||
|
||||
.count {
|
||||
/* display: none; */
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.sendBtn {
|
||||
background: url(./icons/send.svg) center center no-repeat;
|
||||
}
|
||||
|
||||
.sendBtn:hover {
|
||||
background-image: url(./icons/send-active.svg);
|
||||
background-color: #EBF5FF;
|
||||
}
|
||||
|
||||
.textArea:focus+div .count {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.textArea:focus+div .sendBtn {
|
||||
background-image: url(./icons/send-active.svg);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
'use client'
|
||||
import React, { FC, ReactNode } from 'react'
|
||||
import cn from 'classnames'
|
||||
|
||||
export interface IFeaturePanelProps {
|
||||
className?: string
|
||||
headerIcon: ReactNode
|
||||
title: ReactNode
|
||||
headerRight: ReactNode
|
||||
hasHeaderBottomBorder?: boolean
|
||||
isFocus?: boolean
|
||||
noBodySpacing?: boolean
|
||||
children?: ReactNode
|
||||
}
|
||||
|
||||
const FeaturePanel: FC<IFeaturePanelProps> = ({
|
||||
className,
|
||||
headerIcon,
|
||||
title,
|
||||
headerRight,
|
||||
hasHeaderBottomBorder,
|
||||
isFocus,
|
||||
noBodySpacing,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(className, isFocus && 'border border-[#2D0DEE]', 'rounded-xl bg-gray-50 pt-2 pb-3', noBodySpacing && '!pb-0')}
|
||||
style={isFocus ? {
|
||||
boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
|
||||
} : {}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className={cn('pb-2 px-3', hasHeaderBottomBorder && 'border-b border-gray-100')}>
|
||||
<div className='flex justify-between items-center h-8'>
|
||||
<div className='flex items-center space-x-1 shrink-0'>
|
||||
<div className='flex items-center justify-center w-4 h-4'>{headerIcon}</div>
|
||||
<div className='text-sm font-semibold text-gray-800'>{title}</div>
|
||||
</div>
|
||||
<div>
|
||||
{headerRight}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Body */}
|
||||
{children && (
|
||||
<div className={cn(!noBodySpacing && 'mt-1 px-3')}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(FeaturePanel)
|
||||
@@ -0,0 +1,23 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
|
||||
export interface IGroupNameProps {
|
||||
name: string
|
||||
}
|
||||
|
||||
const GroupName: FC<IGroupNameProps> = ({
|
||||
name
|
||||
}) => {
|
||||
return (
|
||||
<div className='flex items-center mb-1'>
|
||||
<div className='mr-3 leading-[18px] text-xs font-semibold text-gray-500 uppercase'>{name}</div>
|
||||
<div className='grow h-[1px]'
|
||||
style={{
|
||||
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)',
|
||||
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(GroupName)
|
||||
@@ -0,0 +1,13 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
|
||||
const MoreLikeThisIcon: FC = ({ }) => {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M5.83914 0.666748H10.1609C10.6975 0.666741 11.1404 0.666734 11.5012 0.696212C11.8759 0.726829 12.2204 0.792538 12.544 0.957399C13.0457 1.21306 13.4537 1.62101 13.7093 2.12277C13.8742 2.44633 13.9399 2.7908 13.9705 3.16553C14 3.52633 14 3.96923 14 4.50587V7.41171C14 7.62908 14 7.73776 13.9652 7.80784C13.9303 7.87806 13.8939 7.91566 13.8249 7.95288C13.756 7.99003 13.6262 7.99438 13.3665 8.00307C12.8879 8.01909 12.4204 8.14633 11.997 8.36429C10.9478 7.82388 9.62021 7.82912 8.53296 8.73228C7.15064 9.88056 6.92784 11.8645 8.0466 13.2641C8.36602 13.6637 8.91519 14.1949 9.40533 14.6492C9.49781 14.7349 9.54405 14.7777 9.5632 14.8041C9.70784 15.003 9.5994 15.2795 9.35808 15.3271C9.32614 15.3334 9.26453 15.3334 9.14129 15.3334H5.83912C5.30248 15.3334 4.85958 15.3334 4.49878 15.304C4.12405 15.2733 3.77958 15.2076 3.45603 15.0428C2.95426 14.7871 2.54631 14.3792 2.29065 13.8774C2.12579 13.5538 2.06008 13.2094 2.02946 12.8346C1.99999 12.4738 1.99999 12.0309 2 11.4943V4.50587C1.99999 3.96924 1.99999 3.52632 2.02946 3.16553C2.06008 2.7908 2.12579 2.44633 2.29065 2.12277C2.54631 1.62101 2.95426 1.21306 3.45603 0.957399C3.77958 0.792538 4.12405 0.726829 4.49878 0.696212C4.85957 0.666734 5.3025 0.666741 5.83914 0.666748ZM4.66667 5.33342C4.29848 5.33342 4 5.63189 4 6.00008C4 6.36827 4.29848 6.66675 4.66667 6.66675H8.66667C9.03486 6.66675 9.33333 6.36827 9.33333 6.00008C9.33333 5.63189 9.03486 5.33342 8.66667 5.33342H4.66667ZM4 8.66675C4 8.29856 4.29848 8.00008 4.66667 8.00008H6C6.36819 8.00008 6.66667 8.29856 6.66667 8.66675C6.66667 9.03494 6.36819 9.33342 6 9.33342H4.66667C4.29848 9.33342 4 9.03494 4 8.66675ZM4.66667 2.66675C4.29848 2.66675 4 2.96523 4 3.33342C4 3.7016 4.29848 4.00008 4.66667 4.00008H10.6667C11.0349 4.00008 11.3333 3.7016 11.3333 3.33342C11.3333 2.96523 11.0349 2.66675 10.6667 2.66675H4.66667Z" fill="#DD2590" />
|
||||
<path d="M11.9977 10.0256C11.3313 9.26808 10.2199 9.06432 9.3849 9.75796C8.54988 10.4516 8.43232 11.6113 9.08807 12.4317C9.58479 13.0531 10.9986 14.3025 11.655 14.8719C11.7744 14.9754 11.8341 15.0272 11.9037 15.0477C11.9642 15.0654 12.0312 15.0654 12.0917 15.0477C12.1613 15.0272 12.221 14.9754 12.3404 14.8719C12.9968 14.3025 14.4106 13.0531 14.9074 12.4317C15.5631 11.6113 15.4599 10.4443 14.6105 9.75796C13.7612 9.07161 12.6642 9.26808 11.9977 10.0256Z" fill="#DD2590" />
|
||||
</svg>
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(MoreLikeThisIcon)
|
||||
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
import React, { FC, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
|
||||
export interface IRemoveIconProps {
|
||||
className?: string
|
||||
isHoverStatus?: boolean
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const RemoveIcon: FC<IRemoveIconProps> = ({
|
||||
className,
|
||||
isHoverStatus,
|
||||
onClick
|
||||
}) => {
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const computedIsHovered = isHoverStatus || isHovered
|
||||
return (
|
||||
<div
|
||||
className={cn(className, computedIsHovered && 'bg-[#FEE4E2]', 'flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-[#FEE4E2]')}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 6H14M6 8H18M16.6667 8L16.1991 15.0129C16.129 16.065 16.0939 16.5911 15.8667 16.99C15.6666 17.3412 15.3648 17.6235 15.0011 17.7998C14.588 18 14.0607 18 13.0062 18H10.9938C9.93927 18 9.41202 18 8.99889 17.7998C8.63517 17.6235 8.33339 17.3412 8.13332 16.99C7.90607 16.5911 7.871 16.065 7.80086 15.0129L7.33333 8M10.6667 11V14.3333M13.3333 11V14.3333" stroke={computedIsHovered ? "#D92D20" : "#667085"} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(RemoveIcon)
|
||||
@@ -0,0 +1,11 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
|
||||
const SuggestedQuestionsAfterAnswerIcon: FC = () => {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.8275 1.33325H5.17245C4.63581 1.33324 4.19289 1.33324 3.8321 1.36272C3.45737 1.39333 3.1129 1.45904 2.78934 1.6239C2.28758 1.87956 1.87963 2.28751 1.62397 2.78928C1.45911 3.11284 1.3934 3.4573 1.36278 3.83204C1.3333 4.19283 1.33331 4.63574 1.33332 5.17239L1.33328 9.42497C1.333 9.95523 1.33278 10.349 1.42418 10.6901C1.67076 11.6103 2.38955 12.3291 3.3098 12.5757C3.51478 12.6306 3.73878 12.6525 3.99998 12.6611L3.99998 13.5806C3.99995 13.7374 3.99992 13.8973 4.01182 14.0283C4.0232 14.1536 4.05333 14.3901 4.21844 14.5969C4.40843 14.8349 4.69652 14.9734 5.00106 14.973C5.26572 14.9728 5.46921 14.8486 5.57416 14.7792C5.6839 14.7066 5.80872 14.6067 5.93117 14.5087L7.53992 13.2217C7.88564 12.9451 7.98829 12.8671 8.09494 12.8126C8.20192 12.7579 8.3158 12.718 8.43349 12.6938C8.55081 12.6697 8.67974 12.6666 9.12248 12.6666H10.8275C11.3642 12.6666 11.8071 12.6666 12.1679 12.6371C12.5426 12.6065 12.8871 12.5408 13.2106 12.3759C13.7124 12.1203 14.1203 11.7123 14.376 11.2106C14.5409 10.887 14.6066 10.5425 14.6372 10.1678C14.6667 9.80701 14.6667 9.36411 14.6667 8.82747V5.17237C14.6667 4.63573 14.6667 4.19283 14.6372 3.83204C14.6066 3.4573 14.5409 3.11284 14.376 2.78928C14.1203 2.28751 13.7124 1.87956 13.2106 1.6239C12.8871 1.45904 12.5426 1.39333 12.1679 1.36272C11.8071 1.33324 11.3642 1.33324 10.8275 1.33325ZM8.99504 4.99992C8.99504 4.44763 9.44275 3.99992 9.99504 3.99992C10.5473 3.99992 10.995 4.44763 10.995 4.99992C10.995 5.5522 10.5473 5.99992 9.99504 5.99992C9.44275 5.99992 8.99504 5.5522 8.99504 4.99992ZM4.92837 7.79996C5.222 7.57974 5.63816 7.63837 5.85961 7.93051C5.90071 7.98295 5.94593 8.03229 5.99199 8.08035C6.09019 8.18282 6.23775 8.32184 6.42882 8.4608C6.81353 8.74059 7.3454 8.99996 7.99504 8.99996C8.64469 8.99996 9.17655 8.74059 9.56126 8.4608C9.75233 8.32184 9.89989 8.18282 9.99809 8.08035C10.0441 8.0323 10.0894 7.98294 10.1305 7.93051C10.3519 7.63837 10.7681 7.57974 11.0617 7.79996C11.3563 8.02087 11.416 8.43874 11.195 8.73329C11.1967 8.73112 11.1928 8.7361 11.186 8.74466C11.1697 8.7651 11.1372 8.80597 11.1261 8.81916C11.087 8.86575 11.0317 8.92884 10.9607 9.00289C10.8194 9.15043 10.6128 9.34474 10.3455 9.53912C9.81353 9.92599 9.01206 10.3333 7.99504 10.3333C6.97802 10.3333 6.17655 9.92599 5.64459 9.53912C5.37733 9.34474 5.17072 9.15043 5.02934 9.00289C4.95837 8.92884 4.90305 8.86575 4.86395 8.81916C4.84438 8.79585 4.82881 8.77659 4.81731 8.76207C4.58702 8.46455 4.61798 8.03275 4.92837 7.79996ZM5.99504 3.99992C5.44275 3.99992 4.99504 4.44763 4.99504 4.99992C4.99504 5.5522 5.44275 5.99992 5.99504 5.99992C6.54732 5.99992 6.99504 5.5522 6.99504 4.99992C6.99504 4.44763 6.54732 3.99992 5.99504 3.99992Z" fill="#06AED4" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
export default React.memo(SuggestedQuestionsAfterAnswerIcon)
|
||||
11
web/app/components/app/configuration/base/icons/var-icon.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
|
||||
const VarIcon: FC = () => {
|
||||
return (
|
||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M3.71922 2.96714C3.91321 3.05239 4.06546 3.21112 4.14255 3.4085C4.21964 3.60588 4.21528 3.82578 4.13042 4.01994C3.51419 5.43299 3.1974 6.95838 3.20002 8.49994C3.20002 10.0943 3.53282 11.6087 4.13122 12.9799C4.20853 13.173 4.20766 13.3885 4.1288 13.5809C4.04993 13.7732 3.89927 13.9274 3.70873 14.0106C3.51819 14.0938 3.30274 14.0995 3.10802 14.0266C2.91331 13.9537 2.75464 13.8078 2.66562 13.6199C1.96073 12.0052 1.59791 10.2619 1.60002 8.49994C1.60002 6.68074 1.98002 4.94794 2.66562 3.37994C2.70767 3.28364 2.76829 3.19658 2.84401 3.12371C2.91973 3.05085 3.00906 2.99361 3.1069 2.95528C3.20474 2.91694 3.30917 2.89826 3.41424 2.9003C3.5193 2.90233 3.62293 2.92505 3.71922 2.96714ZM10.368 6.09994C10.0082 6.10002 9.65293 6.18102 9.32862 6.33695C9.0043 6.49288 8.7192 6.71974 8.49441 7.00074L8.23201 7.32874L8.14321 7.10554C8.02447 6.80879 7.81958 6.55441 7.55494 6.37518C7.2903 6.19594 6.97804 6.10008 6.65841 6.09994H6.40001C6.18784 6.09994 5.98436 6.18423 5.83433 6.33426C5.6843 6.48429 5.60002 6.68777 5.60002 6.89994C5.60002 7.11212 5.6843 7.3156 5.83433 7.46563C5.98436 7.61566 6.18784 7.69994 6.40001 7.69994H6.65841L7.08401 8.76394L6.25602 9.79994C6.18103 9.8936 6.08594 9.96919 5.97779 10.0211C5.86964 10.073 5.75119 10.1 5.63122 10.0999H5.60002C5.38784 10.0999 5.18436 10.1842 5.03433 10.3343C4.8843 10.4843 4.80001 10.6878 4.80001 10.8999C4.80001 11.1121 4.8843 11.3156 5.03433 11.4656C5.18436 11.6157 5.38784 11.6999 5.60002 11.6999H5.63122C5.99107 11.6999 6.34629 11.6189 6.67061 11.4629C6.99493 11.307 7.28003 11.0801 7.50481 10.7991L7.76721 10.4711L7.85601 10.6943C7.97481 10.9912 8.17982 11.2457 8.44462 11.4249C8.70942 11.6042 9.02185 11.7 9.34161 11.6999H9.60001C9.81219 11.6999 10.0157 11.6157 10.1657 11.4656C10.3157 11.3156 10.4 11.1121 10.4 10.8999C10.4 10.6878 10.3157 10.4843 10.1657 10.3343C10.0157 10.1842 9.81219 10.0999 9.60001 10.0999H9.34161L8.91602 9.03594L9.74401 7.99994C9.819 7.90629 9.91409 7.8307 10.0222 7.77877C10.1304 7.72684 10.2488 7.6999 10.3688 7.69994H10.4C10.6122 7.69994 10.8157 7.61566 10.9657 7.46563C11.1157 7.3156 11.2 7.11212 11.2 6.89994C11.2 6.68777 11.1157 6.48429 10.9657 6.33426C10.8157 6.18423 10.6122 6.09994 10.4 6.09994H10.3688H10.368ZM11.8672 4.01994C11.7837 3.82571 11.7804 3.6063 11.8581 3.40966C11.9359 3.21303 12.0883 3.05517 12.2821 2.97059C12.4759 2.88602 12.6953 2.8816 12.8923 2.9583C13.0894 3.03501 13.248 3.1866 13.3336 3.37994C14.0388 4.99468 14.4019 6.73795 14.4 8.49994C14.4 10.3191 14.02 12.0519 13.3344 13.6199C13.2946 13.7194 13.2352 13.8098 13.1597 13.8859C13.0843 13.9619 12.9943 14.0221 12.8952 14.0627C12.7961 14.1033 12.6898 14.1236 12.5827 14.1224C12.4756 14.1212 12.3698 14.0985 12.2716 14.0556C12.1735 14.0127 12.0849 13.9506 12.0112 13.8728C11.9375 13.7951 11.8802 13.7033 11.8426 13.603C11.805 13.5027 11.788 13.3959 11.7925 13.2888C11.797 13.1818 11.8229 13.0768 11.8688 12.9799C12.4853 11.567 12.8024 10.0416 12.8 8.49994C12.8 6.90554 12.4672 5.39114 11.868 4.01994H11.8672Z" fill="#FF8A4C" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
export default React.memo(VarIcon)
|
||||
@@ -0,0 +1,39 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PlusIcon } from '@heroicons/react/20/solid'
|
||||
|
||||
export interface IOperationBtnProps {
|
||||
type: 'add' | 'edit'
|
||||
actionName?: string
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const iconMap = {
|
||||
add: <PlusIcon className='w-3.5 h-3.5' />,
|
||||
edit: (<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.99998 11.6666H12.25M1.75 11.6666H2.72682C3.01217 11.6666 3.15485 11.6666 3.28912 11.6344C3.40816 11.6058 3.52196 11.5587 3.62635 11.4947C3.74408 11.4226 3.84497 11.3217 4.04675 11.1199L11.375 3.79164C11.8583 3.30839 11.8583 2.52488 11.375 2.04164C10.8918 1.55839 10.1083 1.55839 9.62501 2.04164L2.29674 9.3699C2.09496 9.57168 1.99407 9.67257 1.92192 9.7903C1.85795 9.89469 1.81081 10.0085 1.78224 10.1275C1.75 10.2618 1.75 10.4045 1.75 10.6898V11.6666Z" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const OperationBtn: FC<IOperationBtnProps> = ({
|
||||
type,
|
||||
actionName,
|
||||
onClick
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div
|
||||
className='flex items-center rounded-md h-7 px-3 space-x-1 text-gray-700 cursor-pointer hover:bg-gray-200'
|
||||
onClick={onClick}>
|
||||
<div>
|
||||
{iconMap[type]}
|
||||
</div>
|
||||
<div className='text-xs font-medium'>
|
||||
{actionName || t(`common.operation.${type}`)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(OperationBtn)
|
||||
@@ -0,0 +1,36 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
|
||||
import s from './style.module.css'
|
||||
|
||||
export interface IVarHighlightProps {
|
||||
name: string
|
||||
}
|
||||
|
||||
const VarHighlight: FC<IVarHighlightProps> = ({
|
||||
name,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
className={`${s.item} flex mb-2 items-center justify-center rounded-md px-1 h-5 text-xs font-medium text-primary-600`}
|
||||
>
|
||||
<span className='opacity-60'>{'{{'}</span>
|
||||
<span>{name}</span>
|
||||
<span className='opacity-60'>{'}}'}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const varHighlightHTML = ({ name }: IVarHighlightProps) => {
|
||||
const html = `<div class="${s.item} inline-flex mb-2 items-center justify-center px-1 rounded-md h-5 text-xs font-medium text-primary-600">
|
||||
<span class='opacity-60'>{{</span>
|
||||
<span>${name}</span>
|
||||
<span class='opacity-60'>}}</span>
|
||||
</div>`
|
||||
return html
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default React.memo(VarHighlight)
|
||||
@@ -0,0 +1,3 @@
|
||||
.item {
|
||||
background-color: rgba(21, 94, 239, 0.05);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import WarningMask from '.'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
export interface IFormattingChangedProps {
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
const icon = (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.33337 6.66667C1.33337 6.66667 2.67003 4.84548 3.75593 3.75883C4.84183 2.67218 6.34244 2 8.00004 2C11.3137 2 14 4.68629 14 8C14 11.3137 11.3137 14 8.00004 14C5.26465 14 2.95678 12.1695 2.23455 9.66667M1.33337 6.66667V2.66667M1.33337 6.66667H5.33337" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const FormattingChanged: FC<IFormattingChangedProps> = ({
|
||||
onConfirm,
|
||||
onCancel
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<WarningMask
|
||||
title={t('appDebug.formattingChangedTitle')}
|
||||
description={t('appDebug.formattingChangedText')}
|
||||
footer={
|
||||
<div className='flex space-x-2'>
|
||||
<Button type='primary' className='flex items-center space-x-2' onClick={onConfirm}>
|
||||
{icon}
|
||||
<span>{t('common.operation.refresh')}</span>
|
||||
</Button>
|
||||
<Button onClick={onCancel}>{t('common.operation.cancel') as string}</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(FormattingChanged)
|
||||
@@ -0,0 +1,37 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import WarningMask from '.'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
export interface IHasNotSetAPIProps {
|
||||
isTrailFinished: boolean
|
||||
onSetting: () => void
|
||||
}
|
||||
|
||||
const icon = (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 6.00001L14 2.00001M14 2.00001H9.99999M14 2.00001L8 8M6.66667 2H5.2C4.0799 2 3.51984 2 3.09202 2.21799C2.71569 2.40973 2.40973 2.71569 2.21799 3.09202C2 3.51984 2 4.07989 2 5.2V10.8C2 11.9201 2 12.4802 2.21799 12.908C2.40973 13.2843 2.71569 13.5903 3.09202 13.782C3.51984 14 4.07989 14 5.2 14H10.8C11.9201 14 12.4802 14 12.908 13.782C13.2843 13.5903 13.5903 13.2843 13.782 12.908C14 12.4802 14 11.9201 14 10.8V9.33333" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
|
||||
)
|
||||
|
||||
const HasNotSetAPI: FC<IHasNotSetAPIProps> = ({
|
||||
isTrailFinished,
|
||||
onSetting
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<WarningMask
|
||||
title={isTrailFinished ? t('appDebug.notSetAPIKey.trailFinished') : t('appDebug.notSetAPIKey.title')}
|
||||
description={t('appDebug.notSetAPIKey.description')}
|
||||
footer={
|
||||
<Button type='primary' className='flex items-center space-x-2' onClick={onSetting}>
|
||||
<span>{t('appDebug.notSetAPIKey.settingBtn')}</span>
|
||||
{icon}
|
||||
</Button>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(HasNotSetAPI)
|
||||
@@ -0,0 +1,42 @@
|
||||
'use client'
|
||||
import React, { FC } from 'react'
|
||||
|
||||
import s from './style.module.css'
|
||||
|
||||
export interface IWarningMaskProps {
|
||||
title: string
|
||||
description: string
|
||||
footer: React.ReactNode
|
||||
}
|
||||
|
||||
const warningIcon = (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.99996 13.3334V10.0001M9.99996 6.66675H10.0083M18.3333 10.0001C18.3333 14.6025 14.6023 18.3334 9.99996 18.3334C5.39759 18.3334 1.66663 14.6025 1.66663 10.0001C1.66663 5.39771 5.39759 1.66675 9.99996 1.66675C14.6023 1.66675 18.3333 5.39771 18.3333 10.0001Z" stroke="#F79009" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const WarningMask: FC<IWarningMaskProps> = ({
|
||||
title,
|
||||
description,
|
||||
footer,
|
||||
}) => {
|
||||
return (
|
||||
<div className={`${s.mask} absolute z-10 inset-0 pt-16`}
|
||||
>
|
||||
<div className='mx-auto w-[535px]'>
|
||||
<div className={`${s.icon} flex items-center justify-center w-11 h-11 rounded-xl bg-white`}>{warningIcon}</div>
|
||||
<div className='mt-4 text-[24px] leading-normal font-semibold text-gray-800'>
|
||||
{title}
|
||||
</div>
|
||||
<div className='mt-3 text-base text-gray-500'>
|
||||
{description}
|
||||
</div>
|
||||
<div className='mt-6'>
|
||||
{footer}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(WarningMask)
|
||||
@@ -0,0 +1,8 @@
|
||||
.mask {
|
||||
background-color: rgba(239, 244, 255, 0.9);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.icon {
|
||||
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
|
||||
}
|
||||
243
web/app/components/app/configuration/config-model/index.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Slider from '@/app/components/base/slider'
|
||||
|
||||
export type IParamIteProps = {
|
||||
id: number
|
||||
name: string
|
||||
tip: string
|
||||
value: number
|
||||
step?: number
|
||||
min?: number
|
||||
max: number
|
||||
onChange: (id: number, value: number) => void
|
||||
}
|
||||
|
||||
const ParamIte: FC<IParamIteProps> = ({ id, name, tip, step = 0.1, min = 0, max, value, onChange }) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-[6px]">{name}</span>
|
||||
{/* Give tooltip different tip to avoiding hide bug */}
|
||||
<Tooltip htmlContent={<div className="w-[200px]">{tip}</div>} position='top' selector={`param-name-tooltip-${id}`}>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.66667 10.6667H8V8H7.33333M8 5.33333H8.00667M14 8C14 8.78793 13.8448 9.56815 13.5433 10.2961C13.2417 11.0241 12.7998 11.6855 12.2426 12.2426C11.6855 12.7998 11.0241 13.2417 10.2961 13.5433C9.56815 13.8448 8.78793 14 8 14C7.21207 14 6.43185 13.8448 5.7039 13.5433C4.97595 13.2417 4.31451 12.7998 3.75736 12.2426C3.20021 11.6855 2.75825 11.0241 2.45672 10.2961C2.15519 9.56815 2 8.78793 2 8C2 6.4087 2.63214 4.88258 3.75736 3.75736C4.88258 2.63214 6.4087 2 8 2C9.5913 2 11.1174 2.63214 12.2426 3.75736C13.3679 4.88258 14 6.4087 14 8Z" stroke="#9CA3AF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-4 w-[120px]">
|
||||
<Slider value={max < 5 ? value * 10 : value} min={min < 0 ? min * 10 : min} max={max < 5 ? max * 10 : max} onChange={value => onChange(id, value / (max < 5 ? 10 : 1))} />
|
||||
</div>
|
||||
<input type="number" min={min} max={max} step={step} className="block w-[64px] h-9 leading-9 rounded-lg border-0 pl-1 pl py-1.5 bg-gray-50 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-primary-600" value={value} onChange={(e) => {
|
||||
const value = parseFloat(e.target.value)
|
||||
if (value < 0 || value > max)
|
||||
return
|
||||
|
||||
onChange(id, value)
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ParamIte)
|
||||
@@ -0,0 +1,72 @@
|
||||
'use client'
|
||||
import React, { FC, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useClickAway } from 'ahooks'
|
||||
import VarHighlight from '../../base/var-highlight'
|
||||
|
||||
export interface IConfirmAddVarProps {
|
||||
varNameArr: string[]
|
||||
onConfrim: () => void
|
||||
onCancel: () => void
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
const VarIcon = (
|
||||
<svg width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.8683 0.704745C13.7051 0.374685 13.3053 0.239393 12.9752 0.402563C12.6452 0.565732 12.5099 0.965573 12.673 1.29563C13.5221 3.01316 13.9999 4.94957 13.9999 7.00019C13.9999 9.05081 13.5221 10.9872 12.673 12.7047C12.5099 13.0348 12.6452 13.4346 12.9752 13.5978C13.3053 13.761 13.7051 13.6257 13.8683 13.2956C14.8063 11.3983 15.3333 9.26009 15.3333 7.00019C15.3333 4.74029 14.8063 2.60209 13.8683 0.704745Z" fill="#FD853A" />
|
||||
<path d="M3.32687 1.29563C3.49004 0.965573 3.35475 0.565732 3.02469 0.402563C2.69463 0.239393 2.29479 0.374685 2.13162 0.704745C1.19364 2.60209 0.666626 4.74029 0.666626 7.00019C0.666626 9.26009 1.19364 11.3983 2.13162 13.2956C2.29479 13.6257 2.69463 13.761 3.02469 13.5978C3.35475 13.4346 3.49004 13.0348 3.32687 12.7047C2.47779 10.9872 1.99996 9.05081 1.99996 7.00019C1.99996 4.94957 2.47779 3.01316 3.32687 1.29563Z" fill="#FD853A" />
|
||||
<path d="M9.33238 4.8413C9.74208 4.36081 10.3411 4.08337 10.9726 4.08337H11.0324C11.4006 4.08337 11.6991 4.38185 11.6991 4.75004C11.6991 5.11823 11.4006 5.41671 11.0324 5.41671H10.9726C10.7329 5.41671 10.5042 5.52196 10.347 5.7064L8.78693 7.536L9.28085 9.27382C9.29145 9.31112 9.32388 9.33337 9.35696 9.33337H10.2864C10.6545 9.33337 10.953 9.63185 10.953 10C10.953 10.3682 10.6545 10.6667 10.2864 10.6667H9.35696C8.72382 10.6667 8.17074 10.245 7.99832 9.63834L7.74732 8.75524L6.76373 9.90878C6.35403 10.3893 5.75501 10.6667 5.1235 10.6667H5.06372C4.69553 10.6667 4.39705 10.3682 4.39705 10C4.39705 9.63185 4.69553 9.33337 5.06372 9.33337H5.1235C5.3632 9.33337 5.59189 9.22812 5.74915 9.04368L7.30926 7.21399L6.81536 5.47626C6.80476 5.43897 6.77233 5.41671 6.73925 5.41671H5.80986C5.44167 5.41671 5.14319 5.11823 5.14319 4.75004C5.14319 4.38185 5.44167 4.08337 5.80986 4.08337H6.73925C7.37239 4.08337 7.92547 4.50508 8.0979 5.11174L8.34887 5.99475L9.33238 4.8413Z" fill="#FD853A" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const ConfirmAddVar: FC<IConfirmAddVarProps> = ({
|
||||
varNameArr,
|
||||
onConfrim,
|
||||
onCancel,
|
||||
onHide,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const mainContentRef = useRef<HTMLDivElement>(null)
|
||||
useClickAway(() => {
|
||||
onHide()
|
||||
}, mainContentRef)
|
||||
return (
|
||||
<div className='absolute inset-0 flex items-center justify-center rounded-xl'
|
||||
style={{
|
||||
backgroundColor: 'rgba(35, 56, 118, 0.2)'
|
||||
}}>
|
||||
<div
|
||||
ref={mainContentRef}
|
||||
className='w-[420px] rounded-xl bg-gray-50 p-6'
|
||||
style={{
|
||||
boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)'
|
||||
}}
|
||||
>
|
||||
<div className='flex items-start space-x-3'>
|
||||
<div
|
||||
className='shrink-0 flex items-center justify-center h-10 w-10 rounded-xl border border-gray-100'
|
||||
style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||
boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)'
|
||||
}}
|
||||
>{VarIcon}</div>
|
||||
<div className='grow-1'>
|
||||
<div className='text-sm font-medium text-gray-900'>{t('appDebug.autoAddVar')}</div>
|
||||
<div className='flex flex-wrap mt-[15px] max-h-[66px] overflow-y-auto px-1 space-x-1'>
|
||||
{varNameArr.map((name) => (
|
||||
<VarHighlight key={name} name={name} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-7 flex justify-end space-x-2'>
|
||||
<Button className='w-20 h-8 !text-[13px]' onClick={onCancel}>{t('common.operation.cancel')}</Button>
|
||||
<Button className='w-20 h-8 !text-[13px]' type='primary' onClick={onConfrim}>{t('common.operation.add')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ConfirmAddVar)
|
||||
95
web/app/components/app/configuration/config-prompt/index.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import BlockInput from '@/app/components/base/block-input'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { AppType } from '@/types/app'
|
||||
import { getNewVar } from '@/utils/var'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import ConfirmAddVar from './confirm-add-var'
|
||||
|
||||
export type IPromptProps = {
|
||||
mode: AppType
|
||||
promptTemplate: string
|
||||
promptVariables: PromptVariable[]
|
||||
onChange: (promp: string, promptVariables: PromptVariable[]) => void
|
||||
}
|
||||
|
||||
const Prompt: FC<IPromptProps> = ({
|
||||
mode,
|
||||
promptTemplate,
|
||||
promptVariables,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const promptVariablesObj = (() => {
|
||||
const obj: Record<string, boolean> = {}
|
||||
promptVariables.forEach((item) => {
|
||||
obj[item.key] = true
|
||||
})
|
||||
return obj
|
||||
})()
|
||||
|
||||
const [newPromptVariables, setNewPromptVariables] = React.useState<PromptVariable[]>(promptVariables)
|
||||
const [newTemplates, setNewTemplates] = React.useState('')
|
||||
const [isShowConfirmAddVar, { setTrue: showConfirmAddVar, setFalse: hideConfirmAddVar }] = useBoolean(false)
|
||||
|
||||
const handleChange = (newTemplates: string, keys: string[]) => {
|
||||
// const hasRemovedKeysInput = promptVariables.filter(input => keys.includes(input.key))
|
||||
const newPromptVariables = keys.filter(key => !(key in promptVariablesObj)).map(key => getNewVar(key))
|
||||
if (newPromptVariables.length > 0) {
|
||||
setNewPromptVariables(newPromptVariables)
|
||||
setNewTemplates(newTemplates)
|
||||
showConfirmAddVar()
|
||||
return
|
||||
}
|
||||
onChange(newTemplates, [])
|
||||
}
|
||||
|
||||
const handleAutoAdd = (isAdd: boolean) => {
|
||||
return () => {
|
||||
onChange(newTemplates, isAdd ? newPromptVariables : [])
|
||||
hideConfirmAddVar()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='relative rounded-xl border border-[#2D0DEE] bg-gray-25'>
|
||||
<div className="flex items-center h-11 pl-3 gap-1">
|
||||
<svg width="14" height="13" viewBox="0 0 14 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M3.00001 0.100098C3.21218 0.100098 3.41566 0.184383 3.56569 0.334412C3.71572 0.484441 3.80001 0.687924 3.80001 0.900098V1.7001H4.60001C4.81218 1.7001 5.01566 1.78438 5.16569 1.93441C5.31572 2.08444 5.40001 2.28792 5.40001 2.5001C5.40001 2.71227 5.31572 2.91575 5.16569 3.06578C5.01566 3.21581 4.81218 3.3001 4.60001 3.3001H3.80001V4.1001C3.80001 4.31227 3.71572 4.51575 3.56569 4.66578C3.41566 4.81581 3.21218 4.9001 3.00001 4.9001C2.78783 4.9001 2.58435 4.81581 2.43432 4.66578C2.28429 4.51575 2.20001 4.31227 2.20001 4.1001V3.3001H1.40001C1.18783 3.3001 0.98435 3.21581 0.834321 3.06578C0.684292 2.91575 0.600006 2.71227 0.600006 2.5001C0.600006 2.28792 0.684292 2.08444 0.834321 1.93441C0.98435 1.78438 1.18783 1.7001 1.40001 1.7001H2.20001V0.900098C2.20001 0.687924 2.28429 0.484441 2.43432 0.334412C2.58435 0.184383 2.78783 0.100098 3.00001 0.100098ZM3.00001 8.1001C3.21218 8.1001 3.41566 8.18438 3.56569 8.33441C3.71572 8.48444 3.80001 8.68792 3.80001 8.9001V9.7001H4.60001C4.81218 9.7001 5.01566 9.78438 5.16569 9.93441C5.31572 10.0844 5.40001 10.2879 5.40001 10.5001C5.40001 10.7123 5.31572 10.9158 5.16569 11.0658C5.01566 11.2158 4.81218 11.3001 4.60001 11.3001H3.80001V12.1001C3.80001 12.3123 3.71572 12.5158 3.56569 12.6658C3.41566 12.8158 3.21218 12.9001 3.00001 12.9001C2.78783 12.9001 2.58435 12.8158 2.43432 12.6658C2.28429 12.5158 2.20001 12.3123 2.20001 12.1001V11.3001H1.40001C1.18783 11.3001 0.98435 11.2158 0.834321 11.0658C0.684292 10.9158 0.600006 10.7123 0.600006 10.5001C0.600006 10.2879 0.684292 10.0844 0.834321 9.93441C0.98435 9.78438 1.18783 9.7001 1.40001 9.7001H2.20001V8.9001C2.20001 8.68792 2.28429 8.48444 2.43432 8.33441C2.58435 8.18438 2.78783 8.1001 3.00001 8.1001ZM8.60001 0.100098C8.77656 0.100041 8.94817 0.158388 9.0881 0.266047C9.22802 0.373706 9.32841 0.52463 9.37361 0.695298L10.3168 4.2601L13 5.8073C13.1216 5.87751 13.2226 5.9785 13.2928 6.10011C13.363 6.22173 13.4 6.35967 13.4 6.5001C13.4 6.64052 13.363 6.77847 13.2928 6.90008C13.2226 7.02169 13.1216 7.12268 13 7.1929L10.3168 8.7409L9.37281 12.3049C9.32753 12.4754 9.22716 12.6262 9.08732 12.7337C8.94748 12.8413 8.77602 12.8996 8.59961 12.8996C8.42319 12.8996 8.25173 12.8413 8.11189 12.7337C7.97205 12.6262 7.87169 12.4754 7.82641 12.3049L6.88321 8.7401L4.20001 7.1929C4.0784 7.12268 3.97742 7.02169 3.90721 6.90008C3.837 6.77847 3.80004 6.64052 3.80004 6.5001C3.80004 6.35967 3.837 6.22173 3.90721 6.10011C3.97742 5.9785 4.0784 5.87751 4.20001 5.8073L6.88321 4.2593L7.82721 0.695298C7.87237 0.524762 7.97263 0.373937 8.1124 0.266291C8.25216 0.158646 8.42359 0.100217 8.60001 0.100098Z" fill="#5850EC" />
|
||||
</svg>
|
||||
<div className="h2">{mode === AppType.chat ? t('appDebug.chatSubTitle') : t('appDebug.completionSubTitle')}</div>
|
||||
<Tooltip
|
||||
htmlContent={<div className='w-[180px]'>
|
||||
{t('appDebug.promptTip')}
|
||||
</div>}
|
||||
selector='config-prompt-tooltip'>
|
||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.66667 11.1667H8V8.5H7.33333M8 5.83333H8.00667M14 8.5C14 9.28793 13.8448 10.0681 13.5433 10.7961C13.2417 11.5241 12.7998 12.1855 12.2426 12.7426C11.6855 13.2998 11.0241 13.7417 10.2961 14.0433C9.56815 14.3448 8.78793 14.5 8 14.5C7.21207 14.5 6.43185 14.3448 5.7039 14.0433C4.97595 13.7417 4.31451 13.2998 3.75736 12.7426C3.20021 12.1855 2.75825 11.5241 2.45672 10.7961C2.15519 10.0681 2 9.28793 2 8.5C2 6.9087 2.63214 5.38258 3.75736 4.25736C4.88258 3.13214 6.4087 2.5 8 2.5C9.5913 2.5 11.1174 3.13214 12.2426 4.25736C13.3679 5.38258 14 6.9087 14 8.5Z" stroke="#9CA3AF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<BlockInput
|
||||
value={promptTemplate}
|
||||
onConfirm={(value: string, vars: string[]) => {
|
||||
handleChange(value, vars)
|
||||
}}
|
||||
/>
|
||||
|
||||
{isShowConfirmAddVar && (
|
||||
<ConfirmAddVar
|
||||
varNameArr={newPromptVariables.map((v) => v.name)}
|
||||
onConfrim={handleAutoAdd(true)}
|
||||
onCancel={handleAutoAdd(false)}
|
||||
onHide={hideConfirmAddVar}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Prompt)
|
||||
@@ -0,0 +1,115 @@
|
||||
'use client'
|
||||
import React, { FC, useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import ModalFoot from '../modal-foot'
|
||||
import ConfigSelect, { Options } from '../config-select'
|
||||
import ConfigString from '../config-string'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
import SelectTypeItem from '../select-type-item'
|
||||
import { getNewVar } from '@/utils/var'
|
||||
|
||||
import s from './style.module.css'
|
||||
|
||||
export interface IConfigModalProps {
|
||||
payload: PromptVariable
|
||||
type?: string
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
onConfirm: (newValue: { type: string, value: any }) => void
|
||||
}
|
||||
|
||||
const ConfigModal: FC<IConfigModalProps> = ({
|
||||
payload,
|
||||
isShow,
|
||||
onClose,
|
||||
onConfirm
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { type, name, key, options, max_length } = payload || getNewVar('')
|
||||
|
||||
const [tempType, setTempType] = useState(type)
|
||||
useEffect(() => {
|
||||
setTempType(type)
|
||||
}, [type])
|
||||
const handleTypeChange = (type: string) => {
|
||||
return () => {
|
||||
setTempType(type)
|
||||
}
|
||||
}
|
||||
|
||||
const isStringInput = tempType === 'string'
|
||||
const title = isStringInput ? t('appDebug.variableConig.maxLength') : t('appDebug.variableConig.options')
|
||||
|
||||
// string type
|
||||
const [tempMaxLength, setTempMaxValue] = useState(max_length)
|
||||
useEffect(() => {
|
||||
setTempMaxValue(max_length)
|
||||
}, [max_length])
|
||||
|
||||
// select type
|
||||
const [tempOptions, setTempOptions] = useState<Options>(options || [])
|
||||
useEffect(() => {
|
||||
setTempOptions(options || [])
|
||||
}, [options])
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (isStringInput) {
|
||||
onConfirm({ type: tempType, value: tempMaxLength })
|
||||
} else {
|
||||
if (tempOptions.length === 0) {
|
||||
Toast.notify({ type: 'error', message: 'At least one option requied' })
|
||||
return
|
||||
}
|
||||
const obj: Record<string, boolean> = {}
|
||||
let hasRepeatedItem = false
|
||||
tempOptions.forEach(o => {
|
||||
if (obj[o]) {
|
||||
hasRepeatedItem = true
|
||||
return
|
||||
}
|
||||
obj[o] = true
|
||||
})
|
||||
if (hasRepeatedItem) {
|
||||
Toast.notify({ type: 'error', message: 'Has repeat items' })
|
||||
return
|
||||
}
|
||||
onConfirm({ type: tempType, value: tempOptions })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('appDebug.variableConig.modalTitle')}
|
||||
isShow={isShow}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className='mb-8'>
|
||||
<div className='mt-2 mb-8 text-sm text-gray-500'>{t('appDebug.variableConig.description', { varName: `{{${name || key}}}` })}</div>
|
||||
<div className='mb-2'>
|
||||
<div className={s.title}>{t('appDebug.variableConig.fieldType')}</div>
|
||||
<div className='flex space-x-2'>
|
||||
<SelectTypeItem type='string' selected={isStringInput} onClick={handleTypeChange('string')} />
|
||||
<SelectTypeItem type='select' selected={!isStringInput} onClick={handleTypeChange('select')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mt-6'>
|
||||
<div className={s.title}>{title}</div>
|
||||
{isStringInput ? (
|
||||
<ConfigString value={tempMaxLength} onChange={setTempMaxValue} />
|
||||
) : (
|
||||
<ConfigSelect options={tempOptions} onChange={setTempOptions} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<ModalFoot
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={onClose}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
export default React.memo(ConfigModal)
|
||||
@@ -0,0 +1,8 @@
|
||||
.title {
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
font-weight: 500;
|
||||
color: #101828;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
'use client'
|
||||
import React, { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PlusIcon } from '@heroicons/react/24/outline'
|
||||
import RemoveIcon from '../../base/icons/remove-icon'
|
||||
|
||||
import s from './style.module.css'
|
||||
|
||||
export type Options = string[]
|
||||
export interface IConfigSelectProps {
|
||||
options: Options
|
||||
onChange: (options: Options) => void
|
||||
}
|
||||
|
||||
|
||||
const ConfigSelect: FC<IConfigSelectProps> = ({
|
||||
options,
|
||||
onChange
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div>
|
||||
{options.length > 0 && (
|
||||
<div className='mb-1 space-y-1 '>
|
||||
{options.map((o, index) => (
|
||||
<div className={`${s.inputWrap} relative`}>
|
||||
<input
|
||||
key={index}
|
||||
type="input"
|
||||
value={o || ''}
|
||||
onChange={e => {
|
||||
let value = e.target.value
|
||||
onChange(options.map((item, i) => {
|
||||
if (index === i) {
|
||||
return value
|
||||
}
|
||||
return item
|
||||
}))
|
||||
}}
|
||||
className={`${s.input} w-full px-3 text-sm leading-9 text-gray-900 border-0 grow h-9 bg-transparent focus:outline-none cursor-pointer`}
|
||||
/>
|
||||
<RemoveIcon
|
||||
className={`${s.deleteBtn} absolute top-1/2 translate-y-[-50%] right-1.5 items-center justify-center w-6 h-6 rounded-md cursor-pointer hover:bg-[#FEE4E2]`}
|
||||
onClick={() => {
|
||||
onChange(options.filter((_, i) => index !== i))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
onClick={() => { onChange([...options, '']) }}
|
||||
className='flex items-center h-9 px-3 gap-2 rounded-lg cursor-pointer text-gray-400 bg-gray-100'>
|
||||
<PlusIcon width={16} height={16}></PlusIcon>
|
||||
<div className='text-gray-500 text-[13px]'>{t('appDebug.variableConig.addOption')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ConfigSelect)
|
||||