mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 00:31:19 +08:00
Signed-off-by: kenwoodjw <blackxin55+@gmail.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: zxhlyh <jasonapring2015@outlook.com> Co-authored-by: 起岚 <155608604+xiaoyaoqilan@users.noreply.github.com> Co-authored-by: Parman Mohammadalizadeh <prmma23@gmail.com> Co-authored-by: Escape0707 <tothesong@gmail.com> Co-authored-by: Xin Zhang <zhangxin@dify.ai> Co-authored-by: iridescentWen <66297467+iridescentWen@users.noreply.github.com> Co-authored-by: Bond Zhu <37842169+MRZHUH@users.noreply.github.com> Co-authored-by: KVOJJJin <jzongcode@gmail.com> Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: 非法操作 <hjlarry@163.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> Co-authored-by: Asuka Minato <i@asukaminato.eu.org> Co-authored-by: Chester <superdiao6@gmail.com> Co-authored-by: WH-2099 <wh2099@pm.me> Co-authored-by: Jony <619963502@qq.com> Co-authored-by: Jony <13896935+zyz619963502zyz@users.noreply.github.com> Co-authored-by: Harsh Kashyap <harsh.kashyap2001@gmail.com> Co-authored-by: Harsh Kashyap <Harsh23Kashyap@users.noreply.github.com> Co-authored-by: kenwoodjw <blackxin55+@gmail.com> Co-authored-by: csurong <csurong1@gmail.com> Co-authored-by: caosurong <surong.cao@thinkingdata.cn> Co-authored-by: Taranum Wasu <81034301+Taranum01@users.noreply.github.com> Co-authored-by: Taranum01 <50813317+Taranum01@users.noreply.github.com> Co-authored-by: Pranav Agarwal <agarwalpranav0711@gmail.com> Co-authored-by: Stephen Zhou <hi@hyoban.cc> Co-authored-by: zl86790 <marshal_li_b@163.com> Co-authored-by: QuantumGhost <obelisk.reg+git@gmail.com> Co-authored-by: yunlu.wen <yunlu.wen@dify.ai> Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
322 lines
12 KiB
TypeScript
322 lines
12 KiB
TypeScript
'use client'
|
|
|
|
import type { SkillUploadQueueItem } from './shared'
|
|
import type { SkillUploadDecision, SkillUploadReviewItem } from './upload-workflow'
|
|
import { Button } from '@langgenius/dify-ui/button'
|
|
import { cn } from '@langgenius/dify-ui/cn'
|
|
import {
|
|
Dialog,
|
|
DialogClose,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogTitle,
|
|
} from '@langgenius/dify-ui/dialog'
|
|
import { IconButton } from '@langgenius/dify-ui/icon-button'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { getPathBaseName } from './shared'
|
|
import { isUploadReviewItemSkipped, isUploadReviewResolved } from './upload-workflow'
|
|
|
|
function getUploadIconClass(name: string) {
|
|
const extension = name.split('.').at(-1)?.toLowerCase()
|
|
if (extension === 'pdf') return 'i-ri-file-pdf-2-fill text-util-colors-red-red-600'
|
|
if (extension === 'md') return 'i-ri-markdown-fill text-util-colors-blue-blue-600'
|
|
if (['xls', 'xlsx'].includes(extension ?? ''))
|
|
return 'i-ri-file-excel-2-fill text-util-colors-green-green-600'
|
|
return 'i-ri-file-fill text-text-quaternary'
|
|
}
|
|
|
|
function UploadRowShell({
|
|
actions,
|
|
message,
|
|
name,
|
|
success = false,
|
|
}: {
|
|
actions?: React.ReactNode
|
|
message: string
|
|
name: string
|
|
success?: boolean
|
|
}) {
|
|
return (
|
|
<div className="flex min-h-[50px] items-center gap-2 rounded-[10px] bg-background-section px-3 py-1.5">
|
|
<span aria-hidden className={cn('size-5 shrink-0', getUploadIconClass(name))} />
|
|
<div className="min-w-0 flex-1">
|
|
<div className="truncate system-sm-medium text-text-primary">{name}</div>
|
|
<div
|
|
className={cn(
|
|
'truncate system-xs-regular',
|
|
success ? 'text-text-success' : 'text-text-warning-secondary',
|
|
)}
|
|
>
|
|
{message}
|
|
</div>
|
|
</div>
|
|
{!!actions && <div className="flex shrink-0 items-center gap-1">{actions}</div>}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function DecisionButton({ children, onClick }: { children: React.ReactNode; onClick: () => void }) {
|
|
return (
|
|
<Button size="small" variant="secondary" onClick={onClick}>
|
|
{children}
|
|
</Button>
|
|
)
|
|
}
|
|
|
|
function getCheckMessage(
|
|
item: SkillUploadReviewItem,
|
|
t: ReturnType<typeof useTranslation<'skill'>>['t'],
|
|
) {
|
|
const code = item.check.errors?.[0]?.code
|
|
if (code === 'file_already_exists' || code === 'duplicate_file_path')
|
|
return t(($) => $['skillManagement.detail.uploadConflict'])
|
|
if (code === 'invalid_filename') {
|
|
const suggestion = item.suggestedPath
|
|
? ` Suggested: ${getPathBaseName(item.suggestedPath)}`
|
|
: ''
|
|
return `${t(($) => $['skillManagement.detail.uploadInvalidName'])}${suggestion}`
|
|
}
|
|
if (code === 'invalid_file_extension')
|
|
return t(($) => $['skillManagement.detail.uploadInvalidExtension'])
|
|
if (code === 'missing_file_extension')
|
|
return t(($) => $['skillManagement.detail.uploadMissingExtension'])
|
|
return t(($) => $['skillManagement.detail.uploadInvalidPath'])
|
|
}
|
|
|
|
function getResolvedMessage(
|
|
item: SkillUploadReviewItem,
|
|
t: ReturnType<typeof useTranslation<'skill'>>['t'],
|
|
) {
|
|
if (item.decision === 'replace') return t(($) => $['skillManagement.detail.uploadWillReplace'])
|
|
return t(($) => $['skillManagement.detail.uploadWillUseName'], {
|
|
name: getPathBaseName(item.resolvedPath ?? item.originalPath),
|
|
})
|
|
}
|
|
|
|
function UploadReviewRow({
|
|
item,
|
|
onDecision,
|
|
}: {
|
|
item: SkillUploadReviewItem
|
|
onDecision: (id: string, decision: SkillUploadDecision) => void
|
|
}) {
|
|
const { t } = useTranslation('skill')
|
|
const resolved = item.kind === 'ready' || (item.decision && item.decision !== 'skip')
|
|
const actions =
|
|
!item.decision && item.kind === 'conflict' ? (
|
|
<>
|
|
<DecisionButton onClick={() => onDecision(item.id, 'replace')}>
|
|
{t(($) => $['skillManagement.detail.uploadReplace'])}
|
|
</DecisionButton>
|
|
<DecisionButton onClick={() => onDecision(item.id, 'keep-both')}>
|
|
{t(($) => $['skillManagement.detail.uploadKeepBoth'])}
|
|
</DecisionButton>
|
|
<DecisionButton onClick={() => onDecision(item.id, 'skip')}>
|
|
{t(($) => $['skillManagement.detail.uploadSkip'])}
|
|
</DecisionButton>
|
|
</>
|
|
) : !item.decision && item.kind === 'invalid-name' ? (
|
|
<>
|
|
<DecisionButton onClick={() => onDecision(item.id, 'use-suggestion')}>
|
|
{t(($) => $['skillManagement.detail.uploadSuggestion'])}
|
|
</DecisionButton>
|
|
<DecisionButton onClick={() => onDecision(item.id, 'skip')}>
|
|
{t(($) => $['skillManagement.detail.uploadSkip'])}
|
|
</DecisionButton>
|
|
</>
|
|
) : undefined
|
|
|
|
return (
|
|
<UploadRowShell
|
|
name={getPathBaseName(item.originalPath)}
|
|
message={resolved ? getResolvedMessage(item, t) : getCheckMessage(item, t)}
|
|
success={Boolean(resolved)}
|
|
actions={actions}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export function SkillUploadReviewDialog({
|
|
items,
|
|
onDecision,
|
|
onOpenChange,
|
|
onUpload,
|
|
open,
|
|
}: {
|
|
items: SkillUploadReviewItem[]
|
|
onDecision: (id: string, decision: SkillUploadDecision) => void
|
|
onOpenChange: (open: boolean) => void
|
|
onUpload: () => void
|
|
open: boolean
|
|
}) {
|
|
const { t } = useTranslation('skill')
|
|
const { t: tCommon } = useTranslation('common')
|
|
const skippedItems = items.filter(isUploadReviewItemSkipped)
|
|
const candidateItems = items.filter((item) => !isUploadReviewItemSkipped(item))
|
|
const unresolvedCount = candidateItems.filter((item) => !isUploadReviewResolved(item)).length
|
|
const readyCount = candidateItems.filter((item) => isUploadReviewResolved(item)).length
|
|
const summary =
|
|
unresolvedCount > 0
|
|
? t(($) => $['skillManagement.detail.uploadReadySummary'], {
|
|
decisions: unresolvedCount,
|
|
ready: readyCount,
|
|
skipped: skippedItems.length,
|
|
})
|
|
: t(($) => $['skillManagement.detail.uploadResolvedSummary'], {
|
|
ready: readyCount,
|
|
skipped: skippedItems.length,
|
|
})
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="w-[600px] max-w-[calc(100vw-32px)] overflow-hidden! p-0!">
|
|
<DialogClose
|
|
render={
|
|
<IconButton
|
|
aria-label={tCommon(($) => $['operation.close'])}
|
|
size="lg"
|
|
className="absolute inset-e-5 top-5"
|
|
>
|
|
<span aria-hidden className="i-ri-close-line size-4" />
|
|
</IconButton>
|
|
}
|
|
/>
|
|
<header className="px-6 pt-6 pr-14 pb-3">
|
|
<DialogTitle className="title-2xl-semi-bold text-text-primary">
|
|
{t(($) => $['skillManagement.detail.uploadReviewTitle'])}
|
|
</DialogTitle>
|
|
<DialogDescription className="mt-1 system-xs-regular text-text-tertiary">
|
|
{summary}
|
|
</DialogDescription>
|
|
</header>
|
|
<div className="flex max-h-[min(520px,60dvh)] flex-col gap-4 overflow-y-auto px-6 py-3">
|
|
{candidateItems.length > 0 && (
|
|
<div className="flex flex-col gap-1">
|
|
{candidateItems.map((item) => (
|
|
<UploadReviewRow key={item.id} item={item} onDecision={onDecision} />
|
|
))}
|
|
</div>
|
|
)}
|
|
{skippedItems.length > 0 && (
|
|
<section className="flex flex-col gap-1">
|
|
<div className="flex h-6 items-center system-sm-medium text-text-secondary">
|
|
{t(($) => $['skillManagement.detail.uploadSkippedGroup'], {
|
|
count: skippedItems.length,
|
|
})}
|
|
</div>
|
|
{skippedItems.map((item) => (
|
|
<UploadReviewRow key={item.id} item={item} onDecision={onDecision} />
|
|
))}
|
|
</section>
|
|
)}
|
|
</div>
|
|
<footer className="flex h-[76px] items-start justify-end gap-2 px-6 pt-5 pb-6">
|
|
<DialogClose render={<Button size="large" />}>
|
|
{tCommon(($) => $['operation.cancel'])}
|
|
</DialogClose>
|
|
<Button
|
|
size="large"
|
|
variant="primary"
|
|
disabled={unresolvedCount > 0 || readyCount === 0}
|
|
onClick={onUpload}
|
|
>
|
|
{t(($) => $['skillManagement.detail.uploadFilesButton'], { count: readyCount })}
|
|
</Button>
|
|
</footer>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
export function SkillUploadFailuresDialog({
|
|
items,
|
|
onDecision,
|
|
onDismiss,
|
|
onRetry,
|
|
onRetryItem,
|
|
open,
|
|
}: {
|
|
items: SkillUploadQueueItem[]
|
|
onDecision: (id: string, decision: SkillUploadDecision) => void
|
|
onDismiss: () => void
|
|
onRetry: () => void
|
|
onRetryItem: (id: string) => void
|
|
open: boolean
|
|
}) {
|
|
const { t } = useTranslation('skill')
|
|
const { t: tCommon } = useTranslation('common')
|
|
const failedItems = items.filter((item) => item.status === 'failed')
|
|
const retryableItems = failedItems.filter((item) => item.failureKind !== 'conflict')
|
|
return (
|
|
<Dialog open={open} onOpenChange={(nextOpen) => !nextOpen && onDismiss()}>
|
|
<DialogContent className="w-[600px] max-w-[calc(100vw-32px)] overflow-hidden! p-0!">
|
|
<DialogClose
|
|
render={
|
|
<IconButton
|
|
aria-label={tCommon(($) => $['operation.close'])}
|
|
size="lg"
|
|
className="absolute inset-e-5 top-5"
|
|
>
|
|
<span aria-hidden className="i-ri-close-line size-4" />
|
|
</IconButton>
|
|
}
|
|
/>
|
|
<header className="px-6 pt-6 pr-14 pb-3">
|
|
<DialogTitle className="title-2xl-semi-bold text-text-primary">
|
|
{t(($) => $['skillManagement.detail.uploadFailureTitle'])}
|
|
</DialogTitle>
|
|
<DialogDescription className="mt-1 system-xs-regular text-text-tertiary">
|
|
{t(($) => $['skillManagement.detail.uploadFailureSummary'], {
|
|
failed: failedItems.length,
|
|
total: items.length,
|
|
})}
|
|
</DialogDescription>
|
|
</header>
|
|
<div className="flex max-h-[min(520px,60dvh)] flex-col gap-1 overflow-y-auto px-6 py-3">
|
|
{failedItems.map((item) => (
|
|
<UploadRowShell
|
|
key={item.id}
|
|
name={getPathBaseName(item.name)}
|
|
message={item.error ?? t(($) => $['skillManagement.detail.uploadNetworkFailure'])}
|
|
actions={
|
|
item.failureKind === 'conflict' ? (
|
|
<>
|
|
<DecisionButton onClick={() => onDecision(item.id, 'replace')}>
|
|
{t(($) => $['skillManagement.detail.uploadReplace'])}
|
|
</DecisionButton>
|
|
<DecisionButton onClick={() => onDecision(item.id, 'keep-both')}>
|
|
{t(($) => $['skillManagement.detail.uploadKeepBoth'])}
|
|
</DecisionButton>
|
|
<DecisionButton onClick={() => onDecision(item.id, 'skip')}>
|
|
{t(($) => $['skillManagement.detail.uploadSkip'])}
|
|
</DecisionButton>
|
|
</>
|
|
) : (
|
|
<DecisionButton onClick={() => onRetryItem(item.id)}>
|
|
{tCommon(($) => $['operation.retry'])}
|
|
</DecisionButton>
|
|
)
|
|
}
|
|
/>
|
|
))}
|
|
</div>
|
|
<footer className="flex h-[76px] items-start justify-end gap-2 px-6 pt-5 pb-6">
|
|
<DialogClose render={<Button size="large" />}>
|
|
{tCommon(($) => $['operation.cancel'])}
|
|
</DialogClose>
|
|
<Button
|
|
size="large"
|
|
variant="primary"
|
|
disabled={retryableItems.length === 0}
|
|
onClick={onRetry}
|
|
>
|
|
{t(($) => $['skillManagement.detail.uploadRetryCount'], {
|
|
count: retryableItems.length,
|
|
})}
|
|
</Button>
|
|
</footer>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|