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: environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-} CONSOLE_API_URL: ${CONSOLE_API_URL:-}
APP_API_URL: ${APP_API_URL:-} APP_API_URL: ${APP_API_URL:-}
MARKETPLACE_API_URL: ${MARKETPLACE_API_URL:-}
MARKETPLACE_URL: ${MARKETPLACE_URL:-}
SENTRY_DSN: ${WEB_SENTRY_DSN:-} SENTRY_DSN: ${WEB_SENTRY_DSN:-}
NEXT_TELEMETRY_DISABLED: ${NEXT_TELEMETRY_DISABLED:-0} NEXT_TELEMETRY_DISABLED: ${NEXT_TELEMETRY_DISABLED:-0}
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000} TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}

View File

@ -41,6 +41,8 @@ ENV EDITION=SELF_HOSTED
ENV DEPLOY_ENV=PRODUCTION ENV DEPLOY_ENV=PRODUCTION
ENV CONSOLE_API_URL=http://127.0.0.1:5001 ENV CONSOLE_API_URL=http://127.0.0.1:5001
ENV APP_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 PORT=3000
ENV NEXT_TELEMETRY_DISABLED=1 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 Card from '@/app/components/plugins/card'
import { customTool, extensionDallE, modelGPT4, toolNotion } from '@/app/components/plugins/card/card-mock' 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 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 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] const pluginList = [toolNotion, extensionDallE, modelGPT4, customTool]
return ( return (
<div className='pb-3 bg-white'> <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 '> <div className='mx-3 '>
<h2 className='my-3'>Dify Plugin list</h2> {/* <h2 className='my-3'>Dify Plugin list</h2> */}
<div className='grid grid-cols-2 gap-3'> {/* <div className='grid grid-cols-2 gap-3'>
{pluginList.map((plugin, index) => ( {pluginList.map((plugin, index) => (
<PluginItem <PluginItem
key={index} key={index}
@ -21,7 +45,7 @@ const PluginList = async () => {
onDelete={handleDelete} onDelete={handleDelete}
/> />
))} ))}
</div> </div> */}
<h2 className='my-3'>Install Plugin / Package under bundle</h2> <h2 className='my-3'>Install Plugin / Package under bundle</h2>
<div className='w-[512px] rounded-2xl bg-background-section-burn p-2'> <div className='w-[512px] rounded-2xl bg-background-section-burn p-2'>
@ -33,21 +57,21 @@ const PluginList = async () => {
} }
/> />
</div> </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'> <div className='w-[512px] rounded-2xl bg-background-section-burn p-2'>
<Card <Card
payload={toolNotion as any} payload={toolNotion as any}
descriptionLineRows={1} descriptionLineRows={1}
installed 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'> <div className='grid grid-cols-2 gap-3'>
{pluginList.map((plugin, index) => ( {pluginList.map((plugin, index) => (
<ProviderCard key={index} payload={plugin as any} /> <ProviderCard key={index} payload={plugin as any} />
))} ))}
</div> </div> */}
<div className='my-3 h-[px] bg-gray-50'></div> <div className='my-3 h-[px] bg-gray-50'></div>
<h2 className='my-3'>Marketplace Plugin list</h2> <h2 className='my-3'>Marketplace Plugin list</h2>
@ -67,8 +91,8 @@ const PluginList = async () => {
) )
} }
export const metadata = { // export const metadata = {
title: 'Plugins - Card', // title: 'Plugins - Card',
} // }
export default PluginList export default PluginList

View File

