33import { useCallback , useEffect , useRef , useState } from 'react' ;
44import { deleteDocument , listDocuments , uploadDocument } from '@/lib/api' ;
55import type { DocumentDTO } from '@/lib/types' ;
6- import { PlusIcon , SpinnerIcon , TrashIcon } from './icons' ;
6+ import { LockIcon , PlusIcon , SpinnerIcon , TrashIcon } from './icons' ;
77
88function formatSize ( bytes : number ) : string {
99 if ( bytes < 1024 ) return `${ bytes } o` ;
1010 if ( bytes < 1024 * 1024 ) return `${ Math . round ( bytes / 1024 ) } Ko` ;
1111 return `${ ( bytes / 1024 / 1024 ) . toFixed ( 1 ) } Mo` ;
1212}
1313
14- /** Documents de référence rattachés à un Cadre : dépôt, liste, suppression. */
15- export function DocumentsPanel ( { frameworkId } : { frameworkId : string } ) {
14+ /**
15+ * Documents de référence rattachés à un Cadre : dépôt, liste, suppression.
16+ * `locked` (pendant la création d'un Cadre, avant son enregistrement) : la
17+ * section reste visible mais floutée/désactivée, avec un message explicatif.
18+ */
19+ export function DocumentsPanel ( {
20+ frameworkId,
21+ locked = false ,
22+ } : {
23+ frameworkId ?: string ;
24+ locked ?: boolean ;
25+ } ) {
1626 const [ documents , setDocuments ] = useState < DocumentDTO [ ] > ( [ ] ) ;
1727 const [ loading , setLoading ] = useState ( true ) ;
1828 const [ uploading , setUploading ] = useState ( false ) ;
@@ -21,21 +31,26 @@ export function DocumentsPanel({ frameworkId }: { frameworkId: string }) {
2131
2232 const refresh = useCallback (
2333 async ( signal ?: AbortSignal ) => {
34+ if ( ! frameworkId ) return ;
2435 setDocuments ( await listDocuments ( frameworkId , signal ) ) ;
2536 } ,
2637 [ frameworkId ] ,
2738 ) ;
2839
2940 useEffect ( ( ) => {
41+ if ( locked || ! frameworkId ) {
42+ setLoading ( false ) ;
43+ return ;
44+ }
3045 const controller = new AbortController ( ) ;
3146 refresh ( controller . signal )
3247 . catch ( ( ) => undefined )
3348 . finally ( ( ) => setLoading ( false ) ) ;
3449 return ( ) => controller . abort ( ) ;
35- } , [ refresh ] ) ;
50+ } , [ refresh , locked , frameworkId ] ) ;
3651
3752 const onFiles = async ( files : FileList | null ) => {
38- if ( ! files || files . length === 0 ) return ;
53+ if ( ! files || files . length === 0 || ! frameworkId ) return ;
3954 setError ( null ) ;
4055 setUploading ( true ) ;
4156 try {
@@ -62,61 +77,94 @@ export function DocumentsPanel({ frameworkId }: { frameworkId: string }) {
6277 sources.
6378 </ p >
6479
65- < div className = "mt-4" >
66- < input
67- ref = { inputRef }
68- type = "file"
69- accept = ".pdf,.txt,.md,.markdown,application/pdf,text/plain,text/markdown"
70- multiple
71- className = "sr-only"
72- onChange = { ( e ) => onFiles ( e . target . files ) }
73- />
74- < button
75- type = "button"
76- onClick = { ( ) => inputRef . current ?. click ( ) }
77- disabled = { uploading }
78- className = "inline-flex items-center gap-1.5 rounded-button border border-border bg-surface px-3 py-2 text-sm font-medium transition-colors hover:border-accent hover:bg-surface-muted disabled:opacity-60"
80+ < div className = "relative mt-4" >
81+ < div
82+ aria-hidden = { locked || undefined }
83+ className = { locked ? 'pointer-events-none select-none blur-[3px] opacity-50' : undefined }
7984 >
80- { uploading ? < SpinnerIcon className = "size-4" /> : < PlusIcon className = "size-4" /> }
81- { uploading ? 'Analyse en cours…' : 'Déposer un document' }
82- </ button >
83- </ div >
85+ < input
86+ ref = { inputRef }
87+ type = "file"
88+ accept = ".pdf,.txt,.md,.markdown,application/pdf,text/plain,text/markdown"
89+ multiple
90+ disabled = { locked }
91+ className = "sr-only"
92+ onChange = { ( e ) => onFiles ( e . target . files ) }
93+ />
94+ < button
95+ type = "button"
96+ onClick = { ( ) => inputRef . current ?. click ( ) }
97+ disabled = { uploading || locked }
98+ className = "inline-flex items-center gap-1.5 rounded-button border border-border bg-surface px-3 py-2 text-sm font-medium transition-colors hover:border-accent hover:bg-surface-muted disabled:opacity-60"
99+ >
100+ { uploading ? < SpinnerIcon className = "size-4" /> : < PlusIcon className = "size-4" /> }
101+ { uploading ? 'Analyse en cours…' : 'Déposer un document' }
102+ </ button >
84103
85- { error ? (
86- < p role = "alert" className = "mt-2 text-sm text-danger" >
87- { error }
88- </ p >
89- ) : null }
104+ { error ? (
105+ < p role = "alert" className = "mt-2 text-sm text-danger" >
106+ { error }
107+ </ p >
108+ ) : null }
90109
91- < div className = "mt-3" >
92- { loading ? (
93- < div className = "flex items-center gap-2 text-sm text-muted-foreground" >
94- < SpinnerIcon className = "size-4" /> Chargement…
95- </ div >
96- ) : documents . length === 0 ? (
97- < p className = "text-sm text-muted-foreground" > Aucun document pour ce cadre.</ p >
98- ) : (
99- < ul className = "divide-y divide-border" >
100- { documents . map ( ( document ) => (
101- < li key = { document . id } className = "flex items-center justify-between gap-3 py-2" >
102- < div className = "min-w-0" >
103- < p className = "truncate text-sm font-medium" > { document . filename } </ p >
104- < p className = "text-xs text-muted-foreground" >
105- { formatSize ( document . sizeBytes ) } · { document . chunkCount } passage(s)
106- </ p >
110+ { locked ? (
111+ // Aperçu factice (squelette) pour donner à voir la section à venir.
112+ < ul className = "mt-3 divide-y divide-border" >
113+ { [ '•••••••••.pdf' , '••••••.txt' ] . map ( ( label ) => (
114+ < li key = { label } className = "flex items-center justify-between gap-3 py-2" >
115+ < div className = "min-w-0" >
116+ < p className = "truncate text-sm font-medium" > { label } </ p >
117+ < p className = "text-xs text-muted-foreground" > — · — passage(s)</ p >
118+ </ div >
119+ < TrashIcon className = "size-4 text-muted-foreground" />
120+ </ li >
121+ ) ) }
122+ </ ul >
123+ ) : (
124+ < div className = "mt-3" >
125+ { loading ? (
126+ < div className = "flex items-center gap-2 text-sm text-muted-foreground" >
127+ < SpinnerIcon className = "size-4" /> Chargement…
107128 </ div >
108- < button
109- type = "button"
110- onClick = { ( ) => onDelete ( document . id ) }
111- aria-label = { `Supprimer ${ document . filename } ` }
112- className = "inline-flex size-8 shrink-0 items-center justify-center rounded-button text-muted-foreground hover:bg-surface-muted hover:text-danger"
113- >
114- < TrashIcon className = "size-4" />
115- </ button >
116- </ li >
117- ) ) }
118- </ ul >
119- ) }
129+ ) : documents . length === 0 ? (
130+ < p className = "text-sm text-muted-foreground" > Aucun document pour ce cadre.</ p >
131+ ) : (
132+ < ul className = "divide-y divide-border" >
133+ { documents . map ( ( document ) => (
134+ < li key = { document . id } className = "flex items-center justify-between gap-3 py-2" >
135+ < div className = "min-w-0" >
136+ < p className = "truncate text-sm font-medium" > { document . filename } </ p >
137+ < p className = "text-xs text-muted-foreground" >
138+ { formatSize ( document . sizeBytes ) } · { document . chunkCount } passage(s)
139+ </ p >
140+ </ div >
141+ < button
142+ type = "button"
143+ onClick = { ( ) => onDelete ( document . id ) }
144+ aria-label = { `Supprimer ${ document . filename } ` }
145+ className = "inline-flex size-8 shrink-0 items-center justify-center rounded-button text-muted-foreground hover:bg-surface-muted hover:text-danger"
146+ >
147+ < TrashIcon className = "size-4" />
148+ </ button >
149+ </ li >
150+ ) ) }
151+ </ ul >
152+ ) }
153+ </ div >
154+ ) }
155+ </ div >
156+
157+ { locked ? (
158+ < div className = "absolute inset-0 flex flex-col items-center justify-center gap-1.5 rounded-card bg-surface/55 text-center" >
159+ < span className = "inline-flex size-9 items-center justify-center rounded-full bg-surface-muted text-muted-foreground shadow-card" >
160+ < LockIcon className = "size-4" />
161+ </ span >
162+ < p className = "text-sm font-semibold text-foreground" > Enregistrez d’abord le cadre</ p >
163+ < p className = "max-w-xs text-xs text-muted-foreground" >
164+ Vous pourrez ensuite y ajouter des documents de référence.
165+ </ p >
166+ </ div >
167+ ) : null }
120168 </ div >
121169 </ section >
122170 ) ;
0 commit comments