2026-06-15 01:14:16 +02:00
import React , { useEffect , useMemo , useRef , useState } from 'react'
2026-05-06 04:40:01 +02:00
import desktopApi from './desktop/desktopApi'
2026-03-19 23:57:17 +01:00
const TOAST _DURATION _MS = {
info : 3600 ,
success : 4800 ,
warning : 5600
2026-03-19 21:07:22 +01:00
}
2026-06-15 01:14:16 +02:00
function itemSyncMeta ( item ) {
const sync = item ? . sync || { }
2026-03-19 21:45:27 +01:00
const status = String ( sync . status || 'pending' )
const progress = Math . max ( 0 , Math . min ( 100 , Number ( sync . progress ) || 0 ) )
const detail = String ( sync . detail || '' ) . trim ( )
2026-06-15 01:14:16 +02:00
const enrichEnabled = ! ! item ? . enrich _enabled
2026-06-15 02:05:23 +02:00
const metadataReady = [ 'ready' , 'fallback' ] . includes ( item ? . metadata ? . status )
2026-03-19 21:45:27 +01:00
if ( status === 'ready' ) {
return {
status ,
progress : 100 ,
label : 'Available' ,
2026-06-15 02:05:23 +02:00
detail : detail || (
metadataReady
? ( enrichEnabled ? 'Ready with standard metadata and deep enrichment.' : 'Ready with standard metadata.' )
: 'Ready for chat. Automatic metadata has not been generated yet.'
)
2026-03-19 21:45:27 +01:00
}
}
if ( status === 'failed' ) {
return {
status ,
progress : 100 ,
label : 'Sync failed' ,
2026-06-15 01:14:16 +02:00
detail : String ( sync . error || '' ) . trim ( ) || detail || 'Heimgeist could not finish syncing this content.'
2026-03-19 21:45:27 +01:00
}
}
if ( status === 'syncing' ) {
return {
status ,
progress ,
label : progress > 0 ? ` Syncing ${ Math . round ( progress ) } % ` : 'Syncing' ,
2026-06-15 01:14:16 +02:00
detail : detail || 'Rebuilding the database search indexes.'
2026-03-19 21:45:27 +01:00
}
}
return {
status : 'pending' ,
progress : 6 ,
label : 'Queued' ,
2026-03-19 22:15:34 +01:00
detail : 'Waiting to rebuild the retrieval pipeline.'
2026-03-19 21:45:27 +01:00
}
}
2026-06-15 01:14:16 +02:00
function kindLabel ( item ) {
if ( item ? . kind === 'text' ) return 'Text'
if ( item ? . kind === 'website' ) return 'Website'
2026-06-15 06:14:22 +02:00
if ( item ? . kind === 'video' ) return 'Video'
2026-06-15 03:53:10 +02:00
if ( item ? . kind === 'chat_message' ) return 'Chat Message'
2026-06-15 01:14:16 +02:00
return 'File'
}
async function expectJson ( response ) {
const raw = await response . text ( )
let data = null
try {
data = raw ? JSON . parse ( raw ) : null
} catch { }
if ( ! response . ok ) {
throw new Error ( data ? . detail || raw || ` HTTP ${ response . status } ` )
}
return data
}
2026-06-15 03:53:10 +02:00
export default function LibraryManager ( { apiBase , library , jobs , onOpenChatMessage , onRefresh } ) {
2026-03-19 21:07:22 +01:00
const [ busy , setBusy ] = useState ( false )
const [ errorMessage , setErrorMessage ] = useState ( '' )
2026-03-19 23:57:17 +01:00
const [ toasts , setToasts ] = useState ( [ ] )
2026-06-15 01:14:16 +02:00
const [ search , setSearch ] = useState ( '' )
const [ formMode , setFormMode ] = useState ( null )
const [ editingItemId , setEditingItemId ] = useState ( null )
const [ textTitle , setTextTitle ] = useState ( '' )
const [ textContent , setTextContent ] = useState ( '' )
const [ websiteTitle , setWebsiteTitle ] = useState ( '' )
const [ websiteUrl , setWebsiteUrl ] = useState ( '' )
2026-06-15 06:14:22 +02:00
const [ websiteProcessing , setWebsiteProcessing ] = useState ( false )
2026-06-15 01:25:15 +02:00
const [ expandedItemId , setExpandedItemId ] = useState ( null )
const [ contentPreviews , setContentPreviews ] = useState ( { } )
2026-03-19 23:57:17 +01:00
const toastTimeoutsRef = useRef ( new Map ( ) )
const toastIdRef = useRef ( 0 )
const previousLibraryStateRef = useRef ( null )
2026-03-19 21:07:22 +01:00
useEffect ( ( ) => {
setErrorMessage ( '' )
2026-06-15 01:14:16 +02:00
setSearch ( '' )
2026-06-15 01:25:35 +02:00
setExpandedItemId ( null )
setContentPreviews ( { } )
2026-06-15 01:14:16 +02:00
closeForm ( )
2026-03-19 21:07:22 +01:00
} , [ library ? . slug , library ? . name ] )
2026-03-19 23:57:17 +01:00
function dismissToast ( id ) {
const timeoutId = toastTimeoutsRef . current . get ( id )
2026-06-15 01:14:16 +02:00
if ( timeoutId ) clearTimeout ( timeoutId )
toastTimeoutsRef . current . delete ( id )
2026-03-19 23:57:17 +01:00
setToasts ( current => current . filter ( toast => toast . id !== id ) )
}
function clearToasts ( ) {
toastTimeoutsRef . current . forEach ( timeoutId => clearTimeout ( timeoutId ) )
toastTimeoutsRef . current . clear ( )
setToasts ( [ ] )
}
function queueToast ( message , tone = 'info' ) {
setToasts ( current => {
2026-06-15 01:14:16 +02:00
if ( current . some ( toast => toast . message === message && toast . tone === tone ) ) return current
2026-03-19 23:57:17 +01:00
const id = ` library-toast- ${ toastIdRef . current ++ } `
const timeoutId = window . setTimeout ( ( ) => dismissToast ( id ) , TOAST _DURATION _MS [ tone ] || TOAST _DURATION _MS . info )
toastTimeoutsRef . current . set ( id , timeoutId )
2026-06-15 01:14:16 +02:00
return [ ... current , { id , message , tone } ] . slice ( - 3 )
2026-03-19 23:57:17 +01:00
} )
}
2026-06-15 01:15:35 +02:00
useEffect ( ( ) => ( ) => {
toastTimeoutsRef . current . forEach ( timeoutId => clearTimeout ( timeoutId ) )
toastTimeoutsRef . current . clear ( )
} , [ ] )
2026-03-19 23:57:17 +01:00
2026-06-15 01:14:16 +02:00
function closeForm ( ) {
setFormMode ( null )
setEditingItemId ( null )
setTextTitle ( '' )
setTextContent ( '' )
setWebsiteTitle ( '' )
setWebsiteUrl ( '' )
2026-03-19 21:07:22 +01:00
}
2026-06-15 01:25:15 +02:00
async function toggleContentPreview ( item ) {
const itemId = item ? . item _id
if ( ! library || ! itemId ) return
if ( expandedItemId === itemId ) {
setExpandedItemId ( null )
return
}
setExpandedItemId ( itemId )
if ( contentPreviews [ itemId ] ) return
setContentPreviews ( current => ( { ... current , [ itemId ] : { loading : true } } ) )
try {
const response = await fetch ( ` ${ apiBase } /libraries/ ${ library . slug } /items/ ${ itemId } ` )
const data = await expectJson ( response )
setContentPreviews ( current => ( { ... current , [ itemId ] : { data } } ) )
} catch ( error ) {
setContentPreviews ( current => ( {
... current ,
[ itemId ] : { error : String ( error ? . message || error ) }
} ) )
}
}
2026-06-15 01:14:16 +02:00
async function runAction ( fn , successMessage ) {
2026-03-19 21:07:22 +01:00
setBusy ( true )
2026-06-15 01:14:16 +02:00
setErrorMessage ( '' )
2026-03-19 21:07:22 +01:00
try {
2026-06-15 01:14:16 +02:00
const result = await fn ( )
if ( successMessage ) queueToast ( successMessage , 'success' )
return result
} catch ( error ) {
setErrorMessage ( String ( error ? . message || error ) )
throw error
2026-03-19 21:07:22 +01:00
} finally {
setBusy ( false )
await onRefresh ( )
}
}
2026-03-20 10:18:12 +01:00
async function addPaths ( ) {
2026-03-19 21:07:22 +01:00
if ( ! library ) return
2026-05-06 04:41:31 +02:00
const paths = await desktopApi . pickPaths ( )
2026-03-19 21:07:22 +01:00
if ( ! Array . isArray ( paths ) || paths . length === 0 ) return
try {
2026-06-15 01:14:16 +02:00
await runAction ( async ( ) => {
const response = await fetch ( ` ${ apiBase } /libraries/ ${ library . slug } /files/register ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { paths } )
} )
return expectJson ( response )
} , 'Files added. Heimgeist is updating the database.' )
} catch { }
}
async function submitText ( event ) {
event . preventDefault ( )
if ( ! library ) return
try {
await runAction ( async ( ) => {
const editing = Boolean ( editingItemId )
const response = await fetch (
editing
? ` ${ apiBase } /libraries/ ${ library . slug } /texts/ ${ editingItemId } `
: ` ${ apiBase } /libraries/ ${ library . slug } /texts ` ,
{
method : editing ? 'PATCH' : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { title : textTitle , content : textContent } )
}
)
return expectJson ( response )
} , editingItemId ? 'Text updated.' : 'Text added to the database.' )
2026-06-15 01:26:47 +02:00
if ( editingItemId ) {
setContentPreviews ( current => {
const next = { ... current }
delete next [ editingItemId ]
return next
} )
setExpandedItemId ( current => current === editingItemId ? null : current )
}
2026-06-15 01:14:16 +02:00
closeForm ( )
} catch { }
}
async function editText ( item ) {
if ( ! library || ! item ? . item _id ) return
setBusy ( true )
setErrorMessage ( '' )
try {
const response = await fetch ( ` ${ apiBase } /libraries/ ${ library . slug } /items/ ${ item . item _id } ` )
const data = await expectJson ( response )
setEditingItemId ( item . item _id )
setTextTitle ( data . title || item . title || item . name || '' )
setTextContent ( data . content || '' )
setFormMode ( 'text' )
2026-03-19 21:07:22 +01:00
} catch ( error ) {
setErrorMessage ( String ( error ? . message || error ) )
2026-06-15 01:14:16 +02:00
} finally {
setBusy ( false )
2026-03-19 21:07:22 +01:00
}
}
2026-06-15 01:14:16 +02:00
async function submitWebsite ( event ) {
event . preventDefault ( )
if ( ! library ) return
2026-06-15 06:14:22 +02:00
setWebsiteProcessing ( true )
2026-06-15 01:14:16 +02:00
try {
2026-06-15 06:14:22 +02:00
const result = await runAction ( async ( ) => {
2026-06-15 01:14:16 +02:00
const response = await fetch ( ` ${ apiBase } /libraries/ ${ library . slug } /websites ` , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { url : websiteUrl , title : websiteTitle || null } )
} )
return expectJson ( response )
2026-06-15 06:14:22 +02:00
} )
queueToast (
result ? . content _kind === 'video'
? 'Video transcribed and summarized. Heimgeist is indexing it.'
: 'Website saved. Heimgeist is indexing its text.' ,
'success'
)
2026-06-15 01:14:16 +02:00
closeForm ( )
2026-06-15 06:14:22 +02:00
} catch {
} finally {
setWebsiteProcessing ( false )
}
2026-06-15 01:14:16 +02:00
}
async function refreshWebsite ( item ) {
if ( ! library || ! item ? . item _id ) return
try {
const result = await runAction ( async ( ) => {
const response = await fetch ( ` ${ apiBase } /libraries/ ${ library . slug } /websites/ ${ item . item _id } /refresh ` , {
method : 'POST'
} )
return expectJson ( response )
} )
2026-06-15 01:26:47 +02:00
setContentPreviews ( current => {
const next = { ... current }
delete next [ item . item _id ]
return next
} )
setExpandedItemId ( current => current === item . item _id ? null : current )
2026-06-15 01:14:16 +02:00
queueToast ( result ? . changed ? 'Website changed and is being reindexed.' : 'Website checked. No text changes found.' , 'success' )
} catch { }
}
2026-06-15 06:14:22 +02:00
async function refreshVideo ( item ) {
if ( ! library || ! item ? . item _id ) return
setWebsiteProcessing ( true )
try {
const result = await runAction ( async ( ) => {
const response = await fetch ( ` ${ apiBase } /libraries/ ${ library . slug } /videos/ ${ item . item _id } /refresh ` , {
method : 'POST'
} )
return expectJson ( response )
} )
setContentPreviews ( current => {
const next = { ... current }
delete next [ item . item _id ]
return next
} )
setExpandedItemId ( current => current === item . item _id ? null : current )
queueToast ( result ? . changed ? 'Video reprocessed and changed.' : 'Video reprocessed. Transcript content is unchanged.' , 'success' )
} catch {
} finally {
setWebsiteProcessing ( false )
}
}
2026-06-15 01:14:16 +02:00
async function removeItem ( item ) {
2026-03-19 21:07:22 +01:00
if ( ! library ) return
try {
await runAction ( async ( ) => {
const response = await fetch ( ` ${ apiBase } /libraries/ ${ library . slug } /files ` , {
method : 'DELETE' ,
headers : { 'Content-Type' : 'application/json' } ,
2026-06-15 01:14:16 +02:00
body : JSON . stringify ( { rel : item . rel } )
2026-03-19 21:07:22 +01:00
} )
2026-06-15 01:14:16 +02:00
return expectJson ( response )
} , ` ${ kindLabel ( item ) } removed. ` )
} catch { }
2026-03-19 21:07:22 +01:00
}
2026-06-15 01:14:16 +02:00
async function updateEnrichment ( item , enabled ) {
2026-03-19 22:15:16 +01:00
if ( ! library ) return
try {
await runAction ( async ( ) => {
const response = await fetch ( ` ${ apiBase } /libraries/ ${ library . slug } /files/enrichment ` , {
method : 'PATCH' ,
headers : { 'Content-Type' : 'application/json' } ,
2026-06-15 01:14:16 +02:00
body : JSON . stringify ( { rel : item . rel , enabled } )
2026-03-19 22:15:16 +01:00
} )
2026-06-15 01:14:16 +02:00
return expectJson ( response )
2026-06-15 02:05:01 +02:00
} , enabled ? 'Deep enrichment enabled. Metadata is being updated.' : 'Using standard metadata only.' )
} catch { }
}
async function generateMetadata ( ) {
if ( ! library ) return
try {
await runAction ( async ( ) => {
const response = await fetch ( ` ${ apiBase } /libraries/ ${ library . slug } /jobs/prepare ` , { method : 'POST' } )
return expectJson ( response )
} , 'Generating metadata for this database.' )
2026-06-15 01:14:16 +02:00
} catch { }
2026-03-19 22:15:16 +01:00
}
2026-03-19 21:48:18 +01:00
async function retrySync ( ) {
if ( ! library ) return
try {
await runAction ( async ( ) => {
2026-06-15 01:14:16 +02:00
const response = await fetch ( ` ${ apiBase } /libraries/ ${ library . slug } /jobs/prepare ` , { method : 'POST' } )
return expectJson ( response )
2026-03-19 21:48:18 +01:00
} )
2026-06-15 01:14:16 +02:00
} catch { }
2026-03-19 21:48:18 +01:00
}
2026-03-19 23:57:54 +01:00
const librarySlug = library ? . slug || null
const isSyncing = ! ! librarySlug && ( jobs || [ ] ) . some (
job => job . slug === librarySlug && ( job . status === 'queued' || job . status === 'running' )
)
const isReadyForChat = ! ! library ? . states ? . is _indexed
2026-06-15 01:14:16 +02:00
const items = library ? . files || [ ]
const hasFailedItems = items . some ( item => item ? . sync ? . status === 'failed' )
const filteredItems = useMemo ( ( ) => {
const term = search . trim ( ) . toLowerCase ( )
if ( ! term ) return items
2026-06-15 02:05:01 +02:00
return items . filter ( item => [
item . title ,
item . name ,
item . path ,
item . url ,
item . kind ,
2026-06-15 03:53:10 +02:00
item . source _session _title ,
item . source _role ,
2026-06-15 06:14:22 +02:00
item . channel ,
item . video _summary ,
2026-06-15 02:05:01 +02:00
item . metadata ? . headline ,
item . metadata ? . summary ,
... ( item . metadata ? . keywords || [ ] ) ,
... ( item . metadata ? . entities || [ ] ) . map ( entity => entity ? . name )
]
2026-06-15 01:14:16 +02:00
. some ( value => String ( value || '' ) . toLowerCase ( ) . includes ( term ) ) )
} , [ items , search ] )
2026-06-15 02:05:01 +02:00
const hasMissingMetadata = items . some ( item => ! [ 'ready' , 'fallback' ] . includes ( item ? . metadata ? . status ) )
2026-03-19 21:07:22 +01:00
2026-03-19 23:57:17 +01:00
useEffect ( ( ) => {
if ( ! library ? . slug ) {
previousLibraryStateRef . current = null
clearToasts ( )
return
}
const nextState = {
slug : library . slug ,
2026-06-15 01:14:16 +02:00
hasItems : items . length > 0 ,
2026-03-19 23:57:17 +01:00
isSyncing ,
isReadyForChat ,
2026-06-15 01:14:16 +02:00
hasFailedItems
2026-03-19 23:57:17 +01:00
}
const previousState = previousLibraryStateRef . current
if ( ! previousState || previousState . slug !== nextState . slug ) {
previousLibraryStateRef . current = nextState
return
}
if ( ! previousState . isSyncing && nextState . isSyncing ) {
2026-06-15 01:14:16 +02:00
queueToast ( 'Syncing this database. Heimgeist is rebuilding its search indexes.' )
} else if ( previousState . isSyncing && ! nextState . isSyncing ) {
if ( nextState . hasFailedItems ) queueToast ( 'Some content did not finish syncing.' , 'warning' )
else if ( nextState . isReadyForChat ) queueToast ( 'Sync complete. This database is ready in chat.' , 'success' )
2026-03-19 23:57:17 +01:00
}
previousLibraryStateRef . current = nextState
2026-06-15 01:14:16 +02:00
} , [ library ? . slug , items . length , hasFailedItems , isReadyForChat , isSyncing ] )
2026-03-19 21:14:55 +01:00
2026-03-19 23:57:54 +01:00
if ( ! library ) {
2026-06-15 01:14:16 +02:00
return < div className = "placeholder-view" > < p > Create a database , then add files , your own texts , or websites . < / p > < / div >
2026-03-19 23:57:54 +01:00
}
2026-03-19 23:57:17 +01:00
return (
< div className = "library-panel" >
< div className = "library-panel-scroll" >
{ errorMessage && < div className = "form-error" > { errorMessage } < / div > }
2026-06-15 01:14:16 +02:00
{ formMode === 'text' && (
< form className = "library-inline-form library-content-form" onSubmit = { submitText } >
< div className = "library-form-header" >
< strong > { editingItemId ? 'Edit text' : 'Add text' } < / strong >
< button type = "button" className = "button ghost" onClick = { closeForm } > Cancel < / button >
< / div >
< label >
Title
< input value = { textTitle } onChange = { event => setTextTitle ( event . target . value ) } autoFocus required / >
< / label >
< label >
Text
< textarea value = { textContent } onChange = { event => setTextContent ( event . target . value ) } rows = { 12 } required / >
< / label >
< div className = "library-form-actions" >
< button className = "button" type = "submit" disabled = { busy } > { editingItemId ? 'Save Changes' : 'Add Text' } < / button >
< / div >
< / form >
) }
{ formMode === 'website' && (
< form className = "library-inline-form library-content-form" onSubmit = { submitWebsite } >
< div className = "library-form-header" >
2026-06-15 06:14:51 +02:00
< strong > Add website or video < / strong >
2026-06-15 01:14:16 +02:00
< button type = "button" className = "button ghost" onClick = { closeForm } > Cancel < / button >
2026-03-19 23:57:17 +01:00
< / div >
2026-06-15 06:14:51 +02:00
< p className = "muted-copy library-video-help" >
Supported video links are detected automatically , downloaded temporarily , transcribed with Whisper , and summarized locally . Video and audio files are discarded afterwards .
< / p >
2026-06-15 01:14:16 +02:00
< label >
2026-06-15 06:14:51 +02:00
Website or video URL
< input type = "url" value = { websiteUrl } onChange = { event => setWebsiteUrl ( event . target . value ) } placeholder = "https://example.com/article-or-video" autoFocus required / >
2026-06-15 01:14:16 +02:00
< / label >
< label >
Custom title < span className = "muted-copy" > ( optional ) < / span >
< input value = { websiteTitle } onChange = { event => setWebsiteTitle ( event . target . value ) } / >
< / label >
2026-06-15 06:14:51 +02:00
< p className = "muted-copy" > Normal webpages are saved as one local text snapshot . Heimgeist will not crawl linked pages . < / p >
2026-06-15 01:14:16 +02:00
< div className = "library-form-actions" >
2026-06-15 06:14:51 +02:00
< button className = "button" type = "submit" disabled = { busy } > { websiteProcessing ? 'Processing content...' : 'Add Content' } < / button >
2026-06-15 01:14:16 +02:00
< / div >
2026-06-15 06:14:51 +02:00
{ websiteProcessing && < div className = "library-video-processing" > Video processing can take several minutes . Heimgeist is downloading audio , transcribing it , and generating a local summary . < / div > }
2026-06-15 01:14:16 +02:00
< / form >
) }
< div className = "library-content-heading" >
< div >
< h2 > Contents < / h2 >
2026-06-15 06:14:51 +02:00
< p className = "muted-copy" > Files , texts , website snapshots , videos , and saved chat messages are searched together when this database is selected . < / p >
2026-06-15 01:14:16 +02:00
< / div >
< input
className = "library-search"
value = { search }
onChange = { event => setSearch ( event . target . value ) }
placeholder = "Search contents"
aria - label = "Search database contents"
/ >
2026-03-19 23:57:17 +01:00
< / div >
2026-06-15 01:14:16 +02:00
{ filteredItems . length ? (
< div className = "library-content-grid" >
{ filteredItems . map ( item => {
const sync = itemSyncMeta ( item )
const label = kindLabel ( item )
2026-06-15 03:53:10 +02:00
const source = item . kind === 'website'
? item . url
2026-06-15 06:14:51 +02:00
: item . kind === 'video'
? ` ${ item . channel ? ` ${ item . channel } · ` : '' } ${ item . duration _text || item . url } `
2026-06-15 03:53:10 +02:00
: item . kind === 'text'
? 'Written in Heimgeist'
: item . kind === 'chat_message'
? ` ${ item . source _role === 'user' ? 'User message' : 'Assistant message' } · ${ item . source _session _title || 'Chat' } `
: item . path
2026-06-15 01:25:15 +02:00
const itemId = item . item _id || item . sha256 || item . rel
const isExpanded = expandedItemId === itemId
const preview = contentPreviews [ itemId ]
2026-06-15 02:05:01 +02:00
const metadata = item . metadata || { }
2026-06-15 02:42:36 +02:00
const keywords = Array . isArray ( metadata . keywords ) ? metadata . keywords : [ ]
const entities = Array . isArray ( metadata . entities ) ? metadata . entities : [ ]
const qaPairs = Array . isArray ( metadata . qa ) ? metadata . qa : [ ]
const qualityFlags = Array . isArray ( metadata . quality _flags ) ? metadata . quality _flags : [ ]
2026-06-15 01:14:16 +02:00
return (
2026-06-15 01:25:15 +02:00
< article key = { itemId } className = { ` library-content-card kind- ${ item . kind || 'file' } ${ isExpanded ? 'expanded' : '' } ` } >
2026-06-15 01:14:16 +02:00
< div className = "library-content-card-header" >
< span className = "library-kind-badge" > { label } < / span >
< span className = { ` library-file-sync-label ${ sync . status } ` } > { sync . label } < / span >
< / div >
< div className = "library-file-name" > { item . title || item . name || item . path } < / div >
< div className = "library-file-path" > { source } < / div >
< div className = { ` library-file-mode ${ item . enrich _enabled ? 'enabled' : '' } ` } >
2026-06-15 02:05:01 +02:00
{ item . enrich _enabled ? 'Deep enrichment' : 'Standard' }
< / div >
2026-06-15 06:14:51 +02:00
{ item . kind === 'video' && item . video _summary && (
< div className = "library-video-summary" >
< div className = "library-metadata-label" > Video summary < / div >
< p > { item . video _summary } < / p >
< div className = "library-metadata-details" >
{ item . transcription _model && < span > Whisper : { item . transcription _model } < / span > }
{ item . transcription _workers && < span > Workers : { item . transcription _workers } < / span > }
{ item . summary _model && < span > Summary : { item . summary _model } < / span > }
{ item . yt _dlp _version && < span > yt - dlp : { item . yt _dlp _version } < / span > }
< / div >
< / div >
) }
2026-06-15 02:05:01 +02:00
< div className = { ` library-item-metadata status- ${ metadata . status || 'missing' } ` } >
2026-06-15 02:42:36 +02:00
{ metadata . headline && < h3 > { metadata . headline } < / h3 > }
2026-06-15 02:05:01 +02:00
{ metadata . summary ? (
< p > { metadata . summary } < / p >
) : (
< p className = "muted-copy" >
{ metadata . status === 'pending' ? 'Metadata is being generated.' : 'No automatic metadata has been generated yet.' }
< / p >
) }
{ keywords . length > 0 && (
< div className = "library-metadata-chips" aria - label = "Keywords" >
{ keywords . map ( keyword => < span key = { keyword } > { keyword } < / span > ) }
< / div >
) }
{ entities . length > 0 && (
< div className = "library-metadata-entities" >
{ entities . map ( entity => (
< span key = { ` ${ entity . name } - ${ entity . type } ` } > { entity . name } { entity . type ? ` · ${ entity . type } ` : '' } < / span >
) ) }
< / div >
) }
2026-06-15 02:42:36 +02:00
{ qaPairs . length > 0 && (
< div className = "library-metadata-qa" >
< div className = "library-metadata-label" > Generated Q & amp ; A < / div >
{ qaPairs . map ( ( qa , index ) => (
< div className = "library-metadata-qa-pair" key = { ` ${ qa . q } - ${ index } ` } >
< strong > { qa . q } < / strong >
< span > { qa . a } < / span >
< / div >
) ) }
< / div >
) }
{ ( metadata . model || metadata . language || metadata . strategy || qualityFlags . length > 0 ) && (
< div className = "library-metadata-details" >
{ metadata . model && < span > Model : { metadata . model } < / span > }
{ metadata . language && < span > Language : { metadata . language } < / span > }
{ metadata . strategy && < span > Strategy : { metadata . strategy } < / span > }
{ qualityFlags . length > 0 && < span > Flags : { qualityFlags . join ( ', ' ) } < / span > }
< / div >
) }
2026-06-15 02:05:01 +02:00
{ metadata . status === 'fallback' && < div className = "library-metadata-warning" > Local model metadata was unavailable ; Heimgeist used a text fallback . < / div > }
2026-06-15 02:08:36 +02:00
{ metadata . status === 'failed' && < div className = "library-metadata-warning" > { metadata . error || 'Metadata generation failed.' } < / div > }
2026-06-15 01:14:16 +02:00
< / div >
< div className = "library-file-sync" >
< div className = "library-file-sync-detail" > { sync . detail } < / div >
< div className = { ` library-file-progress ${ sync . status } ` } role = "progressbar" aria - valuemin = "0" aria - valuemax = "100" aria - valuenow = { Math . round ( sync . progress ) } >
< div className = "library-file-progress-bar" style = { { width : ` ${ sync . progress } % ` } } / >
< / div >
< / div >
2026-06-15 01:25:15 +02:00
{ isExpanded && (
< div className = "library-content-preview" >
< div className = "library-content-preview-label" >
2026-06-15 06:14:51 +02:00
{ item . kind === 'website' ? 'Saved website text' : item . kind === 'video' ? 'Video summary and transcript' : item . kind === 'chat_message' ? 'Stored chat knowledge' : item . kind === 'text' ? 'Stored text' : 'Extracted text used by RAG' }
2026-06-15 01:25:15 +02:00
< / div >
{ preview ? . loading && < div className = "muted-copy" > Loading content … < / div > }
{ preview ? . error && < div className = "form-error" > { preview . error } < / div > }
{ preview ? . data && (
< >
< pre > { preview . data . content || 'No readable text was extracted from this item.' } < / pre >
{ preview . data . content _truncated && < div className = "muted-copy" > Preview shortened . The indexed source contains more text . < / div > }
< / >
) }
< / div >
) }
2026-06-15 01:14:16 +02:00
< div className = "library-file-actions" >
2026-06-15 01:25:15 +02:00
< button className = "button ghost" disabled = { busy } onClick = { ( ) => toggleContentPreview ( item ) } >
{ isExpanded ? 'Hide Content' : 'View Content' }
< / button >
2026-06-15 03:53:10 +02:00
{ ( item . kind === 'text' || item . kind === 'chat_message' ) && < button className = "button ghost" disabled = { busy } onClick = { ( ) => editText ( item ) } > Edit < / button > }
{ item . kind === 'chat_message' && < button className = "button ghost" onClick = { ( ) => onOpenChatMessage ? . ( item ) } > Open Chat < / button > }
2026-06-15 01:14:16 +02:00
{ item . kind === 'website' && < button className = "button ghost" onClick = { ( ) => desktopApi . openExternalLink ( item . url ) } > Open < / button > }
{ item . kind === 'website' && < button className = "button ghost" disabled = { busy } onClick = { ( ) => refreshWebsite ( item ) } > Refresh < / button > }
2026-06-15 06:14:51 +02:00
{ item . kind === 'video' && < button className = "button ghost" onClick = { ( ) => desktopApi . openExternalLink ( item . url ) } > Open < / button > }
{ item . kind === 'video' && < button className = "button ghost" disabled = { busy } onClick = { ( ) => refreshVideo ( item ) } > Reprocess < / button > }
2026-06-15 01:14:16 +02:00
{ ( ! item . kind || item . kind === 'file' ) && < button className = "button ghost" onClick = { ( ) => desktopApi . openPath ( item . path ) } > Open < / button > }
< button className = "button ghost" disabled = { busy || isSyncing } onClick = { ( ) => updateEnrichment ( item , ! item . enrich _enabled ) } >
2026-06-15 02:05:01 +02:00
{ item . enrich _enabled ? 'Use Standard' : 'Enable Deep' }
2026-06-15 01:14:16 +02:00
< / button >
< button className = "button ghost" disabled = { busy || isSyncing } onClick = { ( ) => removeItem ( item ) } > Remove < / button >
< / div >
< / article >
)
} ) }
< / div >
) : (
< p className = "muted-copy" > { items . length ? 'No matching contents.' : 'No content added yet.' } < / p >
) }
2026-03-19 21:07:22 +01:00
< / div >
2026-03-19 23:57:17 +01:00
2026-03-20 00:00:46 +01:00
< div className = "library-footer-actions" >
2026-03-20 10:18:12 +01:00
< button className = "button" disabled = { busy } onClick = { addPaths } > Add Files < / button >
2026-06-15 01:14:16 +02:00
< button className = "button" disabled = { busy } onClick = { ( ) => { closeForm ( ) ; setFormMode ( 'text' ) } } > Add Text < / button >
< button className = "button" disabled = { busy } onClick = { ( ) => { closeForm ( ) ; setFormMode ( 'website' ) } } > Add Website < / button >
2026-06-15 02:05:01 +02:00
{ items . length > 0 && hasMissingMetadata && ! isSyncing && isReadyForChat && (
< button className = "button ghost" disabled = { busy } onClick = { generateMetadata } > Generate Metadata < / button >
) }
2026-06-15 01:14:16 +02:00
{ items . length > 0 && ! isSyncing && ! isReadyForChat && (
2026-03-20 00:00:46 +01:00
< button className = "button ghost" disabled = { busy } onClick = { retrySync } > Retry Sync < / button >
) }
< / div >
2026-03-19 23:57:17 +01:00
{ toasts . length > 0 && (
< div className = "library-toast-stack" aria - live = "polite" >
{ toasts . map ( toast => (
2026-06-15 01:14:16 +02:00
< div key = { toast . id } className = { ` library-toast ${ toast . tone } ` } role = { toast . tone === 'warning' ? 'alert' : 'status' } >
2026-03-19 23:57:17 +01:00
{ toast . message }
< / div >
) ) }
< / div >
) }
2026-03-19 21:07:22 +01:00
< / div >
)
}