Files
kamidso-front/src/features/assistant/session-files/AttachmentRail.tsx
T
javadandCursor b17782eec2 Add Didar connections and a data hub for attaching sources.
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>
2026-10-12 19:26:46 +03:30

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>
)
}