@ -23,7 +23,7 @@ const FeaturePanel: FC<IFeaturePanelProps> = ({
children, children,
}) => { }) => {
return ( 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 */} {/* Header */}
<div className={cn('px-3 pt-2', hasHeaderBottomBorder && 'border-b border-divider-subtle')}> <div className={cn('px-3 pt-2', hasHeaderBottomBorder && 'border-b border-divider-subtle')}>
<div className='flex justify-between items-center h-8'> <div className='flex justify-between items-center h-8'>

View File

@ -92,6 +92,7 @@ const AgentTools: FC = () => {
tool_name: tool.tool_name, tool_name: tool.tool_name,
tool_label: tool.tool_label, tool_label: tool.tool_label,
tool_parameters: tool.params, tool_parameters: tool.params,
notAuthor: !tool.is_team_authorization,
enabled: true, enabled: true,
}) })
}) })
@ -101,7 +102,7 @@ const AgentTools: FC = () => {
return ( return (
<> <>
<Panel <Panel
className="mt-2" className={cn('mt-2', tools.length === 0 && 'pb-2')}
noBodySpacing={tools.length === 0} noBodySpacing={tools.length === 0}
headerIcon={ headerIcon={
<RiHammerFill className='w-4 h-4 text-primary-500' /> <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 { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { getRedirection } from '@/utils/app-redirection' import { getRedirection } from '@/utils/app-redirection'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useMutationCheckDependenciesBeforeImportDSL } from '@/service/use-plugins'
type CreateFromDSLModalProps = { type CreateFromDSLModalProps = {
show: boolean show: boolean
@ -43,6 +44,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
const [fileContent, setFileContent] = useState<string>() const [fileContent, setFileContent] = useState<string>()
const [currentTab, setCurrentTab] = useState(activeTab) const [currentTab, setCurrentTab] = useState(activeTab)
const [dslUrlValue, setDslUrlValue] = useState(dslUrl) const [dslUrlValue, setDslUrlValue] = useState(dslUrl)
const { mutateAsync } = useMutationCheckDependenciesBeforeImportDSL()
const readFile = (file: File) => { const readFile = (file: File) => {
const reader = new FileReader() const reader = new FileReader()
@ -78,11 +80,21 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
let app let app
if (currentTab === CreateFromDSLModalTab.FROM_FILE) { if (currentTab === CreateFromDSLModalTab.FROM_FILE) {
const leakedData = await mutateAsync({ dslString: fileContent })
if (leakedData?.leaked.length) {
isCreatingRef.current = false
return
}
app = await importApp({ app = await importApp({
data: fileContent || '', data: fileContent || '',
}) })
} }
if (currentTab === CreateFromDSLModalTab.FROM_URL) { if (currentTab === CreateFromDSLModalTab.FROM_URL) {
const leakedData = await mutateAsync({ url: dslUrlValue })
if (leakedData?.leaked.length) {
isCreatingRef.current = false
return
}
app = await importAppFromUrl({ app = await importAppFromUrl({
url: dslUrlValue || '', url: dslUrlValue || '',
}) })

View File

@ -2,15 +2,15 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
background-color: #ffffff; background-color: var(--color-components-chat-input-audio-bg-alt);
border-radius: 10px; border-radius: 10px;
padding: 8px; padding: 8px;
min-width: 240px; min-width: 240px;
max-width: 420px; max-width: 420px;
max-height: 40px; max-height: 40px;
backdrop-filter: blur(5px); backdrop-filter: blur(5px);
border: 1px solid rgba(16, 24, 40, 0.08); border: 1px solid var(--color-components-panel-border-subtle);
box-shadow: 0 1px 2px rgba(9, 9, 11, 0.05); box-shadow: 0 1px 2px var(--color-shadow-shadow-3);
gap: 8px; gap: 8px;
} }
@ -19,8 +19,8 @@
width: 16px; width: 16px;
height: 16px; height: 16px;
border-radius: 50%; border-radius: 50%;
background-color: #296DFF; background-color: var(--color-components-button-primary-bg);
color: white; color: var(--color-components-chat-input-audio-bg-alt);
border: none; border: none;
cursor: pointer; cursor: pointer;
align-items: center; align-items: center;
@ -30,16 +30,15 @@
} }
.playButton:hover { .playButton:hover {
background-color: #3367d6; background-color: var(--color-components-button-primary-bg-hover);
} }
.playButton:disabled { .playButton:disabled {
background-color: #bdbdbf; background-color: var(--color-components-button-primary-bg-disabled);
} }
.audioControls { .audioControls {
flex-grow: 1; flex-grow: 1;
} }
.progressBarContainer { .progressBarContainer {
@ -76,8 +75,8 @@
.timeDisplay { .timeDisplay {
/* position: absolute; */ /* position: absolute; */
color: #296DFF; color: var(--color-text-accent-secondary);
border-radius: 2px; font-size: 12px;
order: 0; order: 0;
height: 100%; height: 100%;
width: 50px; width: 50px;
@ -97,7 +96,6 @@
} */ } */
.duration { .duration {
background-color: rgba(255, 255, 255, 0.8);
padding: 2px 4px; padding: 2px 4px;
border-radius: 10px; border-radius: 10px;
} }
@ -114,6 +112,6 @@
} }
.playButton svg path, .playButton svg path,
.playButton svg rect{ .playButton svg rect {
fill:currentColor; 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 // DON NOT EDIT IT MANUALLY
import * as React from 'react' import * as React from 'react'
import s from './BaichuanTextCn.module.css'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import s from './BaichuanTextCn.module.css'
const Icon = React.forwardRef<HTMLSpanElement, React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>>(( const Icon = React.forwardRef<HTMLSpanElement, React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>>((
{ className, ...restProps }, { className, ...restProps },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,8 +2,8 @@
// DON NOT EDIT IT MANUALLY // DON NOT EDIT IT MANUALLY
import * as React from 'react' import * as React from 'react'
import s from './WxyyTextCn.module.css'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import s from './WxyyTextCn.module.css'
const Icon = React.forwardRef<HTMLSpanElement, React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>>(( const Icon = React.forwardRef<HTMLSpanElement, React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>>((
{ className, ...restProps }, { 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 DiagonalDividingLine } from './DiagonalDividingLine'
export { default as Dify } from './Dify' export { default as Dify } from './Dify'
export { default as Github } from './Github' export { default as Github } from './Github'
export { default as Highlight } from './Highlight'
export { default as Line3 } from './Line3' export { default as Line3 } from './Line3'
export { default as Lock } from './Lock'
export { default as MessageChatSquare } from './MessageChatSquare' export { default as MessageChatSquare } from './MessageChatSquare'
export { default as MultiPathRetrieval } from './MultiPathRetrieval' export { default as MultiPathRetrieval } from './MultiPathRetrieval'
export { default as NTo1Retrieval } from './NTo1Retrieval' export { default as NTo1Retrieval } from './NTo1Retrieval'
export { default as Notion } from './Notion' 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 }) => { 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) export default React.memo(VideoGallery)

View File

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

View File

@ -6,13 +6,17 @@ import { RiArrowDownSLine } from '@remixicon/react'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { switchWorkspace } from '@/service/common' import { switchWorkspace } from '@/service/common'
import { useWorkspacesContext } from '@/context/workspace-context' import { useWorkspacesContext } from '@/context/workspace-context'
import { useProviderContext } from '@/context/provider-context'
import { ToastContext } from '@/app/components/base/toast' import { ToastContext } from '@/app/components/base/toast'
import PremiumBadge from '@/app/components/base/premium-badge'
const WorkplaceSelector = () => { const WorkplaceSelector = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { plan } = useProviderContext()
const { notify } = useContext(ToastContext) const { notify } = useContext(ToastContext)
const { workspaces } = useWorkspacesContext() const { workspaces } = useWorkspacesContext()
const currentWorkspace = workspaces.find(v => v.current) const currentWorkspace = workspaces.find(v => v.current)
const isFreePlan = plan.type === 'sandbox'
const handleSwitchWorkspace = async (tenant_id: string) => { const handleSwitchWorkspace = async (tenant_id: string) => {
try { try {
if (currentWorkspace?.id === tenant_id) 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'> <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> <span className='flex-1 text-text-tertiary system-xs-medium-uppercase'>{t('common.userProfile.workspace')}</span>
</div> </div>
@ -65,7 +69,16 @@ const WorkplaceSelector = () => {
workspaces.map(workspace => ( 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 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='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> </div>
)) ))
} }

View File

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

View File

@ -1,8 +1,6 @@
import { Fragment, useCallback, useEffect } from 'react' import { Fragment, useCallback, useEffect } from 'react'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { RiCloseLine } from '@remixicon/react'
import { Dialog, Transition } from '@headlessui/react' import { Dialog, Transition } from '@headlessui/react'
import Button from '@/app/components/base/button'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
type DialogProps = { type DialogProps = {
@ -47,18 +45,7 @@ const MenuDialog = ({
leaveTo="opacity-0 scale-95" 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)}> <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-0 right-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>
{children} {children}
</Dialog.Panel> </Dialog.Panel>
</Transition.Child> </Transition.Child>

View File

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

View File

@ -19,11 +19,12 @@ const ModelIcon: FC<ModelIconProps> = ({
}) => { }) => {
const language = useLanguage() 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}`}/> return <OpenaiViolet className={`w-4 h-4 ${className}`}/>
if (provider?.icon_small) { if (provider?.icon_small) {
return ( return (
// eslint-disable-next-line @next/next/no-img-element
<img <img
alt='model-icon' alt='model-icon'
src={`${provider.icon_small[language] || provider.icon_small.en_US}`} 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 type { FC } from 'react'
import { ValidatingTip } from '../../key-validator/ValidateStatus' import { ValidatingTip } from '../../key-validator/ValidateStatus'
import type { import type {
@ -17,6 +17,9 @@ import cn from '@/utils/classnames'
import { SimpleSelect } from '@/app/components/base/select' import { SimpleSelect } from '@/app/components/base/select'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import Radio from '@/app/components/base/radio' 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 = { type FormProps = {
className?: string className?: string
itemClassName?: string itemClassName?: string
@ -67,6 +70,24 @@ const Form: FC<FormProps> = ({
onChange({ ...value, [key]: val, ...shouldClearVariable }) 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 renderField = (formSchema: CredentialFormSchema) => {
const tooltip = formSchema.tooltip const tooltip = formSchema.tooltip
const tooltipContent = (tooltip && ( const tooltipContent = (tooltip && (
@ -94,7 +115,7 @@ const Form: FC<FormProps> = ({
const disabled = readonly || (isEditMode && (variable === '__model_type' || variable === '__model_name')) const disabled = readonly || (isEditMode && (variable === '__model_type' || variable === '__model_name'))
return ( return (
<div key={variable} className={cn(itemClassName, 'py-3')}> <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} {label[language] || label.en_US}
{ {
required && ( required && (
@ -135,7 +156,7 @@ const Form: FC<FormProps> = ({
return ( return (
<div key={variable} className={cn(itemClassName, 'py-3')}> <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} {label[language] || label.en_US}
{ {
required && ( 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 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'} ${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> </div>
)) ))
} }
@ -176,7 +197,7 @@ const Form: FC<FormProps> = ({
) )
} }
if (formSchema.type === 'select') { if (formSchema.type === FormTypeEnum.select) {
const { const {
options, options,
variable, variable,
@ -191,7 +212,7 @@ const Form: FC<FormProps> = ({
return ( return (
<div key={variable} className={cn(itemClassName, 'py-3')}> <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} {label[language] || label.en_US}
{ {
@ -202,6 +223,7 @@ const Form: FC<FormProps> = ({
{tooltipContent} {tooltipContent}
</div> </div>
<SimpleSelect <SimpleSelect
wrapperClassName='h-8'
className={cn(inputClassName)} className={cn(inputClassName)}
disabled={readonly} disabled={readonly}
defaultValue={(isShowDefaultValue && ((value[variable] as string) === '' || value[variable] === undefined || value[variable] === null)) ? formSchema.default : value[variable]} 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 { const {
variable, variable,
label, label,
@ -233,9 +255,9 @@ const Form: FC<FormProps> = ({
return ( return (
<div key={variable} className={cn(itemClassName, 'py-3')}> <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'> <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 && ( required && (
<span className='ml-1 text-red-500'>*</span> <span className='ml-1 text-red-500'>*</span>
@ -256,6 +278,77 @@ const Form: FC<FormProps> = ({
</div> </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 ( return (

View File

@ -26,14 +26,14 @@ const Input: FC<InputProps> = ({
max, max,
}) => { }) => {
const toLimit = (v: string) => { const toLimit = (v: string) => {
const minNum = parseFloat(`${min}`) const minNum = Number.parseFloat(`${min}`)
const maxNum = parseFloat(`${max}`) const maxNum = Number.parseFloat(`${max}`)
if (!isNaN(minNum) && parseFloat(v) < minNum) { if (!isNaN(minNum) && Number.parseFloat(v) < minNum) {
onChange(`${min}`) onChange(`${min}`)
return return
} }
if (!isNaN(maxNum) && parseFloat(v) > maxNum) if (!isNaN(maxNum) && Number.parseFloat(v) > maxNum)
onChange(`${max}`) onChange(`${max}`)
} }
return ( return (
@ -41,9 +41,9 @@ const Input: FC<InputProps> = ({
<input <input
tabIndex={0} tabIndex={0}
className={` 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 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 focus:bg-white focus:border-gray-300 focus:shadow-xs
placeholder:text-sm placeholder:text-gray-400 placeholder:text-sm placeholder:text-gray-400
${validated && 'pr-[30px]'} ${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> = ({ const ModelParameterModal: FC<ModelParameterModalProps> = ({
popupClassName, popupClassName,
portalToFollowElemContentClassName, portalToFollowElemContentClassName,
@ -190,26 +190,22 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
) )
} }
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent className={cn(portalToFollowElemContentClassName, 'z-[60]')}> <PortalToFollowElemContent className={cn('z-[60]', portalToFollowElemContentClassName)}>
<div className={cn(popupClassName, 'w-[496px] rounded-xl border border-gray-100 bg-white shadow-xl')}> <div className={cn(popupClassName, 'w-[389px] rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg')}>
<div className={cn( <div className={cn('max-h-[420px] p-4 pt-3 overflow-y-auto')}>
'max-h-[480px] overflow-y-auto', <div className='relative'>
!isInWorkflow && 'px-10 pt-6 pb-8', <div className={cn('mb-1 h-6 flex items-center text-text-secondary system-sm-semibold')}>
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]')}>
{t('common.modelProvider.model').toLocaleUpperCase()} {t('common.modelProvider.model').toLocaleUpperCase()}
</div> </div>
<ModelSelector <ModelSelector
defaultModel={(provider || modelId) ? { provider, model: modelId } : undefined} defaultModel={(provider || modelId) ? { provider, model: modelId } : undefined}
modelList={activeTextGenerationModelList} modelList={activeTextGenerationModelList}
onSelect={handleChangeModel} onSelect={handleChangeModel}
triggerClassName='max-w-[295px]'
/> />
</div> </div>
{ {
!!parameterRules.length && ( !!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 && ( !isLoading && !!parameterRules.length && (
<div className='flex items-center justify-between mb-4'> <div className='flex items-center justify-between mb-2'>
<div className={cn('font-semibold text-gray-900', isInWorkflow && 'text-[13px]')}>{t('common.modelProvider.parameters')}</div> <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) && ( PROVIDER_WITH_PRESET_TONE.includes(provider) && (
<PresetsParameter onSelect={handleSelectPresetParameter} /> <PresetsParameter onSelect={handleSelectPresetParameter} />
@ -237,7 +233,6 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
].map(parameter => ( ].map(parameter => (
<ParameterItem <ParameterItem
key={`${modelId}-${parameter.name}`} key={`${modelId}-${parameter.name}`}
className='mb-4'
parameterRule={parameter} parameterRule={parameter}
value={completionParams?.[parameter.name]} value={completionParams?.[parameter.name]}
onChange={v => handleParamChange(parameter.name, v)} onChange={v => handleParamChange(parameter.name, v)}
@ -250,7 +245,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
</div> </div>
{!hideDebugWithMultipleModel && ( {!hideDebugWithMultipleModel && (
<div <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?.()} onClick={() => onDebugWithMultipleModelChange?.()}
> >
{ {

View File

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

View File

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

View File

@ -18,7 +18,7 @@ import {
validateModelProvider, validateModelProvider,
} from '@/service/common' } 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' export const DEFAULT_BACKGROUND_COLOR = '#F3F4F6'

View File

@ -4,7 +4,8 @@ import Link from 'next/link'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import { useSelectedLayoutSegment } from 'next/navigation' import { useSelectedLayoutSegment } from 'next/navigation'
import { Bars3Icon } from '@heroicons/react/20/solid' 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 AccountDropdown from './account-dropdown'
import AppNav from './app-nav' import AppNav from './app-nav'
import DatasetNav from './dataset-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 useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { useModalContext } from '@/context/modal-context' import { useModalContext } from '@/context/modal-context'
import { useTranslation } from 'react-i18next'
const navClassName = ` const navClassName = `
flex items-center relative mr-0 sm:mr-3 px-3 h-8 rounded-xl flex items-center relative mr-0 sm:mr-3 px-3 h-8 rounded-xl
@ -29,6 +31,7 @@ const navClassName = `
const Header = () => { const Header = () => {
const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext() const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext()
const { t } = useTranslation()
const selectedSegment = useSelectedLayoutSegment() const selectedSegment = useSelectedLayoutSegment()
const media = useBreakpoints() const media = useBreakpoints()
@ -69,7 +72,14 @@ const Header = () => {
</WorkspaceProvider> </WorkspaceProvider>
{enableBilling && ( {enableBilling && (
<div className='select-none'> <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>
)} )}
</div> </div>
@ -84,7 +94,14 @@ const Header = () => {
<div className='font-light text-divider-deep'>/</div> <div className='font-light text-divider-deep'>/</div>
{enableBilling && ( {enableBilling && (
<div className='select-none'> <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>
)} )}
<GithubStar /> <GithubStar />
@ -98,7 +115,7 @@ const Header = () => {
{!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />} {!isCurrentWorkspaceDatasetOperator && <ToolsNav className={navClassName} />}
</div> </div>
)} )}
<div className='flex items-center flex-shrink-0'> <div className='flex items-center shrink-0'>
<EnvNav /> <EnvNav />
<div className='mr-3'> <div className='mr-3'>
<PluginsNav /> <PluginsNav />

View File

@ -4,6 +4,8 @@ import { useTranslation } from 'react-i18next'
import Link from 'next/link' import Link from 'next/link'
import classNames from '@/utils/classnames' import classNames from '@/utils/classnames'
import { Group } from '@/app/components/base/icons/src/vender/other' import { Group } from '@/app/components/base/icons/src/vender/other'
import { useSelectedLayoutSegment } from 'next/navigation'
type PluginsNavProps = { type PluginsNavProps = {
className?: string className?: string
} }
@ -12,12 +14,17 @@ const PluginsNav = ({
className, className,
}: PluginsNavProps) => { }: PluginsNavProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const selectedSegment = useSelectedLayoutSegment()
const activated = selectedSegment === 'plugins'
return ( return (
<Link href="/plugins" className={classNames( <Link href="/plugins" className={classNames(
className, 'group', 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'> <div className='flex w-4 h-4 justify-center items-center'>
<Group /> <Group />
</div> </div>

View File

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

View File

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

View File

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

View File

@ -87,3 +87,42 @@ export const useTags = (translateFromOut?: TFunction) => {
tagsMap, 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 { apiPrefix } from '@/config'
import { fetchWorkspaces } from '@/service/common' import { useSelector } from '@/context/app-context'
let tenantId: string | null | undefined = null
const useGetIcon = () => { const useGetIcon = () => {
const getIconUrl = async (fileName: string) => { const currentWorkspace = useSelector(s => s.currentWorkspace)
if (!tenantId) { const getIconUrl = useCallback((fileName: string) => {
const { workspaces } = await fetchWorkspaces({ return `${apiPrefix}/workspaces/current/plugin/icon?tenant_id=${currentWorkspace.id}&filename=${fileName}`
url: '/workspaces', }, [currentWorkspace.id])
params: {},
})
tenantId = workspaces.find(v => v.current)?.id
}
return `${apiPrefix}/workspaces/current/plugin/icon?tenant_id=${tenantId}&filename=${fileName}`
}
return { return {
getIconUrl, 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 Badge, { BadgeState } from '@/app/components/base/badge/index'
import { pluginManifestToCardPluginProps } from '../../utils' import { pluginManifestToCardPluginProps } from '../../utils'
import { useTranslation } from 'react-i18next' 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 { 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 checkTaskStatus from '../../base/check-task-status'
import { parseGitHubUrl } from '../../utils' import { parseGitHubUrl } from '../../utils'
@ -40,7 +41,8 @@ const Loaded: React.FC<LoadedProps> = ({
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [isInstalling, setIsInstalling] = React.useState(false) const [isInstalling, setIsInstalling] = React.useState(false)
const setPluginTasksWithPolling = usePluginTasksStore(s => s.setPluginTasksWithPolling) const { mutateAsync: installPackageFromGitHub } = useInstallPackageFromGitHub()
const { handleRefetch } = usePluginTaskList()
const { check } = checkTaskStatus() const { check } = checkTaskStatus()
const handleInstall = async () => { const handleInstall = async () => {
@ -49,28 +51,49 @@ const Loaded: React.FC<LoadedProps> = ({
try { try {
const { owner, repo } = parseGitHubUrl(repoUrl) const { owner, repo } = parseGitHubUrl(repoUrl)
const { all_installed: isInstalled, task_id: taskId } = await installPackageFromGitHub( if (updatePayload) {
`${owner}/${repo}`, const { all_installed: isInstalled, task_id: taskId } = await updateFromGitHub(
selectedVersion, `${owner}/${repo}`,
selectedPackage, selectedVersion,
uniqueIdentifier, selectedPackage,
) updatePayload.originalPackageInfo.id,
uniqueIdentifier,
)
if (updatePayload && isInstalled) if (isInstalled) {
await uninstallPlugin(updatePayload.originalPackageInfo.id) onInstalled()
return
}
handleRefetch()
await check({
taskId,
pluginUniqueIdentifier: uniqueIdentifier,
})
if (isInstalled) {
onInstalled() onInstalled()
return
} }
else {
const { all_installed: isInstalled, task_id: taskId } = await installPackageFromGitHub({
repoUrl: `${owner}/${repo}`,
selectedVersion,
selectedPackage,
uniqueIdentifier,
})
setPluginTasksWithPolling() if (isInstalled) {
await check({ onInstalled()
taskId, return
pluginUniqueIdentifier: uniqueIdentifier, }
})
onInstalled() handleRefetch()
await check({
taskId,
pluginUniqueIdentifier: uniqueIdentifier,
})
onInstalled()
}
} }
catch (e) { catch (e) {
if (typeof e === 'string') { if (typeof e === 'string') {

View File

@ -48,7 +48,6 @@ const SelectPackage: React.FC<SelectPackageProps> = ({
const handleUploadPackage = async () => { const handleUploadPackage = async () => {
if (isUploading) return if (isUploading) return
setIsUploading(true) setIsUploading(true)
try { try {
const repo = repoUrl.replace('https://github.com/', '') const repo = repoUrl.replace('https://github.com/', '')
await handleUpload(repo, selectedVersion, selectedPackage, (GitHubPackage) => { 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 { Trans, useTranslation } from 'react-i18next'
import { RiLoader2Line } from '@remixicon/react' import { RiLoader2Line } from '@remixicon/react'
import Badge, { BadgeState } from '@/app/components/base/badge/index' 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 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' const i18nPrefix = 'plugin.installModal'
@ -33,6 +33,8 @@ const Installed: FC<Props> = ({
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [isInstalling, setIsInstalling] = React.useState(false) const [isInstalling, setIsInstalling] = React.useState(false)
const { mutateAsync: installPackageFromLocal } = useInstallPackageFromLocal()
const { const {
check, check,
stop, stop,
@ -43,7 +45,7 @@ const Installed: FC<Props> = ({
onCancel() onCancel()
} }
const setPluginTasksWithPolling = usePluginTasksStore(s => s.setPluginTasksWithPolling) const { handleRefetch } = usePluginTaskList()
const handleInstall = async () => { const handleInstall = async () => {
if (isInstalling) return if (isInstalling) return
setIsInstalling(true) setIsInstalling(true)
@ -58,7 +60,7 @@ const Installed: FC<Props> = ({
onInstalled() onInstalled()
return return
} }
setPluginTasksWithPolling() handleRefetch()
await check({ await check({
taskId, taskId,
pluginUniqueIdentifier: uniqueIdentifier, pluginUniqueIdentifier: uniqueIdentifier,

View File

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

View File

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

View File

@ -11,6 +11,7 @@ const Description = async ({
}: DescriptionProps) => { }: DescriptionProps) => {
const localeDefault = getLocaleOnServer() const localeDefault = getLocaleOnServer()
const { t } = await translate(localeFromProps || localeDefault, 'plugin') const { t } = await translate(localeFromProps || localeDefault, 'plugin')
const { t: tCommon } = await translate(localeFromProps || localeDefault, 'common')
return ( return (
<> <>
@ -19,22 +20,23 @@ const Description = async ({
</h1> </h1>
<h2 className='shrink-0 flex justify-center items-center text-center body-md-regular text-text-tertiary'> <h2 className='shrink-0 flex justify-center items-center text-center body-md-regular text-text-tertiary'>
{t('marketplace.discover')} {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"> <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]">
{t('category.models')} <span className='relative z-[2] lowercase'>{t('category.models')}</span>
</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"> <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]">
{t('category.tools')} <span className='relative z-[2] lowercase'>{t('category.tools')}</span>
</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"> <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]">
{t('category.extensions')} <span className='relative z-[2] lowercase'>{t('category.extensions')}</span>
</span> </span>
{t('marketplace.and')} {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"> <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]">
{t('category.bundles')} <span className='relative z-[2] lowercase'>{t('category.bundles')}</span>
</span> </span>
{t('marketplace.inDifyMarketplace')} <span className='mr-1'>{tCommon('operation.in')}</span>
{t('marketplace.difyMarketplace')}
</h2> </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 { Group } from '@/app/components/base/icons/src/vender/other'
import Line from './line' import Line from './line'
import cn from '@/utils/classnames'
const Empty = () => { const Empty = () => {
const { t } = useTranslation()
return ( return (
<div <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) => ( Array.from({ length: 16 }).map((_, index) => (
<div <div
key={index} 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>
)) ))
} }
<div <div
className='absolute inset-0 z-[1]' className='absolute inset-0 bg-marketplace-plugin-empty z-[1]'
style={{
backgroundImage: 'linear-gradient(180deg, rgba(255,255,255,0.01), #FCFCFD)',
}}
></div> ></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='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'> <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' /> <Line className='absolute top-full left-1/2 -translate-x-1/2 -translate-y-1/2 rotate-90' />
</div> </div>
<div className='text-center system-md-regular text-text-tertiary'> <div className='text-center system-md-regular text-text-tertiary'>
No plugin found {t('plugin.marketplace.noPluginFound')}
</div> </div>
</div> </div>
</div> </div>

View File

@ -25,13 +25,61 @@ const CardWrapper = ({
setFalse: hideInstallFromMarketplace, setFalse: hideInstallFromMarketplace,
}] = useBoolean(false) }] = 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 ( return (
<div <a
className='group relative rounded-xl cursor-pointer' className='group inline-block relative rounded-xl cursor-pointer'
onClick={() => { href={`${MARKETPLACE_URL_PREFIX}/plugins/${plugin.org}/${plugin.name}`}
if (!showInstallButton)
window.open(`${MARKETPLACE_URL_PREFIX}/plugin/${plugin.org}/${plugin.name}`)
}}
> >
<Card <Card
key={plugin.name} key={plugin.name}
@ -65,17 +113,7 @@ const CardWrapper = ({
</div> </div>
) )
} }
{ </a>
isShowInstallFromMarketplace && (
<InstallFromMarketplace
manifest={plugin as any}
uniqueIdentifier={plugin.latest_package_identifier}
onClose={hideInstallFromMarketplace}
onSuccess={hideInstallFromMarketplace}
/>
)
}
</div>
) )
} }

View File

@ -1,9 +1,11 @@
'use client' 'use client'
import { useTranslation } from 'react-i18next'
import type { Plugin } from '../../types' import type { Plugin } from '../../types'
import type { MarketplaceCollection } from '../types' import type { MarketplaceCollection } from '../types'
import { useMarketplaceContext } from '../context' import { useMarketplaceContext } from '../context'
import List from './index' import List from './index'
import SortDropdown from '../sort-dropdown' import SortDropdown from '../sort-dropdown'
import Loading from '@/app/components/base/loading'
type ListWrapperProps = { type ListWrapperProps = {
marketplaceCollections: MarketplaceCollection[] marketplaceCollections: MarketplaceCollection[]
@ -17,28 +19,41 @@ const ListWrapper = ({
showInstallButton, showInstallButton,
locale, locale,
}: ListWrapperProps) => { }: ListWrapperProps) => {
const { t } = useTranslation()
const plugins = useMarketplaceContext(v => v.plugins) const plugins = useMarketplaceContext(v => v.plugins)
const marketplaceCollectionsFromClient = useMarketplaceContext(v => v.marketplaceCollectionsFromClient) const marketplaceCollectionsFromClient = useMarketplaceContext(v => v.marketplaceCollectionsFromClient)
const marketplaceCollectionPluginsMapFromClient = useMarketplaceContext(v => v.marketplaceCollectionPluginsMapFromClient) const marketplaceCollectionPluginsMapFromClient = useMarketplaceContext(v => v.marketplaceCollectionPluginsMapFromClient)
const isLoading = useMarketplaceContext(v => v.isLoading)
return ( 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 && ( plugins && (
<div className='flex items-center mb-4 pt-3'> <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> <div className='mx-3 w-[1px] h-3.5 bg-divider-regular'></div>
<SortDropdown /> <SortDropdown />
</div> </div>
) )
} }
<List {
marketplaceCollections={marketplaceCollectionsFromClient || marketplaceCollections} isLoading && (
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMapFromClient || marketplaceCollectionPluginsMap} <div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
plugins={plugins} <Loading />
showInstallButton={showInstallButton} </div>
locale={locale} )
/> }
{
!isLoading && (
<List
marketplaceCollections={marketplaceCollectionsFromClient || marketplaceCollections}
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMapFromClient || marketplaceCollectionPluginsMap}
plugins={plugins}
showInstallButton={showInstallButton}
locale={locale}
/>
)
}
</div> </div>
) )
} }

View File

@ -4,6 +4,7 @@ import {
RiArrowDownSLine, RiArrowDownSLine,
RiCheckLine, RiCheckLine,
} from '@remixicon/react' } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useMarketplaceContext } from '../context' import { useMarketplaceContext } from '../context'
import { import {
PortalToFollowElem, PortalToFollowElem,
@ -11,30 +12,30 @@ import {
PortalToFollowElemTrigger, PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem' } 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 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 sort = useMarketplaceContext(v => v.sort)
const handleSortChange = useMarketplaceContext(v => v.handleSortChange) const handleSortChange = useMarketplaceContext(v => v.handleSortChange)
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
@ -53,7 +54,7 @@ const SortDropdown = () => {
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}> <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'> <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'> <span className='mr-1 system-sm-regular'>
Sort by {t('plugin.marketplace.sortBy')}
</span> </span>
<span className='mr-1 system-sm-medium'> <span className='mr-1 system-sm-medium'>
{selectedOption.text} {selectedOption.text}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,6 +22,7 @@ import cn from '@/utils/classnames'
import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config' import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config'
import { useLanguage } from '../../header/account-setting/model-provider-page/hooks' import { useLanguage } from '../../header/account-setting/model-provider-page/hooks'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins' import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useCategories } from '../hooks'
type Props = { type Props = {
className?: string className?: string
@ -34,6 +35,7 @@ const PluginItem: FC<Props> = ({
}) => { }) => {
const locale = useLanguage() const locale = useLanguage()
const { t } = useTranslation() const { t } = useTranslation()
const { categoriesMap } = useCategories()
const currentPluginDetail = usePluginPageContext(v => v.currentPluginDetail) const currentPluginDetail = usePluginPageContext(v => v.currentPluginDetail)
const setCurrentPluginDetail = usePluginPageContext(v => v.setCurrentPluginDetail) const setCurrentPluginDetail = usePluginPageContext(v => v.setCurrentPluginDetail)
const invalidateInstalledPluginList = useInvalidateInstalledPluginList() const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
@ -42,10 +44,10 @@ const PluginItem: FC<Props> = ({
source, source,
tenant_id, tenant_id,
installation_id, installation_id,
plugin_unique_identifier,
endpoints_active, endpoints_active,
meta, meta,
plugin_id, plugin_id,
version,
} = plugin } = plugin
const { category, author, name, label, description, icon, verified } = plugin.declaration 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-[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', : '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)}> <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 */} {/* Header */}
<div className="flex"> <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'> <div className='flex items-center justify-center w-10 h-10 overflow-hidden border-components-panel-border-subtle border-[1px] rounded-xl'>
<img <img
className='w-full h-full' className='w-full h-full'
src={`${API_PREFIX}/workspaces/current/plugin/icon?tenant_id=${tenant_id}&filename=${icon}`} 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>
<div className="ml-3 w-0 grow"> <div className="ml-3 w-0 grow">
@ -84,10 +88,10 @@ const PluginItem: FC<Props> = ({
<Description text={description[locale]} descriptionLineRows={1}></Description> <Description text={description[locale]} descriptionLineRows={1}></Description>
<div onClick={e => e.stopPropagation()}> <div onClick={e => e.stopPropagation()}>
<Action <Action
pluginUniqueIdentifier={plugin_unique_identifier}
installationId={installation_id} installationId={installation_id}
author={author} author={author}
pluginName={name} pluginName={name}
version={version}
usedInApps={5} usedInApps={5}
isShowFetchNewVersion={source === PluginSource.github} isShowFetchNewVersion={source === PluginSource.github}
isShowInfo={source === PluginSource.github} isShowInfo={source === PluginSource.github}

View File

@ -68,7 +68,7 @@ export const PluginPageContextProvider = ({
{ value: 'plugins', text: t('common.menus.plugins') }, { value: 'plugins', text: t('common.menus.plugins') },
...( ...(
enable_marketplace 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 { t } = useTranslation()
const { data: info, isLoading } = useDebugKey() 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 if (isLoading) return null
return ( return (
@ -26,7 +29,7 @@ const DebugInfo: FC = () => {
popupContent={ popupContent={
<> <>
<div className='flex items-center gap-1 self-stretch'> <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'> <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> <span className='system-xs-medium'>{t(`${i18nPrefix}.viewDocs`)}</span>
<RiArrowRightUpLine className='w-3 h-3' /> <RiArrowRightUpLine className='w-3 h-3' />
@ -40,6 +43,7 @@ const DebugInfo: FC = () => {
<KeyValueItem <KeyValueItem
label={'Key'} label={'Key'}
value={info?.key || ''} value={info?.key || ''}
maskedValue={maskedKey}
/> />
</div> </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 { useSelector as useAppContextSelector } from '@/context/app-context'
import Line from '../../marketplace/empty/line' import Line from '../../marketplace/empty/line'
import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins' import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useTranslation } from 'react-i18next'
const Empty = () => { const Empty = () => {
const { t } = useTranslation()
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const [selectedAction, setSelectedAction] = useState<string | null>(null) const [selectedAction, setSelectedAction] = useState<string | null>(null)
const [selectedFile, setSelectedFile] = useState<File | null>(null) const [selectedFile, setSelectedFile] = useState<File | null>(null)
@ -30,9 +32,9 @@ const Empty = () => {
const text = useMemo(() => { const text = useMemo(() => {
if (pluginList?.plugins.length === 0) 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) if (filters.categories.length > 0 || filters.tags.length > 0 || filters.searchQuery)
return 'No plugins found' return t('plugin.list.notFound')
}, [pluginList, filters]) }, [pluginList, filters])
return ( return (
@ -70,11 +72,11 @@ const Empty = () => {
{[ {[
...( ...(
(enable_marketplace || true) (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: Github, text: t('plugin.list.source.github'), action: 'github' },
{ icon: FileZip, text: 'Local Package File', action: 'local' }, { icon: FileZip, text: t('plugin.list.source.local'), action: 'local' },
].map(({ icon: Icon, text, action }) => ( ].map(({ icon: Icon, text, action }) => (
<div <div
key={action} key={action}
@ -90,7 +92,7 @@ const Empty = () => {
}} }}
> >
<Icon className="w-4 h-4 text-text-tertiary" /> <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>
))} ))}
</div> </div>

View File

@ -13,6 +13,8 @@ import {
import Checkbox from '@/app/components/base/checkbox' import Checkbox from '@/app/components/base/checkbox'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import { useCategories } from '../../hooks'
import { useTranslation } from 'react-i18next'
type CategoriesFilterProps = { type CategoriesFilterProps = {
value: string[] value: string[]
@ -22,27 +24,11 @@ const CategoriesFilter = ({
value, value,
onChange, onChange,
}: CategoriesFilterProps) => { }: CategoriesFilterProps) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [searchText, setSearchText] = useState('') const [searchText, setSearchText] = useState('')
const options = [ const { categories: options, categoriesMap } = useCategories()
{ const filteredOptions = options.filter(option => option.name.toLowerCase().includes(searchText.toLowerCase()))
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 handleCheck = (id: string) => { const handleCheck = (id: string) => {
if (value.includes(id)) if (value.includes(id))
onChange(value.filter(tag => tag !== id)) onChange(value.filter(tag => tag !== id))
@ -70,10 +56,10 @@ const CategoriesFilter = ({
'flex items-center p-1 system-sm-medium', '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 && ( selectedTagsLength > 2 && (
@ -110,23 +96,23 @@ const CategoriesFilter = ({
showLeftIcon showLeftIcon
value={searchText} value={searchText}
onChange={e => setSearchText(e.target.value)} onChange={e => setSearchText(e.target.value)}
placeholder='Search categories' placeholder={t('plugin.searchCategories')}
/> />
</div> </div>
<div className='p-1 max-h-[448px] overflow-y-auto'> <div className='p-1 max-h-[448px] overflow-y-auto'>
{ {
filteredOptions.map(option => ( filteredOptions.map(option => (
<div <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' 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 <Checkbox
className='mr-1' className='mr-1'
checked={value.includes(option.value)} checked={value.includes(option.name)}
/> />
<div className='px-1 system-sm-medium text-text-secondary'> <div className='px-1 system-sm-medium text-text-secondary'>
{option.text} {option.label}
</div> </div>
</div> </div>
)) ))

View File

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

View File

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

View File

@ -16,8 +16,7 @@ import InstallPluginDropdown from './install-plugin-dropdown'
import { useUploader } from './use-uploader' import { useUploader } from './use-uploader'
import usePermission from './use-permission' import usePermission from './use-permission'
import DebugInfo from './debug-info' import DebugInfo from './debug-info'
import { usePluginTasksStore } from './store' import PluginTasks from './plugin-tasks'
import InstallInfo from './install-info'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import TabSlider from '@/app/components/base/tab-slider' import TabSlider from '@/app/components/base/tab-slider'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
@ -102,8 +101,6 @@ const PluginPage = ({
const options = usePluginPageContext(v => v.options) const options = usePluginPageContext(v => v.options)
const [activeTab, setActiveTab] = usePluginPageContext(v => [v.activeTab, v.setActiveTab]) const [activeTab, setActiveTab] = usePluginPageContext(v => [v.activeTab, v.setActiveTab])
const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures) 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({ const uploaderProps = useUploader({
onFileChange: setCurrentFile, onFileChange: setCurrentFile,
@ -113,12 +110,6 @@ const PluginPage = ({
const { dragging, fileUploader, fileChangeHandle, removeFile } = uploaderProps const { dragging, fileUploader, fileChangeHandle, removeFile } = uploaderProps
const setPluginTasksWithPolling = usePluginTasksStore(s => s.setPluginTasksWithPolling)
useEffect(() => {
setPluginTasksWithPolling()
}, [setPluginTasksWithPolling])
return ( return (
<div <div
id='marketplace-container' id='marketplace-container'
@ -141,8 +132,8 @@ const PluginPage = ({
options={options} options={options}
/> />
</div> </div>
<div className='flex flex-shrink-0 items-center gap-1'> <div className='flex shrink-0 items-center gap-1'>
<InstallInfo /> <PluginTasks />
{canManagement && ( {canManagement && (
<InstallPluginDropdown <InstallPluginDropdown
onSwitchToMarketplaceTab={() => setActiveTab('discover')} 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'}`}> <div className={`flex py-4 justify-center items-center gap-2 ${dragging ? 'text-text-accent' : 'text-text-quaternary'}`}>
<RiDragDropLine className="w-4 h-4" /> <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> </div>
{currentFile && ( {currentFile && (
<InstallFromLocalPackage <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' } from '@/app/components/base/portal-to-follow-elem'
import { useSelector as useAppContextSelector } from '@/context/app-context' import { useSelector as useAppContextSelector } from '@/context/app-context'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins' import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useTranslation } from 'react-i18next'
type Props = { type Props = {
onSwitchToMarketplaceTab: () => void onSwitchToMarketplaceTab: () => void
@ -23,6 +24,7 @@ type Props = {
const InstallPluginDropdown = ({ const InstallPluginDropdown = ({
onSwitchToMarketplaceTab, onSwitchToMarketplaceTab,
}: Props) => { }: Props) => {
const { t } = useTranslation()
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const [isMenuOpen, setIsMenuOpen] = useState(false) const [isMenuOpen, setIsMenuOpen] = useState(false)
const [selectedAction, setSelectedAction] = useState<string | null>(null) 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')} className={cn('w-full h-full p-2 text-components-button-secondary-text', isMenuOpen && 'bg-state-base-hover')}
> >
<RiAddLine className='w-4 h-4' /> <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' /> <RiArrowDownSLine className='w-4 h-4 ml-1' />
</Button> </Button>
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1002]'> <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'> <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'> <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> </span>
<input <input
type='file' type='file'
@ -85,11 +87,11 @@ const InstallPluginDropdown = ({
{[ {[
...( ...(
(enable_marketplace || true) (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: Github, text: t('plugin.source.github'), action: 'github' },
{ icon: FileZip, text: 'Local Package File', action: 'local' }, { icon: FileZip, text: t('plugin.source.local'), action: 'local' },
].map(({ icon: Icon, text, action }) => ( ].map(({ icon: Icon, text, action }) => (
<div <div
key={action} 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 endpoints_active: number
version: string version: string
latest_version: string latest_version: string
latest_unique_identifier: string
source: PluginSource source: PluginSource
meta?: MetaData meta?: MetaData
} }
@ -194,19 +195,10 @@ export type GitHubUrlInfo = {
} }
// endpoint // endpoint
export type CreateEndpointRequest = {
plugin_unique_identifier: string
settings: Record<string, any>
name: string
}
export type EndpointOperationResponse = { export type EndpointOperationResponse = {
result: 'success' | 'error' result: 'success' | 'error'
} }
export type EndpointsRequest = {
page_size: number
page: number
plugin_id: string
}
export type EndpointsResponse = { export type EndpointsResponse = {
endpoints: EndpointListItem[] endpoints: EndpointListItem[]
has_more: boolean has_more: boolean
@ -246,6 +238,11 @@ export type InstallPackageResponse = {
task_id: string task_id: string
} }
export type updatePackageResponse = {
all_installed: boolean
task_id: string
}
export type uploadGitHubResponse = { export type uploadGitHubResponse = {
unique_identifier: string unique_identifier: string
manifest: PluginDeclaration manifest: PluginDeclaration
@ -268,6 +265,9 @@ export type PluginStatus = {
plugin_id: string plugin_id: string
status: TaskStatus status: TaskStatus
message: string message: string
icon: string
labels: Record<Locale, string>
taskId: string
} }
export type PluginTask = { export type PluginTask = {
@ -305,3 +305,15 @@ export type UninstallPluginResponse = {
export type PluginsFromMarketplaceResponse = { export type PluginsFromMarketplaceResponse = {
plugins: Plugin[] 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 useGetIcon from '../install-plugin/base/use-get-icon'
import { updateFromMarketPlace } from '@/service/plugins' import { updateFromMarketPlace } from '@/service/plugins'
import checkTaskStatus from '@/app/components/plugins/install-plugin/base/check-task-status' 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' const i18nPrefix = 'plugin.upgrade'
@ -56,7 +56,7 @@ const UpdatePluginModal: FC<Props> = ({
} }
const [uploadStep, setUploadStep] = useState<UploadStep>(UploadStep.notStarted) const [uploadStep, setUploadStep] = useState<UploadStep>(UploadStep.notStarted)
const setPluginTasksWithPolling = usePluginTasksStore(s => s.setPluginTasksWithPolling) const { handleRefetch } = usePluginTaskList()
const configBtnText = useMemo(() => { const configBtnText = useMemo(() => {
return ({ return ({
@ -69,29 +69,36 @@ const UpdatePluginModal: FC<Props> = ({
const handleConfirm = useCallback(async () => { const handleConfirm = useCallback(async () => {
if (uploadStep === UploadStep.notStarted) { if (uploadStep === UploadStep.notStarted) {
setUploadStep(UploadStep.upgrading) setUploadStep(UploadStep.upgrading)
const { try {
all_installed: isInstalled, const {
task_id: taskId, all_installed: isInstalled,
} = await updateFromMarketPlace({ task_id: taskId,
original_plugin_unique_identifier: originalPackageInfo.id, } = await updateFromMarketPlace({
new_plugin_unique_identifier: targetPackageInfo.id, original_plugin_unique_identifier: originalPackageInfo.id,
}) new_plugin_unique_identifier: targetPackageInfo.id,
if (isInstalled) { })
if (isInstalled) {
onSave()
return
}
handleRefetch()
await check({
taskId,
pluginUniqueIdentifier: targetPackageInfo.id,
})
onSave() onSave()
return
} }
setPluginTasksWithPolling() catch (e) {
await check({ setUploadStep(UploadStep.notStarted)
taskId, }
pluginUniqueIdentifier: targetPackageInfo.id, return
})
onSave()
} }
if (uploadStep === UploadStep.installed) { if (uploadStep === UploadStep.installed) {
onSave() onSave()
onCancel() onCancel()
} }
}, [onCancel, onSave, uploadStep, check, originalPackageInfo.id, setPluginTasksWithPolling, targetPackageInfo.id]) }, [onCancel, onSave, uploadStep, check, originalPackageInfo.id, handleRefetch, targetPackageInfo.id])
const usedInAppInfo = useMemo(() => { const usedInAppInfo = useMemo(() => {
return ( return (
<div className='flex px-0.5 justify-center items-center gap-0.5'> <div className='flex px-0.5 justify-center items-center gap-0.5'>

View File

@ -5,6 +5,7 @@ import {
useMarketplaceCollectionsAndPlugins, useMarketplaceCollectionsAndPlugins,
useMarketplacePlugins, useMarketplacePlugins,
} from '@/app/components/plugins/marketplace/hooks' } from '@/app/components/plugins/marketplace/hooks'
import { PluginType } from '@/app/components/plugins/types'
export const useMarketplace = (searchPluginText: string, filterPluginTags: string[]) => { export const useMarketplace = (searchPluginText: string, filterPluginTags: string[]) => {
const { const {
@ -25,18 +26,20 @@ export const useMarketplace = (searchPluginText: string, filterPluginTags: strin
if (searchPluginText || filterPluginTags.length) { if (searchPluginText || filterPluginTags.length) {
if (searchPluginText) { if (searchPluginText) {
queryPluginsWithDebounced({ queryPluginsWithDebounced({
category: PluginType.tool,
query: searchPluginText, query: searchPluginText,
tags: filterPluginTags, tags: filterPluginTags,
}) })
return return
} }
queryPlugins({ queryPlugins({
category: PluginType.tool,
query: searchPluginText, query: searchPluginText,
tags: filterPluginTags, tags: filterPluginTags,
}) })
} }
else { else {
queryMarketplaceCollectionsAndPlugins() queryMarketplaceCollectionsAndPlugins({ category: PluginType.tool })
resetPlugins() resetPlugins()
} }
}, [searchPluginText, filterPluginTags, queryPlugins, queryMarketplaceCollectionsAndPlugins, queryPluginsWithDebounced, 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 { useTranslation } from 'react-i18next'
import { useMarketplace } from './hooks' import { useMarketplace } from './hooks'
import List from '@/app/components/plugins/marketplace/list' import List from '@/app/components/plugins/marketplace/list'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import { getLocaleOnClient } from '@/i18n' import { getLocaleOnClient } from '@/i18n'
import { MARKETPLACE_URL_PREFIX } from '@/config'
type MarketplaceProps = { type MarketplaceProps = {
searchPluginText: string searchPluginText: string
@ -25,7 +29,7 @@ const Marketplace = ({
} = useMarketplace(searchPluginText, filterPluginTags) } = useMarketplace(searchPluginText, filterPluginTags)
return ( 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 <RiArrowUpDoubleLine
className='absolute top-2 left-1/2 -translate-x-1/2 w-4 h-4 text-text-quaternary cursor-pointer' className='absolute top-2 left-1/2 -translate-x-1/2 w-4 h-4 text-text-quaternary cursor-pointer'
onClick={() => onMarketplaceScroll()} 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"> <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')} {t('plugin.category.bundles')}
</span> </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>
</div> </div>
{ {

View File

@ -10,10 +10,10 @@ import LabelFilter from '@/app/components/tools/labels/filter'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import ProviderDetail from '@/app/components/tools/provider/detail' import ProviderDetail from '@/app/components/tools/provider/detail'
import Empty from '@/app/components/tools/add-tool-modal/empty' import Empty from '@/app/components/tools/add-tool-modal/empty'
import { fetchCollectionList } from '@/service/tools'
import Card from '@/app/components/plugins/card' import Card from '@/app/components/plugins/card'
import CardMoreInfo from '@/app/components/plugins/card/card-more-info' import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
import { useSelector as useAppContextSelector } from '@/context/app-context' import { useSelector as useAppContextSelector } from '@/context/app-context'
import { useAllToolProviders } from '@/service/use-tools'
const ProviderList = () => { const ProviderList = () => {
const { t } = useTranslation() const { t } = useTranslation()
@ -36,8 +36,7 @@ const ProviderList = () => {
const handleKeywordsChange = (value: string) => { const handleKeywordsChange = (value: string) => {
setKeywords(value) setKeywords(value)
} }
const { data: collectionList, refetch } = useAllToolProviders()
const [collectionList, setCollectionList] = useState<Collection[]>([])
const filteredCollectionList = useMemo(() => { const filteredCollectionList = useMemo(() => {
return collectionList.filter((collection) => { return collectionList.filter((collection) => {
if (collection.type !== activeTab) if (collection.type !== activeTab)
@ -49,13 +48,6 @@ const ProviderList = () => {
return true return true
}) })
}, [activeTab, tagFilterValue, keywords, collectionList]) }, [activeTab, tagFilterValue, keywords, collectionList])
const getProviderList = async () => {
const list = await fetchCollectionList()
setCollectionList([...list])
}
useEffect(() => {
getProviderList()
}, [])
const [currentProvider, setCurrentProvider] = useState<Collection | undefined>() const [currentProvider, setCurrentProvider] = useState<Collection | undefined>()
useEffect(() => { useEffect(() => {
@ -106,7 +98,7 @@ const ProviderList = () => {
> >
<Card <Card
className={cn( 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', currentProvider?.id === collection.id && 'border-components-option-card-option-selected-border',
)} )}
hideCornerMark hideCornerMark
@ -140,7 +132,7 @@ const ProviderList = () => {
<ProviderDetail <ProviderDetail
collection={currentProvider} collection={currentProvider}
onHide={() => setCurrentProvider(undefined)} onHide={() => setCurrentProvider(undefined)}
onRefreshData={getProviderList} onRefreshData={refetch}
/> />
)} )}
</div> </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 { ConfigurationMethodEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { useInvalidateAllWorkflowTools } from '@/service/use-tools'
type Props = { type Props = {
collection: Collection collection: Collection
@ -65,7 +66,7 @@ const ProviderDetail = ({
const isBuiltIn = collection.type === CollectionType.builtIn const isBuiltIn = collection.type === CollectionType.builtIn
const isModel = collection.type === CollectionType.model const isModel = collection.type === CollectionType.model
const { isCurrentWorkspaceManager } = useAppContext() const { isCurrentWorkspaceManager } = useAppContext()
const invalidateAllWorkflowTools = useInvalidateAllWorkflowTools()
const [isDetailLoading, setIsDetailLoading] = useState(false) const [isDetailLoading, setIsDetailLoading] = useState(false)
// built in provider // built in provider
@ -164,6 +165,7 @@ const ProviderDetail = ({
workflow_tool_id: string workflow_tool_id: string
}>) => { }>) => {
await saveWorkflowToolProvider(data) await saveWorkflowToolProvider(data)
invalidateAllWorkflowTools()
onRefreshData() onRefreshData()
getWorkflowToolProvider() getWorkflowToolProvider()
Toast.notify({ 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 { Emoji, WorkflowToolProviderParameter, WorkflowToolProviderRequest, WorkflowToolProviderResponse } from '@/app/components/tools/types'
import type { InputVar } from '@/app/components/workflow/types' import type { InputVar } from '@/app/components/workflow/types'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { useInvalidateAllWorkflowTools } from '@/service/use-tools'
type Props = { type Props = {
disabled: boolean disabled: boolean
@ -46,6 +47,7 @@ const WorkflowToolConfigureButton = ({
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [detail, setDetail] = useState<WorkflowToolProviderResponse>() const [detail, setDetail] = useState<WorkflowToolProviderResponse>()
const { isCurrentWorkspaceManager } = useAppContext() const { isCurrentWorkspaceManager } = useAppContext()
const invalidateAllWorkflowTools = useInvalidateAllWorkflowTools()
const outdated = useMemo(() => { const outdated = useMemo(() => {
if (!detail) if (!detail)
@ -135,6 +137,7 @@ const WorkflowToolConfigureButton = ({
const createHandle = async (data: WorkflowToolProviderRequest & { workflow_app_id: string }) => { const createHandle = async (data: WorkflowToolProviderRequest & { workflow_app_id: string }) => {
try { try {
await createWorkflowToolProvider(data) await createWorkflowToolProvider(data)
invalidateAllWorkflowTools()
onRefreshData?.() onRefreshData?.()
getDetail(workflowAppId) getDetail(workflowAppId)
Toast.notify({ Toast.notify({
@ -156,6 +159,7 @@ const WorkflowToolConfigureButton = ({
await handlePublish() await handlePublish()
await saveWorkflowToolProvider(data) await saveWorkflowToolProvider(data)
onRefreshData?.() onRefreshData?.()
invalidateAllWorkflowTools()
getDetail(workflowAppId) getDetail(workflowAppId)
Toast.notify({ Toast.notify({
type: 'success', type: 'success',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,17 +10,19 @@ import type { ToolDefaultValue } from '../../types'
type Props = { type Props = {
groupName: string groupName: string
toolList: ToolWithProvider[] toolList: ToolWithProvider[]
hasSearchText: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
} }
const Item: FC<Props> = ({ const Item: FC<Props> = ({
groupName, groupName,
toolList, toolList,
hasSearchText,
onSelect, onSelect,
}) => { }) => {
return ( return (
<div> <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} {groupName}
</div> </div>
<div> <div>
@ -29,7 +31,8 @@ const Item: FC<Props> = ({
key={tool.id} key={tool.id}
payload={tool} payload={tool}
viewType={ViewType.tree} viewType={ViewType.tree}
isShowLetterIndex isShowLetterIndex={false}
hasSearchText={hasSearchText}
onSelect={onSelect} onSelect={onSelect}
/> />
))} ))}

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