feat: complete release preparation, 10+ ad mediation, CI/CD, and docker deployment
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
This commit is contained in:
parent
5cd1de6859
commit
708e20f747
406 changed files with 42464 additions and 6199 deletions
|
|
@ -1,14 +1,13 @@
|
|||
// src/renderer/pages/KnowledgeBasePage.tsx
|
||||
// Phase 13.2: 로컬 RAG Knowledge Base UI
|
||||
// 문서 관리 + 질문 입력 + 답변 표시
|
||||
// Local RAG Knowledge Base & Semantic Memory Intelligence
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { Box, TextField, IconButton, Tooltip, LinearProgress } from '@mui/material'
|
||||
import { Plus, Trash2, RefreshCw, Search, Send, FileText } from 'lucide-react'
|
||||
import { MetalCard, PhosphorText, Led, PhysicalButton, ScreenPanel } from '@d3ro/ui/components/ds'
|
||||
import { Plus, Trash2, RefreshCw, Search, Send, FileText, Database, Sparkles, CheckCircle2 } from 'lucide-react'
|
||||
import { MetalCard, PhosphorText, Led, PhysicalButton, ScreenPanel, DoubleBezelCard, TactileBadge } from '@d3ro/ui/components/ds'
|
||||
import { PageHeader, EmptyStateCard } from '../components/shared'
|
||||
import { isImeComposingEvent } from '../utils/keyboard'
|
||||
import { d3roPalette, d3roFontMono, d3roTypo } from '@d3ro/ui/theme'
|
||||
import { d3roPalette, d3roFontSans, d3roFontMono, d3roTypo, d3roRadius, d3roShadow } from '@d3ro/ui/theme'
|
||||
import { useI18n } from '@d3ro/i18n'
|
||||
import type { RAGDocument, RAGQueryResult, RAGIndexProgress } from '@d3ro/core/types'
|
||||
|
||||
|
|
@ -30,7 +29,9 @@ export function KnowledgeBasePage(): React.ReactElement {
|
|||
setLoading(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => { loadDocuments() }, [loadDocuments])
|
||||
useEffect(() => {
|
||||
loadDocuments()
|
||||
}, [loadDocuments])
|
||||
|
||||
useEffect(() => {
|
||||
const unsubProgress = window.electronAPI.rag.onIndexProgress((data) => {
|
||||
|
|
@ -40,27 +41,41 @@ export function KnowledgeBasePage(): React.ReactElement {
|
|||
setIndexProgress(null)
|
||||
loadDocuments()
|
||||
})
|
||||
return () => { unsubProgress(); unsubComplete() }
|
||||
}, [loadDocuments])
|
||||
|
||||
const handleAddDocument = useCallback(async () => {
|
||||
setAdding(true)
|
||||
setAddError(null)
|
||||
const resp = await window.electronAPI.rag.addDocument()
|
||||
setAdding(false)
|
||||
if (resp.success) {
|
||||
loadDocuments()
|
||||
} else {
|
||||
if (!resp.error.message.includes('cancelled')) {
|
||||
setAddError(resp.error.message)
|
||||
}
|
||||
return () => {
|
||||
unsubProgress()
|
||||
unsubComplete()
|
||||
}
|
||||
}, [loadDocuments])
|
||||
|
||||
const handleRemoveDocument = useCallback(async (docId: string) => {
|
||||
await window.electronAPI.rag.removeDocument({ documentId: docId })
|
||||
loadDocuments()
|
||||
}, [loadDocuments])
|
||||
const isAddingRef = useRef(false)
|
||||
|
||||
const handleAddDocument = useCallback(async () => {
|
||||
if (isAddingRef.current || adding) return
|
||||
isAddingRef.current = true
|
||||
setAdding(true)
|
||||
setAddError(null)
|
||||
try {
|
||||
const resp = await window.electronAPI.rag.addDocument()
|
||||
if (resp.success) {
|
||||
loadDocuments()
|
||||
} else {
|
||||
if (!resp.error.message.includes('cancelled') && !resp.error.message.includes('already active')) {
|
||||
setAddError(resp.error.message)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setAdding(false)
|
||||
isAddingRef.current = false
|
||||
}
|
||||
}, [adding, loadDocuments])
|
||||
|
||||
const handleRemoveDocument = useCallback(
|
||||
async (docId: string) => {
|
||||
await window.electronAPI.rag.removeDocument({ documentId: docId })
|
||||
loadDocuments()
|
||||
},
|
||||
[loadDocuments],
|
||||
)
|
||||
|
||||
const handleReindex = useCallback(async (docId: string) => {
|
||||
await window.electronAPI.rag.reindex(docId)
|
||||
|
|
@ -77,53 +92,65 @@ export function KnowledgeBasePage(): React.ReactElement {
|
|||
setQuerying(false)
|
||||
}, [query])
|
||||
|
||||
const handleQueryKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (isImeComposingEvent(e)) return
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleQuery()
|
||||
}
|
||||
}, [handleQuery])
|
||||
const handleQueryKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (isImeComposingEvent(e)) return
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleQuery()
|
||||
}
|
||||
},
|
||||
[handleQuery],
|
||||
)
|
||||
|
||||
return (
|
||||
<Box sx={{ maxWidth: 900, mx: 'auto', p: 4, pb: 8 }}>
|
||||
<Box sx={{ maxWidth: 1060, mx: 'auto', p: { xs: 2.5, md: 4 }, pb: 10 }}>
|
||||
<PageHeader
|
||||
title={t('rag.title').toUpperCase()}
|
||||
title={t('rag.title')}
|
||||
count={`${documents.length} ${t('rag.documents')}`}
|
||||
action={
|
||||
<PhysicalButton size="small" onClick={handleAddDocument} disabled={adding}>
|
||||
<Plus size={14} style={{ marginRight: 4 }} /> {adding ? t('rag.adding') : t('rag.addDocument')}
|
||||
<PhysicalButton
|
||||
tone="accent"
|
||||
size="small"
|
||||
onClick={handleAddDocument}
|
||||
disabled={adding}
|
||||
trailingIcon={<Plus size={14} />}
|
||||
>
|
||||
{adding ? t('rag.adding') : t('rag.addDocument')}
|
||||
</PhysicalButton>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 인덱싱 진행률 */}
|
||||
{/* Indexing Progress Card */}
|
||||
{indexProgress && (
|
||||
<MetalCard sx={{ mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
||||
<Led color="amber" pulse />
|
||||
<PhosphorText variant="body">
|
||||
{t('rag.indexing')}: {indexProgress.fileName}
|
||||
<MetalCard sx={{ mb: 2.5, p: 2.5 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1.25 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Led color="amber" pulse />
|
||||
<PhosphorText variant="body">
|
||||
{t('rag.indexing')}: {indexProgress.fileName}
|
||||
</PhosphorText>
|
||||
</Box>
|
||||
<PhosphorText variant="meta">
|
||||
{indexProgress.currentChunk} / {indexProgress.totalChunks} Chunks ({indexProgress.percent}%)
|
||||
</PhosphorText>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={indexProgress.percent}
|
||||
sx={{
|
||||
height: 4,
|
||||
borderRadius: 2,
|
||||
height: 6,
|
||||
borderRadius: d3roRadius.pill,
|
||||
bgcolor: d3roPalette.bg.inset,
|
||||
'& .MuiLinearProgress-bar': { bgcolor: d3roPalette.accent.main },
|
||||
}}
|
||||
/>
|
||||
<PhosphorText variant="dim" sx={{ mt: 0.5 }}>
|
||||
{indexProgress.currentChunk}/{indexProgress.totalChunks}
|
||||
</PhosphorText>
|
||||
</MetalCard>
|
||||
)}
|
||||
|
||||
{/* 에러 표시 */}
|
||||
{/* Error Card */}
|
||||
{addError && (
|
||||
<MetalCard sx={{ mb: 2, border: `1px solid ${d3roPalette.tag.red}` }}>
|
||||
<MetalCard sx={{ mb: 2.5, p: 2, border: `1px solid ${d3roPalette.tag.redBg}` }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Led color="red" />
|
||||
<PhosphorText variant="body" sx={{ color: d3roPalette.tag.red }}>
|
||||
|
|
@ -133,67 +160,10 @@ export function KnowledgeBasePage(): React.ReactElement {
|
|||
</MetalCard>
|
||||
)}
|
||||
|
||||
{/* 추가 중 인디케이터 */}
|
||||
{adding && !indexProgress && (
|
||||
<MetalCard sx={{ mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Led color="amber" pulse />
|
||||
<PhosphorText variant="body">{t('rag.parsing')}</PhosphorText>
|
||||
</Box>
|
||||
</MetalCard>
|
||||
)}
|
||||
|
||||
{/* 문서 목록 */}
|
||||
<PhosphorText variant="label" sx={{ mb: 1.5, display: 'block', color: d3roPalette.text.inactive }}>
|
||||
{t('rag.documents').toUpperCase()} ({documents.length})
|
||||
</PhosphorText>
|
||||
|
||||
{documents.length === 0 && !loading ? (
|
||||
<EmptyStateCard message={t('rag.noDocuments')} />
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 3 }}>
|
||||
{documents.map((doc) => (
|
||||
<MetalCard key={doc.id}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<FileText size={20} style={{ color: d3roPalette.text.inactive }} />
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<PhosphorText variant="body">{doc.fileName}</PhosphorText>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.25 }}>
|
||||
<PhosphorText variant="dim">
|
||||
{doc.fileType.toUpperCase()} · {doc.chunkCount} {t('rag.chunks')} ·
|
||||
</PhosphorText>
|
||||
<Led color={doc.indexed ? 'green' : 'amber'} size={6} />
|
||||
<PhosphorText variant="dim">
|
||||
{doc.indexed ? t('rag.indexed') : t('rag.pending')}
|
||||
</PhosphorText>
|
||||
</Box>
|
||||
</Box>
|
||||
<Tooltip title={t('rag.reindex')}>
|
||||
<IconButton size="small" onClick={() => handleReindex(doc.id)}
|
||||
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.accent.main } }}>
|
||||
<RefreshCw size={16} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('common.delete')}>
|
||||
<IconButton size="small" onClick={() => handleRemoveDocument(doc.id)}
|
||||
sx={{ color: d3roPalette.text.inactive, '&:hover': { color: d3roPalette.tag.red } }}>
|
||||
<Trash2 size={16} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</MetalCard>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 질문 입력 */}
|
||||
<PhosphorText variant="label" sx={{ mb: 1.5, display: 'block', color: d3roPalette.text.inactive }}>
|
||||
{t('rag.askQuestion').toUpperCase()}
|
||||
</PhosphorText>
|
||||
|
||||
<MetalCard>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Search size={20} style={{ color: d3roPalette.text.inactive }} />
|
||||
{/* Asking / Semantic Query Bar */}
|
||||
<DoubleBezelCard innerPadding={2.5} sx={{ mb: 3.5 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<Search size={18} style={{ color: d3roPalette.accent.light }} />
|
||||
<TextField
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
|
|
@ -203,57 +173,228 @@ export function KnowledgeBasePage(): React.ReactElement {
|
|||
fullWidth
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
fontFamily: d3roFontMono,
|
||||
fontFamily: d3roFontSans,
|
||||
fontSize: d3roTypo.compact.size,
|
||||
bgcolor: d3roPalette.bg.inset,
|
||||
'& fieldset': { borderColor: d3roPalette.border.subtle },
|
||||
'&:hover fieldset': { borderColor: d3roPalette.accent.main },
|
||||
borderRadius: d3roRadius.inner,
|
||||
border: `1px solid ${d3roPalette.glass.hairline}`,
|
||||
'& fieldset': { border: 'none' },
|
||||
'&:hover': { borderColor: d3roPalette.glass.hairlineStrong },
|
||||
'&.Mui-focused': { borderColor: d3roPalette.accent.main },
|
||||
},
|
||||
'& .MuiOutlinedInput-input': { color: d3roPalette.text.primary },
|
||||
'& .MuiOutlinedInput-input': { color: d3roPalette.text.primary, py: 1.25 },
|
||||
}}
|
||||
/>
|
||||
<IconButton onClick={handleQuery} disabled={!query.trim() || querying}
|
||||
sx={{ color: query.trim() ? d3roPalette.accent.main : d3roPalette.text.inactive }}>
|
||||
<Send size={20} />
|
||||
</IconButton>
|
||||
<PhysicalButton
|
||||
tone="accent"
|
||||
onClick={handleQuery}
|
||||
disabled={!query.trim() || querying}
|
||||
sx={{ height: 40, px: 2.5 }}
|
||||
>
|
||||
<Send size={15} style={{ marginRight: 4 }} />
|
||||
{querying ? t('common.loading') : 'Search'}
|
||||
</PhysicalButton>
|
||||
</Box>
|
||||
</MetalCard>
|
||||
|
||||
{/* 답변 */}
|
||||
{querying && (
|
||||
<Box sx={{ mt: 2, textAlign: 'center' }}>
|
||||
<Led color="amber" pulse />
|
||||
<PhosphorText variant="dim" sx={{ ml: 1 }}>{t('rag.searching')}</PhosphorText>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<ScreenPanel sx={{ p: 3 }}>
|
||||
<PhosphorText variant="body" sx={{ whiteSpace: 'pre-wrap', lineHeight: 1.8 }}>
|
||||
{result.answer}
|
||||
{/* Quick Suggestion Prompt Chips */}
|
||||
{documents.length > 0 && !result && !querying && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 2, flexWrap: 'wrap' }}>
|
||||
<PhosphorText variant="meta" sx={{ color: d3roPalette.text.dimLabel, mr: 0.5 }}>
|
||||
PROMPTS:
|
||||
</PhosphorText>
|
||||
</ScreenPanel>
|
||||
{[
|
||||
'이 문서들의 핵심 내용 3줄 요약해줘',
|
||||
'주요 일정 및 마일스톤 정리해줘',
|
||||
'보안 및 개인정보 관련 정책은?',
|
||||
].map((sample) => (
|
||||
<TactileBadge
|
||||
key={sample}
|
||||
onClick={() => {
|
||||
setQuery(sample)
|
||||
setTimeout(handleQuery, 50)
|
||||
}}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
{sample}
|
||||
</TactileBadge>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{result.results.length > 0 && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<PhosphorText variant="label" sx={{ color: d3roPalette.text.dimLabel, mb: 1, display: 'block' }}>
|
||||
{t('rag.sources').toUpperCase()}
|
||||
{/* Answer Result Display */}
|
||||
{querying && (
|
||||
<Box sx={{ mt: 3, textAlign: 'center', py: 2 }}>
|
||||
<Led color="amber" pulse size={8} />
|
||||
<PhosphorText variant="dim" sx={{ ml: 1.5 }}>
|
||||
{t('rag.searching')}
|
||||
</PhosphorText>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<Box sx={{ mt: 3, pt: 2.5, borderTop: `1px solid ${d3roPalette.glass.hairline}` }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.5 }}>
|
||||
<Sparkles size={16} style={{ color: d3roPalette.accent.light }} />
|
||||
<PhosphorText variant="heading" sx={{ color: d3roPalette.accent.light }}>
|
||||
Semantic Synthesis
|
||||
</PhosphorText>
|
||||
{result.results.slice(0, 3).map((r, i) => (
|
||||
<MetalCard key={i} sx={{ mb: 1 }}>
|
||||
<PhosphorText variant="dim">
|
||||
[{i + 1}] {r.fileName} ({Math.round(r.similarity * 100)}%)
|
||||
</PhosphorText>
|
||||
<PhosphorText variant="compact" sx={{ mt: 0.5, opacity: 0.7 }}>
|
||||
{r.content.slice(0, 150)}...
|
||||
</PhosphorText>
|
||||
</MetalCard>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
p: 2.5,
|
||||
borderRadius: d3roRadius.inner,
|
||||
bgcolor: d3roPalette.bg.inset,
|
||||
border: `1px solid ${d3roPalette.glass.hairlineStrong}`,
|
||||
boxShadow: d3roShadow.inset,
|
||||
fontFamily: d3roFontSans,
|
||||
fontSize: '14px',
|
||||
lineHeight: 1.7,
|
||||
color: d3roPalette.text.primary,
|
||||
whiteSpace: 'pre-wrap',
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
{result.answer}
|
||||
</Box>
|
||||
|
||||
{/* Sources Attribution */}
|
||||
{result.results.length > 0 && (
|
||||
<Box>
|
||||
<PhosphorText variant="meta" sx={{ color: d3roPalette.text.dimLabel, mb: 1.25, display: 'block' }}>
|
||||
{t('rag.sources').toUpperCase()} ({result.results.length})
|
||||
</PhosphorText>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: 'repeat(3, 1fr)' }, gap: 1.5 }}>
|
||||
{result.results.slice(0, 3).map((r, i) => (
|
||||
<MetalCard key={i} sx={{ p: 1.75 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 0.75 }}>
|
||||
<PhosphorText variant="compact" sx={{ fontWeight: 600, color: d3roPalette.text.primary }}>
|
||||
[{i + 1}] {r.fileName}
|
||||
</PhosphorText>
|
||||
<TactileBadge mono tone="accent">
|
||||
{Math.round(r.similarity * 100)}%
|
||||
</TactileBadge>
|
||||
</Box>
|
||||
<PhosphorText variant="small" sx={{ color: d3roPalette.text.secondary, opacity: 0.85, lineHeight: 1.5 }}>
|
||||
{r.content.slice(0, 120)}...
|
||||
</PhosphorText>
|
||||
</MetalCard>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</DoubleBezelCard>
|
||||
|
||||
{/* Document Library Section */}
|
||||
<Box sx={{ mt: 4 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<PhosphorText variant="label" sx={{ color: d3roPalette.text.dimLabel }}>
|
||||
{t('rag.documents').toUpperCase()} ({documents.length})
|
||||
</PhosphorText>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{documents.length === 0 && !loading ? (
|
||||
<EmptyStateCard
|
||||
message={t('rag.noDocuments')}
|
||||
icon={<Database />}
|
||||
action={
|
||||
<PhysicalButton tone="accent" onClick={handleAddDocument}>
|
||||
<Plus size={15} style={{ marginRight: 4 }} />
|
||||
{t('rag.addDocument')}
|
||||
</PhysicalButton>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 1.75 }}>
|
||||
{documents.map((doc) => (
|
||||
<MetalCard key={doc.id} sx={{ p: 2.5 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: d3roRadius.inner,
|
||||
bgcolor: d3roPalette.glass.raised,
|
||||
border: `1px solid ${d3roPalette.glass.hairline}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: d3roPalette.accent.light,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<FileText size={20} />
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<PhosphorText
|
||||
variant="heading"
|
||||
sx={{
|
||||
mb: 0.5,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{doc.fileName}
|
||||
</PhosphorText>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.25, flexWrap: 'wrap', mt: 1 }}>
|
||||
<TactileBadge mono tone="mono">
|
||||
{doc.fileType.toUpperCase()}
|
||||
</TactileBadge>
|
||||
<TactileBadge mono tone="default">
|
||||
{doc.chunkCount} {t('rag.chunks')}
|
||||
</TactileBadge>
|
||||
<TactileBadge
|
||||
ledColor={doc.indexed ? 'green' : 'amber'}
|
||||
tone={doc.indexed ? 'success' : 'warning'}
|
||||
>
|
||||
{doc.indexed ? t('rag.indexed') : t('rag.pending')}
|
||||
</TactileBadge>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||
<Tooltip title={t('rag.reindex')}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleReindex(doc.id)}
|
||||
sx={{
|
||||
p: 0.6,
|
||||
color: d3roPalette.text.inactive,
|
||||
bgcolor: d3roPalette.glass.raised,
|
||||
border: `1px solid ${d3roPalette.glass.hairline}`,
|
||||
'&:hover': { color: d3roPalette.accent.light, bgcolor: d3roPalette.glass.hairlineStrong },
|
||||
}}
|
||||
>
|
||||
<RefreshCw size={14} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('common.delete')}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleRemoveDocument(doc.id)}
|
||||
sx={{
|
||||
p: 0.6,
|
||||
color: d3roPalette.text.inactive,
|
||||
bgcolor: d3roPalette.glass.raised,
|
||||
border: `1px solid ${d3roPalette.glass.hairline}`,
|
||||
'&:hover': { color: d3roPalette.tag.red, bgcolor: d3roPalette.tag.redBg },
|
||||
}}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
</MetalCard>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue