mirror of
https://github.com/langgenius/dify.git
synced 2026-02-22 19:15:47 +08:00
feat: support picker vars files ui in editor
This commit is contained in:
@ -29,6 +29,8 @@ import {
|
||||
} from 'lexical'
|
||||
import * as React from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { FileReferenceNode } from '@/app/components/workflow/skill/editor/skill-editor/plugins/file-reference-block/node'
|
||||
import FileReferenceReplacementBlock from '@/app/components/workflow/skill/editor/skill-editor/plugins/file-reference-block/replacement-block'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import {
|
||||
@ -41,13 +43,13 @@ import {
|
||||
ContextBlockNode,
|
||||
ContextBlockReplacementBlock,
|
||||
} from './plugins/context-block'
|
||||
|
||||
import {
|
||||
CurrentBlock,
|
||||
CurrentBlockNode,
|
||||
CurrentBlockReplacementBlock,
|
||||
} from './plugins/current-block'
|
||||
import { CustomTextNode } from './plugins/custom-text/node'
|
||||
|
||||
import {
|
||||
ErrorMessageBlock,
|
||||
ErrorMessageBlockNode,
|
||||
@ -106,6 +108,7 @@ export type PromptEditorProps = {
|
||||
lastRunBlock?: LastRunBlockType
|
||||
agentBlock?: AgentBlockType
|
||||
isSupportFileVar?: boolean
|
||||
isSupportSandbox?: boolean
|
||||
}
|
||||
|
||||
const PromptEditor: FC<PromptEditorProps> = ({
|
||||
@ -132,6 +135,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
lastRunBlock,
|
||||
agentBlock,
|
||||
isSupportFileVar,
|
||||
isSupportSandbox,
|
||||
}) => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const initialConfig = {
|
||||
@ -152,6 +156,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
CurrentBlockNode,
|
||||
ErrorMessageBlockNode,
|
||||
LastRunBlockNode, // LastRunBlockNode is used for error message block replacement
|
||||
...(isSupportSandbox ? [FileReferenceNode] : []),
|
||||
],
|
||||
editorState: textToEditorState(value || ''),
|
||||
onError: (error: Error) => {
|
||||
@ -215,6 +220,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
errorMessageBlock={errorMessageBlock}
|
||||
lastRunBlock={lastRunBlock}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
isSupportSandbox={isSupportSandbox}
|
||||
/>
|
||||
{(!agentBlock || agentBlock.show) && (
|
||||
<ComponentPickerBlock
|
||||
@ -244,6 +250,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
errorMessageBlock={errorMessageBlock}
|
||||
lastRunBlock={lastRunBlock}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
isSupportSandbox={isSupportSandbox}
|
||||
/>
|
||||
{
|
||||
contextBlock?.show && (
|
||||
@ -285,6 +292,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
||||
</>
|
||||
)
|
||||
}
|
||||
{isSupportSandbox && <FileReferenceReplacementBlock />}
|
||||
{
|
||||
currentBlock?.show && (
|
||||
<>
|
||||
|
||||
@ -38,9 +38,13 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { GeneratorType } from '@/app/components/app/configuration/config/automatic/types'
|
||||
import { SegmentedControl } from '@/app/components/base/segmented-control'
|
||||
import AgentNodeList from '@/app/components/workflow/nodes/_base/components/agent-node-list'
|
||||
import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars'
|
||||
import { FilePickerPanel } from '@/app/components/workflow/skill/editor/skill-editor/plugins/file-picker-panel'
|
||||
import { $createFileReferenceNode } from '@/app/components/workflow/skill/editor/skill-editor/plugins/file-reference-block/node'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { useBasicTypeaheadTriggerMatch } from '../../hooks'
|
||||
@ -66,6 +70,7 @@ type ComponentPickerProps = {
|
||||
lastRunBlock?: LastRunBlockType
|
||||
agentBlock?: AgentBlockType
|
||||
isSupportFileVar?: boolean
|
||||
isSupportSandbox?: boolean
|
||||
}
|
||||
const ComponentPicker = ({
|
||||
triggerString,
|
||||
@ -80,7 +85,9 @@ const ComponentPicker = ({
|
||||
lastRunBlock,
|
||||
agentBlock,
|
||||
isSupportFileVar,
|
||||
isSupportSandbox,
|
||||
}: ComponentPickerProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const { refs, floatingStyles, isPositioned } = useFloating({
|
||||
placement: 'bottom-start',
|
||||
@ -114,6 +121,7 @@ const ComponentPicker = ({
|
||||
}, [checkForTriggerMatch, editor])
|
||||
|
||||
const [queryString, setQueryString] = useState<string | null>(null)
|
||||
const [activeTab, setActiveTab] = useState<'variables' | 'files'>('variables')
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === INSERT_VARIABLE_VALUE_BLOCK_COMMAND)
|
||||
@ -153,6 +161,17 @@ const ComponentPicker = ({
|
||||
[editor],
|
||||
)
|
||||
|
||||
const handleSelectFileReference = useCallback((resourceId: string) => {
|
||||
editor.update(() => {
|
||||
const match = checkForTriggerMatch(triggerString, editor)
|
||||
const nodeToRemove = match ? $splitNodeContainingQuery(match) : null
|
||||
if (nodeToRemove)
|
||||
nodeToRemove.remove()
|
||||
|
||||
$insertNodes([$createFileReferenceNode({ resourceId })])
|
||||
})
|
||||
}, [checkForTriggerMatch, editor, triggerString])
|
||||
|
||||
const handleSelectWorkflowVariable = useCallback((variables: string[]) => {
|
||||
editor.update(() => {
|
||||
const match = getMatchFromSelection()
|
||||
@ -227,6 +246,10 @@ const ComponentPicker = ({
|
||||
const isAgentTrigger = triggerString === '@' && agentBlock?.show
|
||||
const showAssembleVariables = triggerString === '/'
|
||||
const agentNodes: AgentNode[] = useMemo(() => agentBlock?.agentNodes || [], [agentBlock?.agentNodes])
|
||||
const handleOpen = useCallback(() => {
|
||||
if (isSupportSandbox && triggerString === '/')
|
||||
setActiveTab('variables')
|
||||
}, [isSupportSandbox, triggerString])
|
||||
|
||||
const renderMenu = useCallback<MenuRenderFn<PickerBlockMenuOption>>((
|
||||
anchorElementRef,
|
||||
@ -240,12 +263,87 @@ const ComponentPicker = ({
|
||||
if (!(anchorElementRef.current && (allFlattenOptions.length || workflowVariableBlock?.show)))
|
||||
return null
|
||||
}
|
||||
const isSandboxMenu = isSupportSandbox && triggerString === '/'
|
||||
|
||||
if (!(anchorElementRef.current && (isSandboxMenu || allFlattenOptions.length || workflowVariableBlock?.show)))
|
||||
return null
|
||||
|
||||
setTimeout(() => {
|
||||
if (anchorElementRef.current)
|
||||
refs.setReference(anchorElementRef.current)
|
||||
}, 0)
|
||||
|
||||
if (isSandboxMenu) {
|
||||
return (
|
||||
<>
|
||||
{ReactDOM.createPortal(
|
||||
<div className="h-0 w-0">
|
||||
<div
|
||||
className="w-[300px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-2 shadow-lg"
|
||||
style={{
|
||||
...floatingStyles,
|
||||
visibility: isPositioned ? 'visible' : 'hidden',
|
||||
}}
|
||||
ref={refs.setFloating}
|
||||
>
|
||||
<SegmentedControl
|
||||
size="small"
|
||||
padding="with"
|
||||
activeState="accent"
|
||||
className="w-full"
|
||||
btnClassName="flex-1"
|
||||
options={[
|
||||
{
|
||||
value: 'variables',
|
||||
text: t('promptEditor.variable.outputToolDisabledItem.title', { ns: 'common' }),
|
||||
},
|
||||
{
|
||||
value: 'files',
|
||||
text: t('nodes.llm.files', { ns: 'workflow' }),
|
||||
},
|
||||
]}
|
||||
value={activeTab}
|
||||
onChange={setActiveTab}
|
||||
/>
|
||||
<div className="mt-2">
|
||||
{activeTab === 'variables' && (
|
||||
<VarReferenceVars
|
||||
searchBoxClassName="mt-1"
|
||||
vars={workflowVariableOptions}
|
||||
onChange={(variables: string[]) => {
|
||||
handleSelectWorkflowVariable(variables)
|
||||
handleClose()
|
||||
}}
|
||||
maxHeightClass="max-h-[34vh]"
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
onClose={handleClose}
|
||||
onBlur={handleClose}
|
||||
showManageInputField={workflowVariableBlock?.showManageInputField}
|
||||
onManageInputField={workflowVariableBlock?.onManageInputField}
|
||||
autoFocus={false}
|
||||
isInCodeGeneratorInstructionEditor={currentBlock?.generatorType === GeneratorType.code}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'files' && (
|
||||
<FilePickerPanel
|
||||
onSelectNode={(node) => {
|
||||
handleSelectFileReference(node.id)
|
||||
handleClose()
|
||||
}}
|
||||
className="w-full border-0 bg-transparent p-0 shadow-none"
|
||||
contentClassName="px-0"
|
||||
showHeader={false}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
anchorElementRef.current,
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
@ -261,79 +359,79 @@ const ComponentPicker = ({
|
||||
>
|
||||
{isAgentTrigger
|
||||
? (
|
||||
<AgentNodeList
|
||||
nodes={agentNodes.map(node => ({
|
||||
...node,
|
||||
type: BlockEnum.Agent || BlockEnum.LLM,
|
||||
}))}
|
||||
onSelect={handleSelectAgent}
|
||||
onClose={handleClose}
|
||||
onBlur={handleClose}
|
||||
maxHeightClass="max-h-[34vh]"
|
||||
autoFocus={false}
|
||||
hideSearch={useExternalSearch}
|
||||
externalSearchText={useExternalSearch ? (queryString ?? '') : undefined}
|
||||
enableKeyboardNavigation={useExternalSearch}
|
||||
/>
|
||||
)
|
||||
<AgentNodeList
|
||||
nodes={agentNodes.map(node => ({
|
||||
...node,
|
||||
type: BlockEnum.Agent || BlockEnum.LLM,
|
||||
}))}
|
||||
onSelect={handleSelectAgent}
|
||||
onClose={handleClose}
|
||||
onBlur={handleClose}
|
||||
maxHeightClass="max-h-[34vh]"
|
||||
autoFocus={false}
|
||||
hideSearch={useExternalSearch}
|
||||
externalSearchText={useExternalSearch ? (queryString ?? '') : undefined}
|
||||
enableKeyboardNavigation={useExternalSearch}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<>
|
||||
{
|
||||
workflowVariableBlock?.show && (
|
||||
<div className="p-1">
|
||||
<VarReferenceVars
|
||||
searchBoxClassName="mt-1"
|
||||
vars={workflowVariableOptions}
|
||||
onChange={(variables: string[]) => {
|
||||
handleSelectWorkflowVariable(variables)
|
||||
}}
|
||||
maxHeightClass="max-h-[34vh]"
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
onClose={handleClose}
|
||||
onBlur={handleClose}
|
||||
showManageInputField={workflowVariableBlock.showManageInputField}
|
||||
onManageInputField={workflowVariableBlock.onManageInputField}
|
||||
showAssembleVariables={showAssembleVariables}
|
||||
onAssembleVariables={showAssembleVariables ? handleSelectAssembleVariables : undefined}
|
||||
autoFocus={false}
|
||||
isInCodeGeneratorInstructionEditor={currentBlock?.generatorType === GeneratorType.code}
|
||||
hideSearch={useExternalSearch}
|
||||
externalSearchText={useExternalSearch ? (queryString ?? '') : undefined}
|
||||
enableKeyboardNavigation={useExternalSearch}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
workflowVariableBlock?.show && !!options.length && (
|
||||
<div className="my-1 h-px w-full -translate-x-1 bg-divider-subtle"></div>
|
||||
)
|
||||
}
|
||||
<div>
|
||||
{
|
||||
workflowVariableBlock?.show && (
|
||||
<div className="p-1">
|
||||
<VarReferenceVars
|
||||
searchBoxClassName="mt-1"
|
||||
vars={workflowVariableOptions}
|
||||
onChange={(variables: string[]) => {
|
||||
handleSelectWorkflowVariable(variables)
|
||||
}}
|
||||
maxHeightClass="max-h-[34vh]"
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
onClose={handleClose}
|
||||
onBlur={handleClose}
|
||||
showManageInputField={workflowVariableBlock.showManageInputField}
|
||||
onManageInputField={workflowVariableBlock.onManageInputField}
|
||||
showAssembleVariables={showAssembleVariables}
|
||||
onAssembleVariables={showAssembleVariables ? handleSelectAssembleVariables : undefined}
|
||||
autoFocus={false}
|
||||
isInCodeGeneratorInstructionEditor={currentBlock?.generatorType === GeneratorType.code}
|
||||
hideSearch={useExternalSearch}
|
||||
externalSearchText={useExternalSearch ? (queryString ?? '') : undefined}
|
||||
enableKeyboardNavigation={useExternalSearch}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
options.map((option, index) => (
|
||||
<Fragment key={option.key}>
|
||||
{
|
||||
index !== 0 && options.at(index - 1)?.group !== option.group && (
|
||||
<div className="my-1 h-px w-full -translate-x-1 bg-divider-subtle"></div>
|
||||
)
|
||||
}
|
||||
{option.renderMenuOption({
|
||||
queryString,
|
||||
isSelected: selectedIndex === index,
|
||||
onSelect: () => {
|
||||
selectOptionAndCleanUp(option)
|
||||
},
|
||||
onSetHighlight: () => {
|
||||
setHighlightedIndex(index)
|
||||
},
|
||||
})}
|
||||
</Fragment>
|
||||
))
|
||||
}
|
||||
{
|
||||
workflowVariableBlock?.show && !!options.length && (
|
||||
<div className="my-1 h-px w-full -translate-x-1 bg-divider-subtle"></div>
|
||||
)
|
||||
}
|
||||
<div>
|
||||
{
|
||||
options.map((option, index) => (
|
||||
<Fragment key={option.key}>
|
||||
{
|
||||
index !== 0 && options.at(index - 1)?.group !== option.group && (
|
||||
<div className="my-1 h-px w-full -translate-x-1 bg-divider-subtle"></div>
|
||||
)
|
||||
}
|
||||
{option.renderMenuOption({
|
||||
queryString,
|
||||
isSelected: selectedIndex === index,
|
||||
onSelect: () => {
|
||||
selectOptionAndCleanUp(option)
|
||||
},
|
||||
onSetHighlight: () => {
|
||||
setHighlightedIndex(index)
|
||||
},
|
||||
})}
|
||||
</Fragment>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>,
|
||||
anchorElementRef.current,
|
||||
@ -341,13 +439,14 @@ const ComponentPicker = ({
|
||||
}
|
||||
</>
|
||||
)
|
||||
}, [isAgentTrigger, agentNodes, allFlattenOptions.length, workflowVariableBlock?.show, floatingStyles, isPositioned, refs, handleSelectAgent, handleClose, workflowVariableOptions, isSupportFileVar, currentBlock?.generatorType, handleSelectWorkflowVariable, queryString, workflowVariableBlock?.showManageInputField, workflowVariableBlock?.onManageInputField, showAssembleVariables, handleSelectAssembleVariables, useExternalSearch])
|
||||
}, [isAgentTrigger, isSupportSandbox, triggerString, allFlattenOptions.length, workflowVariableBlock?.show, workflowVariableBlock.showManageInputField, workflowVariableBlock.onManageInputField, floatingStyles, isPositioned, refs, agentNodes, handleSelectAgent, handleClose, useExternalSearch, queryString, workflowVariableOptions, isSupportFileVar, showAssembleVariables, handleSelectAssembleVariables, currentBlock?.generatorType, t, activeTab, handleSelectWorkflowVariable, handleSelectFileReference])
|
||||
|
||||
return (
|
||||
<LexicalTypeaheadMenuPlugin
|
||||
options={allFlattenOptions}
|
||||
options={(isSupportSandbox && triggerString === '/') ? [] : allFlattenOptions}
|
||||
onQueryChange={setQueryString}
|
||||
onSelectOption={onSelectOption}
|
||||
onOpen={handleOpen}
|
||||
// The `translate` class is used to workaround the issue that the `typeahead-menu` menu is not positioned as expected.
|
||||
// See also https://github.com/facebook/lexical/blob/772520509308e8ba7e4a82b6cd1996a78b3298d0/packages/lexical-react/src/shared/LexicalMenu.ts#L498
|
||||
//
|
||||
|
||||
@ -62,6 +62,7 @@ type Props = {
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
isSupportFileVar?: boolean
|
||||
isSupportSandbox?: boolean
|
||||
isSupportPromptGenerator?: boolean
|
||||
onGenerated?: (prompt: string) => void
|
||||
modelConfig?: ModelConfig
|
||||
@ -102,6 +103,7 @@ const Editor: FC<Props> = ({
|
||||
nodesOutputVars,
|
||||
availableNodes = [],
|
||||
isSupportFileVar,
|
||||
isSupportSandbox,
|
||||
isSupportPromptGenerator,
|
||||
isSupportJinja,
|
||||
editionType,
|
||||
@ -295,6 +297,7 @@ const Editor: FC<Props> = ({
|
||||
onFocus={setFocus}
|
||||
editable={!readOnly}
|
||||
isSupportFileVar={isSupportFileVar}
|
||||
isSupportSandbox={isSupportSandbox}
|
||||
/>
|
||||
{/* to patch Editor not support dynamic change editable status */}
|
||||
{readOnly && <div className="absolute inset-0 z-10"></div>}
|
||||
|
||||
@ -40,6 +40,7 @@ type Props = {
|
||||
varList: Variable[]
|
||||
handleAddVariable: (payload: any) => void
|
||||
modelConfig?: ModelConfig
|
||||
isSupportSandbox?: boolean
|
||||
}
|
||||
|
||||
const roleOptions = [
|
||||
@ -82,6 +83,7 @@ const ConfigPromptItem: FC<Props> = ({
|
||||
varList,
|
||||
handleAddVariable,
|
||||
modelConfig,
|
||||
isSupportSandbox,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const workflowStore = useWorkflowStore()
|
||||
@ -149,6 +151,7 @@ const ConfigPromptItem: FC<Props> = ({
|
||||
varList={varList}
|
||||
handleAddVariable={handleAddVariable}
|
||||
isSupportFileVar
|
||||
isSupportSandbox={isSupportSandbox}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ReactSortable } from 'react-sortablejs'
|
||||
import { v4 as uuid4 } from 'uuid'
|
||||
import { useFeatures } from '@/app/components/base/features/hooks'
|
||||
import { DragHandle } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
@ -59,6 +60,8 @@ const ConfigPrompt: FC<Props> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const features = useFeatures(s => s.features)
|
||||
const isSupportSandbox = !!features.sandbox?.enabled
|
||||
const {
|
||||
setControlPromptEditorRerenderKey,
|
||||
} = workflowStore.getState()
|
||||
@ -337,6 +340,7 @@ const ConfigPrompt: FC<Props> = ({
|
||||
varList={varList}
|
||||
handleAddVariable={handleAddVariable}
|
||||
modelConfig={modelConfig}
|
||||
isSupportSandbox={isSupportSandbox}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
@ -410,6 +414,7 @@ const ConfigPrompt: FC<Props> = ({
|
||||
handleAddVariable={handleAddVariable}
|
||||
onGenerated={handleGenerated}
|
||||
modelConfig={modelConfig}
|
||||
isSupportSandbox={isSupportSandbox}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -109,11 +109,17 @@ FilePickerTreeNode.displayName = 'FilePickerTreeNode'
|
||||
type FilePickerPanelProps = {
|
||||
onSelectNode: (node: TreeNodeData) => void
|
||||
focusNodeId?: string
|
||||
className?: string
|
||||
contentClassName?: string
|
||||
showHeader?: boolean
|
||||
}
|
||||
|
||||
const FilePickerPanel: FC<FilePickerPanelProps> = ({
|
||||
onSelectNode,
|
||||
focusNodeId,
|
||||
className,
|
||||
contentClassName,
|
||||
showHeader = true,
|
||||
}) => {
|
||||
const { t } = useTranslation('workflow')
|
||||
const { data: treeData, isLoading, error } = useSkillAssetTreeData()
|
||||
@ -141,7 +147,10 @@ const FilePickerPanel: FC<FilePickerPanelProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-[280px] overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm"
|
||||
className={cn(
|
||||
'w-[280px] overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm',
|
||||
className,
|
||||
)}
|
||||
onMouseDown={(e) => {
|
||||
const target = e.target as HTMLElement
|
||||
if (target.closest('input, textarea, select'))
|
||||
@ -149,13 +158,22 @@ const FilePickerPanel: FC<FilePickerPanelProps> = ({
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-1 px-4 pb-1 pt-1.5">
|
||||
<span className="flex-1 text-[12px] font-medium uppercase leading-4 text-text-tertiary">
|
||||
{t('skillEditor.referenceFiles')}
|
||||
</span>
|
||||
<RiQuestionLine className="size-4 text-text-tertiary" aria-hidden="true" />
|
||||
</div>
|
||||
<div ref={containerRef} className="max-h-[320px] min-h-[120px] px-2 pb-2">
|
||||
{showHeader && (
|
||||
<div className="flex items-center gap-1 px-4 pb-1 pt-1.5">
|
||||
<span className="flex-1 text-[12px] font-medium uppercase leading-4 text-text-tertiary">
|
||||
{t('skillEditor.referenceFiles')}
|
||||
</span>
|
||||
<RiQuestionLine className="size-4 text-text-tertiary" aria-hidden="true" />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn(
|
||||
'max-h-[320px] min-h-[120px] px-2 pb-2',
|
||||
!showHeader && 'pt-2',
|
||||
contentClassName,
|
||||
)}
|
||||
>
|
||||
{isLoading && (
|
||||
<div className="flex h-full items-center justify-center py-6">
|
||||
<Loading type="area" />
|
||||
|
||||
Reference in New Issue
Block a user