This commit is contained in:
Joel 2024-11-15 12:11:00 +08:00
commit ebaa94be15
132 changed files with 10906 additions and 2584 deletions

View File

@ -324,6 +324,8 @@ services:
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
APP_API_URL: ${APP_API_URL:-}
MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-}
MARKETPLACE_URL: ${MARKETPLACE_URL:-}
SENTRY_DSN: ${WEB_SENTRY_DSN:-}
NEXT_TELEMETRY_DISABLED: ${NEXT_TELEMETRY_DISABLED:-0}
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}

View File

@ -41,6 +41,8 @@ ENV EDITION=SELF_HOSTED
ENV DEPLOY_ENV=PRODUCTION
ENV CONSOLE_API_URL=http://127.0.0.1:5001
ENV APP_API_URL=http://127.0.0.1:5001
ENV MARKETPLACE_API_URL=http://127.0.0.1:5001
ENV MARKETPLACE_URL=http://127.0.0.1:5001
ENV PORT=3000
ENV NEXT_TELEMETRY_DISABLED=1

View File

@ -1,19 +1,43 @@
import { handleDelete } from './actions'
'use client'
import Card from '@/app/components/plugins/card'
import { customTool, extensionDallE, modelGPT4, toolNotion } from '@/app/components/plugins/card/card-mock'
import PluginItem from '@/app/components/plugins/plugin-item'
// import PluginItem from '@/app/components/plugins/plugin-item'
import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
import ProviderCard from '@/app/components/plugins/provider-card'
// import ProviderCard from '@/app/components/plugins/provider-card'
import Badge from '@/app/components/base/badge'
import InstallBundle from '@/app/components/plugins/install-plugin/install-bundle'
const PluginList = async () => {
const PluginList = () => {
const pluginList = [toolNotion, extensionDallE, modelGPT4, customTool]
return (
<div className='pb-3 bg-white'>
<InstallBundle onClose={() => { }} fromDSLPayload={[
{
type: 'marketplace',
value: {
plugin_unique_identifier: 'langgenius/google:0.0.2@dcb354c9d0fee60e6e9c9eb996e1e485bbef343ba8cd545c0cfb3ec80970f6f1',
},
},
{
type: 'github',
value: {
repo: 'YIXIAO0/test',
version: '1.11.5',
package: 'test.difypkg',
github_plugin_unique_identifier: 'yixiao0/test:0.0.1@3592166c87afcf944b4f13f27467a5c8f9e00bd349cb42033a072734a37431b4',
},
},
{
type: 'marketplace',
value: {
plugin_unique_identifier: 'langgenius/openai:0.0.1@f88fdb98d104466db16a425bfe3af8c1bcad45047a40fb802d98a989ac57a5a3',
},
},
]} />
<div className='mx-3 '>
<h2 className='my-3'>Dify Plugin list</h2>
<div className='grid grid-cols-2 gap-3'>
{/* <h2 className='my-3'>Dify Plugin list</h2> */}
{/* <div className='grid grid-cols-2 gap-3'>
{pluginList.map((plugin, index) => (
<PluginItem
key={index}
@ -21,7 +45,7 @@ const PluginList = async () => {
onDelete={handleDelete}
/>
))}
</div>
</div> */}
<h2 className='my-3'>Install Plugin / Package under bundle</h2>
<div className='w-[512px] rounded-2xl bg-background-section-burn p-2'>
@ -33,21 +57,21 @@ const PluginList = async () => {
}
/>
</div>
<h3 className='my-1'>Installed</h3>
{/* <h3 className='my-1'>Installed</h3>
<div className='w-[512px] rounded-2xl bg-background-section-burn p-2'>
<Card
payload={toolNotion as any}
descriptionLineRows={1}
installed
/>
</div>
</div> */}
<h3 className='my-1'>Install model provide</h3>
{/* <h3 className='my-1'>Install model provide</h3>
<div className='grid grid-cols-2 gap-3'>
{pluginList.map((plugin, index) => (
<ProviderCard key={index} payload={plugin as any} />
))}
</div>
</div> */}
<div className='my-3 h-[px] bg-gray-50'></div>
<h2 className='my-3'>Marketplace Plugin list</h2>
@ -67,8 +91,8 @@ const PluginList = async () => {
)
}
export const metadata = {
title: 'Plugins - Card',
}
// export const metadata = {
// title: 'Plugins - Card',
// }
export default PluginList

View File

@ -23,7 +23,7 @@ const FeaturePanel: FC<IFeaturePanelProps> = ({
children,
}) => {
return (
<div className={cn('rounded-xl border-t-[0.5px] border-l-[0.5px] bg-background-section-burn pb-3', noBodySpacing && '!pb-0', className)}>
<div className={cn('rounded-xl border-t-[0.5px] border-l-[0.5px] bg-background-section-burn pb-3', noBodySpacing && 'pb-0', className)}>
{/* Header */}
<div className={cn('px-3 pt-2', hasHeaderBottomBorder && 'border-b border-divider-subtle')}>
<div className='flex justify-between items-center h-8'>

View File

@ -92,6 +92,7 @@ const AgentTools: FC = () => {
tool_name: tool.tool_name,
tool_label: tool.tool_label,
tool_parameters: tool.params,
notAuthor: !tool.is_team_authorization,
enabled: true,
})
})
@ -101,7 +102,7 @@ const AgentTools: FC = () => {
return (
<>
<Panel
className="mt-2"
className={cn('mt-2', tools.length === 0 && 'pb-2')}
noBodySpacing={tools.length === 0}
headerIcon={
<RiHammerFill className='w-4 h-4 text-primary-500' />

View File

@ -21,6 +21,7 @@ import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { getRedirection } from '@/utils/app-redirection'
import cn from '@/utils/classnames'
import { useMutationCheckDependenciesBeforeImportDSL } from '@/service/use-plugins'
type CreateFromDSLModalProps = {
show: boolean
@ -43,6 +44,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
const [fileContent, setFileContent] = useState<string>()
const [currentTab, setCurrentTab] = useState(activeTab)
const [dslUrlValue, setDslUrlValue] = useState(dslUrl)
const { mutateAsync } = useMutationCheckDependenciesBeforeImportDSL()
const readFile = (file: File) => {
const reader = new FileReader()
@ -78,11 +80,21 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
let app
if (currentTab === CreateFromDSLModalTab.FROM_FILE) {
const leakedData = await mutateAsync({ dslString: fileContent })
if (leakedData?.leaked.length) {
isCreatingRef.current = false
return
}
app = await importApp({
data: fileContent || '',
})
}
if (currentTab === CreateFromDSLModalTab.FROM_URL) {
const leakedData = await mutateAsync({ url: dslUrlValue })
if (leakedData?.leaked.length) {
isCreatingRef.current = false
return
}
app = await importAppFromUrl({
url: dslUrlValue || '',
})

View File

@ -2,15 +2,15 @@
display: flex;
flex-direction: row;
align-items: center;
background-color: #ffffff;
background-color: var(--color-components-chat-input-audio-bg-alt);
border-radius: 10px;
padding: 8px;
min-width: 240px;
max-width: 420px;
max-height: 40px;
backdrop-filter: blur(5px);
border: 1px solid rgba(16, 24, 40, 0.08);
box-shadow: 0 1px 2px rgba(9, 9, 11, 0.05);
border: 1px solid var(--color-components-panel-border-subtle);
box-shadow: 0 1px 2px var(--color-shadow-shadow-3);
gap: 8px;
}
@ -19,8 +19,8 @@
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #296DFF;
color: white;
background-color: var(--color-components-button-primary-bg);
color: var(--color-components-chat-input-audio-bg-alt);
border: none;
cursor: pointer;
align-items: center;
@ -30,16 +30,15 @@
}
.playButton:hover {
background-color: #3367d6;
background-color: var(--color-components-button-primary-bg-hover);
}
.playButton:disabled {
background-color: #bdbdbf;
background-color: var(--color-components-button-primary-bg-disabled);
}
.audioControls {
flex-grow: 1;
}
.progressBarContainer {
@ -76,8 +75,8 @@
.timeDisplay {
/* position: absolute; */
color: #296DFF;
border-radius: 2px;
color: var(--color-text-accent-secondary);
font-size: 12px;
order: 0;
height: 100%;
width: 50px;
@ -97,7 +96,6 @@
} */
.duration {
background-color: rgba(255, 255, 255, 0.8);
padding: 2px 4px;
border-radius: 10px;
}
@ -114,6 +112,6 @@
}
.playButton svg path,
.playButton svg rect{
fill:currentColor;
.playButton svg rect {
fill: currentColor;
}

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="46" height="24" viewBox="0 0 46 24" fill="none">
<path opacity="0.5" d="M-6.5 8C-6.5 3.58172 -2.91828 0 1.5 0H45.5L33.0248 24H1.49999C-2.91829 24 -6.5 20.4183 -6.5 16V8Z" fill="url(#paint0_linear_6333_42118)"/>
<defs>
<linearGradient id="paint0_linear_6333_42118" x1="1.81679" y1="5.47784e-07" x2="101.257" y2="30.3866" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.12"/>
<stop offset="1" stop-color="white" stop-opacity="0.3"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 561 B

View File

@ -0,0 +1,6 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="sparkles-soft">
<path id="Vector" opacity="0.5" d="M10.9963 1.36798C10.9839 1.25339 10.8909 1.16677 10.7802 1.16666C10.6695 1.16654 10.5763 1.25295 10.5636 1.36752C10.5045 1.90085 10.3525 2.26673 10.1143 2.5149C9.87599 2.76307 9.52476 2.92145 9.01275 2.98296C8.90277 2.99618 8.81983 3.09324 8.81995 3.20856C8.82006 3.32388 8.90322 3.42076 9.0132 3.43373C9.51653 3.49312 9.87583 3.65148 10.1201 3.90135C10.3631 4.14986 10.518 4.51523 10.563 5.04321C10.573 5.16035 10.6673 5.25012 10.7802 5.24999C10.8931 5.24986 10.9872 5.15987 10.9969 5.0427C11.0401 4.52364 11.1949 4.15004 11.4394 3.89528C11.684 3.64052 12.0426 3.47926 12.5409 3.43433C12.6534 3.42419 12.7398 3.32619 12.7399 3.20858C12.7401 3.09097 12.6539 2.99277 12.5414 2.98236C12.0346 2.93546 11.6838 2.77407 11.4452 2.52098C11.2054 2.2665 11.0533 1.89229 10.9963 1.36798Z" fill="#F5F8FF"/>
<path id="Vector_2" d="M7.13646 2.85102C7.10442 2.55638 6.8653 2.33365 6.5806 2.33334C6.29595 2.33304 6.05633 2.55526 6.02374 2.84984C5.87186 4.22127 5.48089 5.1621 4.86827 5.80025C4.25565 6.43838 3.35245 6.84566 2.03587 7.00386C1.75307 7.03781 1.53975 7.28742 1.54004 7.58393C1.54033 7.88049 1.75415 8.12958 2.03701 8.16294C3.33132 8.31566 4.25509 8.72289 4.88328 9.36543C5.50807 10.0045 5.90647 10.9439 6.02222 12.3016C6.04793 12.6029 6.29035 12.8337 6.58066 12.8333C6.87102 12.833 7.11294 12.6016 7.13797 12.3003C7.24885 10.9656 7.64695 10.0049 8.27583 9.34979C8.90477 8.69471 9.82698 8.28002 11.1083 8.16452C11.3976 8.13844 11.6197 7.88644 11.62 7.58399C11.6204 7.28159 11.3988 7.02906 11.1096 7.00229C9.8062 6.88171 8.90432 6.46673 8.29084 5.81589C7.674 5.16152 7.28306 4.19926 7.13646 2.85102Z" fill="#F5F8FF"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -2,8 +2,8 @@
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import s from './BaichuanTextCn.module.css'
import cn from '@/utils/classnames'
import s from './BaichuanTextCn.module.css'
const Icon = React.forwardRef<HTMLSpanElement, React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>>((
{ className, ...restProps },

View File

@ -2,8 +2,8 @@
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import s from './Minimax.module.css'
import cn from '@/utils/classnames'
import s from './Minimax.module.css'
const Icon = React.forwardRef<HTMLSpanElement, React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>>((
{ className, ...restProps },

View File

@ -2,8 +2,8 @@
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import s from './MinimaxText.module.css'
import cn from '@/utils/classnames'
import s from './MinimaxText.module.css'
const Icon = React.forwardRef<HTMLSpanElement, React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>>((
{ className, ...restProps },

View File

@ -2,8 +2,8 @@
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import s from './Tongyi.module.css'
import cn from '@/utils/classnames'
import s from './Tongyi.module.css'
const Icon = React.forwardRef<HTMLSpanElement, React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>>((
{ className, ...restProps },

View File

@ -2,8 +2,8 @@
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import s from './TongyiText.module.css'
import cn from '@/utils/classnames'
import s from './TongyiText.module.css'
const Icon = React.forwardRef<HTMLSpanElement, React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>>((
{ className, ...restProps },

View File

@ -2,8 +2,8 @@
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import s from './TongyiTextCn.module.css'
import cn from '@/utils/classnames'
import s from './TongyiTextCn.module.css'
const Icon = React.forwardRef<HTMLSpanElement, React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>>((
{ className, ...restProps },

View File

@ -2,8 +2,8 @@
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import s from './Wxyy.module.css'
import cn from '@/utils/classnames'
import s from './Wxyy.module.css'
const Icon = React.forwardRef<HTMLSpanElement, React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>>((
{ className, ...restProps },

View File

@ -2,8 +2,8 @@
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import s from './WxyyText.module.css'
import cn from '@/utils/classnames'
import s from './WxyyText.module.css'
const Icon = React.forwardRef<HTMLSpanElement, React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>>((
{ className, ...restProps },

View File

@ -2,8 +2,8 @@
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import s from './WxyyTextCn.module.css'
import cn from '@/utils/classnames'
import s from './WxyyTextCn.module.css'
const Icon = React.forwardRef<HTMLSpanElement, React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>>((
{ className, ...restProps },

View File

@ -0,0 +1,67 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"xmlns": "http://www.w3.org/2000/svg",
"width": "46",
"height": "24",
"viewBox": "0 0 46 24",
"fill": "none"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"opacity": "0.5",
"d": "M-6.5 8C-6.5 3.58172 -2.91828 0 1.5 0H45.5L33.0248 24H1.49999C-2.91829 24 -6.5 20.4183 -6.5 16V8Z",
"fill": "url(#paint0_linear_6333_42118)"
},
"children": []
},
{
"type": "element",
"name": "defs",
"attributes": {},
"children": [
{
"type": "element",
"name": "linearGradient",
"attributes": {
"id": "paint0_linear_6333_42118",
"x1": "1.81679",
"y1": "5.47784e-07",
"x2": "101.257",
"y2": "30.3866",
"gradientUnits": "userSpaceOnUse"
},
"children": [
{
"type": "element",
"name": "stop",
"attributes": {
"stop-color": "white",
"stop-opacity": "0.12"
},
"children": []
},
{
"type": "element",
"name": "stop",
"attributes": {
"offset": "1",
"stop-color": "white",
"stop-opacity": "0.3"
},
"children": []
}
]
}
]
}
]
},
"name": "Highlight"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Highlight.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Highlight'
export default Icon

View File

@ -0,0 +1,38 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "16",
"height": "16",
"viewBox": "0 0 16 16",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "lock"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector",
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M8 1.75C6.27411 1.75 4.875 3.14911 4.875 4.875V6.125C3.83947 6.125 3 6.96444 3 8V12.375C3 13.4106 3.83947 14.25 4.875 14.25H11.125C12.1606 14.25 13 13.4106 13 12.375V8C13 6.96444 12.1606 6.125 11.125 6.125V4.875C11.125 3.14911 9.72587 1.75 8 1.75ZM9.875 6.125V4.875C9.875 3.83947 9.03556 3 8 3C6.96444 3 6.125 3.83947 6.125 4.875V6.125H9.875ZM8 8.625C8.34519 8.625 8.625 8.90481 8.625 9.25V11.125C8.625 11.4702 8.34519 11.75 8 11.75C7.65481 11.75 7.375 11.4702 7.375 11.125V9.25C7.375 8.90481 7.65481 8.625 8 8.625Z",
"fill": "#155AEF"
},
"children": []
}
]
}
]
},
"name": "Lock"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Lock.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'Lock'
export default Icon

View File

@ -0,0 +1,47 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "14",
"height": "14",
"viewBox": "0 0 14 14",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "sparkles-soft"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector",
"opacity": "0.5",
"d": "M10.9963 1.36798C10.9839 1.25339 10.8909 1.16677 10.7802 1.16666C10.6695 1.16654 10.5763 1.25295 10.5636 1.36752C10.5045 1.90085 10.3525 2.26673 10.1143 2.5149C9.87599 2.76307 9.52476 2.92145 9.01275 2.98296C8.90277 2.99618 8.81983 3.09324 8.81995 3.20856C8.82006 3.32388 8.90322 3.42076 9.0132 3.43373C9.51653 3.49312 9.87583 3.65148 10.1201 3.90135C10.3631 4.14986 10.518 4.51523 10.563 5.04321C10.573 5.16035 10.6673 5.25012 10.7802 5.24999C10.8931 5.24986 10.9872 5.15987 10.9969 5.0427C11.0401 4.52364 11.1949 4.15004 11.4394 3.89528C11.684 3.64052 12.0426 3.47926 12.5409 3.43433C12.6534 3.42419 12.7398 3.32619 12.7399 3.20858C12.7401 3.09097 12.6539 2.99277 12.5414 2.98236C12.0346 2.93546 11.6838 2.77407 11.4452 2.52098C11.2054 2.2665 11.0533 1.89229 10.9963 1.36798Z",
"fill": "#F5F8FF"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector_2",
"d": "M7.13646 2.85102C7.10442 2.55638 6.8653 2.33365 6.5806 2.33334C6.29595 2.33304 6.05633 2.55526 6.02374 2.84984C5.87186 4.22127 5.48089 5.1621 4.86827 5.80025C4.25565 6.43838 3.35245 6.84566 2.03587 7.00386C1.75307 7.03781 1.53975 7.28742 1.54004 7.58393C1.54033 7.88049 1.75415 8.12958 2.03701 8.16294C3.33132 8.31566 4.25509 8.72289 4.88328 9.36543C5.50807 10.0045 5.90647 10.9439 6.02222 12.3016C6.04793 12.6029 6.29035 12.8337 6.58066 12.8333C6.87102 12.833 7.11294 12.6016 7.13797 12.3003C7.24885 10.9656 7.64695 10.0049 8.27583 9.34979C8.90477 8.69471 9.82698 8.28002 11.1083 8.16452C11.3976 8.13844 11.6197 7.88644 11.62 7.58399C11.6204 7.28159 11.3988 7.02906 11.1096 7.00229C9.8062 6.88171 8.90432 6.46673 8.29084 5.81589C7.674 5.16152 7.28306 4.19926 7.13646 2.85102Z",
"fill": "#F5F8FF"
},
"children": []
}
]
}
]
},
"name": "SparklesSoft"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './SparklesSoft.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'SparklesSoft'
export default Icon

View File

@ -2,8 +2,11 @@ export { default as D } from './D'
export { default as DiagonalDividingLine } from './DiagonalDividingLine'
export { default as Dify } from './Dify'
export { default as Github } from './Github'
export { default as Highlight } from './Highlight'
export { default as Line3 } from './Line3'
export { default as Lock } from './Lock'
export { default as MessageChatSquare } from './MessageChatSquare'
export { default as MultiPathRetrieval } from './MultiPathRetrieval'
export { default as NTo1Retrieval } from './NTo1Retrieval'
export { default as Notion } from './Notion'
export { default as SparklesSoft } from './SparklesSoft'

View File

@ -0,0 +1,48 @@
@tailwind components;
@layer components {
.premium-badge {
@apply inline-flex justify-center items-center rounded-full border box-border border-[rgba(255,255,255,0.8)] text-white
}
/* m is for the regular button */
.premium-badge-m {
@apply border shadow-lg !p-1 h-6 w-auto
}
.premium-badge-s {
@apply border-[0.5px] shadow-xs !px-1 !py-[3px] h-[18px] w-auto
}
.premium-badge-blue {
@apply bg-gradient-to-r from-[#5289ffe6] to-[#155aefe6] bg-util-colors-blue-blue-200
}
.premium-badge-indigo {
@apply bg-gradient-to-r from-[#8098f9e6] to-[#444ce7e6] bg-util-colors-indigo-indigo-200
}
.premium-badge-gray {
@apply bg-gradient-to-r from-[#98a2b2e6] to-[#676f83e6] bg-util-colors-gray-gray-200
}
.premium-badge-orange {
@apply bg-gradient-to-r from-[#ff692ee6] to-[#e04f16e6] bg-util-colors-orange-orange-200
}
.premium-badge-blue.allowHover:hover {
@apply bg-gradient-to-r from-[#296dffe6] to-[#004aebe6] bg-util-colors-blue-blue-300 cursor-pointer
}
.premium-badge-indigo.allowHover:hover {
@apply bg-gradient-to-r from-[#6172f3e6] to-[#2d31a6e6] bg-util-colors-indigo-indigo-300 cursor-pointer
}
.premium-badge-gray.allowHover:hover {
@apply bg-gradient-to-r from-[#676f83e6] to-[#354052e6] bg-util-colors-gray-gray-300 cursor-pointer
}
.premium-badge-orange.allowHover:hover {
@apply bg-gradient-to-r from-[#ff4405e6] to-[#b93815e6] bg-util-colors-orange-orange-300 cursor-pointer
}
}

View File

@ -0,0 +1,78 @@
import type { CSSProperties, ReactNode } from 'react'
import React from 'react'
import { type VariantProps, cva } from 'class-variance-authority'
import classNames from '@/utils/classnames'
import './index.css'
import { Highlight } from '../icons/src/public/common'
const PremiumBadgeVariants = cva(
'premium-badge',
{
variants: {
size: {
s: 'premium-badge-s',
m: 'premium-badge-m',
},
color: {
blue: 'premium-badge-blue',
indigo: 'premium-badge-indigo',
gray: 'premium-badge-gray',
orange: 'premium-badge-orange',
},
allowHover: {
true: 'allowHover',
false: '',
},
},
defaultVariants: {
size: 'm',
color: 'blue',
allowHover: false,
},
},
)
type PremiumBadgeProps = {
size?: 's' | 'm'
color?: 'blue' | 'indigo' | 'gray' | 'orange'
allowHover?: boolean
styleCss?: CSSProperties
children?: ReactNode
} & React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof PremiumBadgeVariants>
const PremiumBadge: React.FC<PremiumBadgeProps> = ({
className,
size,
color,
allowHover,
styleCss,
children,
...props
}) => {
return (
<div
className={classNames(
PremiumBadgeVariants({ size, color, allowHover, className }),
'relative text-nowrap',
)}
style={styleCss}
{...props}
>
{children}
<Highlight
className={classNames(
'absolute top-0 opacity-50 hover:opacity-80',
size === 's' ? 'h-4.5 w-12' : 'h-6 w-12',
)}
style={{
right: '50%',
transform: 'translateX(10%)',
}}
/>
</div>
)
}
PremiumBadge.displayName = 'PremiumBadge'
export default PremiumBadge
export { PremiumBadge, PremiumBadgeVariants }

View File

@ -6,7 +6,7 @@ type Props = {
}
const VideoGallery: React.FC<Props> = ({ srcs }) => {
return (<><br/>{srcs.map((src, index) => (<><br/><VideoPlayer key={`video_${index}`} src={src}/></>))}</>)
return (<><br/>{srcs.map((src, index) => (<React.Fragment key={`video_${index}`}><br/><VideoPlayer src={src}/></React.Fragment>))}</>)
}
export default React.memo(VideoGallery)

View File

@ -2,9 +2,8 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { GoldCoin } from '../../base/icons/src/vender/solid/FinanceAndECommerce'
import { Sparkles } from '../../base/icons/src/public/billing'
import s from './style.module.css'
import PremiumBadge from '../../base/premium-badge'
import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
import cn from '@/utils/classnames'
import { useModalContext } from '@/context/modal-context'
@ -36,9 +35,7 @@ const PlainBtn = ({ className, onClick }: { className?: string; onClick: () => v
const UpgradeBtn: FC<Props> = ({
className,
isPlain = false,
isFull = false,
isShort = false,
size = 'md',
onClick: _onClick,
loc,
}) => {
@ -63,22 +60,19 @@ const UpgradeBtn: FC<Props> = ({
return <PlainBtn onClick={onClick} className={className} />
return (
<div
className={cn(
s.upgradeBtn,
className,
isFull ? 'justify-center' : 'px-3',
size === 'lg' ? 'h-10' : 'h-9',
'relative flex items-center cursor-pointer border rounded-[20px] border-[#0096EA] text-white',
)}
<PremiumBadge
size="m"
color="blue"
allowHover={true}
onClick={onClick}
>
<GoldCoin className='mr-1 w-3.5 h-3.5' />
<div className='text-xs font-normal text-nowrap'>{t(`billing.upgradeBtn.${isShort ? 'encourageShort' : 'encourage'}`)}</div>
<Sparkles
className='absolute -right-1 -top-2 w-4 h-5 bg-cover'
/>
</div>
<SparklesSoft className='flex items-center py-[1px] pl-[3px] w-3.5 h-3.5 text-components-premium-badge-indigo-text-stop-0' />
<div className='system-xs-medium'>
<span className='p-1'>
{t(`billing.upgradeBtn.${isShort ? 'encourageShort' : 'encourage'}`)}
</span>
</div>
</PremiumBadge>
)
}
export default React.memo(UpgradeBtn)

View File

@ -6,13 +6,17 @@ import { RiArrowDownSLine } from '@remixicon/react'
import cn from '@/utils/classnames'
import { switchWorkspace } from '@/service/common'
import { useWorkspacesContext } from '@/context/workspace-context'
import { useProviderContext } from '@/context/provider-context'
import { ToastContext } from '@/app/components/base/toast'
import PremiumBadge from '@/app/components/base/premium-badge'
const WorkplaceSelector = () => {
const { t } = useTranslation()
const { plan } = useProviderContext()
const { notify } = useContext(ToastContext)
const { workspaces } = useWorkspacesContext()
const currentWorkspace = workspaces.find(v => v.current)
const isFreePlan = plan.type === 'sandbox'
const handleSwitchWorkspace = async (tenant_id: string) => {
try {
if (currentWorkspace?.id === tenant_id)
@ -57,7 +61,7 @@ const WorkplaceSelector = () => {
`,
)}
>
<div className="flex flex-col p-1 pb-2 items-start self-stretch w-full rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadows-shadow-lg ">
<div className="flex flex-col p-1 pb-2 items-start self-stretch w-full rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg ">
<div className='flex px-3 pt-1 pb-0.5 items-start self-stretch'>
<span className='flex-1 text-text-tertiary system-xs-medium-uppercase'>{t('common.userProfile.workspace')}</span>
</div>
@ -65,7 +69,16 @@ const WorkplaceSelector = () => {
workspaces.map(workspace => (
<div className='flex py-1 pl-3 pr-2 items-center gap-2 self-stretch hover:bg-state-base-hover rounded-lg' key={workspace.id} onClick={() => handleSwitchWorkspace(workspace.id)}>
<div className='flex items-center justify-center w-7 h-7 bg-[#EFF4FF] rounded-md text-xs font-medium text-primary-600'>{workspace.name[0].toLocaleUpperCase()}</div>
<div className='line-clamp-1 flex-grow overflow-hidden text-text-secondary text-ellipsis system-md-regular cursor-pointer'>{workspace.name}</div>
<div className='line-clamp-1 grow overflow-hidden text-text-secondary text-ellipsis system-md-regular cursor-pointer'>{workspace.name}</div>
{
<PremiumBadge size='s' color='gray' allowHover={false}>
<div className='system-2xs-medium'>
<span className='p-[2px]'>
{plan.type === 'professional' ? 'PRO' : plan.type.toUpperCase()}
</span>
</div>
</PremiumBadge>
}
</div>
))
}

View File

@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from 'react'
import {
RiBrain2Fill,
RiBrain2Line,
RiCloseLine,
RiColorFilterFill,
RiColorFilterLine,
RiDatabase2Fill,
@ -16,6 +17,7 @@ import {
RiPuzzle2Line,
RiTranslate2,
} from '@remixicon/react'
import Button from '../../base/button'
import MembersPage from './members-page'
import LanguagePage from './language-page'
import ApiBasedExtensionPage from './api-based-extension-page'
@ -178,34 +180,47 @@ export default function AccountSetting({
}
</div>
</div>
<div ref={scrollRef} className='relative w-[824px] pb-4 bg-components-panel-bg overflow-y-auto'>
<div className={cn('sticky top-0 mx-8 pt-[27px] pb-2 mb-[18px] flex items-center bg-components-panel-bg z-20', scrolled && 'border-b')}>
<div className='shrink-0 text-text-primary title-2xl-semi-bold'>{activeItem?.name}</div>
{
activeItem?.description && (
<div className='shrink-0 ml-2 text-xs text-gray-600'>{activeItem?.description}</div>
)
}
{activeItem?.key === 'provider' && (
<div className='grow flex justify-end'>
<Input
showLeftIcon
wrapperClassName='!w-[200px]'
className='!h-8 !text-[13px]'
onChange={e => setSearchValue(e.target.value)}
value={searchValue}
/>
</div>
)}
<div className='relative flex w-[824px]'>
<div className='absolute top-6 -right-11 flex flex-col items-center z-[9999]'>
<Button
variant='tertiary'
size='large'
className='px-2'
onClick={onCancel}
>
<RiCloseLine className='w-5 h-5' />
</Button>
<div className='mt-1 text-text-tertiary system-2xs-medium-uppercase'>ESC</div>
</div>
<div className='px-4 sm:px-8 pt-2'>
{activeMenu === 'provider' && <ModelProviderPage searchText={searchValue} />}
{activeMenu === 'members' && <MembersPage />}
{activeMenu === 'billing' && <BillingPage />}
{activeMenu === 'data-source' && <DataSourcePage />}
{activeMenu === 'api-based-extension' && <ApiBasedExtensionPage />}
{activeMenu === 'custom' && <CustomPage />}
{activeMenu === 'language' && <LanguagePage />}
<div ref={scrollRef} className='w-full pb-4 bg-components-panel-bg overflow-y-auto'>
<div className={cn('sticky top-0 mx-8 pt-[27px] pb-2 mb-[18px] flex items-center bg-components-panel-bg z-20', scrolled && 'border-b')}>
<div className='shrink-0 text-text-primary title-2xl-semi-bold'>{activeItem?.name}</div>
{
activeItem?.description && (
<div className='shrink-0 ml-2 text-xs text-gray-600'>{activeItem?.description}</div>
)
}
{activeItem?.key === 'provider' && (
<div className='grow flex justify-end'>
<Input
showLeftIcon
wrapperClassName='!w-[200px]'
className='!h-8 !text-[13px]'
onChange={e => setSearchValue(e.target.value)}
value={searchValue}
/>
</div>
)}
</div>
<div className='px-4 sm:px-8 pt-2'>
{activeMenu === 'provider' && <ModelProviderPage searchText={searchValue} />}
{activeMenu === 'members' && <MembersPage />}
{activeMenu === 'billing' && <BillingPage />}
{activeMenu === 'data-source' && <DataSourcePage />}
{activeMenu === 'api-based-extension' && <ApiBasedExtensionPage />}
{activeMenu === 'custom' && <CustomPage />}
{activeMenu === 'language' && <LanguagePage />}
</div>
</div>
</div>
</div>

View File

@ -1,8 +1,6 @@
import { Fragment, useCallback, useEffect } from 'react'
import type { ReactNode } from 'react'
import { RiCloseLine } from '@remixicon/react'
import { Dialog, Transition } from '@headlessui/react'
import Button from '@/app/components/base/button'
import cn from '@/utils/classnames'
type DialogProps = {
@ -47,18 +45,7 @@ const MenuDialog = ({
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className={cn('grow relative w-full h-full p-0 overflow-hidden text-left align-middle transition-all transform bg-background-sidenav-bg backdrop-blur-md', className)}>
<div className='absolute right-0 top-0 h-full w-1/2 bg-components-panel-bg'/>
<div className='absolute top-6 right-6 flex flex-col items-center'>
<Button
variant='tertiary'
size='large'
className='px-2'
onClick={close}
>
<RiCloseLine className='w-5 h-5' />
</Button>
<div className='mt-1 text-text-tertiary system-2xs-medium-uppercase'>ESC</div>
</div>
<div className='absolute top-0 right-0 h-full w-1/2 bg-components-panel-bg' />
{children}
</Dialog.Panel>
</Transition.Child>

View File

@ -1,6 +1,6 @@
export type FormValue = Record<string, any>
export interface TypeWithI18N<T = string> {
export type TypeWithI18N<T = string> = {
en_US: T
zh_Hans: T
[key: string]: T
@ -15,9 +15,12 @@ export enum FormTypeEnum {
boolean = 'boolean',
files = 'files',
file = 'file',
modelSelector = 'model-selector',
toolSelector = 'tool-selector',
appSelector = 'app-selector',
}
export interface FormOption {
export type FormOption = {
label: TypeWithI18N
value: string
show_on: FormShowOnObject[]
@ -89,12 +92,12 @@ export enum CustomConfigurationStatusEnum {
noConfigure = 'no-configure',
}
export interface FormShowOnObject {
export type FormShowOnObject = {
variable: string
value: string
}
export interface CredentialFormSchemaBase {
export type CredentialFormSchemaBase = {
variable: string
label: TypeWithI18N
type: FormTypeEnum
@ -112,7 +115,7 @@ export type CredentialFormSchemaRadio = CredentialFormSchemaBase & { options: Fo
export type CredentialFormSchemaSecretInput = CredentialFormSchemaBase & { placeholder?: TypeWithI18N }
export type CredentialFormSchema = CredentialFormSchemaTextInput | CredentialFormSchemaSelect | CredentialFormSchemaRadio | CredentialFormSchemaSecretInput
export interface ModelItem {
export type ModelItem = {
model: string
label: TypeWithI18N
model_type: ModelTypeEnum
@ -141,7 +144,7 @@ export enum QuotaUnitEnum {
credits = 'credits',
}
export interface QuotaConfiguration {
export type QuotaConfiguration = {
quota_type: CurrentSystemQuotaTypeEnum
quota_unit: QuotaUnitEnum
quota_limit: number
@ -150,7 +153,7 @@ export interface QuotaConfiguration {
is_valid: boolean
}
export interface ModelProvider {
export type ModelProvider = {
provider: string
label: TypeWithI18N
description?: TypeWithI18N
@ -184,7 +187,7 @@ export interface ModelProvider {
}
}
export interface Model {
export type Model = {
provider: string
icon_large: TypeWithI18N
icon_small: TypeWithI18N
@ -193,7 +196,7 @@ export interface Model {
status: ModelStatusEnum
}
export interface DefaultModelResponse {
export type DefaultModelResponse = {
model: string
model_type: ModelTypeEnum
provider: {
@ -203,17 +206,17 @@ export interface DefaultModelResponse {
}
}
export interface DefaultModel {
export type DefaultModel = {
provider: string
model: string
}
export interface CustomConfigurationModelFixedFields {
export type CustomConfigurationModelFixedFields = {
__model_name: string
__model_type: ModelTypeEnum
}
export interface ModelParameterRule {
export type ModelParameterRule = {
default?: number | string | boolean | string[]
help?: TypeWithI18N
label: TypeWithI18N
@ -228,7 +231,7 @@ export interface ModelParameterRule {
tagPlaceholder?: TypeWithI18N
}
export interface ModelLoadBalancingConfigEntry {
export type ModelLoadBalancingConfigEntry = {
/** model balancing config entry id */
id?: string
/** is config entry enabled */
@ -243,7 +246,7 @@ export interface ModelLoadBalancingConfigEntry {
ttl?: number
}
export interface ModelLoadBalancingConfig {
export type ModelLoadBalancingConfig = {
enabled: boolean
configs: ModelLoadBalancingConfigEntry[]
}

View File

@ -19,11 +19,12 @@ const ModelIcon: FC<ModelIconProps> = ({
}) => {
const language = useLanguage()
if (provider?.provider === 'openai' && (modelName?.startsWith('gpt-4') || modelName?.includes('4o')))
if (provider?.provider.includes('openai') && (modelName?.startsWith('gpt-4') || modelName?.includes('4o')))
return <OpenaiViolet className={`w-4 h-4 ${className}`}/>
if (provider?.icon_small) {
return (
// eslint-disable-next-line @next/next/no-img-element
<img
alt='model-icon'
src={`${provider.icon_small[language] || provider.icon_small.en_US}`}

View File

@ -1,4 +1,4 @@
import { useState } from 'react'
import { useCallback, useState } from 'react'
import type { FC } from 'react'
import { ValidatingTip } from '../../key-validator/ValidateStatus'
import type {
@ -17,6 +17,9 @@ import cn from '@/utils/classnames'
import { SimpleSelect } from '@/app/components/base/select'
import Tooltip from '@/app/components/base/tooltip'
import Radio from '@/app/components/base/radio'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
import ToolSelector from '@/app/components/tools/tool-selector'
type FormProps = {
className?: string
itemClassName?: string
@ -67,6 +70,24 @@ const Form: FC<FormProps> = ({
onChange({ ...value, [key]: val, ...shouldClearVariable })
}
const handleModelChanged = useCallback((key: string, model: { provider: string; modelId: string; mode?: string }) => {
const newValue = {
...value[key],
provider: model.provider,
model: model.modelId,
mode: model.mode,
}
onChange({ ...value, [key]: newValue })
}, [onChange, value])
const handleCompletionParamsChange = useCallback((key: string, newParams: Record<string, any>) => {
const newValue = {
...value[key],
completion_params: newParams,
}
onChange({ ...value, [key]: newValue })
}, [onChange, value])
const renderField = (formSchema: CredentialFormSchema) => {
const tooltip = formSchema.tooltip
const tooltipContent = (tooltip && (
@ -94,7 +115,7 @@ const Form: FC<FormProps> = ({
const disabled = readonly || (isEditMode && (variable === '__model_type' || variable === '__model_name'))
return (
<div key={variable} className={cn(itemClassName, 'py-3')}>
<div className={cn(fieldLabelClassName, 'flex items-center py-2 text-sm text-gray-900')}>
<div className={cn(fieldLabelClassName, 'flex items-center py-2 system-sm-semibold text-text-secondary')}>
{label[language] || label.en_US}
{
required && (
@ -135,7 +156,7 @@ const Form: FC<FormProps> = ({
return (
<div key={variable} className={cn(itemClassName, 'py-3')}>
<div className={cn(fieldLabelClassName, 'flex items-center py-2 text-sm text-gray-900')}>
<div className={cn(fieldLabelClassName, 'flex items-center py-2 system-sm-semibold text-text-secondary')}>
{label[language] || label.en_US}
{
required && (
@ -165,7 +186,7 @@ const Form: FC<FormProps> = ({
flex justify-center items-center mr-2 w-4 h-4 border border-gray-300 rounded-full
${value[variable] === option.value && 'border-[5px] border-primary-600'}
`} />
<div className='text-sm text-gray-900'>{option.label[language] || option.label.en_US}</div>
<div className='system-sm-regular text-text-secondary'>{option.label[language] || option.label.en_US}</div>
</div>
))
}
@ -176,7 +197,7 @@ const Form: FC<FormProps> = ({
)
}
if (formSchema.type === 'select') {
if (formSchema.type === FormTypeEnum.select) {
const {
options,
variable,
@ -191,7 +212,7 @@ const Form: FC<FormProps> = ({
return (
<div key={variable} className={cn(itemClassName, 'py-3')}>
<div className={cn(fieldLabelClassName, 'flex items-center py-2 text-sm text-gray-900')}>
<div className={cn(fieldLabelClassName, 'flex items-center py-2 system-sm-semibold text-text-secondary')}>
{label[language] || label.en_US}
{
@ -202,6 +223,7 @@ const Form: FC<FormProps> = ({
{tooltipContent}
</div>
<SimpleSelect
wrapperClassName='h-8'
className={cn(inputClassName)}
disabled={readonly}
defaultValue={(isShowDefaultValue && ((value[variable] as string) === '' || value[variable] === undefined || value[variable] === null)) ? formSchema.default : value[variable]}
@ -220,7 +242,7 @@ const Form: FC<FormProps> = ({
)
}
if (formSchema.type === 'boolean') {
if (formSchema.type === FormTypeEnum.boolean) {
const {
variable,
label,
@ -233,9 +255,9 @@ const Form: FC<FormProps> = ({
return (
<div key={variable} className={cn(itemClassName, 'py-3')}>
<div className='flex items-center justify-between py-2 text-sm text-gray-900'>
<div className='flex items-center justify-between py-2 system-sm-semibold text-text-secondary'>
<div className='flex items-center space-x-2'>
<span className={cn(fieldLabelClassName, 'flex items-center py-2 text-sm text-gray-900')}>{label[language] || label.en_US}</span>
<span className={cn(fieldLabelClassName, 'flex items-center py-2 system-sm-regular text-text-secondary')}>{label[language] || label.en_US}</span>
{
required && (
<span className='ml-1 text-red-500'>*</span>
@ -256,6 +278,77 @@ const Form: FC<FormProps> = ({
</div>
)
}
if (formSchema.type === FormTypeEnum.modelSelector) {
const {
variable,
label,
required,
} = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
return (
<div key={variable} className={cn(itemClassName, 'py-3')}>
<div className={cn(fieldLabelClassName, 'flex items-center py-2 system-sm-semibold text-text-secondary')}>
{label[language] || label.en_US}
{
required && (
<span className='ml-1 text-red-500'>*</span>
)
}
{tooltipContent}
</div>
<ModelParameterModal
popupClassName='!w-[387px]'
isAdvancedMode
isInWorkflow
provider={value[variable]?.provider}
modelId={value[variable]?.name}
mode={value[variable]?.mode}
completionParams={value[variable]?.completion_params}
setModel={model => handleModelChanged(variable, model)}
onCompletionParamsChange={params => handleCompletionParamsChange(variable, params)}
hideDebugWithMultipleModel
debugWithMultipleModel={false}
readonly={readonly}
/>
{fieldMoreInfo?.(formSchema)}
{validating && changeKey === variable && <ValidatingTip />}
</div>
)
}
if (formSchema.type === FormTypeEnum.toolSelector) {
const {
variable,
label,
required,
} = formSchema as (CredentialFormSchemaTextInput | CredentialFormSchemaSecretInput)
return (
<div key={variable} className={cn(itemClassName, 'py-3')}>
<div className={cn(fieldLabelClassName, 'flex items-center py-2 system-sm-semibold text-text-secondary')}>
{label[language] || label.en_US}
{
required && (
<span className='ml-1 text-red-500'>*</span>
)
}
{tooltipContent}
</div>
<ToolSelector
disabled={readonly}
value={value[variable]}
onSelect={item => handleFormChange(variable, item as any)}
/>
{fieldMoreInfo?.(formSchema)}
{validating && changeKey === variable && <ValidatingTip />}
</div>
)
}
if (formSchema.type === FormTypeEnum.appSelector) {
// TODO
}
}
return (

View File

@ -26,14 +26,14 @@ const Input: FC<InputProps> = ({
max,
}) => {
const toLimit = (v: string) => {
const minNum = parseFloat(`${min}`)
const maxNum = parseFloat(`${max}`)
if (!isNaN(minNum) && parseFloat(v) < minNum) {
const minNum = Number.parseFloat(`${min}`)
const maxNum = Number.parseFloat(`${max}`)
if (!isNaN(minNum) && Number.parseFloat(v) < minNum) {
onChange(`${min}`)
return
}
if (!isNaN(maxNum) && parseFloat(v) > maxNum)
if (!isNaN(maxNum) && Number.parseFloat(v) > maxNum)
onChange(`${max}`)
}
return (
@ -41,9 +41,9 @@ const Input: FC<InputProps> = ({
<input
tabIndex={0}
className={`
block px-3 w-full h-9 bg-gray-100 text-sm rounded-lg border border-transparent
block px-3 w-full h-8 bg-components-input-bg-normal text-sm rounded-lg border border-transparent
appearance-none outline-none caret-primary-600
hover:border-[rgba(0,0,0,0.08)] hover:bg-gray-50
hover:border-[rgba(0,0,0,0.08)] hover:bg-state-hover-alt
focus:bg-white focus:border-gray-300 focus:shadow-xs
placeholder:text-sm placeholder:text-gray-400
${validated && 'pr-[30px]'}

View File

@ -68,7 +68,7 @@ const stopParameterRule: ModelParameterRule = {
},
}
const PROVIDER_WITH_PRESET_TONE = ['openai', 'azure_openai']
const PROVIDER_WITH_PRESET_TONE = ['langgenius/openai/openai', 'langgenius/azure_openai/azure_openai']
const ModelParameterModal: FC<ModelParameterModalProps> = ({
popupClassName,
portalToFollowElemContentClassName,
@ -190,26 +190,22 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
)
}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className={cn(portalToFollowElemContentClassName, 'z-[60]')}>
<div className={cn(popupClassName, 'w-[496px] rounded-xl border border-gray-100 bg-white shadow-xl')}>
<div className={cn(
'max-h-[480px] overflow-y-auto',
!isInWorkflow && 'px-10 pt-6 pb-8',
isInWorkflow && 'p-4')}>
<div className='flex items-center justify-between h-8'>
<div className={cn('font-semibold text-gray-900 shrink-0', isInWorkflow && 'text-[13px]')}>
<PortalToFollowElemContent className={cn('z-[60]', portalToFollowElemContentClassName)}>
<div className={cn(popupClassName, 'w-[389px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg')}>
<div className={cn('max-h-[420px] p-4 pt-3 overflow-y-auto')}>
<div className='relative'>
<div className={cn('mb-1 h-6 flex items-center text-text-secondary system-sm-semibold')}>
{t('common.modelProvider.model').toLocaleUpperCase()}
</div>
<ModelSelector
defaultModel={(provider || modelId) ? { provider, model: modelId } : undefined}
modelList={activeTextGenerationModelList}
onSelect={handleChangeModel}
triggerClassName='max-w-[295px]'
/>
</div>
{
!!parameterRules.length && (
<div className='my-5 h-[1px] bg-gray-100' />
<div className='my-3 h-[1px] bg-divider-subtle' />
)
}
{
@ -219,8 +215,8 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
}
{
!isLoading && !!parameterRules.length && (
<div className='flex items-center justify-between mb-4'>
<div className={cn('font-semibold text-gray-900', isInWorkflow && 'text-[13px]')}>{t('common.modelProvider.parameters')}</div>
<div className='flex items-center justify-between mb-2'>
<div className={cn('h-6 flex items-center text-text-secondary system-sm-semibold')}>{t('common.modelProvider.parameters')}</div>
{
PROVIDER_WITH_PRESET_TONE.includes(provider) && (
<PresetsParameter onSelect={handleSelectPresetParameter} />
@ -237,7 +233,6 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
].map(parameter => (
<ParameterItem
key={`${modelId}-${parameter.name}`}
className='mb-4'
parameterRule={parameter}
value={completionParams?.[parameter.name]}
onChange={v => handleParamChange(parameter.name, v)}
@ -250,7 +245,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
</div>
{!hideDebugWithMultipleModel && (
<div
className='flex items-center justify-between px-6 h-[50px] bg-gray-50 border-t border-t-gray-100 text-xs font-medium text-primary-600 cursor-pointer rounded-b-xl'
className='flex items-center justify-between px-4 h-[50px] bg-components-section-burn border-t border-t-divider-subtle system-sm-regular text-text-accent cursor-pointer rounded-b-xl'
onClick={() => onDebugWithMultipleModelChange?.()}
>
{

View File

@ -17,7 +17,6 @@ type ParameterItemProps = {
parameterRule: ModelParameterRule
value?: ParameterValue
onChange?: (value: ParameterValue) => void
className?: string
onSwitch?: (checked: boolean, assignValue: ParameterValue) => void
isInWorkflow?: boolean
}
@ -25,7 +24,6 @@ const ParameterItem: FC<ParameterItemProps> = ({
parameterRule,
value,
onChange,
className,
onSwitch,
isInWorkflow,
}) => {
@ -249,9 +247,20 @@ const ParameterItem: FC<ParameterItemProps> = ({
}
return (
<div className={`flex items-center justify-between ${className}`}>
<div>
<div className={cn(isInWorkflow ? 'w-[140px]' : 'w-full', 'ml-4 shrink-0 flex items-center')}>
<div className='flex items-center justify-between mb-2'>
<div className='shrink-0 basis-1/2'>
<div className={cn('shrink-0 w-full flex items-center')}>
{
!parameterRule.required && parameterRule.name !== 'stop' && (
<div className='mr-2 w-7'>
<Switch
defaultValue={!isNullOrUndefined(value)}
onChange={handleSwitch}
size='md'
/>
</div>
)
}
<div
className='mr-0.5 text-[13px] font-medium text-gray-700 truncate'
title={parameterRule.label[language] || parameterRule.label.en_US}
@ -269,16 +278,6 @@ const ParameterItem: FC<ParameterItemProps> = ({
/>
)
}
{
!parameterRule.required && parameterRule.name !== 'stop' && (
<Switch
className='mr-1'
defaultValue={!isNullOrUndefined(value)}
onChange={handleSwitch}
size='md'
/>
)
}
</div>
{
parameterRule.type === 'tag' && (

View File

@ -2,12 +2,13 @@ import type { FC } from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { RiArrowDownSLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
import Dropdown from '@/app/components/base/dropdown'
import { SlidersH } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
import { Brush01 } from '@/app/components/base/icons/src/vender/solid/editor'
import { Scales02 } from '@/app/components/base/icons/src/vender/solid/FinanceAndECommerce'
import { Target04 } from '@/app/components/base/icons/src/vender/solid/general'
import { TONE_LIST } from '@/config'
import cn from '@/utils/classnames'
type PresetsParameterProps = {
onSelect: (toneId: number) => void
@ -18,19 +19,16 @@ const PresetsParameter: FC<PresetsParameterProps> = ({
const { t } = useTranslation()
const renderTrigger = useCallback((open: boolean) => {
return (
<div
className={`
flex items-center px-[7px] h-7 rounded-md border-[0.5px] border-gray-200 shadow-xs
text-xs font-medium text-gray-700 cursor-pointer
${open && 'bg-gray-100'}
`}
<Button
size={'small'}
variant={'secondary'}
className={cn(open && 'bg-state-base-hover')}
>
<SlidersH className='mr-[5px] w-3.5 h-3.5 text-gray-500' />
{t('common.modelProvider.loadPresets')}
<RiArrowDownSLine className='ml-0.5 w-3.5 h-3.5 text-gray-500' />
</div>
<RiArrowDownSLine className='ml-0.5 w-3.5 h-3.5' />
</Button>
)
}, [])
}, [t])
const getToneIcon = (toneId: number) => {
const className = 'mr-2 w-[14px] h-[14px]'
const res = ({

View File

@ -18,7 +18,7 @@ import {
validateModelProvider,
} from '@/service/common'
export const MODEL_PROVIDER_QUOTA_GET_PAID = ['anthropic', 'openai', 'azure_openai']
export const MODEL_PROVIDER_QUOTA_GET_PAID = ['langgenius/anthropic/anthropic', 'langgenius/openai/openai', 'langgenius/azure_openai/azure_openai']
export const DEFAULT_BACKGROUND_COLOR = '#F3F4F6'

View File

@ -4,7 +4,8 @@ import Link from 'next/link'
import { useBoolean } from 'ahooks'
import { useSelectedLayoutSegment } from 'next/navigation'
import { Bars3Icon } from '@heroicons/react/20/solid'
import HeaderBillingBtn from '../billing/header-billing-btn'
import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
import PremiumBadge from '../base/premium-badge'
import AccountDropdown from './account-dropdown'
import AppNav from './app-nav'
import DatasetNav from './dataset-nav'
@ -20,6 +21,7 @@ import WorkplaceSelector from '@/app/components/header/account-dropdown/workplac
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { useProviderContext } from '@/context/provider-context'
import { useModalContext } from '@/context/modal-context'
import { useTranslation } from 'react-i18next'
const navClassName = `
flex items-center relative mr-0 sm:mr-3 px-3 h-8 rounded-xl
@ -29,6 +31,7 @@ const navClassName = `
const Header = () => {
const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext()
const { t } = useTranslation()
const selectedSegment = useSelectedLayoutSegment()
const media = useBreakpoints()
@ -69,7 +72,14 @@ const Header = () => {
</WorkspaceProvider>
{enableBilling && (
<div className='select-none'>
<HeaderBillingBtn onClick={handlePlanClick} />
<PremiumBadge color='blue' allowHover={true} onClick={handlePlanClick}>
<SparklesSoft className='flex items-center py-[1px] pl-[3px] w-3.5 h-3.5 text-components-premium-badge-indigo-text-stop-0' />
<div className='system-xs-medium'>
<span className='p-1'>
{t('billing.upgradeBtn.encourage')}
</span>
</div>
</PremiumBadge>
</div>
)}
</div>
@ -84,7 +94,14 @@ const Header = () => {
<div className='font-light text-divider-deep'>/</div>
{enableBilling && (
<div className='select-none'>
<HeaderBillingBtn onClick={handlePlanClick} />
<PremiumBadge color='blue' allowHover={true} onClick={handlePlanClick}>
<SparklesSoft className='flex items-center py-[1px] pl-[3px] w-3.5 h-3.5 text-components-premium-badge-indigo-text-stop-0' />
<div className='system-xs-medium'>
<span className='p-1'>
{t('billing.upgradeBtn.encourage')}
</span>
</div>
</PremiumBadge>
</div>
)}
<GithubStar />
@ -98,7 +115,7 @@ const Header = () => {
{!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />}
</div>
)}
<div className='flex items-center flex-shrink-0'>
<div className='flex items-center shrink-0'>
<EnvNav />
<div className='mr-3'>
<PluginsNav />

View File

@ -4,6 +4,8 @@ import { useTranslation } from 'react-i18next'
import Link from 'next/link'
import classNames from '@/utils/classnames'
import { Group } from '@/app/components/base/icons/src/vender/other'
import { useSelectedLayoutSegment } from 'next/navigation'
type PluginsNavProps = {
className?: string
}
@ -12,12 +14,17 @@ const PluginsNav = ({
className,
}: PluginsNavProps) => {
const { t } = useTranslation()
const selectedSegment = useSelectedLayoutSegment()
const activated = selectedSegment === 'plugins'
return (
<Link href="/plugins" className={classNames(
className, 'group',
)}>
<div className='flex flex-row h-8 p-1.5 gap-0.5 items-center justify-center rounded-xl system-sm-medium-uppercase hover:bg-state-base-hover text-text-tertiary hover:text-text-secondary'>
<div className={`flex flex-row h-8 p-1.5 gap-0.5 items-center justify-center
rounded-xl system-sm-medium-uppercase ${activated
? 'border border-components-main-nav-nav-button-border bg-components-main-nav-nav-button-bg-active shadow-md text-components-main-nav-nav-button-text'
: 'text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary'}`}>
<div className='flex w-4 h-4 justify-center items-center'>
<Group />
</div>

View File

@ -15,6 +15,7 @@ type Props = {
label: string
labelWidthClassName?: string
value: string
maskedValue?: string
valueMaxWidthClassName?: string
}
@ -22,6 +23,7 @@ const KeyValueItem: FC<Props> = ({
label,
labelWidthClassName = 'w-10',
value,
maskedValue,
valueMaxWidthClassName = 'max-w-[162px]',
}) => {
const { t } = useTranslation()
@ -49,7 +51,7 @@ const KeyValueItem: FC<Props> = ({
<span className={cn('flex flex-col justify-center items-start text-text-tertiary system-xs-medium', labelWidthClassName)}>{label}</span>
<div className='flex justify-center items-center gap-0.5'>
<span className={cn(valueMaxWidthClassName, ' truncate system-xs-medium text-text-secondary')}>
{value}
{maskedValue || value}
</span>
<Tooltip popupContent={t(`common.operation.${isCopied ? 'copied' : 'copy'}`)} position='top'>
<ActionButton onClick={handleCopy}>

View File

@ -4,7 +4,7 @@ import cn from '@/utils/classnames'
type Props = {
wrapClassName: string
loadingFileName: string
loadingFileName?: string
}
export const LoadingPlaceholder = ({ className }: { className?: string }) => (
@ -27,7 +27,11 @@ const Placeholder = ({
</div>
<div className="ml-3 grow">
<div className="flex items-center h-5">
<Title title={loadingFileName} />
{loadingFileName ? (
<Title title={loadingFileName} />
) : (
<LoadingPlaceholder className="w-[260px]" />
)}
</div>
<div className={cn('flex items-center h-4 space-x-0.5')}>
<LoadingPlaceholder className="w-[41px]" />

View File

@ -2,6 +2,7 @@ import type { PluginDeclaration } from '../types'
import { PluginType } from '../types'
export const toolNeko: PluginDeclaration = {
plugin_unique_identifier: 'xxxxxx',
version: '0.0.1',
author: 'langgenius',
name: 'neko',

View File

@ -87,3 +87,42 @@ export const useTags = (translateFromOut?: TFunction) => {
tagsMap,
}
}
type Category = {
name: string
label: string
}
export const useCategories = (translateFromOut?: TFunction) => {
const { t: translation } = useTranslation()
const t = translateFromOut || translation
const categories = [
{
name: 'model',
label: t('plugin.category.models'),
},
{
name: 'tool',
label: t('plugin.category.tools'),
},
{
name: 'extension',
label: t('plugin.category.extensions'),
},
{
name: 'bundle',
label: t('plugin.category.bundles'),
},
]
const categoriesMap = categories.reduce((acc, category) => {
acc[category.name] = category
return acc
}, {} as Record<string, Category>)
return {
categories,
categoriesMap,
}
}

View File

@ -1,19 +1,12 @@
import { useCallback } from 'react'
import { apiPrefix } from '@/config'
import { fetchWorkspaces } from '@/service/common'
let tenantId: string | null | undefined = null
import { useSelector } from '@/context/app-context'
const useGetIcon = () => {
const getIconUrl = async (fileName: string) => {
if (!tenantId) {
const { workspaces } = await fetchWorkspaces({
url: '/workspaces',
params: {},
})
tenantId = workspaces.find(v => v.current)?.id
}
return `${apiPrefix}/workspaces/current/plugin/icon?tenant_id=${tenantId}&filename=${fileName}`
}
const currentWorkspace = useSelector(s => s.currentWorkspace)
const getIconUrl = useCallback((fileName: string) => {
return `${apiPrefix}/workspaces/current/plugin/icon?tenant_id=${currentWorkspace.id}&filename=${fileName}`
}, [currentWorkspace.id])
return {
getIconUrl,

View File

@ -0,0 +1,66 @@
'use client'
import type { FC } from 'react'
import Modal from '@/app/components/base/modal'
import React, { useCallback, useState } from 'react'
import { InstallStep } from '../../types'
import type { Dependency } from '../../types'
import Install from './steps/install'
import { useTranslation } from 'react-i18next'
const i18nPrefix = 'plugin.installModal'
export enum InstallType {
fromLocal = 'fromLocal',
fromMarketplace = 'fromMarketplace',
fromDSL = 'fromDSL',
}
type Props = {
installType?: InstallType
fromDSLPayload: Dependency[]
// plugins?: PluginDeclaration[]
onClose: () => void
}
const InstallBundle: FC<Props> = ({
installType = InstallType.fromMarketplace,
fromDSLPayload,
onClose,
}) => {
const { t } = useTranslation()
const [step, setStep] = useState<InstallStep>(installType === InstallType.fromMarketplace ? InstallStep.readyToInstall : InstallStep.uploading)
const getTitle = useCallback(() => {
if (step === InstallStep.uploadFailed)
return t(`${i18nPrefix}.uploadFailed`)
if (step === InstallStep.installed)
return t(`${i18nPrefix}.installedSuccessfully`)
if (step === InstallStep.installFailed)
return t(`${i18nPrefix}.installFailed`)
return t(`${i18nPrefix}.installPlugin`)
}, [step, t])
return (
<Modal
isShow={true}
onClose={onClose}
className='flex min-w-[560px] p-0 flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadows-shadow-xl'
closable
>
<div className='flex pt-6 pl-6 pb-3 pr-14 items-start gap-2 self-stretch'>
<div className='self-stretch text-text-primary title-2xl-semi-bold'>
{getTitle()}
</div>
</div>
{step === InstallStep.readyToInstall && (
<Install
fromDSLPayload={fromDSLPayload}
onCancel={onClose}
/>
)}
</Modal>
)
}
export default React.memo(InstallBundle)

View File

@ -0,0 +1,47 @@
'use client'
import type { FC } from 'react'
import React, { useEffect } from 'react'
import type { Dependency, Plugin } from '../../../types'
import { pluginManifestToCardPluginProps } from '../../utils'
import { useUploadGitHub } from '@/service/use-plugins'
import Loading from './loading'
import LoadedItem from './loaded-item'
type Props = {
checked: boolean
onCheckedChange: (plugin: Plugin) => void
dependency: Dependency
onFetchedPayload: (payload: Plugin) => void
}
const Item: FC<Props> = ({
checked,
onCheckedChange,
dependency,
onFetchedPayload,
}) => {
const info = dependency.value
const { data } = useUploadGitHub({
repo: info.repo!,
version: info.version!,
package: info.package!,
})
const [payload, setPayload] = React.useState<Plugin | null>(null)
useEffect(() => {
if (data) {
const payload = pluginManifestToCardPluginProps(data.manifest)
onFetchedPayload(payload)
setPayload(payload)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data])
if (!payload) return <Loading />
return (
<LoadedItem
payload={payload}
checked={checked}
onCheckedChange={onCheckedChange}
/>
)
}
export default React.memo(Item)

View File

@ -0,0 +1,36 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import type { Plugin } from '../../../types'
import Card from '../../../card'
import Checkbox from '@/app/components/base/checkbox'
import Badge, { BadgeState } from '@/app/components/base/badge/index'
type Props = {
checked: boolean
onCheckedChange: (plugin: Plugin) => void
payload: Plugin
}
const LoadedItem: FC<Props> = ({
checked,
onCheckedChange,
payload,
}) => {
return (
<div className='flex items-center space-x-2'>
<Checkbox
className='shrink-0'
checked={checked}
onCheck={() => onCheckedChange(payload)}
/>
<Card
className='grow'
payload={payload}
titleLeft={payload.version ? <Badge className='mx-1' size="s" state={BadgeState.Default}>{payload.version}</Badge> : null}
/>
</div>
)
}
export default React.memo(LoadedItem)

View File

@ -0,0 +1,23 @@
'use client'
import React from 'react'
import Placeholder from '../../../card/base/placeholder'
import Checkbox from '@/app/components/base/checkbox'
const Loading = () => {
return (
<div className='flex items-center space-x-2'>
<Checkbox
className='shrink-0'
checked={false}
disabled
/>
<div className='grow relative p-4 pb-3 border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg hover-bg-components-panel-on-panel-item-bg rounded-xl shadow-xs'>
<Placeholder
wrapClassName='w-full'
/>
</div>
</div>
)
}
export default React.memo(Loading)

View File

@ -0,0 +1,29 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import type { Plugin } from '../../../types'
import Loading from './loading'
import LoadedItem from './loaded-item'
type Props = {
checked: boolean
onCheckedChange: (plugin: Plugin) => void
payload?: Plugin
}
const MarketPlaceItem: FC<Props> = ({
checked,
onCheckedChange,
payload,
}) => {
if (!payload) return <Loading />
return (
<LoadedItem
checked={checked}
onCheckedChange={onCheckedChange}
payload={payload}
/>
)
}
export default React.memo(MarketPlaceItem)

View File

@ -0,0 +1,90 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useEffect, useMemo } from 'react'
import type { Dependency, Plugin } from '../../../types'
import MarketplaceItem from '../item/marketplace-item'
import GithubItem from '../item/github-item'
import { useFetchPluginsInMarketPlaceByIds } from '@/service/use-plugins'
import produce from 'immer'
import { useGetState } from 'ahooks'
type Props = {
fromDSLPayload: Dependency[]
selectedPlugins: Plugin[]
onSelect: (plugin: Plugin, selectedIndex: number) => void
onLoadedAllPlugin: () => void
}
const InstallByDSLList: FC<Props> = ({
fromDSLPayload,
selectedPlugins,
onSelect,
onLoadedAllPlugin,
}) => {
const { isLoading: isFetchingMarketplaceData, data: marketplaceRes } = useFetchPluginsInMarketPlaceByIds(fromDSLPayload.filter(d => d.type === 'marketplace').map(d => d.value.plugin_unique_identifier!))
const [plugins, setPlugins, getPlugins] = useGetState<Plugin[]>([])
const handlePlugInFetched = useCallback((index: number) => {
return (p: Plugin) => {
setPlugins(plugins.map((item, i) => i === index ? p : item))
}
}, [plugins])
const marketPlaceInDSLIndex = useMemo(() => {
const res: number[] = []
fromDSLPayload.forEach((d, index) => {
if (d.type === 'marketplace')
res.push(index)
})
return res
}, [fromDSLPayload])
useEffect(() => {
if (!isFetchingMarketplaceData && marketplaceRes?.data.plugins && marketplaceRes?.data.plugins.length > 0) {
const payloads = marketplaceRes?.data.plugins
const nextPlugins = produce(getPlugins(), (draft) => {
marketPlaceInDSLIndex.forEach((index, i) => {
draft[index] = payloads[i]
})
})
setPlugins(nextPlugins)
// marketplaceRes?.data.plugins
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isFetchingMarketplaceData])
const isLoadedAllData = fromDSLPayload.length === plugins.length && plugins.every(p => !!p)
useEffect(() => {
if (isLoadedAllData)
onLoadedAllPlugin()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoadedAllData])
const handleSelect = useCallback((index: number) => {
return () => {
onSelect(plugins[index], index)
}
}, [onSelect, plugins])
return (
<>
{fromDSLPayload.map((d, index) => (
d.type === 'github'
? <GithubItem
key={index}
checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)}
onCheckedChange={handleSelect(index)}
dependency={d}
onFetchedPayload={handlePlugInFetched(index)}
/>
: <MarketplaceItem
key={index}
checked={!!selectedPlugins.find(p => p.plugin_id === plugins[index]?.plugin_id)}
onCheckedChange={handleSelect(index)}
payload={plugins[index] as Plugin}
/>
))}
</>
)
}
export default React.memo(InstallByDSLList)

View File

@ -0,0 +1,88 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import type { Dependency, Plugin } from '../../../types'
import Button from '@/app/components/base/button'
import { RiLoader2Line } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import InstallByDSLList from './install-by-dsl-list'
import { useInstallFromMarketplaceAndGitHub } from '@/service/use-plugins'
const i18nPrefix = 'plugin.installModal'
type Props = {
fromDSLPayload: Dependency[]
onCancel: () => void
}
const Install: FC<Props> = ({
fromDSLPayload,
onCancel,
}) => {
const { t } = useTranslation()
const [selectedPlugins, setSelectedPlugins] = React.useState<Plugin[]>([])
const [selectedIndexes, setSelectedIndexes] = React.useState<number[]>([])
const selectedPluginsNum = selectedPlugins.length
const handleSelect = (plugin: Plugin, selectedIndex: number) => {
const isSelected = !!selectedPlugins.find(p => p.plugin_id === plugin.plugin_id)
let nextSelectedPlugins
if (isSelected)
nextSelectedPlugins = selectedPlugins.filter(p => p.plugin_id !== plugin.plugin_id)
else
nextSelectedPlugins = [...selectedPlugins, plugin]
setSelectedPlugins(nextSelectedPlugins)
const nextSelectedIndexes = isSelected ? selectedIndexes.filter(i => i !== selectedIndex) : [...selectedIndexes, selectedIndex]
setSelectedIndexes(nextSelectedIndexes)
}
const [canInstall, setCanInstall] = React.useState(false)
const handleLoadedAllPlugin = useCallback(() => {
setCanInstall(true)
}, [selectedPlugins, selectedIndexes])
// Install from marketplace and github
const { mutate: installFromMarketplaceAndGitHub, isPending: isInstalling } = useInstallFromMarketplaceAndGitHub({
onSuccess: () => {
console.log('success!')
},
})
console.log(canInstall, !isInstalling, selectedPlugins.length === 0)
const handleInstall = () => {
installFromMarketplaceAndGitHub(fromDSLPayload.filter((_d, index) => selectedIndexes.includes(index)))
}
return (
<>
<div className='flex flex-col px-6 py-3 justify-center items-start gap-4 self-stretch'>
<div className='text-text-secondary system-md-regular'>
<p>{t(`${i18nPrefix}.${selectedPluginsNum > 1 ? 'readyToInstallPackages' : 'readyToInstallPackage'}`, { num: selectedPluginsNum })}</p>
</div>
<div className='w-full p-2 rounded-2xl bg-background-section-burn space-y-1'>
<InstallByDSLList
fromDSLPayload={fromDSLPayload}
selectedPlugins={selectedPlugins}
onSelect={handleSelect}
onLoadedAllPlugin={handleLoadedAllPlugin}
/>
</div>
</div>
{/* Action Buttons */}
<div className='flex p-6 pt-5 justify-end items-center gap-2 self-stretch'>
{!canInstall && (
<Button variant='secondary' className='min-w-[72px]' onClick={onCancel}>
{t('common.operation.cancel')}
</Button>
)}
<Button
variant='primary'
className='min-w-[72px] flex space-x-0.5'
disabled={!canInstall || isInstalling || selectedPlugins.length === 0}
onClick={handleInstall}
>
{isInstalling && <RiLoader2Line className='w-4 h-4 animate-spin-slow' />}
<span>{t(`${i18nPrefix}.${isInstalling ? 'installing' : 'install'}`)}</span>
</Button>
</div>
</>
)
}
export default React.memo(Install)

View File

@ -7,9 +7,10 @@ import Card from '../../../card'
import Badge, { BadgeState } from '@/app/components/base/badge/index'
import { pluginManifestToCardPluginProps } from '../../utils'
import { useTranslation } from 'react-i18next'
import { installPackageFromGitHub, uninstallPlugin } from '@/service/plugins'
import { updateFromGitHub } from '@/service/plugins'
import { useInstallPackageFromGitHub } from '@/service/use-plugins'
import { RiLoader2Line } from '@remixicon/react'
import { usePluginTasksStore } from '@/app/components/plugins/plugin-page/store'
import { usePluginTaskList } from '@/service/use-plugins'
import checkTaskStatus from '../../base/check-task-status'
import { parseGitHubUrl } from '../../utils'
@ -40,7 +41,8 @@ const Loaded: React.FC<LoadedProps> = ({
}) => {
const { t } = useTranslation()
const [isInstalling, setIsInstalling] = React.useState(false)
const setPluginTasksWithPolling = usePluginTasksStore(s => s.setPluginTasksWithPolling)
const { mutateAsync: installPackageFromGitHub } = useInstallPackageFromGitHub()
const { handleRefetch } = usePluginTaskList()
const { check } = checkTaskStatus()
const handleInstall = async () => {
@ -49,28 +51,49 @@ const Loaded: React.FC<LoadedProps> = ({
try {
const { owner, repo } = parseGitHubUrl(repoUrl)
const { all_installed: isInstalled, task_id: taskId } = await installPackageFromGitHub(
`${owner}/${repo}`,
selectedVersion,
selectedPackage,
uniqueIdentifier,
)
if (updatePayload) {
const { all_installed: isInstalled, task_id: taskId } = await updateFromGitHub(
`${owner}/${repo}`,
selectedVersion,
selectedPackage,
updatePayload.originalPackageInfo.id,
uniqueIdentifier,
)
if (updatePayload && isInstalled)
await uninstallPlugin(updatePayload.originalPackageInfo.id)
if (isInstalled) {
onInstalled()
return
}
handleRefetch()
await check({
taskId,
pluginUniqueIdentifier: uniqueIdentifier,
})
if (isInstalled) {
onInstalled()
return
}
else {
const { all_installed: isInstalled, task_id: taskId } = await installPackageFromGitHub({
repoUrl: `${owner}/${repo}`,
selectedVersion,
selectedPackage,
uniqueIdentifier,
})
setPluginTasksWithPolling()
await check({
taskId,
pluginUniqueIdentifier: uniqueIdentifier,
})
if (isInstalled) {
onInstalled()
return
}
onInstalled()
handleRefetch()
await check({
taskId,
pluginUniqueIdentifier: uniqueIdentifier,
})
onInstalled()
}
}
catch (e) {
if (typeof e === 'string') {

View File

@ -48,7 +48,6 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
const handleUploadPackage = async () => {
if (isUploading) return
setIsUploading(true)
try {
const repo = repoUrl.replace('https://github.com/', '')
await handleUpload(repo, selectedVersion, selectedPackage, (GitHubPackage) => {

View File

@ -8,9 +8,9 @@ import Button from '@/app/components/base/button'
import { Trans, useTranslation } from 'react-i18next'
import { RiLoader2Line } from '@remixicon/react'
import Badge, { BadgeState } from '@/app/components/base/badge/index'
import { installPackageFromLocal } from '@/service/plugins'
import { useInstallPackageFromLocal } from '@/service/use-plugins'
import checkTaskStatus from '../../base/check-task-status'
import { usePluginTasksStore } from '@/app/components/plugins/plugin-page/store'
import { usePluginTaskList } from '@/service/use-plugins'
const i18nPrefix = 'plugin.installModal'
@ -33,6 +33,8 @@ const Installed: FC<Props> = ({
}) => {
const { t } = useTranslation()
const [isInstalling, setIsInstalling] = React.useState(false)
const { mutateAsync: installPackageFromLocal } = useInstallPackageFromLocal()
const {
check,
stop,
@ -43,7 +45,7 @@ const Installed: FC<Props> = ({
onCancel()
}
const setPluginTasksWithPolling = usePluginTasksStore(s => s.setPluginTasksWithPolling)
const { handleRefetch } = usePluginTaskList()
const handleInstall = async () => {
if (isInstalling) return
setIsInstalling(true)
@ -58,7 +60,7 @@ const Installed: FC<Props> = ({
onInstalled()
return
}
setPluginTasksWithPolling()
handleRefetch()
await check({
taskId,
pluginUniqueIdentifier: uniqueIdentifier,

View File

@ -48,6 +48,7 @@ const Uploading: FC<Props> = ({
React.useEffect(() => {
handleUpload()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<>

View File

@ -41,6 +41,7 @@ export type MarketplaceContextValue = {
setMarketplaceCollectionsFromClient: (collections: MarketplaceCollection[]) => void
marketplaceCollectionPluginsMapFromClient?: Record<string, Plugin[]>
setMarketplaceCollectionPluginsMapFromClient: (map: Record<string, Plugin[]>) => void
isLoading: boolean
}
export const MarketplaceContext = createContext<MarketplaceContextValue>({
@ -60,6 +61,7 @@ export const MarketplaceContext = createContext<MarketplaceContextValue>({
setMarketplaceCollectionsFromClient: () => {},
marketplaceCollectionPluginsMapFromClient: {},
setMarketplaceCollectionPluginsMapFromClient: () => {},
isLoading: false,
})
type MarketplaceContextProviderProps = {
@ -88,12 +90,14 @@ export const MarketplaceContextProvider = ({
marketplaceCollectionPluginsMap: marketplaceCollectionPluginsMapFromClient,
setMarketplaceCollectionPluginsMap: setMarketplaceCollectionPluginsMapFromClient,
queryMarketplaceCollectionsAndPlugins,
isLoading,
} = useMarketplaceCollectionsAndPlugins()
const {
plugins,
resetPlugins,
queryPlugins,
queryPluginsWithDebounced,
isLoading: isPluginsLoading,
} = useMarketplacePlugins()
const handleSearchPluginTextChange = useCallback((text: string) => {
@ -194,6 +198,7 @@ export const MarketplaceContextProvider = ({
setMarketplaceCollectionsFromClient,
marketplaceCollectionPluginsMapFromClient,
setMarketplaceCollectionPluginsMapFromClient,
isLoading: isLoading || isPluginsLoading,
}}
>
{children}

View File

@ -11,6 +11,7 @@ const Description = async ({
}: DescriptionProps) => {
const localeDefault = getLocaleOnServer()
const { t } = await translate(localeFromProps || localeDefault, 'plugin')
const { t: tCommon } = await translate(localeFromProps || localeDefault, 'common')
return (
<>
@ -19,22 +20,23 @@ const Description = async ({
</h1>
<h2 className='shrink-0 flex justify-center items-center text-center body-md-regular text-text-tertiary'>
{t('marketplace.discover')}
<span className="relative ml-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
{t('category.models')}
<span className="relative ml-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected z-[1]">
<span className='relative z-[2] lowercase'>{t('category.models')}</span>
</span>
,
<span className="relative ml-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
{t('category.tools')}
<span className="relative ml-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected z-[1]">
<span className='relative z-[2] lowercase'>{t('category.tools')}</span>
</span>
,
<span className="relative ml-1 mr-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
{t('category.extensions')}
<span className="relative ml-1 mr-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected z-[1]">
<span className='relative z-[2] lowercase'>{t('category.extensions')}</span>
</span>
{t('marketplace.and')}
<span className="relative ml-1 mr-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
{t('category.bundles')}
<span className="relative ml-1 mr-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected z-[1]">
<span className='relative z-[2] lowercase'>{t('category.bundles')}</span>
</span>
{t('marketplace.inDifyMarketplace')}
<span className='mr-1'>{tCommon('operation.in')}</span>
{t('marketplace.difyMarketplace')}
</h2>
</>
)

View File

@ -1,25 +1,31 @@
'use client'
import { useTranslation } from 'react-i18next'
import { Group } from '@/app/components/base/icons/src/vender/other'
import Line from './line'
import cn from '@/utils/classnames'
const Empty = () => {
const { t } = useTranslation()
return (
<div
className='grow relative h-0 grid grid-cols-4 grid-rows-4 gap-3 p-2 overflow-hidden'
className='grow relative h-0 flex flex-wrap p-2 overflow-hidden'
>
{
Array.from({ length: 16 }).map((_, index) => (
<div
key={index}
className='h-[144px] rounded-xl bg-background-section-burn'
className={cn(
'mr-3 mb-3 h-[144px] w-[calc((100%-36px)/4)] rounded-xl bg-background-section-burn',
index % 4 === 3 && 'mr-0',
index > 11 && 'mb-0',
)}
>
</div>
))
}
<div
className='absolute inset-0 z-[1]'
style={{
backgroundImage: 'linear-gradient(180deg, rgba(255,255,255,0.01), #FCFCFD)',
}}
className='absolute inset-0 bg-marketplace-plugin-empty z-[1]'
></div>
<div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[2] flex flex-col items-center'>
<div className='relative flex items-center justify-center mb-3 w-14 h-14 rounded-xl border border-divider-subtle bg-components-card-bg shadow-lg'>
@ -30,7 +36,7 @@ const Empty = () => {
<Line className='absolute top-full left-1/2 -translate-x-1/2 -translate-y-1/2 rotate-90' />
</div>
<div className='text-center system-md-regular text-text-tertiary'>
No plugin found
{t('plugin.marketplace.noPluginFound')}
</div>
</div>
</div>

View File

@ -25,13 +25,61 @@ const CardWrapper = ({
setFalse: hideInstallFromMarketplace,
}] = useBoolean(false)
if (showInstallButton) {
return (
<div
className='group relative rounded-xl cursor-pointer'
>
<Card
key={plugin.name}
payload={plugin}
locale={locale}
footer={
<CardMoreInfo
downloadCount={plugin.install_count}
tags={plugin.tags.map(tag => tag.name)}
/>
}
/>
{
showInstallButton && (
<div className='hidden absolute bottom-0 group-hover:flex items-center space-x-2 px-4 pt-8 pb-4 w-full bg-gradient-to-tr from-[#f9fafb] to-[rgba(249,250,251,0)] rounded-b-xl'>
<Button
variant='primary'
className='flex-1'
onClick={showInstallFromMarketplace}
>
{t('plugin.detailPanel.operation.install')}
</Button>
<Button
className='flex-1'
>
<a href={`${MARKETPLACE_URL_PREFIX}/plugin/${plugin.org}/${plugin.name}`} target='_blank' className='flex items-center gap-0.5'>
{t('plugin.detailPanel.operation.detail')}
<RiArrowRightUpLine className='ml-1 w-4 h-4' />
</a>
</Button>
</div>
)
}
{
isShowInstallFromMarketplace && (
<InstallFromMarketplace
manifest={plugin as any}
uniqueIdentifier={plugin.latest_package_identifier}
onClose={hideInstallFromMarketplace}
onSuccess={hideInstallFromMarketplace}
/>
)
}
</div>
)
}
return (
<div
className='group relative rounded-xl cursor-pointer'
onClick={() => {
if (!showInstallButton)
window.open(`${MARKETPLACE_URL_PREFIX}/plugin/${plugin.org}/${plugin.name}`)
}}
<a
className='group inline-block relative rounded-xl cursor-pointer'
href={`${MARKETPLACE_URL_PREFIX}/plugins/${plugin.org}/${plugin.name}`}
>
<Card
key={plugin.name}
@ -65,17 +113,7 @@ const CardWrapper = ({
</div>
)
}
{
isShowInstallFromMarketplace && (
<InstallFromMarketplace
manifest={plugin as any}
uniqueIdentifier={plugin.latest_package_identifier}
onClose={hideInstallFromMarketplace}
onSuccess={hideInstallFromMarketplace}
/>
)
}
</div>
</a>
)
}

View File

@ -1,9 +1,11 @@
'use client'
import { useTranslation } from 'react-i18next'
import type { Plugin } from '../../types'
import type { MarketplaceCollection } from '../types'
import { useMarketplaceContext } from '../context'
import List from './index'
import SortDropdown from '../sort-dropdown'
import Loading from '@/app/components/base/loading'
type ListWrapperProps = {
marketplaceCollections: MarketplaceCollection[]
@ -17,28 +19,41 @@ const ListWrapper = ({
showInstallButton,
locale,
}: ListWrapperProps) => {
const { t } = useTranslation()
const plugins = useMarketplaceContext(v => v.plugins)
const marketplaceCollectionsFromClient = useMarketplaceContext(v => v.marketplaceCollectionsFromClient)
const marketplaceCollectionPluginsMapFromClient = useMarketplaceContext(v => v.marketplaceCollectionPluginsMapFromClient)
const isLoading = useMarketplaceContext(v => v.isLoading)
return (
<div className='flex flex-col grow px-12 py-2 bg-background-default-subtle'>
<div className='relative flex flex-col grow px-12 py-2 bg-background-default-subtle'>
{
plugins && (
<div className='flex items-center mb-4 pt-3'>
<div className='title-xl-semi-bold text-text-primary'>{plugins.length} results</div>
<div className='title-xl-semi-bold text-text-primary'>{t('plugin.marketplace.pluginsResult', { num: plugins.length })}</div>
<div className='mx-3 w-[1px] h-3.5 bg-divider-regular'></div>
<SortDropdown />
</div>
)
}
<List
marketplaceCollections={marketplaceCollectionsFromClient || marketplaceCollections}
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMapFromClient || marketplaceCollectionPluginsMap}
plugins={plugins}
showInstallButton={showInstallButton}
locale={locale}
/>
{
isLoading && (
<div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
<Loading />
</div>
)
}
{
!isLoading && (
<List
marketplaceCollections={marketplaceCollectionsFromClient || marketplaceCollections}
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMapFromClient || marketplaceCollectionPluginsMap}
plugins={plugins}
showInstallButton={showInstallButton}
locale={locale}
/>
)
}
</div>
)
}

View File

@ -4,6 +4,7 @@ import {
RiArrowDownSLine,
RiCheckLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useMarketplaceContext } from '../context'
import {
PortalToFollowElem,
@ -11,30 +12,30 @@ import {
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
const options = [
{
value: 'install_count',
order: 'DESC',
text: 'Most Popular',
},
{
value: 'version_updated_at',
order: 'DESC',
text: 'Recently Updated',
},
{
value: 'created_at',
order: 'DESC',
text: 'Newly Released',
},
{
value: 'created_at',
order: 'ASC',
text: 'First Released',
},
]
const SortDropdown = () => {
const { t } = useTranslation()
const options = [
{
value: 'install_count',
order: 'DESC',
text: t('plugin.marketplace.sortOption.mostPopular'),
},
{
value: 'version_updated_at',
order: 'DESC',
text: t('plugin.marketplace.sortOption.recentlyUpdated'),
},
{
value: 'created_at',
order: 'DESC',
text: t('plugin.marketplace.sortOption.newlyReleased'),
},
{
value: 'created_at',
order: 'ASC',
text: t('plugin.marketplace.sortOption.firstReleased'),
},
]
const sort = useMarketplaceContext(v => v.sort)
const handleSortChange = useMarketplaceContext(v => v.handleSortChange)
const [open, setOpen] = useState(false)
@ -53,7 +54,7 @@ const SortDropdown = () => {
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
<div className='flex items-center px-2 pr-3 h-8 rounded-lg bg-state-base-hover-alt cursor-pointer'>
<span className='mr-1 system-sm-regular'>
Sort by
{t('plugin.marketplace.sortBy')}
</span>
<span className='mr-1 system-sm-medium'>
{selectedOption.text}

View File

@ -1,5 +1,4 @@
import React, { useState } from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
import { useAppContext } from '@/context/app-context'
@ -9,28 +8,39 @@ import Indicator from '@/app/components/header/indicator'
import ToolItem from '@/app/components/tools/provider/tool-item'
import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
import {
fetchBuiltInToolList,
fetchCollectionDetail,
removeBuiltInToolCredential,
updateBuiltInToolCredential,
} from '@/service/tools'
useBuiltinProviderInfo,
useBuiltinTools,
useInvalidateBuiltinProviderInfo,
useRemoveProviderCredentials,
useUpdateProviderCredentials,
} from '@/service/use-tools'
const ActionList = () => {
const { t } = useTranslation()
const { isCurrentWorkspaceManager } = useAppContext()
const currentPluginDetail = usePluginPageContext(v => v.currentPluginDetail)
const { data: provider } = useSWR(
`builtin/${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`,
fetchCollectionDetail,
)
const { data } = useSWR(
`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`,
fetchBuiltInToolList,
)
const { data: provider } = useBuiltinProviderInfo(`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`)
const invalidateProviderInfo = useInvalidateBuiltinProviderInfo()
const { data } = useBuiltinTools(`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`)
const [showSettingAuth, setShowSettingAuth] = useState(false)
const handleCredentialSettingUpdate = () => {}
const handleCredentialSettingUpdate = () => {
invalidateProviderInfo(`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
setShowSettingAuth(false)
}
const { mutate: updatePermission } = useUpdateProviderCredentials({
onSuccess: handleCredentialSettingUpdate,
})
const { mutate: removePermission } = useRemoveProviderCredentials({
onSuccess: handleCredentialSettingUpdate,
})
if (!data || !provider)
return null
@ -77,24 +87,11 @@ const ActionList = () => {
<ConfigCredential
collection={provider}
onCancel={() => setShowSettingAuth(false)}
onSaved={async (value) => {
await updateBuiltInToolCredential(provider.name, value)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
handleCredentialSettingUpdate()
setShowSettingAuth(false)
}}
onRemove={async () => {
await removeBuiltInToolCredential(provider.name)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
handleCredentialSettingUpdate()
setShowSettingAuth(false)
}}
onSaved={async value => updatePermission({
providerName: provider.name,
credentials: value,
})}
onRemove={async () => removePermission(provider.name)}
/>
)}
</div>

View File

@ -28,7 +28,7 @@ import { Github } from '@/app/components/base/icons/src/public/common'
import { uninstallPlugin } from '@/service/plugins'
import { useGetLanguage } from '@/context/i18n'
import { useModalContext } from '@/context/modal-context'
import UpdateFromMarketplace from '@/app/components/plugins/update-plugin/from-market-place'
import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config'
import cn from '@/utils/classnames'
@ -55,17 +55,35 @@ const DetailHeader = ({
source,
tenant_id,
version,
latest_unique_identifier,
latest_version,
meta,
} = detail
const { author, name, label, description, icon, verified } = detail.declaration
const isFromGitHub = source === PluginSource.github
const isFromMarketplace = source === PluginSource.marketplace
const hasNewVersion = useMemo(() => {
return source === PluginSource.github && latest_version !== version
}, [source, latest_version, version])
if (isFromGitHub)
return latest_version !== version
if (isFromMarketplace)
return !!latest_version && latest_version !== version
return false
}, [isFromGitHub, isFromMarketplace, latest_version, version])
const [isShowUpdateModal, {
setTrue: showUpdateModal,
setFalse: hideUpdateModal,
}] = useBoolean(false)
const handleUpdate = async () => {
if (isFromMarketplace) {
showUpdateModal()
return
}
try {
const fetchedReleases = await fetchReleases(author, name)
if (fetchedReleases.length === 0)
@ -106,6 +124,11 @@ const DetailHeader = ({
}
}
const handleUpdatedFromMarketplace = () => {
onUpdate()
hideUpdateModal()
}
const [isShowPluginInfo, {
setTrue: showPluginInfo,
setFalse: hidePluginInfo,
@ -222,6 +245,24 @@ const DetailHeader = ({
isDisabled={deleting}
/>
)}
{
isShowUpdateModal && (
<UpdateFromMarketplace
payload={{
originalPackageInfo: {
id: detail.plugin_unique_identifier,
payload: detail.declaration,
},
targetPackageInfo: {
id: latest_unique_identifier,
version: latest_version,
},
}}
onCancel={hideUpdateModal}
onSave={handleUpdatedFromMarketplace}
/>
)
}
</div>
)
}

View File

@ -13,11 +13,11 @@ import Indicator from '@/app/components/header/indicator'
import Switch from '@/app/components/base/switch'
import Toast from '@/app/components/base/toast'
import {
deleteEndpoint,
disableEndpoint,
enableEndpoint,
updateEndpoint,
} from '@/service/plugins'
useDeleteEndpoint,
useDisableEndpoint,
useEnableEndpoint,
useUpdateEndpoint,
} from '@/service/use-endpoints'
type Props = {
data: EndpointListItem
@ -32,43 +32,34 @@ const EndpointCard = ({
const [active, setActive] = useState(data.enabled)
const endpointID = data.id
// switch
const [isShowDisableConfirm, {
setTrue: showDisableConfirm,
setFalse: hideDisableConfirm,
}] = useBoolean(false)
const activeEndpoint = async () => {
try {
await enableEndpoint({
url: '/workspaces/current/endpoints/enable',
endpointID,
})
const { mutate: enableEndpoint } = useEnableEndpoint({
onSuccess: async () => {
await handleChange()
}
catch (error: any) {
console.error(error)
},
onError: () => {
Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
setActive(false)
}
}
const inactiveEndpoint = async () => {
try {
await disableEndpoint({
url: '/workspaces/current/endpoints/disable',
endpointID,
})
},
})
const { mutate: disableEndpoint } = useDisableEndpoint({
onSuccess: async () => {
await handleChange()
hideDisableConfirm()
}
catch (error) {
console.error(error)
},
onError: () => {
Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
setActive(true)
}
}
setActive(false)
},
})
const handleSwitch = (state: boolean) => {
if (state) {
setActive(true)
activeEndpoint()
enableEndpoint(endpointID)
}
else {
setActive(false)
@ -76,30 +67,26 @@ const EndpointCard = ({
}
}
// delete
const [isShowDeleteConfirm, {
setTrue: showDeleteConfirm,
setFalse: hideDeleteConfirm,
}] = useBoolean(false)
const handleDelete = async () => {
try {
await deleteEndpoint({
url: '/workspaces/current/endpoints/delete',
endpointID,
})
const { mutate: deleteEndpoint } = useDeleteEndpoint({
onSuccess: async () => {
await handleChange()
hideDeleteConfirm()
}
catch (error) {
console.error(error)
},
onError: () => {
Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
}
}
},
})
// update
const [isShowEndpointModal, {
setTrue: showEndpointModalConfirm,
setFalse: hideEndpointModalConfirm,
}] = useBoolean(false)
const formSchemas = useMemo(() => {
return toolCredentialToFormSchemas([NAME_FIELD, ...data.declaration.settings])
}, [data.declaration.settings])
@ -110,27 +97,19 @@ const EndpointCard = ({
}
return addDefaultValue(formValue, formSchemas)
}, [data.name, data.settings, formSchemas])
const handleUpdate = async (state: any) => {
const newName = state.name
delete state.name
try {
await updateEndpoint({
url: '/workspaces/current/endpoints/update',
body: {
endpoint_id: data.id,
settings: state,
name: newName,
},
})
const { mutate: updateEndpoint } = useUpdateEndpoint({
onSuccess: async () => {
await handleChange()
hideEndpointModalConfirm()
}
catch (error) {
console.error(error)
},
onError: () => {
Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
}
}
},
})
const handleUpdate = (state: any) => updateEndpoint({
endpointID,
state,
})
return (
<div className='p-0.5 bg-background-section-burn rounded-xl'>
@ -192,7 +171,7 @@ const EndpointCard = ({
hideDisableConfirm()
setActive(true)
}}
onConfirm={inactiveEndpoint}
onConfirm={() => disableEndpoint(endpointID)}
/>
)}
{isShowDeleteConfirm && (
@ -201,7 +180,7 @@ const EndpointCard = ({
title={t('plugin.detailPanel.endpointDeleteTip')}
content={<div>{t('plugin.detailPanel.endpointDeleteContent', { name: data.name })}</div>}
onCancel={hideDeleteConfirm}
onConfirm={handleDelete}
onConfirm={() => deleteEndpoint(endpointID)}
/>
)}
{isShowEndpointModal && (

View File

@ -1,6 +1,5 @@
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
import { useBoolean } from 'ahooks'
import { RiAddLine } from '@remixicon/react'
import EndpointModal from './endpoint-modal'
@ -12,9 +11,10 @@ import Tooltip from '@/app/components/base/tooltip'
import Toast from '@/app/components/base/toast'
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
import {
createEndpoint,
fetchEndpointList,
} from '@/service/plugins'
useCreateEndpoint,
useEndpointList,
useInvalidateEndpointList,
} from '@/service/use-endpoints'
import cn from '@/utils/classnames'
type Props = {
@ -25,17 +25,9 @@ const EndpointList = ({ showTopBorder }: Props) => {
const pluginDetail = usePluginPageContext(v => v.currentPluginDetail)
const pluginUniqueID = pluginDetail.plugin_unique_identifier
const declaration = pluginDetail.declaration.endpoint
const { data, mutate } = useSWR(
{
url: '/workspaces/current/endpoints/list/plugin',
params: {
plugin_id: pluginDetail.plugin_id,
page: 1,
page_size: 100,
},
},
fetchEndpointList,
)
const { data } = useEndpointList(pluginDetail.plugin_id)
const invalidateEndpointList = useInvalidateEndpointList()
const [isShowEndpointModal, {
setTrue: showEndpointModal,
setFalse: hideEndpointModal,
@ -45,26 +37,20 @@ const EndpointList = ({ showTopBorder }: Props) => {
return toolCredentialToFormSchemas([NAME_FIELD, ...declaration.settings])
}, [declaration.settings])
const handleCreate = async (state: any) => {
const newName = state.name
delete state.name
try {
await createEndpoint({
url: '/workspaces/current/endpoints/create',
body: {
plugin_unique_identifier: pluginUniqueID,
settings: state,
name: newName,
},
})
await mutate()
const { mutate: createEndpoint } = useCreateEndpoint({
onSuccess: async () => {
await invalidateEndpointList(pluginDetail.plugin_id)
hideEndpointModal()
}
catch (error) {
console.error(error)
},
onError: () => {
Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
}
}
},
})
const handleCreate = (state: any) => createEndpoint({
pluginUniqueID,
state,
})
if (!data)
return null
@ -92,7 +78,7 @@ const EndpointList = ({ showTopBorder }: Props) => {
<EndpointCard
key={index}
data={item}
handleChange={mutate}
handleChange={() => invalidateEndpointList(pluginDetail.plugin_id)}
/>
))}
</div>

View File

@ -69,7 +69,7 @@ const EndpointModal: FC<Props> = ({
isEditMode={true}
showOnVariableMap={{}}
validating={false}
inputClassName='!bg-gray-50'
inputClassName='bg-components-input-bg-normal hover:bg-state-base-hover-alt'
fieldMoreInfo={item => item.url
? (<a
href={item.url}

View File

@ -1,19 +1,14 @@
import React from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context'
import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon'
import ModelName from '@/app/components/header/account-setting/model-provider-page/model-name'
import { fetchModelProviderModelList } from '@/service/common'
import { useModelProviderModelList } from '@/service/use-models'
const ModelList = () => {
const { t } = useTranslation()
const currentPluginDetail = usePluginPageContext(v => v.currentPluginDetail)
const { data: res } = useSWR(
`/workspaces/current/model-providers/${currentPluginDetail.plugin_id}/${currentPluginDetail.name}/models`,
fetchModelProviderModelList,
)
const { data: res } = useModelProviderModelList(`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`)
if (!res)
return null

View File

@ -21,8 +21,8 @@ const i18nPrefix = 'plugin.action'
type Props = {
author: string
installationId: string
pluginUniqueIdentifier: string
pluginName: string
version: string
usedInApps: number
isShowFetchNewVersion: boolean
isShowInfo: boolean
@ -33,8 +33,8 @@ type Props = {
const Action: FC<Props> = ({
author,
installationId,
pluginUniqueIdentifier,
pluginName,
version,
isShowFetchNewVersion,
isShowInfo,
isShowDelete,
@ -61,7 +61,7 @@ const Action: FC<Props> = ({
return
const versions = fetchedReleases.map(release => release.tag_name)
const latestVersion = getLatestVersion(versions)
if (compareVersion(latestVersion, version) === 1) {
if (compareVersion(latestVersion, meta!.version) === 1) {
setShowUpdatePluginModal({
onSaveCallback: () => {
invalidateInstalledPluginList()
@ -70,7 +70,7 @@ const Action: FC<Props> = ({
type: PluginSource.github,
github: {
originalPackageInfo: {
id: installationId,
id: pluginUniqueIdentifier,
repo: meta!.repo,
version: meta!.version,
package: meta!.package,

View File

@ -22,6 +22,7 @@ import cn from '@/utils/classnames'
import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config'
import { useLanguage } from '../../header/account-setting/model-provider-page/hooks'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useCategories } from '../hooks'
type Props = {
className?: string
@ -34,6 +35,7 @@ const PluginItem: FC<Props> = ({
}) => {
const locale = useLanguage()
const { t } = useTranslation()
const { categoriesMap } = useCategories()
const currentPluginDetail = usePluginPageContext(v => v.currentPluginDetail)
const setCurrentPluginDetail = usePluginPageContext(v => v.setCurrentPluginDetail)
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
@ -42,10 +44,10 @@ const PluginItem: FC<Props> = ({
source,
tenant_id,
installation_id,
plugin_unique_identifier,
endpoints_active,
meta,
plugin_id,
version,
} = plugin
const { category, author, name, label, description, icon, verified } = plugin.declaration
@ -61,17 +63,19 @@ const PluginItem: FC<Props> = ({
? 'bg-[repeating-linear-gradient(-45deg,rgba(16,24,40,0.04),rgba(16,24,40,0.04)_5px,rgba(0,0,0,0.02)_5px,rgba(0,0,0,0.02)_10px)]'
: 'bg-background-section-burn',
)}
onClick={() => setCurrentPluginDetail(plugin)}
onClick={() => {
setCurrentPluginDetail(plugin)
}}
>
<div className={cn('relative p-4 pb-3 border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg hover-bg-components-panel-on-panel-item-bg rounded-xl shadow-xs', className)}>
<CornerMark text={category} />
<CornerMark text={categoriesMap[category].label} />
{/* Header */}
<div className="flex">
<div className='flex items-center justify-center w-10 h-10 overflow-hidden border-components-panel-border-subtle border-[1px] rounded-xl'>
<img
className='w-full h-full'
src={`${API_PREFIX}/workspaces/current/plugin/icon?tenant_id=${tenant_id}&filename=${icon}`}
alt={`plugin-${installation_id}-logo`}
alt={`plugin-${plugin_unique_identifier}-logo`}
/>
</div>
<div className="ml-3 w-0 grow">
@ -84,10 +88,10 @@ const PluginItem: FC<Props> = ({
<Description text={description[locale]} descriptionLineRows={1}></Description>
<div onClick={e => e.stopPropagation()}>
<Action
pluginUniqueIdentifier={plugin_unique_identifier}
installationId={installation_id}
author={author}
pluginName={name}
version={version}
usedInApps={5}
isShowFetchNewVersion={source === PluginSource.github}
isShowInfo={source === PluginSource.github}

View File

@ -68,7 +68,7 @@ export const PluginPageContextProvider = ({
{ value: 'plugins', text: t('common.menus.plugins') },
...(
enable_marketplace
? [{ value: 'discover', text: 'Explore Marketplace' }]
? [{ value: 'discover', text: t('common.menus.exploreMarketplace') }]
: []
),
]

View File

@ -17,6 +17,9 @@ const DebugInfo: FC = () => {
const { t } = useTranslation()
const { data: info, isLoading } = useDebugKey()
// info.key likes 4580bdb7-b878-471c-a8a4-bfd760263a53 mask the middle part using *.
const maskedKey = info?.key?.replace(/(.{8})(.*)(.{8})/, '$1********$3')
if (isLoading) return null
return (
@ -26,7 +29,7 @@ const DebugInfo: FC = () => {
popupContent={
<>
<div className='flex items-center gap-1 self-stretch'>
<span className='flex flex-col justify-center items-start flex-grow flex-shrink-0 basis-0 text-text-secondary system-sm-semibold'>{t(`${i18nPrefix}.title`)}</span>
<span className='flex flex-col justify-center items-start grow shrink-0 basis-0 text-text-secondary system-sm-semibold'>{t(`${i18nPrefix}.title`)}</span>
<a href='' target='_blank' className='flex items-center gap-0.5 text-text-accent-light-mode-only cursor-pointer'>
<span className='system-xs-medium'>{t(`${i18nPrefix}.viewDocs`)}</span>
<RiArrowRightUpLine className='w-3 h-3' />
@ -40,6 +43,7 @@ const DebugInfo: FC = () => {
<KeyValueItem
label={'Key'}
value={info?.key || ''}
maskedValue={maskedKey}
/>
</div>
</>

View File

@ -9,8 +9,10 @@ import { Group } from '@/app/components/base/icons/src/vender/other'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import Line from '../../marketplace/empty/line'
import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useTranslation } from 'react-i18next'
const Empty = () => {
const { t } = useTranslation()
const fileInputRef = useRef<HTMLInputElement>(null)
const [selectedAction, setSelectedAction] = useState<string | null>(null)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
@ -30,9 +32,9 @@ const Empty = () => {
const text = useMemo(() => {
if (pluginList?.plugins.length === 0)
return 'No plugins installed'
return t('plugin.list.noInstalled')
if (filters.categories.length > 0 || filters.tags.length > 0 || filters.searchQuery)
return 'No plugins found'
return t('plugin.list.notFound')
}, [pluginList, filters])
return (
@ -70,11 +72,11 @@ const Empty = () => {
{[
...(
(enable_marketplace || true)
? [{ icon: MagicBox, text: 'Marketplace', action: 'marketplace' }]
? [{ icon: MagicBox, text: t('plugin.list.source.marketplace'), action: 'marketplace' }]
: []
),
{ icon: Github, text: 'GitHub', action: 'github' },
{ icon: FileZip, text: 'Local Package File', action: 'local' },
{ icon: Github, text: t('plugin.list.source.github'), action: 'github' },
{ icon: FileZip, text: t('plugin.list.source.local'), action: 'local' },
].map(({ icon: Icon, text, action }) => (
<div
key={action}
@ -90,7 +92,7 @@ const Empty = () => {
}}
>
<Icon className="w-4 h-4 text-text-tertiary" />
<span className='text-text-secondary system-md-regular'>{`Install from ${text}`}</span>
<span className='text-text-secondary system-md-regular'>{text}</span>
</div>
))}
</div>

View File

@ -13,6 +13,8 @@ import {
import Checkbox from '@/app/components/base/checkbox'
import cn from '@/utils/classnames'
import Input from '@/app/components/base/input'
import { useCategories } from '../../hooks'
import { useTranslation } from 'react-i18next'
type CategoriesFilterProps = {
value: string[]
@ -22,27 +24,11 @@ const CategoriesFilter = ({
value,
onChange,
}: CategoriesFilterProps) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const [searchText, setSearchText] = useState('')
const options = [
{
value: 'model',
text: 'Model',
},
{
value: 'tool',
text: 'Tool',
},
{
value: 'extension',
text: 'Extension',
},
{
value: 'bundle',
text: 'Bundle',
},
]
const filteredOptions = options.filter(option => option.text.toLowerCase().includes(searchText.toLowerCase()))
const { categories: options, categoriesMap } = useCategories()
const filteredOptions = options.filter(option => option.name.toLowerCase().includes(searchText.toLowerCase()))
const handleCheck = (id: string) => {
if (value.includes(id))
onChange(value.filter(tag => tag !== id))
@ -70,10 +56,10 @@ const CategoriesFilter = ({
'flex items-center p-1 system-sm-medium',
)}>
{
!selectedTagsLength && 'All Categories'
!selectedTagsLength && t('plugin.allCategories')
}
{
!!selectedTagsLength && value.slice(0, 2).join(',')
!!selectedTagsLength && value.map(val => categoriesMap[val].label).slice(0, 2).join(',')
}
{
selectedTagsLength > 2 && (
@ -110,23 +96,23 @@ const CategoriesFilter = ({
showLeftIcon
value={searchText}
onChange={e => setSearchText(e.target.value)}
placeholder='Search categories'
placeholder={t('plugin.searchCategories')}
/>
</div>
<div className='p-1 max-h-[448px] overflow-y-auto'>
{
filteredOptions.map(option => (
<div
key={option.value}
key={option.name}
className='flex items-center px-2 py-1.5 h-7 rounded-lg cursor-pointer hover:bg-state-base-hover'
onClick={() => handleCheck(option.value)}
onClick={() => handleCheck(option.name)}
>
<Checkbox
className='mr-1'
checked={value.includes(option.value)}
checked={value.includes(option.name)}
/>
<div className='px-1 system-sm-medium text-text-secondary'>
{option.text}
{option.label}
</div>
</div>
))

View File

@ -1,6 +1,7 @@
'use client'
import Input from '@/app/components/base/input'
import { useTranslation } from 'react-i18next'
type SearchBoxProps = {
searchQuery: string
onChange: (query: string) => void
@ -10,13 +11,15 @@ const SearchBox: React.FC<SearchBoxProps> = ({
searchQuery,
onChange,
}) => {
const { t } = useTranslation()
return (
<Input
wrapperClassName='flex w-[200px] items-center rounded-lg'
className='bg-components-input-bg-normal'
showLeftIcon
value={searchQuery}
placeholder='Search'
placeholder={t('plugin.search')}
onChange={(e) => {
onChange(e.target.value)
}}

View File

@ -13,6 +13,8 @@ import {
import Checkbox from '@/app/components/base/checkbox'
import cn from '@/utils/classnames'
import Input from '@/app/components/base/input'
import { useTags } from '../../hooks'
import { useTranslation } from 'react-i18next'
type TagsFilterProps = {
value: string[]
@ -22,19 +24,11 @@ const TagsFilter = ({
value,
onChange,
}: TagsFilterProps) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const [searchText, setSearchText] = useState('')
const options = [
{
value: 'search',
text: 'Search',
},
{
value: 'image',
text: 'Image',
},
]
const filteredOptions = options.filter(option => option.text.toLowerCase().includes(searchText.toLowerCase()))
const { tags: options, tagsMap } = useTags()
const filteredOptions = options.filter(option => option.name.toLowerCase().includes(searchText.toLowerCase()))
const handleCheck = (id: string) => {
if (value.includes(id))
onChange(value.filter(tag => tag !== id))
@ -62,10 +56,10 @@ const TagsFilter = ({
'flex items-center p-1 system-sm-medium',
)}>
{
!selectedTagsLength && 'All Tags'
!selectedTagsLength && t('pluginTags.allTags')
}
{
!!selectedTagsLength && value.slice(0, 2).join(',')
!!selectedTagsLength && value.map(val => tagsMap[val].label).slice(0, 2).join(',')
}
{
selectedTagsLength > 2 && (
@ -97,23 +91,23 @@ const TagsFilter = ({
showLeftIcon
value={searchText}
onChange={e => setSearchText(e.target.value)}
placeholder='Search tags'
placeholder={t('pluginTags.searchTags')}
/>
</div>
<div className='p-1 max-h-[448px] overflow-y-auto'>
{
filteredOptions.map(option => (
<div
key={option.value}
key={option.name}
className='flex items-center px-2 py-1.5 h-7 rounded-lg cursor-pointer hover:bg-state-base-hover'
onClick={() => handleCheck(option.value)}
onClick={() => handleCheck(option.name)}
>
<Checkbox
className='mr-1'
checked={value.includes(option.value)}
checked={value.includes(option.name)}
/>
<div className='px-1 system-sm-medium text-text-secondary'>
{option.text}
{option.label}
</div>
</div>
))

View File

@ -16,8 +16,7 @@ import InstallPluginDropdown from './install-plugin-dropdown'
import { useUploader } from './use-uploader'
import usePermission from './use-permission'
import DebugInfo from './debug-info'
import { usePluginTasksStore } from './store'
import InstallInfo from './install-info'
import PluginTasks from './plugin-tasks'
import Button from '@/app/components/base/button'
import TabSlider from '@/app/components/base/tab-slider'
import Tooltip from '@/app/components/base/tooltip'
@ -102,8 +101,6 @@ const PluginPage = ({
const options = usePluginPageContext(v => v.options)
const [activeTab, setActiveTab] = usePluginPageContext(v => [v.activeTab, v.setActiveTab])
const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures)
const [installed, total] = [2, 3] // Replace this with the actual progress
const progressPercentage = (installed / total) * 100
const uploaderProps = useUploader({
onFileChange: setCurrentFile,
@ -113,12 +110,6 @@ const PluginPage = ({
const { dragging, fileUploader, fileChangeHandle, removeFile } = uploaderProps
const setPluginTasksWithPolling = usePluginTasksStore(s => s.setPluginTasksWithPolling)
useEffect(() => {
setPluginTasksWithPolling()
}, [setPluginTasksWithPolling])
return (
<div
id='marketplace-container'
@ -141,8 +132,8 @@ const PluginPage = ({
options={options}
/>
</div>
<div className='flex flex-shrink-0 items-center gap-1'>
<InstallInfo />
<div className='flex shrink-0 items-center gap-1'>
<PluginTasks />
{canManagement && (
<InstallPluginDropdown
onSwitchToMarketplaceTab={() => setActiveTab('discover')}
@ -181,7 +172,7 @@ const PluginPage = ({
)}
<div className={`flex py-4 justify-center items-center gap-2 ${dragging ? 'text-text-accent' : 'text-text-quaternary'}`}>
<RiDragDropLine className="w-4 h-4" />
<span className="system-xs-regular">Drop plugin package here to install</span>
<span className="system-xs-regular">{t('plugin.installModal.dropPluginToInstall')}</span>
</div>
{currentFile && (
<InstallFromLocalPackage

View File

@ -1,86 +0,0 @@
import {
useState,
} from 'react'
import {
RiCheckboxCircleFill,
RiErrorWarningFill,
RiInstallLine,
} from '@remixicon/react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Tooltip from '@/app/components/base/tooltip'
import Button from '@/app/components/base/button'
// import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
import { useMemo } from 'react'
import cn from '@/utils/classnames'
const InstallInfo = () => {
const [open, setOpen] = useState(false)
const status = 'error'
const statusError = useMemo(() => status === 'error', [status])
return (
<div className='flex items-center'>
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-start'
offset={{
mainAxis: 4,
crossAxis: 79,
}}
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
<Tooltip popupContent='Installing 1/3 plugins...'>
<div
className={cn(
'relative flex items-center justify-center w-8 h-8 rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs hover:bg-components-button-secondary-bg-hover',
statusError && 'border-components-button-destructive-secondary-border-hover bg-state-destructive-hover hover:bg-state-destructive-hover-alt',
)}
>
<RiInstallLine
className={cn(
'w-4 h-4 text-components-button-secondary-text',
statusError && 'text-components-button-destructive-secondary-text',
)}
/>
<div className='absolute -right-1 -top-1'>
{/* <ProgressCircle
percentage={33}
circleFillColor='fill-components-progress-brand-bg'
sectorFillColor='fill-components-progress-error-bg'
circleStrokeColor='stroke-components-progress-error-bg'
/> */}
<RiCheckboxCircleFill className='w-3.5 h-3.5 text-text-success' />
</div>
</div>
</Tooltip>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-10'>
<div className='p-1 pb-2 w-[320px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg'>
<div className='flex items-center px-2 pt-1 h-7 system-sm-semibold-uppercase'>3 plugins failed to install</div>
<div className='flex items-center p-1 pl-2 h-8 rounded-lg hover:bg-state-base-hover'>
<div className='relative flex items-center justify-center mr-2 w-6 h-6 rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge'>
<RiErrorWarningFill className='absolute -right-0.5 -bottom-0.5 w-3 h-3 text-text-destructive' />
</div>
<div className='grow system-md-regular text-text-secondary truncate'>
DuckDuckGo Search
</div>
<Button
size='small'
variant='ghost-accent'
>
Clear
</Button>
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
</div>
)
}
export default InstallInfo

View File

@ -16,6 +16,7 @@ import {
} from '@/app/components/base/portal-to-follow-elem'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useTranslation } from 'react-i18next'
type Props = {
onSwitchToMarketplaceTab: () => void
@ -23,6 +24,7 @@ type Props = {
const InstallPluginDropdown = ({
onSwitchToMarketplaceTab,
}: Props) => {
const { t } = useTranslation()
const fileInputRef = useRef<HTMLInputElement>(null)
const [isMenuOpen, setIsMenuOpen] = useState(false)
const [selectedAction, setSelectedAction] = useState<string | null>(null)
@ -65,14 +67,14 @@ const InstallPluginDropdown = ({
className={cn('w-full h-full p-2 text-components-button-secondary-text', isMenuOpen && 'bg-state-base-hover')}
>
<RiAddLine className='w-4 h-4' />
<span className='pl-1'>Install plugin</span>
<span className='pl-1'>{t('plugin.installPlugin')}</span>
<RiArrowDownSLine className='w-4 h-4 ml-1' />
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1002]'>
<div className='flex flex-col p-1 pb-2 items-start w-[200px] bg-components-panel-bg-blur border border-components-panel-border rounded-xl shadows-shadow-lg'>
<span className='flex pt-1 pb-0.5 pl-2 pr-3 items-start self-stretch text-text-tertiary system-xs-medium-uppercase'>
Install From
{t('plugin.installFrom')}
</span>
<input
type='file'
@ -85,11 +87,11 @@ const InstallPluginDropdown = ({
{[
...(
(enable_marketplace || true)
? [{ icon: MagicBox, text: 'Marketplace', action: 'marketplace' }]
? [{ icon: MagicBox, text: t('plugin.source.marketplace'), action: 'marketplace' }]
: []
),
{ icon: Github, text: 'GitHub', action: 'github' },
{ icon: FileZip, text: 'Local Package File', action: 'local' },
{ icon: Github, text: t('plugin.source.github'), action: 'github' },
{ icon: FileZip, text: t('plugin.source.local'), action: 'local' },
].map(({ icon: Icon, text, action }) => (
<div
key={action}

View File

@ -0,0 +1,47 @@
import { useCallback } from 'react'
import { TaskStatus } from '@/app/components/plugins/types'
import type { PluginStatus } from '@/app/components/plugins/types'
import {
useMutationClearTaskPlugin,
usePluginTaskList,
} from '@/service/use-plugins'
export const usePluginTaskStatus = () => {
const {
pluginTasks,
} = usePluginTaskList()
const { mutate } = useMutationClearTaskPlugin()
const allPlugins = pluginTasks.map(task => task.plugins.map((plugin) => {
return {
...plugin,
taskId: task.id,
}
})).flat()
const errorPlugins: PluginStatus[] = []
const successPlugins: PluginStatus[] = []
const runningPlugins: PluginStatus[] = []
allPlugins.forEach((plugin) => {
if (plugin.status === TaskStatus.running)
runningPlugins.push(plugin)
if (plugin.status === TaskStatus.failed)
errorPlugins.push(plugin)
if (plugin.status === TaskStatus.success)
successPlugins.push(plugin)
})
const handleClearErrorPlugin = useCallback((taskId: string, pluginId: string) => {
mutate({
taskId,
pluginId,
})
}, [mutate])
return {
errorPlugins,
successPlugins,
runningPlugins,
totalPluginsLength: allPlugins.length,
handleClearErrorPlugin,
}
}

View File

@ -0,0 +1,152 @@
import {
useMemo,
useState,
} from 'react'
import {
RiCheckboxCircleFill,
RiErrorWarningFill,
RiInstallLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { usePluginTaskStatus } from './hooks'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Tooltip from '@/app/components/base/tooltip'
import Button from '@/app/components/base/button'
import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
import CardIcon from '@/app/components/plugins/card/base/card-icon'
import cn from '@/utils/classnames'
import { useGetLanguage } from '@/context/i18n'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
const PluginTasks = () => {
const { t } = useTranslation()
const language = useGetLanguage()
const [open, setOpen] = useState(false)
const {
errorPlugins,
runningPlugins,
successPlugins,
totalPluginsLength,
handleClearErrorPlugin,
} = usePluginTaskStatus()
const { getIconUrl } = useGetIcon()
const isInstalling = runningPlugins.length > 0 && errorPlugins.length === 0 && successPlugins.length === 0
const isInstallingWithError = errorPlugins.length > 0 && errorPlugins.length < totalPluginsLength
const isSuccess = successPlugins.length === totalPluginsLength && totalPluginsLength > 0
const isFailed = errorPlugins.length === totalPluginsLength && totalPluginsLength > 0
const tip = useMemo(() => {
if (isInstalling)
return t('plugin.task.installing', { installingLength: runningPlugins.length, totalLength: totalPluginsLength })
if (isInstallingWithError)
return t('plugin.task.installingWithError', { installingLength: runningPlugins.length, totalLength: totalPluginsLength, errorLength: errorPlugins.length })
if (isFailed)
return t('plugin.task.installError', { errorLength: errorPlugins.length })
}, [isInstalling, isInstallingWithError, isFailed, errorPlugins, runningPlugins, totalPluginsLength, t])
return (
<div className='flex items-center'>
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-start'
offset={{
mainAxis: 4,
crossAxis: 79,
}}
>
<PortalToFollowElemTrigger
onClick={() => {
if (isFailed || isInstallingWithError)
setOpen(v => !v)
}}
>
<Tooltip popupContent={tip}>
<div
className={cn(
'relative flex items-center justify-center w-8 h-8 rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs hover:bg-components-button-secondary-bg-hover',
(isInstallingWithError || isFailed) && 'border-components-button-destructive-secondary-border-hover bg-state-destructive-hover hover:bg-state-destructive-hover-alt',
)}
>
<RiInstallLine
className={cn(
'w-4 h-4 text-components-button-secondary-text',
(isInstallingWithError || isFailed) && 'text-components-button-destructive-secondary-text',
)}
/>
<div className='absolute -right-1 -top-1'>
{
isInstalling && (
<ProgressCircle
percentage={runningPlugins.length / totalPluginsLength * 100}
/>
)
}
{
isInstallingWithError && (
<ProgressCircle
percentage={runningPlugins.length / totalPluginsLength * 100}
circleFillColor='fill-components-progress-brand-bg'
sectorFillColor='fill-components-progress-error-border'
circleStrokeColor='stroke-components-progress-error-border'
/>
)
}
{
isSuccess && (
<RiCheckboxCircleFill className='w-3.5 h-3.5 text-text-success' />
)
}
{
isFailed && (
<RiErrorWarningFill className='w-3.5 h-3.5 text-text-destructive' />
)
}
</div>
</div>
</Tooltip>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-10'>
<div className='p-1 pb-2 w-[320px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg'>
<div className='flex items-center px-2 pt-1 h-7 system-sm-semibold-uppercase'>{t('plugin.task.installedError')}</div>
{
errorPlugins.map(errorPlugin => (
<div
key={errorPlugin.plugin_unique_identifier}
className='flex items-center p-1 pl-2 h-8 rounded-lg hover:bg-state-base-hover'
>
<div className='relative flex items-center justify-center mr-2 w-6 h-6 rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge'>
<RiErrorWarningFill className='absolute -right-0.5 -bottom-0.5 w-3 h-3 text-text-destructive' />
<CardIcon
size='tiny'
src={getIconUrl(errorPlugin.icon)}
/>
</div>
<div className='grow system-md-regular text-text-secondary truncate'>
{errorPlugin.labels[language]}
</div>
<Button
size='small'
variant='ghost-accent'
onClick={() => handleClearErrorPlugin(errorPlugin.taskId, errorPlugin.plugin_unique_identifier)}
>
{t('common.operation.clear')}
</Button>
</div>
))
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
</div>
)
}
export default PluginTasks

View File

@ -1,40 +0,0 @@
import { create } from 'zustand'
import type { PluginTask } from '../types'
import { fetchPluginTasks } from '@/service/plugins'
type PluginTasksStore = {
pluginTasks: PluginTask[]
setPluginTasks: (tasks: PluginTask[]) => void
setPluginTasksWithPolling: () => void
}
let pluginTasksTimer: NodeJS.Timeout | null = null
export const usePluginTasksStore = create<PluginTasksStore>(set => ({
pluginTasks: [],
setPluginTasks: (tasks: PluginTask[]) => set({ pluginTasks: tasks }),
setPluginTasksWithPolling: async () => {
if (pluginTasksTimer) {
clearTimeout(pluginTasksTimer)
pluginTasksTimer = null
}
const handleUpdatePluginTasks = async () => {
const { tasks } = await fetchPluginTasks()
set({ pluginTasks: tasks })
if (tasks.length && !tasks.every(task => task.status === 'success')) {
pluginTasksTimer = setTimeout(() => {
handleUpdatePluginTasks()
}, 5000)
}
else {
if (pluginTasksTimer) {
clearTimeout(pluginTasksTimer)
pluginTasksTimer = null
}
}
}
handleUpdatePluginTasks()
},
}))

View File

@ -100,6 +100,7 @@ export type PluginDetail = {
endpoints_active: number
version: string
latest_version: string
latest_unique_identifier: string
source: PluginSource
meta?: MetaData
}
@ -194,19 +195,10 @@ export type GitHubUrlInfo = {
}
// endpoint
export type CreateEndpointRequest = {
plugin_unique_identifier: string
settings: Record<string, any>
name: string
}
export type EndpointOperationResponse = {
result: 'success' | 'error'
}
export type EndpointsRequest = {
page_size: number
page: number
plugin_id: string
}
export type EndpointsResponse = {
endpoints: EndpointListItem[]
has_more: boolean
@ -246,6 +238,11 @@ export type InstallPackageResponse = {
task_id: string
}
export type updatePackageResponse = {
all_installed: boolean
task_id: string
}
export type uploadGitHubResponse = {
unique_identifier: string
manifest: PluginDeclaration
@ -268,6 +265,9 @@ export type PluginStatus = {
plugin_id: string
status: TaskStatus
message: string
icon: string
labels: Record<Locale, string>
taskId: string
}
export type PluginTask = {
@ -305,3 +305,15 @@ export type UninstallPluginResponse = {
export type PluginsFromMarketplaceResponse = {
plugins: Plugin[]
}
export type Dependency = {
type: 'github' | 'marketplace' | 'package'
value: {
repo?: string
version?: string
package?: string
github_plugin_unique_identifier?: string
marketplace_plugin_unique_identifier?: string
plugin_unique_identifier?: string
}
}

View File

@ -12,7 +12,7 @@ import { pluginManifestToCardPluginProps } from '@/app/components/plugins/instal
import useGetIcon from '../install-plugin/base/use-get-icon'
import { updateFromMarketPlace } from '@/service/plugins'
import checkTaskStatus from '@/app/components/plugins/install-plugin/base/check-task-status'
import { usePluginTasksStore } from '@/app/components/plugins/plugin-page/store'
import { usePluginTaskList } from '@/service/use-plugins'
const i18nPrefix = 'plugin.upgrade'
@ -56,7 +56,7 @@ const UpdatePluginModal: FC<Props> = ({
}
const [uploadStep, setUploadStep] = useState<UploadStep>(UploadStep.notStarted)
const setPluginTasksWithPolling = usePluginTasksStore(s => s.setPluginTasksWithPolling)
const { handleRefetch } = usePluginTaskList()
const configBtnText = useMemo(() => {
return ({
@ -69,29 +69,36 @@ const UpdatePluginModal: FC<Props> = ({
const handleConfirm = useCallback(async () => {
if (uploadStep === UploadStep.notStarted) {
setUploadStep(UploadStep.upgrading)
const {
all_installed: isInstalled,
task_id: taskId,
} = await updateFromMarketPlace({
original_plugin_unique_identifier: originalPackageInfo.id,
new_plugin_unique_identifier: targetPackageInfo.id,
})
if (isInstalled) {
try {
const {
all_installed: isInstalled,
task_id: taskId,
} = await updateFromMarketPlace({
original_plugin_unique_identifier: originalPackageInfo.id,
new_plugin_unique_identifier: targetPackageInfo.id,
})
if (isInstalled) {
onSave()
return
}
handleRefetch()
await check({
taskId,
pluginUniqueIdentifier: targetPackageInfo.id,
})
onSave()
return
}
setPluginTasksWithPolling()
await check({
taskId,
pluginUniqueIdentifier: targetPackageInfo.id,
})
onSave()
catch (e) {
setUploadStep(UploadStep.notStarted)
}
return
}
if (uploadStep === UploadStep.installed) {
onSave()
onCancel()
}
}, [onCancel, onSave, uploadStep, check, originalPackageInfo.id, setPluginTasksWithPolling, targetPackageInfo.id])
}, [onCancel, onSave, uploadStep, check, originalPackageInfo.id, handleRefetch, targetPackageInfo.id])
const usedInAppInfo = useMemo(() => {
return (
<div className='flex px-0.5 justify-center items-center gap-0.5'>

View File

@ -5,6 +5,7 @@ import {
useMarketplaceCollectionsAndPlugins,
useMarketplacePlugins,
} from '@/app/components/plugins/marketplace/hooks'
import { PluginType } from '@/app/components/plugins/types'
export const useMarketplace = (searchPluginText: string, filterPluginTags: string[]) => {
const {
@ -25,18 +26,20 @@ export const useMarketplace = (searchPluginText: string, filterPluginTags: strin
if (searchPluginText || filterPluginTags.length) {
if (searchPluginText) {
queryPluginsWithDebounced({
category: PluginType.tool,
query: searchPluginText,
tags: filterPluginTags,
})
return
}
queryPlugins({
category: PluginType.tool,
query: searchPluginText,
tags: filterPluginTags,
})
}
else {
queryMarketplaceCollectionsAndPlugins()
queryMarketplaceCollectionsAndPlugins({ category: PluginType.tool })
resetPlugins()
}
}, [searchPluginText, filterPluginTags, queryPlugins, queryMarketplaceCollectionsAndPlugins, queryPluginsWithDebounced, resetPlugins])

View File

@ -1,9 +1,13 @@
import { RiArrowUpDoubleLine } from '@remixicon/react'
import {
RiArrowRightUpLine,
RiArrowUpDoubleLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useMarketplace } from './hooks'
import List from '@/app/components/plugins/marketplace/list'
import Loading from '@/app/components/base/loading'
import { getLocaleOnClient } from '@/i18n'
import { MARKETPLACE_URL_PREFIX } from '@/config'
type MarketplaceProps = {
searchPluginText: string
@ -25,7 +29,7 @@ const Marketplace = ({
} = useMarketplace(searchPluginText, filterPluginTags)
return (
<div className='shrink-0 sticky -bottom-[442px] h-[530px] overflow-y-auto px-12 py-2 pt-0 bg-background-default-subtle'>
<div className='flex flex-col shrink-0 sticky -bottom-[442px] h-[530px] overflow-y-auto px-12 py-2 pt-0 bg-background-default-subtle'>
<RiArrowUpDoubleLine
className='absolute top-2 left-1/2 -translate-x-1/2 w-4 h-4 text-text-quaternary cursor-pointer'
onClick={() => onMarketplaceScroll()}
@ -51,7 +55,15 @@ const Marketplace = ({
<span className="relative ml-1 mr-1 body-md-medium text-text-secondary after:content-[''] after:absolute after:left-0 after:bottom-[1.5px] after:w-full after:h-2 after:bg-text-text-selected">
{t('plugin.category.bundles')}
</span>
{t('plugin.marketplace.inDifyMarketplace')}
{t('common.operation.in')}
<a
href={`${MARKETPLACE_URL_PREFIX}`}
className='flex items-center ml-1 system-sm-medium text-text-accent'
target='_blank'
>
{t('plugin.marketplace.difyMarketplace')}
<RiArrowRightUpLine className='w-4 h-4' />
</a>
</div>
</div>
{

View File

@ -10,10 +10,10 @@ import LabelFilter from '@/app/components/tools/labels/filter'
import Input from '@/app/components/base/input'
import ProviderDetail from '@/app/components/tools/provider/detail'
import Empty from '@/app/components/tools/add-tool-modal/empty'
import { fetchCollectionList } from '@/service/tools'
import Card from '@/app/components/plugins/card'
import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import { useAllToolProviders } from '@/service/use-tools'
const ProviderList = () => {
const { t } = useTranslation()
@ -36,8 +36,7 @@ const ProviderList = () => {
const handleKeywordsChange = (value: string) => {
setKeywords(value)
}
const [collectionList, setCollectionList] = useState<Collection[]>([])
const { data: collectionList, refetch } = useAllToolProviders()
const filteredCollectionList = useMemo(() => {
return collectionList.filter((collection) => {
if (collection.type !== activeTab)
@ -49,13 +48,6 @@ const ProviderList = () => {
return true
})
}, [activeTab, tagFilterValue, keywords, collectionList])
const getProviderList = async () => {
const list = await fetchCollectionList()
setCollectionList([...list])
}
useEffect(() => {
getProviderList()
}, [])
const [currentProvider, setCurrentProvider] = useState<Collection | undefined>()
useEffect(() => {
@ -106,7 +98,7 @@ const ProviderList = () => {
>
<Card
className={cn(
'border-[1.5px] border-transparent',
'border-[1.5px] border-transparent cursor-pointer',
currentProvider?.id === collection.id && 'border-components-option-card-option-selected-border',
)}
hideCornerMark
@ -140,7 +132,7 @@ const ProviderList = () => {
<ProviderDetail
collection={currentProvider}
onHide={() => setCurrentProvider(undefined)}
onRefreshData={getProviderList}
onRefreshData={refetch}
/>
)}
</div>

View File

@ -44,6 +44,7 @@ import { useProviderContext } from '@/context/provider-context'
import { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import Loading from '@/app/components/base/loading'
import { useAppContext } from '@/context/app-context'
import { useInvalidateAllWorkflowTools } from '@/service/use-tools'
type Props = {
collection: Collection
@ -65,7 +66,7 @@ const ProviderDetail = ({
const isBuiltIn = collection.type === CollectionType.builtIn
const isModel = collection.type === CollectionType.model
const { isCurrentWorkspaceManager } = useAppContext()
const invalidateAllWorkflowTools = useInvalidateAllWorkflowTools()
const [isDetailLoading, setIsDetailLoading] = useState(false)
// built in provider
@ -164,6 +165,7 @@ const ProviderDetail = ({
workflow_tool_id: string
}>) => {
await saveWorkflowToolProvider(data)
invalidateAllWorkflowTools()
onRefreshData()
getWorkflowToolProvider()
Toast.notify({

View File

@ -0,0 +1,183 @@
'use client'
import type { FC } from 'react'
import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import ToolTrigger from '@/app/components/tools/tool-selector/tool-trigger'
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import ToolCredentialForm from '@/app/components/tools/tool-selector/tool-credentials-form'
import Toast from '@/app/components/base/toast'
import { useAppContext } from '@/context/app-context'
import {
useAllBuiltInTools,
useAllCustomTools,
useAllWorkflowTools,
useInvalidateAllBuiltInTools,
useUpdateProviderCredentials,
} from '@/service/use-tools'
import { CollectionType } from '@/app/components/tools/types'
import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
import type {
OffsetOptions,
Placement,
} from '@floating-ui/react'
import cn from '@/utils/classnames'
type Props = {
value?: {
provider: string
tool_name: string
}
disabled?: boolean
placement?: Placement
offset?: OffsetOptions
onSelect: (tool: {
provider: string
tool_name: string
}) => void
supportAddCustomTool?: boolean
}
const ToolSelector: FC<Props> = ({
value,
disabled,
placement = 'bottom',
offset = 4,
onSelect,
}) => {
const { t } = useTranslation()
const [isShow, onShowChange] = useState(false)
const handleTriggerClick = () => {
if (disabled) return
onShowChange(true)
}
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
const invalidateAllBuiltinTools = useInvalidateAllBuiltInTools()
const currentProvider = useMemo(() => {
const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || [])]
return mergedTools.find((toolWithProvider) => {
return toolWithProvider.id === value?.provider && toolWithProvider.tools.some(tool => tool.name === value?.tool_name)
})
}, [value, buildInTools, customTools, workflowTools])
const [isShowChooseTool, setIsShowChooseTool] = useState(false)
const handleSelectTool = (tool: ToolDefaultValue) => {
const toolValue = {
provider: tool.provider_id,
tool_name: tool.tool_name,
}
onSelect(toolValue)
setIsShowChooseTool(false)
if (tool.provider_type === CollectionType.builtIn && tool.is_team_authorization)
onShowChange(false)
}
const { isCurrentWorkspaceManager } = useAppContext()
const [isShowSettingAuth, setShowSettingAuth] = useState(false)
const handleCredentialSettingUpdate = () => {
invalidateAllBuiltinTools()
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
setShowSettingAuth(false)
onShowChange(false)
}
const { mutate: updatePermission } = useUpdateProviderCredentials({
onSuccess: handleCredentialSettingUpdate,
})
return (
<>
<PortalToFollowElem
placement={placement}
offset={offset}
open={isShow}
onOpenChange={onShowChange}
>
<PortalToFollowElemTrigger
className='w-full'
onClick={handleTriggerClick}
>
<ToolTrigger
open={isShow}
value={value}
provider={currentProvider}
/>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<div className="relative w-[389px] min-h-20 rounded-xl bg-components-panel-bg-blur border-[0.5px] border-components-panel-border shadow-lg">
<div className='px-4 py-3 flex flex-col gap-1'>
<div className='h-6 flex items-center system-sm-semibold text-text-secondary'>{t('tools.toolSelector.label')}</div>
<ToolPicker
placement='bottom'
offset={offset}
trigger={
<ToolTrigger
open={isShowChooseTool}
value={value}
provider={currentProvider}
/>
}
isShow={isShowChooseTool}
onShowChange={setIsShowChooseTool}
disabled={false}
supportAddCustomTool
onSelect={handleSelectTool}
/>
</div>
{/* authorization panel */}
{isShowSettingAuth && currentProvider && (
<div className='px-4 pb-4 border-t border-divider-subtle'>
<ToolCredentialForm
collection={currentProvider}
onCancel={() => setShowSettingAuth(false)}
onSaved={async value => updatePermission({
providerName: currentProvider.name,
credentials: value,
})}
/>
</div>
)}
{!isShowSettingAuth && currentProvider && currentProvider.type === CollectionType.builtIn && currentProvider.is_team_authorization && currentProvider.allow_delete && (
<div className='px-4 py-3 flex items-center border-t border-divider-subtle'>
<div className='grow mr-3 h-6 flex items-center system-sm-semibold text-text-secondary'>{t('tools.toolSelector.auth')}</div>
{isCurrentWorkspaceManager && (
<Button
variant='secondary'
size='small'
onClick={() => {}}
>
<Indicator className='mr-2' color={'green'} />
{t('tools.auth.authorized')}
</Button>
)}
</div>
)}
{!isShowSettingAuth && currentProvider && currentProvider.type === CollectionType.builtIn && !currentProvider.is_team_authorization && currentProvider.allow_delete && (
<div className='px-4 py-3 flex items-center border-t border-divider-subtle'>
<Button
variant='primary'
className={cn('shrink-0 w-full')}
onClick={() => setShowSettingAuth(true)}
disabled={!isCurrentWorkspaceManager}
>
{t('tools.auth.unauthorized')}
</Button>
</div>
)}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
</>
)
}
export default React.memo(ToolSelector)

View File

@ -0,0 +1,95 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowRightUpLine,
} from '@remixicon/react'
import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import type { Collection } from '@/app/components/tools/types'
import Button from '@/app/components/base/button'
import Toast from '@/app/components/base/toast'
import { fetchBuiltInToolCredential, fetchBuiltInToolCredentialSchema } from '@/service/tools'
import Loading from '@/app/components/base/loading'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import cn from '@/utils/classnames'
type Props = {
collection: Collection
onCancel: () => void
onSaved: (value: Record<string, any>) => void
}
const ToolCredentialForm: FC<Props> = ({
collection,
onCancel,
onSaved,
}) => {
const { t } = useTranslation()
const language = useLanguage()
const [credentialSchema, setCredentialSchema] = useState<any>(null)
const { name: collectionName } = collection
const [tempCredential, setTempCredential] = React.useState<any>({})
useEffect(() => {
fetchBuiltInToolCredentialSchema(collectionName).then(async (res) => {
const toolCredentialSchemas = toolCredentialToFormSchemas(res)
const credentialValue = await fetchBuiltInToolCredential(collectionName)
setTempCredential(credentialValue)
const defaultCredentials = addDefaultValue(credentialValue, toolCredentialSchemas)
setCredentialSchema(toolCredentialSchemas)
setTempCredential(defaultCredentials)
})
}, [])
const handleSave = () => {
for (const field of credentialSchema) {
if (field.required && !tempCredential[field.name]) {
Toast.notify({ type: 'error', message: t('common.errorMsg.fieldRequired', { field: field.label[language] || field.label.en_US }) })
return
}
}
onSaved(tempCredential)
}
return (
<div className='h-full'>
{!credentialSchema
? <div className='pt-3'><Loading type='app' /></div>
: (
<>
<Form
value={tempCredential}
onChange={(v) => {
setTempCredential(v)
}}
formSchemas={credentialSchema}
isEditMode={true}
showOnVariableMap={{}}
validating={false}
inputClassName='bg-components-input-bg-normal hover:bg-state-base-hover-alt'
fieldMoreInfo={item => item.url
? (<a
href={item.url}
target='_blank' rel='noopener noreferrer'
className='inline-flex items-center text-xs text-primary-600'
>
{t('tools.howToGet')}
<RiArrowRightUpLine className='ml-1 w-3 h-3' />
</a>)
: null}
/>
<div className={cn('mt-1 flex justify-end')} >
<div className='flex space-x-2'>
<Button onClick={onCancel}>{t('common.operation.cancel')}</Button>
<Button variant='primary' onClick={handleSave}>{t('common.operation.save')}</Button>
</div>
</div>
</>
)
}
</div >
)
}
export default React.memo(ToolCredentialForm)

View File

@ -0,0 +1,53 @@
'use client'
import React from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowDownSLine,
} from '@remixicon/react'
import BlockIcon from '@/app/components/workflow/block-icon'
import { BlockEnum } from '@/app/components/workflow/types'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type Props = {
open: boolean
provider?: ToolWithProvider
value?: {
provider: string
tool_name: string
}
}
const ToolTrigger = ({
open,
provider,
value,
}: Props) => {
const { t } = useTranslation()
return (
<div className={cn(
'group flex items-center p-2 pl-3 bg-components-input-bg-normal rounded-lg cursor-pointer hover:bg-state-base-hover-alt',
open && 'bg-state-base-hover-alt',
value && 'pl-1.5 py-1.5',
)}>
{value && provider && (
<div className='shrink-0 mr-1 p-px rounded-lg bg-components-panel-bg border border-components-panel-border'>
<BlockIcon
className='!w-4 !h-4'
type={BlockEnum.Tool}
toolIcon={provider.icon}
/>
</div>
)}
{value && (
<div className='grow system-sm-regular text-components-input-text-filled'>{value.tool_name}</div>
)}
{!value && (
<div className='grow text-components-input-text-placeholder system-sm-regular'>{t('tools.toolSelector.placeholder')}</div>
)}
<RiArrowDownSLine className={cn('shrink-0 ml-0.5 w-4 h-4 text-text-quaternary group-hover:text-text-secondary', open && 'text-text-secondary')} />
</div>
)
}
export default ToolTrigger

View File

@ -14,6 +14,7 @@ import { createWorkflowToolProvider, fetchWorkflowToolDetailByAppID, saveWorkflo
import type { Emoji, WorkflowToolProviderParameter, WorkflowToolProviderRequest, WorkflowToolProviderResponse } from '@/app/components/tools/types'
import type { InputVar } from '@/app/components/workflow/types'
import { useAppContext } from '@/context/app-context'
import { useInvalidateAllWorkflowTools } from '@/service/use-tools'
type Props = {
disabled: boolean
@ -46,6 +47,7 @@ const WorkflowToolConfigureButton = ({
const [isLoading, setIsLoading] = useState(false)
const [detail, setDetail] = useState<WorkflowToolProviderResponse>()
const { isCurrentWorkspaceManager } = useAppContext()
const invalidateAllWorkflowTools = useInvalidateAllWorkflowTools()
const outdated = useMemo(() => {
if (!detail)
@ -135,6 +137,7 @@ const WorkflowToolConfigureButton = ({
const createHandle = async (data: WorkflowToolProviderRequest & { workflow_app_id: string }) => {
try {
await createWorkflowToolProvider(data)
invalidateAllWorkflowTools()
onRefreshData?.()
getDetail(workflowAppId)
Toast.notify({
@ -156,6 +159,7 @@ const WorkflowToolConfigureButton = ({
await handlePublish()
await saveWorkflowToolProvider(data)
onRefreshData?.()
invalidateAllWorkflowTools()
getDetail(workflowAppId)
Toast.notify({
type: 'success',

View File

@ -23,6 +23,7 @@ import { useMarketplacePlugins } from '../../plugins/marketplace/hooks'
type AllToolsProps = {
className?: string
searchText: string
tags: string[]
buildInTools: ToolWithProvider[]
customTools: ToolWithProvider[]
workflowTools: ToolWithProvider[]
@ -34,6 +35,7 @@ type AllToolsProps = {
const AllTools = ({
className,
searchText,
tags = [],
onSelect,
buildInTools,
workflowTools,
@ -50,6 +52,7 @@ const AllTools = ({
return text.toLowerCase().includes(keywords.toLowerCase())
}
const hasFilter = searchText || tags.length > 0
const tools = useMemo(() => {
let mergedTools: ToolWithProvider[] = []
if (activeTab === ToolTypeEnum.All)
@ -61,18 +64,15 @@ const AllTools = ({
if (activeTab === ToolTypeEnum.Workflow)
mergedTools = workflowTools
if (!searchText)
if (!hasFilter)
return mergedTools.filter(toolWithProvider => toolWithProvider.tools.length > 0)
return mergedTools.filter((toolWithProvider) => {
return isMatchingKeywords(toolWithProvider.name, searchText)
|| toolWithProvider.tools.some((tool) => {
return Object.values(tool.label).some((label) => {
return isMatchingKeywords(label, searchText)
})
})
return isMatchingKeywords(toolWithProvider.name, searchText) || toolWithProvider.tools.some((tool) => {
return tool.label[language].toLowerCase().includes(searchText.toLowerCase()) || tool.name.toLowerCase().includes(searchText.toLowerCase())
})
})
}, [activeTab, buildInTools, customTools, workflowTools, searchText, language])
}, [activeTab, buildInTools, customTools, workflowTools, searchText, language, hasFilter])
const {
queryPluginsWithDebounced: fetchPlugins,
@ -80,14 +80,15 @@ const AllTools = ({
} = useMarketplacePlugins()
useEffect(() => {
if (searchText) {
if (searchText || tags.length > 0) {
fetchPlugins({
query: searchText,
tags,
category: PluginType.tool,
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchText])
}, [searchText, tags])
const pluginRef = useRef(null)
const wrapElemRef = useRef<HTMLDivElement>(null)
@ -135,12 +136,14 @@ const AllTools = ({
tools={tools}
onSelect={onSelect}
viewType={activeView}
hasSearchText={!!searchText}
/>
{/* Plugins from marketplace */}
<PluginList
wrapElemRef={wrapElemRef}
list={notInstalledPlugins as any} ref={pluginRef}
searchText={searchText}
tags={tags}
/>
</div>
</div>

View File

@ -14,16 +14,19 @@ type Props = {
wrapElemRef: React.RefObject<HTMLElement>
list: Plugin[]
searchText: string
tags: string[]
}
const List = ({
wrapElemRef,
searchText,
tags,
list,
}: Props, ref: any) => {
const { t } = useTranslation()
const hasSearchText = !searchText
const urlWithSearchText = `${marketplaceUrlPrefix}/plugins?q=${searchText}`
const hasFilter = !searchText
const hasRes = list.length > 0
const urlWithSearchText = `${marketplaceUrlPrefix}/marketplace?q=${searchText}&tags=${tags.join(',')}`
const nextToStickyELemRef = useRef<HTMLDivElement>(null)
const { handleScroll, scrollPosition } = useStickyScroll({
@ -58,7 +61,7 @@ const List = ({
window.open(urlWithSearchText, '_blank')
}
if (hasSearchText) {
if (hasFilter) {
return (
<Link
className='sticky bottom-0 z-10 flex h-8 px-4 py-1 system-sm-medium items-center border-t border-[0.5px] border-components-panel-border bg-components-panel-bg-blur rounded-b-lg shadow-lg text-text-accent-light-mode-only cursor-pointer'
@ -73,21 +76,23 @@ const List = ({
return (
<>
<div
className={cn('sticky z-10 flex justify-between h-8 px-4 py-1 text-text-primary system-sm-medium cursor-pointer', stickyClassName)}
onClick={handleHeadClick}
>
<span>{t('plugin.fromMarketplace')}</span>
<Link
href={urlWithSearchText}
target='_blank'
className='flex items-center text-text-accent-light-mode-only'
onClick={e => e.stopPropagation()}
{hasRes && (
<div
className={cn('sticky z-10 flex justify-between h-8 px-4 py-1 text-text-primary system-sm-medium cursor-pointer', stickyClassName)}
onClick={handleHeadClick}
>
<span>{t('plugin.searchInMarketplace')}</span>
<RiArrowRightUpLine className='ml-0.5 w-3 h-3' />
</Link>
</div>
<span>{t('plugin.fromMarketplace')}</span>
<Link
href={urlWithSearchText}
target='_blank'
className='flex items-center text-text-accent-light-mode-only'
onClick={e => e.stopPropagation()}
>
<span>{t('plugin.searchInMarketplace')}</span>
<RiArrowRightUpLine className='ml-0.5 w-3 h-3' />
</Link>
</div>
)}
<div className='p-1' ref={nextToStickyELemRef}>
{list.map((item, index) => (
<Item

View File

@ -48,6 +48,7 @@ const ToolPicker: FC<Props> = ({
}) => {
const { t } = useTranslation()
const [searchText, setSearchText] = useState('')
const [tags, setTags] = useState<string[]>([])
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
@ -105,20 +106,20 @@ const ToolPicker: FC<Props> = ({
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
{ }
<div className="relative w-[320px] min-h-20 rounded-xl bg-components-panel-bg-blur border-[0.5px] border-components-panel-border shadow-lg">
<div className="relative w-[356px] min-h-20 rounded-xl bg-components-panel-bg-blur border-[0.5px] border-components-panel-border shadow-lg">
<div className='p-2 pb-1'>
<SearchBox
search={searchText}
onSearchChange={setSearchText}
tags={[]}
onTagsChange={() => { }}
tags={tags}
onTagsChange={setTags}
size='small'
placeholder={t('plugin.searchTools')!}
/>
</div>
<AllTools
className='mt-1'
tags={tags}
searchText={searchText}
onSelect={handleSelect}
buildInTools={buildInTools || []}

View File

@ -57,6 +57,7 @@ const ToolItem: FC<Props> = ({
tool_name: payload.name,
tool_label: payload.label[language],
title: payload.label[language],
is_team_authorization: provider.is_team_authorization,
params,
})
}}

View File

@ -10,12 +10,14 @@ import { ViewType } from '../../view-type-select'
type Props = {
payload: ToolWithProvider[]
isShowLetterIndex: boolean
hasSearchText: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
}
const ToolViewFlatView: FC<Props> = ({
payload,
isShowLetterIndex,
hasSearchText,
onSelect,
}) => {
return (
@ -26,6 +28,7 @@ const ToolViewFlatView: FC<Props> = ({
payload={tool}
viewType={ViewType.flat}
isShowLetterIndex={isShowLetterIndex}
hasSearchText={hasSearchText}
onSelect={onSelect}
/>
))}

View File

@ -10,17 +10,19 @@ import type { ToolDefaultValue } from '../../types'
type Props = {
groupName: string
toolList: ToolWithProvider[]
hasSearchText: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
}
const Item: FC<Props> = ({
groupName,
toolList,
hasSearchText,
onSelect,
}) => {
return (
<div>
<div className='flex items-start px-3 h-[22px] text-xs font-medium text-gray-500'>
<div className='flex items-center px-3 h-[22px] text-xs font-medium text-gray-500'>
{groupName}
</div>
<div>
@ -29,7 +31,8 @@ const Item: FC<Props> = ({
key={tool.id}
payload={tool}
viewType={ViewType.tree}
isShowLetterIndex
isShowLetterIndex={false}
hasSearchText={hasSearchText}
onSelect={onSelect}
/>
))}

Some files were not shown because too many files have changed in this diff Show More