The assistant can pick connectors and show subagent steps, and the databases page is replaced by connections, sources, and a dashboard. Co-authored-by: Cursor <cursoragent@cursor.com>
200 lines
7.6 KiB
TypeScript
200 lines
7.6 KiB
TypeScript
import { AlertCircle, BookOpen, CheckCircle2, Database, Loader2, Paperclip, Plug, RefreshCw, X } from 'lucide-react'
|
|
import type { ReactNode } from 'react'
|
|
import type { DraftFile, LegacyServerFile } from './types'
|
|
import { formatBytes, toLegacyRailItem, toRailItems } from './attachment-view-model'
|
|
import { t } from '../../../i18n'
|
|
|
|
export type RailDatabase = { id: string; name: string; type?: string }
|
|
export type RailKnowledge = { id: string; name: string }
|
|
export type RailConnector = { id: string; name: string }
|
|
|
|
export interface AttachmentRailProps {
|
|
drafts: readonly DraftFile[]
|
|
legacyFiles?: readonly LegacyServerFile[]
|
|
databases?: readonly RailDatabase[]
|
|
connectors?: readonly RailConnector[]
|
|
knowledge?: RailKnowledge | null
|
|
onRemove?: (clientId: string) => void
|
|
onRetry?: (clientId: string) => void
|
|
onRemoveDatabase?: (id: string) => void
|
|
onRemoveConnector?: (id: string) => void
|
|
onRemoveKnowledge?: () => void
|
|
addControl?: ReactNode
|
|
className?: string
|
|
}
|
|
|
|
export function AttachmentRailAddButton({ onClick }: { onClick: () => void }) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={onClick}
|
|
className="inline-flex h-9 w-9 items-center justify-center rounded-xl border border-dashed border-line text-ink-muted transition hover:border-accent hover:text-accent"
|
|
aria-label="افزودن فایل"
|
|
>
|
|
<Paperclip className="h-4 w-4" />
|
|
</button>
|
|
)
|
|
}
|
|
|
|
export default function AttachmentRail({
|
|
drafts,
|
|
legacyFiles = [],
|
|
databases = [],
|
|
connectors = [],
|
|
knowledge = null,
|
|
onRemove,
|
|
onRetry,
|
|
onRemoveDatabase,
|
|
onRemoveConnector,
|
|
onRemoveKnowledge,
|
|
addControl,
|
|
className = '',
|
|
}: AttachmentRailProps) {
|
|
const draftItems = toRailItems(drafts)
|
|
const legacyItems = legacyFiles.map(toLegacyRailItem)
|
|
const hasKnowledge = Boolean(knowledge?.name)
|
|
if (
|
|
draftItems.length === 0 &&
|
|
legacyItems.length === 0 &&
|
|
databases.length === 0 &&
|
|
connectors.length === 0 &&
|
|
!hasKnowledge &&
|
|
!addControl
|
|
) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div className={`flex flex-wrap items-center gap-2 ${className}`}>
|
|
{databases.map((item) => (
|
|
<div
|
|
key={`db-${item.id}`}
|
|
className="inline-flex max-w-full items-center gap-2 rounded-xl bg-white px-2 py-1.5 ring-1 ring-black/5"
|
|
>
|
|
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-emerald-50 text-emerald-700">
|
|
<Database className="h-4 w-4" />
|
|
</span>
|
|
<div className="min-w-0 flex-1 pe-1">
|
|
<p className="truncate text-xs font-medium text-ink">{item.name}</p>
|
|
<p className="text-[10px] text-ink-muted">{item.type || t('assistant.composer.database')}</p>
|
|
</div>
|
|
{onRemoveDatabase ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => onRemoveDatabase(item.id)}
|
|
className="text-ink-muted hover:text-danger"
|
|
aria-label={t('assistant.files.remove')}
|
|
>
|
|
<X className="h-3.5 w-3.5" />
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
))}
|
|
{connectors.map((item) => (
|
|
<div
|
|
key={`connector-${item.id}`}
|
|
className="inline-flex max-w-full items-center gap-2 rounded-xl bg-white px-2 py-1.5 ring-1 ring-black/5"
|
|
>
|
|
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-sky-50 text-sky-700">
|
|
<Plug className="h-4 w-4" />
|
|
</span>
|
|
<div className="min-w-0 flex-1 pe-1">
|
|
<p className="truncate text-xs font-medium text-ink">{item.name}</p>
|
|
<p className="text-[10px] text-ink-muted">{t('assistant.composer.connectorHint')}</p>
|
|
</div>
|
|
{onRemoveConnector ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => onRemoveConnector(item.id)}
|
|
className="text-ink-muted hover:text-danger"
|
|
aria-label={t('assistant.files.remove')}
|
|
>
|
|
<X className="h-3.5 w-3.5" />
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
))}
|
|
{hasKnowledge ? (
|
|
<div className="inline-flex max-w-full items-center gap-2 rounded-xl bg-white px-2 py-1.5 ring-1 ring-black/5">
|
|
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-amber-50 text-amber-700">
|
|
<BookOpen className="h-4 w-4" />
|
|
</span>
|
|
<div className="min-w-0 flex-1 pe-1">
|
|
<p className="truncate text-xs font-medium text-ink">{knowledge!.name}</p>
|
|
<p className="text-[10px] text-ink-muted">{t('assistant.panel.knowledgeHint')}</p>
|
|
</div>
|
|
{onRemoveKnowledge ? (
|
|
<button
|
|
type="button"
|
|
onClick={onRemoveKnowledge}
|
|
className="text-ink-muted hover:text-danger"
|
|
aria-label={t('assistant.files.remove')}
|
|
>
|
|
<X className="h-3.5 w-3.5" />
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
) : null}
|
|
{legacyItems.map((item) => (
|
|
<div
|
|
key={item.key}
|
|
className="inline-flex max-w-full items-center gap-2 rounded-xl bg-white px-2 py-1.5 ring-1 ring-black/5"
|
|
>
|
|
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-emerald-50 text-emerald-700">
|
|
<Paperclip className="h-4 w-4" />
|
|
</span>
|
|
<div className="min-w-0 flex-1 pe-1">
|
|
<p className="truncate text-xs font-medium text-ink">{item.name}</p>
|
|
<p className="text-[10px] text-ink-muted">{formatBytes(item.size)} · سرور</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{draftItems.map((item) => {
|
|
const failed = item.uploadStatus === 'failed' || item.uploadStatus === 'blocked'
|
|
const uploading = item.uploadStatus === 'uploading' || item.uploadStatus === 'queued'
|
|
const done = item.uploadStatus === 'done'
|
|
return (
|
|
<div
|
|
key={item.clientId}
|
|
className="inline-flex max-w-full items-center gap-2 rounded-xl bg-white px-2 py-1.5 ring-1 ring-black/5"
|
|
>
|
|
<span
|
|
className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${
|
|
failed ? 'bg-danger-soft text-danger' : 'bg-emerald-50 text-emerald-700'
|
|
}`}
|
|
>
|
|
{uploading ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
) : failed ? (
|
|
<AlertCircle className="h-4 w-4" />
|
|
) : done ? (
|
|
<CheckCircle2 className="h-4 w-4" />
|
|
) : (
|
|
<Paperclip className="h-4 w-4" />
|
|
)}
|
|
</span>
|
|
<div className="min-w-0 flex-1 pe-1">
|
|
<p className="truncate text-xs font-medium text-ink">{item.name}</p>
|
|
<p className="text-[10px] text-ink-muted">
|
|
{item.size ? formatBytes(item.size) : ''}
|
|
{uploading ? ` · ${Math.round(item.uploadProgress * 100)}%` : ''}
|
|
</p>
|
|
</div>
|
|
{failed && onRetry ? (
|
|
<button type="button" onClick={() => onRetry(item.clientId)} className="text-accent" aria-label="تلاش مجدد">
|
|
<RefreshCw className="h-3.5 w-3.5" />
|
|
</button>
|
|
) : null}
|
|
{onRemove ? (
|
|
<button type="button" onClick={() => onRemove(item.clientId)} className="text-ink-muted hover:text-danger" aria-label="حذف">
|
|
<X className="h-3.5 w-3.5" />
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
)
|
|
})}
|
|
{addControl}
|
|
</div>
|
|
)
|
|
}
|